diff --git a/Attorney_Online.pro b/Attorney_Online.pro index 13de87d14..c244cdf3c 100644 --- a/Attorney_Online.pro +++ b/Attorney_Online.pro @@ -1,4 +1,4 @@ -QT += core gui widgets network websockets uitools +QT += core gui widgets network websockets uitools multimediawidgets TARGET = Attorney_Online TEMPLATE = app diff --git a/include/aoapplication.h b/include/aoapplication.h index 16b0433dd..2ee9361d6 100644 --- a/include/aoapplication.h +++ b/include/aoapplication.h @@ -96,6 +96,7 @@ class AOApplication : public QApplication { bool desk_mod_supported = false; bool evidence_supported = false; bool cccc_ic_supported = false; + bool video_supported = false; bool arup_supported = false; bool casing_alerts_supported = false; bool modcall_reason_supported = false; @@ -331,6 +332,9 @@ class AOApplication : public QApplication { // Returns if the sfx is defined as looping in char.ini QString get_sfx_looping(QString p_char, int p_emote); + // Returns the video filename for that emote + QString get_video_name(QString p_char, int p_emote); + // Returns if an emote has a frame specific SFX for it QString get_sfx_frame(QString p_char, QString p_emote, int n_frame); diff --git a/include/aographicsview.h b/include/aographicsview.h new file mode 100644 index 000000000..d96e98183 --- /dev/null +++ b/include/aographicsview.h @@ -0,0 +1,23 @@ +#include +#include + +#include +#include +#include + +#include + +class AOGraphicsView : public QGraphicsView +{ + Q_OBJECT + +public: + AOGraphicsView(QWidget *parent = nullptr); + ~AOGraphicsView(); + +protected: + void resizeEvent(QResizeEvent *event) final; + +private: + QGraphicsScene *m_scene; +}; diff --git a/include/courtroom.h b/include/courtroom.h index 3cc734250..581e1a780 100644 --- a/include/courtroom.h +++ b/include/courtroom.h @@ -25,6 +25,8 @@ #include "scrolltext.h" #include "eventfilters.h" #include "aoemotepreview.h" +#include "video/videoscreen.h" +#include "aographicsview.h" #include #include @@ -47,6 +49,9 @@ #include #include +#include +#include + #include #include #include @@ -276,6 +281,9 @@ class Courtroom : public QMainWindow { // Display the evidence image box when presenting evidence in IC void display_evidence_image(); + // Handle video playback before passing over to the next step + void handle_video(); + // Handle the stuff that comes when the character appears on screen and starts animating (preanims etc.) void handle_ic_message(); @@ -564,7 +572,7 @@ class Courtroom : public QMainWindow { // Minumum and maximum number of parameters in the MS packet static const int MS_MINIMUM = 15; - static const int MS_MAXIMUM = 35; + static const int MS_MAXIMUM = 36; QString m_chatmessage[MS_MAXIMUM]; QString previous_ic_message = ""; @@ -759,6 +767,8 @@ class Courtroom : public QMainWindow { QLabel *ui_vp_showname; InterfaceLayer *ui_vp_chat_arrow; QTextEdit *ui_vp_message; + AOGraphicsView *ui_vp_graphics; + VideoScreen *ui_vp_video; SplashLayer *ui_vp_testimony; SplashLayer *ui_vp_wtce; EffectLayer *ui_vp_effect; @@ -942,6 +952,8 @@ class Courtroom : public QMainWindow { void show_evidence(int f_real_id); void set_evidence_page(); + void video_finished(); + void reset_ui(); void regenerate_ic_chatlog(); diff --git a/include/datatypes.h b/include/datatypes.h index 940fc2e4d..e5532c030 100644 --- a/include/datatypes.h +++ b/include/datatypes.h @@ -117,6 +117,7 @@ enum CHAT_MESSAGE { THIRD_EMOTE, THIRD_OFFSET, THIRD_FLIP, + VIDEO, }; enum EMOTE_MOD_TYPE { diff --git a/include/mediatester.h b/include/mediatester.h new file mode 100644 index 000000000..0f23e6fec --- /dev/null +++ b/include/mediatester.h @@ -0,0 +1,20 @@ +#include +#include + +class MediaTester : public QObject +{ + Q_OBJECT + +public: + MediaTester(QObject *parent = nullptr); + ~MediaTester(); + +signals: + void done(); + +private: + QMediaPlayer m_player; + +private slots: + void p_check_status(QMediaPlayer::MediaStatus p_status); +}; \ No newline at end of file diff --git a/include/video/videoscreen.h b/include/video/videoscreen.h new file mode 100644 index 000000000..05b2d6cb9 --- /dev/null +++ b/include/video/videoscreen.h @@ -0,0 +1,64 @@ +#include "aoapplication.h" +#include "options.h" + + +#include +#include +#include + +class VideoScreen : public QGraphicsVideoItem +{ + Q_OBJECT + +public: + VideoScreen(AOApplication *p_ao_app, QGraphicsItem *parent = nullptr); + ~VideoScreen(); + + QString get_file_name() const; + + void update_audio_output(); + + void set_volume(int p_value); + + void set_muted(bool p_toggle); + +public slots: + void set_file_name(QString file_name); + + void play_character_video(QString character, QString video); + + void play(); + + void stop(); + +signals: + void started(); + + void finished(); + +private: + QWidget *m_parent; + + AOApplication *ao_app; + + QString m_file_name; + + bool m_scanned; + + bool m_video_available; + + bool m_running; + + QMediaPlayer *m_player; + + void start_playback(); + + void finish_playback(); + +private slots: + void update_video_availability(bool); + + void check_status(QMediaPlayer::MediaStatus); + + void check_state(QMediaPlayer::State); +}; diff --git a/resource/data_sample.avi b/resource/data_sample.avi new file mode 100644 index 000000000..3e4ab0eab Binary files /dev/null and b/resource/data_sample.avi differ diff --git a/resources.qrc b/resources.qrc index 9ac2fffac..8cdd28d8e 100644 --- a/resources.qrc +++ b/resources.qrc @@ -50,5 +50,6 @@ resource/ui/lobby_assets/tab_motd_off_hover.png resource/ui/lobby_assets/tab_motd_on.png resource/ui/lobby_assets/tab_motd_on_hover.png + resource/data_sample.avi diff --git a/src/aographicsview.cpp b/src/aographicsview.cpp new file mode 100644 index 000000000..21fb5ff65 --- /dev/null +++ b/src/aographicsview.cpp @@ -0,0 +1,39 @@ +#include "aographicsview.h" + +AOGraphicsView::AOGraphicsView(QWidget *parent) + : QGraphicsView(parent) + , m_scene(new QGraphicsScene(this)) +{ + setInteractive(false); + + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + setFrameShape(QFrame::NoFrame); + setFrameStyle(0); + + // Currently only used for video playback so always assume smooth transform for now + setRenderHints(QPainter::SmoothPixmapTransform); + + setScene(m_scene); + + setBackgroundBrush(Qt::transparent); +} + +AOGraphicsView::~AOGraphicsView() +{} + +void AOGraphicsView::resizeEvent(QResizeEvent *event) +{ + QGraphicsView::resizeEvent(event); + for (QGraphicsItem *i_item : scene()->items()) + { + auto l_object = dynamic_cast(i_item); + if (l_object) + { + l_object->setProperty("size", event->size()); + } + } + m_scene->setSceneRect(rect()); + setSceneRect(rect()); +} diff --git a/src/aomusicplayer.cpp b/src/aomusicplayer.cpp index 366335a48..50a8c06e1 100644 --- a/src/aomusicplayer.cpp +++ b/src/aomusicplayer.cpp @@ -137,7 +137,8 @@ QString AOMusicPlayer::play(QString p_song, int channel, bool loop, p_song_clear = p_song_clear.left(p_song_clear.lastIndexOf('.')); if (is_stop) { - return QObject::tr("None"); + // No music just clears out the displayer + return ""; } if (error_code == BASS_ERROR_HANDLE) { // Cheap hack to see if file missing diff --git a/src/courtroom.cpp b/src/courtroom.cpp index 556bf002e..37a3fc20c 100644 --- a/src/courtroom.cpp +++ b/src/courtroom.cpp @@ -96,6 +96,10 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_vp_desk = new BackgroundLayer(ui_viewport, ao_app); ui_vp_desk->setObjectName("ui_vp_desk"); + ui_vp_graphics = new AOGraphicsView(this); + ui_vp_video = new VideoScreen(ao_app); + ui_vp_graphics->scene()->addItem(ui_vp_video); + ui_vp_effect = new EffectLayer(this, ao_app); ui_vp_effect->setAttribute(Qt::WA_TransparentForMouseEvents); ui_vp_effect->setObjectName("ui_vp_effect"); @@ -878,6 +882,8 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() this->setWindowState(Qt::WindowState::WindowActive); }); + connect(ui_vp_video, &VideoScreen::finished, this, &Courtroom::video_finished); + set_widgets(); set_char_select(); } @@ -909,6 +915,7 @@ void Courtroom::update_audio_volume() sfx_player->set_volume(ui_sfx_slider->value() * remaining_percent); objection_player->set_volume(ui_sfx_slider->value() * remaining_percent); blip_player->set_volume(ui_blip_slider->value() * remaining_percent); + ui_vp_video->set_volume(ui_sfx_slider->value() * remaining_percent); } void Courtroom::set_courtroom_size() @@ -1021,6 +1028,8 @@ void Courtroom::set_widgets() ui_vp_pencil->move(26, 20); // ui_vp_pencil->move(45, 3); + ui_vp_graphics->move(ui_viewport->x(), ui_viewport->y()); + ui_vp_graphics->resize(ui_viewport->width(), ui_viewport->height()); ui_vp_background->move_and_center(0, 0); @@ -1959,6 +1968,7 @@ void Courtroom::enter_courtroom() objection_player->set_muted(false); sfx_player->set_muted(false); blip_player->set_muted(false); + ui_vp_video->set_muted(false); // Update the audio sliders update_audio_volume(); @@ -2425,6 +2435,7 @@ void Courtroom::on_chat_return_pressed() } } + // If the server we're on supports Looping SFX and Screenshake, use it if the // emote uses it. if (ao_app->looping_sfx_supported) { @@ -2503,6 +2514,11 @@ void Courtroom::on_chat_return_pressed() } } + // Server allows us to send video packet + if (ao_app->video_supported) { + packet_contents.append(ao_app->get_video_name(current_char, current_emote)); + } + ao_app->send_server_packet(new AOPacket("MS", packet_contents)); } @@ -2674,8 +2690,9 @@ void Courtroom::unpack_chatmessage(QStringList p_contents) ui_vp_evidence_display->reset(); // This chat msg is not objection so we're not waiting on the objection animation to finish to display the character. - if (!handle_objection()) - handle_ic_message(); + if (!handle_objection()) { + handle_video(); + } } void Courtroom::log_chatmessage(QString f_message, int f_char_id, QString f_showname, QString f_char, QString f_objection_mod, int f_evi_id, int f_color, LogMode f_log_mode, bool sender) @@ -2869,6 +2886,7 @@ bool Courtroom::handle_objection() break; m_chatmessage[EMOTE_MOD] = QChar(PREANIM); } + ui_vp_video->set_muted(true); ui_vp_objection->load_image( filename, m_chatmessage[CHAR_NAME], ao_app->get_chat(m_chatmessage[CHAR_NAME])); @@ -2876,13 +2894,17 @@ bool Courtroom::handle_objection() ui_vp_player_char->set_play_once(true); return true; } - if (m_chatmessage[EMOTE] != "") - display_character(); return false; } void Courtroom::display_character() { + // stop video playback without returning "finished" signal + ui_vp_video->stop(); + ui_vp_video->hide(); + // Return transparency + ui_vp_graphics->setBackgroundBrush(Qt::transparent); + // Stop all previously playing animations, effects etc. ui_vp_speedlines->hide(); ui_vp_player_char->stop(); @@ -3094,7 +3116,36 @@ void Courtroom::handle_emote_mod(int emote_mod, bool p_immediate) } } -void Courtroom::objection_done() { handle_ic_message(); } +void Courtroom::objection_done() { handle_video(); } + +void Courtroom::handle_video() +{ + if (ao_app->video_supported && !m_chatmessage[VIDEO].isEmpty()) + { + // Set chatbox visibility so it doesn't overlay the video + ui_vp_chatbox->setVisible(false); + // bye bye text too + ui_vp_message->setVisible(false); + // Black borders + ui_vp_graphics->setBackgroundBrush(Qt::black); + // Make sure the video isn't muted + ui_vp_video->set_muted(false); + ui_vp_video->play_character_video(m_chatmessage[CHAR_NAME], m_chatmessage[VIDEO]); + } + else + { + handle_ic_message(); + } +} + +void Courtroom::video_finished() +{ + ui_vp_video->set_muted(false); + ui_vp_video->hide(); + // Return transparency + ui_vp_graphics->setBackgroundBrush(Qt::transparent); + handle_ic_message(); +} void Courtroom::handle_ic_message() { @@ -6010,7 +6061,7 @@ void Courtroom::onTextChanged() QString text = ui_ic_chat_message->toPlainText(); QString emotion_number = QString::number(current_button_selected + 1); - if (!Options::getInstance().stopTypingIcon() /*&& ao_app->typing_timer_supported*/) { + if (!Options::getInstance().stopTypingIcon() && ao_app->typing_timer_supported) { if (text.isEmpty() && typingTimer->isActive()) { typingTimer->stop(); ao_app->send_server_packet(new AOPacket("TT", {"0", current_char, emotion_number})); @@ -6452,6 +6503,7 @@ void Courtroom::on_change_character_clicked() { sfx_player->set_muted(true); blip_player->set_muted(true); + ui_vp_video->set_muted(true); set_char_select(); @@ -6589,6 +6641,7 @@ void Courtroom::regenerate_ic_chatlog() } } + void Courtroom::on_set_dl_clicked() { QString relative_char = current_char; diff --git a/src/main.cpp b/src/main.cpp index 7fffa50cb..b42d569db 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,8 @@ #include "datatypes.h" #include "lobby.h" #include "networkmanager.h" +#include "mediatester.h" + #include #include #include @@ -70,6 +72,9 @@ int main(int argc, char *argv[]) appTranslator.load("ao_" + p_language, ":/resource/translations/"); main_app.installTranslator(&appTranslator); + MediaTester *l_media_tester = new MediaTester(&main_app); + QObject::connect(l_media_tester, SIGNAL(done()), l_media_tester, SLOT(deleteLater())); + main_app.construct_lobby(); main_app.net_manager->get_server_list(std::bind(&Lobby::list_servers, main_app.w_lobby)); main_app.net_manager->send_heartbeat(); diff --git a/src/mediatester.cpp b/src/mediatester.cpp new file mode 100644 index 000000000..68f923df1 --- /dev/null +++ b/src/mediatester.cpp @@ -0,0 +1,39 @@ +#include "mediatester.h" + +#include + +#include "debug_functions.h" + +MediaTester::MediaTester(QObject *parent) + : QObject(parent) +{ + m_player.setMuted(true); + + connect(&m_player, SIGNAL(mediaStatusChanged(QMediaPlayer::MediaStatus)), this, SLOT(p_check_status(QMediaPlayer::MediaStatus))); + + m_player.setMedia(QUrl("qrc:/resource/data_sample.avi")); +} + +MediaTester::~MediaTester() +{} + +void MediaTester::p_check_status(QMediaPlayer::MediaStatus p_status) +{ + switch (p_status) + { + case QMediaPlayer::InvalidMedia: + call_notice(tr("Your operating system appears to not support video playback!" + "

In order for videos to play properly, you " + "will need to install additional codecs, such as " + "
the K-Lite Basic Codec Pack.")); + emit done(); + break; + + case QMediaPlayer::LoadedMedia: + emit done(); + break; + + default: + break; + } +} diff --git a/src/packet_distribution.cpp b/src/packet_distribution.cpp index 73e6f6ac6..4db8ba341 100644 --- a/src/packet_distribution.cpp +++ b/src/packet_distribution.cpp @@ -50,6 +50,7 @@ void AOApplication::server_packet_received(AOPacket *p_packet) desk_mod_supported = false; evidence_supported = false; cccc_ic_supported = false; + video_supported = false; arup_supported = false; casing_alerts_supported = false; modcall_reason_supported = false; @@ -98,6 +99,7 @@ void AOApplication::server_packet_received(AOPacket *p_packet) desk_mod_supported = false; evidence_supported = false; cccc_ic_supported = false; + video_supported = false; arup_supported = false; casing_alerts_supported = false; modcall_reason_supported = false; @@ -144,6 +146,8 @@ void AOApplication::server_packet_received(AOPacket *p_packet) triplex_supported = true; if (f_packet.contains("typing_timer", Qt::CaseInsensitive)) typing_timer_supported = true; + if (f_packet.contains("video_support", Qt::CaseInsensitive)) + video_supported = true; log_to_demo = false; } else if (header == "PN") { diff --git a/src/text_file_functions.cpp b/src/text_file_functions.cpp index adac8d43a..a2d689408 100644 --- a/src/text_file_functions.cpp +++ b/src/text_file_functions.cpp @@ -703,6 +703,13 @@ QString AOApplication::get_sfx_looping(QString p_char, int p_emote) return f_result; } +QString AOApplication::get_video_name(QString p_char, int p_emote) +{ + QString f_result = + read_char_ini(p_char, QString::number(p_emote + 1), "videos"); + return f_result; +} + QString AOApplication::get_sfx_frame(QString p_char, QString p_emote, int n_frame) { diff --git a/src/video/videoscreen.cpp b/src/video/videoscreen.cpp new file mode 100644 index 000000000..153ecdb37 --- /dev/null +++ b/src/video/videoscreen.cpp @@ -0,0 +1,219 @@ +#include "video/videoscreen.h" + +#include +#include +#include +#include + +VideoScreen::VideoScreen(AOApplication *p_ao_app, QGraphicsItem *parent) + : QGraphicsVideoItem(parent) + , m_scanned(false) + , m_video_available(false) + , m_running(false) + , m_player(new QMediaPlayer(this, QMediaPlayer::LowLatency)) +{ + ao_app = p_ao_app; + m_player->setVideoOutput(this); + + connect(m_player, SIGNAL(videoAvailableChanged(bool)), this, SLOT(update_video_availability(bool))); + connect(m_player, SIGNAL(mediaStatusChanged(QMediaPlayer::MediaStatus)), this, SLOT(check_status(QMediaPlayer::MediaStatus))); + connect(m_player, SIGNAL(stateChanged(QMediaPlayer::State)), this, SLOT(check_state(QMediaPlayer::State))); + + update_audio_output(); +} + +VideoScreen::~VideoScreen() +{} + +QString VideoScreen::get_file_name() const +{ + return m_file_name; +} + +void VideoScreen::set_file_name(QString p_file_name) +{ + if (m_file_name == p_file_name) + { + return; + } + stop(); + qInfo() << "loading media file" << p_file_name; + m_scanned = false; + m_video_available = false; + m_file_name = p_file_name; + if (m_file_name.isEmpty()) + { + m_scanned = true; + } + m_player->setMedia(QUrl::fromLocalFile(m_file_name)); +} + +void VideoScreen::play_character_video(QString p_charname, QString p_video) +{ + QVector pathlist { // cursed character path resolution vector + ao_app->get_character_path( + p_charname, "videos/" + p_video), + ao_app->get_character_path( + p_charname, p_video), + VPath(p_video) // The path by itself after the above fail + }; + + const QString l_filepath = ao_app->get_asset_path(pathlist); + if (l_filepath.isEmpty()) + { + qWarning() << "error: no character media file" << p_charname << p_video; + finish_playback(); + return; + } + QString expand_override = + ao_app->read_design_ini("expand", l_filepath + ".ini"); + + setAspectRatioMode(Qt::KeepAspectRatio); + if (expand_override != "" && expand_override.startsWith("true")) { + setAspectRatioMode(Qt::KeepAspectRatioByExpanding); + } + + set_file_name(l_filepath); + play(); +} + +void VideoScreen::play() +{ + stop(); + m_running = true; + if (!m_scanned) + { + return; + } + if (!m_video_available) + { + finish_playback(); + return; + } + start_playback(); +} + +void VideoScreen::stop() +{ + m_running = false; + if (m_player->state() != QMediaPlayer::StoppedState) + { + m_player->stop(); + } +} + +void VideoScreen::update_video_availability(bool p_video_available) +{ + m_video_available = p_video_available; +} + +void VideoScreen::check_status(QMediaPlayer::MediaStatus p_status) +{ + if (m_running) + { + switch (p_status) + { + case QMediaPlayer::InvalidMedia: + m_scanned = true; + qWarning() << "error: media file is invalid:" << m_file_name; + finish_playback(); + break; + + case QMediaPlayer::NoMedia: + m_scanned = true; + finish_playback(); + break; + + case QMediaPlayer::LoadedMedia: + case QMediaPlayer::BufferedMedia: + m_scanned = true; + start_playback(); + break; + + default: + break; + } + } +} + +void VideoScreen::check_state(QMediaPlayer::State p_state) +{ + switch (p_state) + { + case QMediaPlayer::PlayingState: + emit started(); + break; + + case QMediaPlayer::StoppedState: + if (m_running) + { + finish_playback(); + } + break; + + default: + break; + } +} + +void VideoScreen::start_playback() +{ + this->show(); + if (m_player->state() == QMediaPlayer::StoppedState) + { + update_audio_output(); + m_player->play(); + } +} + +void VideoScreen::finish_playback() +{ + stop(); + emit finished(); +} + +void VideoScreen::update_audio_output() +{ + QMediaService *l_service = m_player->service(); + if (!l_service) + { + qWarning() << "error: missing media service, device unchanged"; + return; + } + + QAudioOutputSelectorControl *l_control = l_service->requestControl(); + if (!l_control) + { + qWarning() << "error: missing audio output control, device unchanged"; + } + else + { + const QStringList l_device_name_list = l_control->availableOutputs(); + for (const QString &i_device_name : l_device_name_list) + { + const QString l_device_description = l_control->outputDescription(i_device_name); + if (i_device_name == Options::getInstance().audioOutputDevice()) + { + l_control->setActiveOutput(i_device_name); + qDebug() << "Media player changed audio device to" << i_device_name; + break; + } + } + return; + } + l_service->releaseControl(l_control); +} + +void VideoScreen::set_volume(int p_value) +{ + if (m_player->volume() == p_value) + { + return; + } + m_player->setVolume(p_value); +} + +void VideoScreen::set_muted(bool p_toggle) +{ + m_player->setMuted(p_toggle); +}