Qt: Add qt video source class for more generic video playback

This commit is contained in:
Megamouse 2025-03-28 23:29:14 +01:00
parent fccb761ef2
commit b9e0a36816
14 changed files with 372 additions and 204 deletions

View file

@ -760,6 +760,7 @@
<ClInclude Include="..\Utilities\Timer.h" />
<ClInclude Include="util\types.hpp" />
<ClInclude Include="..\Utilities\version.h" />
<ClInclude Include="util\video_source.h" />
<ClInclude Include="util\vm.hpp" />
<ClInclude Include="util\asm.hpp" />
<ClInclude Include="Crypto\aes.h" />

View file

@ -2716,6 +2716,9 @@
<ClInclude Include="Emu\RSX\Program\FragmentProgramRegister.h">
<Filter>Emu\GPU\RSX\Program</Filter>
</ClInclude>
<ClInclude Include="util\video_source.h">
<Filter>Utilities</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<None Include="Emu\RSX\Program\GLSLSnippets\GPUDeswizzle.glsl">

View file

@ -807,6 +807,7 @@
<ClCompile Include="rpcs3qt\progress_indicator.cpp" />
<ClCompile Include="rpcs3qt\qt_camera_handler.cpp" />
<ClCompile Include="rpcs3qt\qt_music_handler.cpp" />
<ClCompile Include="rpcs3qt\qt_video_source.cpp" />
<ClCompile Include="rpcs3qt\raw_mouse_settings_dialog.cpp" />
<ClCompile Include="rpcs3qt\recvmessage_dialog_frame.cpp" />
<ClCompile Include="rpcs3qt\render_creator.cpp" />
@ -1514,6 +1515,7 @@
<Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWIN32_LEAN_AND_MEAN -DHAVE_VULKAN -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -DQT_MULTIMEDIAWIDGETS_LIB -DQT_SVG_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\SoundTouch\soundtouch\include" "-I.\..\3rdparty\cubeb\extra" "-I.\..\3rdparty\cubeb\cubeb\include" "-I.\..\3rdparty\flatbuffers\include" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia" "-I$(QTDIR)\include\QtMultimediaWidgets" "-I$(QTDIR)\include\QtSvg"</Command>
</CustomBuild>
<ClInclude Include="rpcs3qt\qt_video_source.h" />
<ClInclude Include="rpcs3qt\richtext_item_delegate.h" />
<CustomBuild Include="rpcs3qt\sendmessage_dialog_frame.h">
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>

View file

@ -196,6 +196,9 @@
<Filter Include="Gui\savestates">
<UniqueIdentifier>{9b51636c-b371-425b-86d3-be003774a1b7}</UniqueIdentifier>
</Filter>
<Filter Include="Io\video">
<UniqueIdentifier>{2bb5cec5-5acb-40c0-a388-68db05dff305}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="main.cpp">
@ -1176,6 +1179,9 @@
<ClCompile Include="rpcs3qt\gui_game_info.cpp">
<Filter>Gui\game list</Filter>
</ClCompile>
<ClCompile Include="rpcs3qt\qt_video_source.cpp">
<Filter>Io\video</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Input\ds4_pad_handler.h">
@ -1385,6 +1391,9 @@
<ClInclude Include="rpcs3qt\gui_game_info.h">
<Filter>Gui\game list</Filter>
</ClInclude>
<ClInclude Include="rpcs3qt\qt_video_source.h">
<Filter>Io\video</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<CustomBuild Include="debug\moc_predefs.h.cbt">

View file

@ -73,6 +73,7 @@ add_library(rpcs3_ui STATIC
qt_camera_video_sink.cpp
qt_music_handler.cpp
qt_utils.cpp
qt_video_source.cpp
raw_mouse_settings_dialog.cpp
register_editor_dialog.cpp
recvmessage_dialog_frame.cpp

View file

@ -33,7 +33,7 @@ void game_list_base::repaint_icons(std::vector<game_info>& game_data, const QCol
IconLoadFunction(game, device_pixel_ratio, cancel);
});
item->call_icon_func();
item->image_change_callback();
}
}
}

View file

@ -21,7 +21,7 @@ game_list_grid::game_list_grid()
connect(this, &game_list_grid::IconReady, this, [this](const movie_item_base* item)
{
if (item) item->call_icon_func();
if (item) item->image_change_callback();
}, Qt::QueuedConnection); // The default 'AutoConnection' doesn't seem to work in this specific case...
connect(this, &flow_widget::ItemSelectionChanged, this, [this](int index)
@ -81,7 +81,7 @@ void game_list_grid::populate(
item->setToolTip(tr("%0 [%1]").arg(title).arg(serial));
}
item->set_icon_func([this, item, game](const QVideoFrame& frame)
item->set_image_change_callback([this, item, game](const QVideoFrame& frame)
{
if (!item || !game)
{
@ -109,7 +109,7 @@ void game_list_grid::populate(
if (play_hover_movies && (game->has_hover_gif || game->has_hover_pam))
{
item->set_movie_path(QString::fromStdString(game->info.movie_path));
item->set_video_path(game->info.movie_path);
}
if (selected_item_id == game->info.path + game->info.icon_path)
@ -154,7 +154,7 @@ void game_list_grid::repaint_icons(std::vector<game_info>& game_data, const QCol
{
// We don't have an icon. Set a placeholder to initialize the layout.
game->pxmap = placeholder;
item->call_icon_func();
item->image_change_callback();
}
item->set_icon_load_func([this, game, device_pixel_ratio, cancel = item->icon_loading_aborted()](int)

View file

@ -54,7 +54,7 @@ game_list_table::game_list_table(game_list_frame* frame, std::shared_ptr<persist
connect(this, &game_list::IconReady, this, [this](const movie_item_base* item)
{
if (item) item->call_icon_func();
if (item) item->image_change_callback();
});
}
@ -242,7 +242,7 @@ void game_list_table::populate(
custom_table_widget_item* icon_item = new custom_table_widget_item;
game->item = icon_item;
icon_item->set_icon_func([this, icon_item, game](const QVideoFrame& frame)
icon_item->set_image_change_callback([this, icon_item, game](const QVideoFrame& frame)
{
if (!icon_item || !game)
{
@ -292,7 +292,7 @@ void game_list_table::populate(
if (play_hover_movies && (game->has_hover_gif || game->has_hover_pam))
{
icon_item->set_movie_path(QString::fromStdString(game->info.movie_path));
icon_item->set_video_path(game->info.movie_path);
}
icon_item->setData(Qt::UserRole, index, true);

View file

@ -1,24 +1,14 @@
#include "stdafx.h"
#include "movie_item_base.h"
#include <QFile>
movie_item_base::movie_item_base()
movie_item_base::movie_item_base() : qt_video_source()
{
init_pointers();
}
movie_item_base::~movie_item_base()
{
if (m_movie)
{
m_movie->stop();
}
if (m_media_player)
{
m_media_player->stop();
}
stop_movie();
wait_for_icon_loading(true);
wait_for_size_on_disk_loading(true);
@ -30,146 +20,6 @@ void movie_item_base::init_pointers()
m_size_on_disk_loading_aborted.reset(new atomic_t<bool>(false));
}
void movie_item_base::set_active(bool active)
{
if (!std::exchange(m_active, active) && active)
{
init_movie();
if (m_movie)
{
m_movie->jumpToFrame(1);
m_movie->start();
}
if (m_media_player)
{
m_media_player->play();
}
}
}
void movie_item_base::init_movie()
{
if (m_movie || m_media_player)
{
// Already initialized
return;
}
if (!m_icon_callback || m_movie_path.isEmpty() || !QFile::exists(m_movie_path))
{
m_movie_path.clear();
return;
}
const QString lower = m_movie_path.toLower();
if (lower.endsWith(".gif"))
{
m_movie.reset(new QMovie(m_movie_path));
m_movie_path.clear();
if (!m_movie->isValid())
{
m_movie.reset();
return;
}
QObject::connect(m_movie.get(), &QMovie::frameChanged, m_movie.get(), [this](int)
{
m_icon_callback({});
});
return;
}
if (lower.endsWith(".pam"))
{
// We can't set PAM files as source of the video player, so we have to feed them as raw data.
QFile file(m_movie_path);
if (!file.open(QFile::OpenModeFlag::ReadOnly))
{
return;
}
// TODO: Decode the pam properly before pushing it to the player
m_movie_data = file.readAll();
if (m_movie_data.isEmpty())
{
return;
}
m_movie_buffer.reset(new QBuffer(&m_movie_data));
m_movie_buffer->open(QIODevice::ReadOnly);
}
m_video_sink.reset(new QVideoSink());
QObject::connect(m_video_sink.get(), &QVideoSink::videoFrameChanged, m_video_sink.get(), [this](const QVideoFrame& frame)
{
m_icon_callback(frame);
});
m_media_player.reset(new QMediaPlayer());
m_media_player->setVideoSink(m_video_sink.get());
m_media_player->setLoops(QMediaPlayer::Infinite);
if (m_movie_buffer)
{
m_media_player->setSourceDevice(m_movie_buffer.get());
}
else
{
m_media_player->setSource(m_movie_path);
}
}
void movie_item_base::stop_movie()
{
if (m_movie)
{
m_movie->stop();
}
m_video_sink.reset();
m_media_player.reset();
m_movie_buffer.reset();
m_movie_data.clear();
}
QPixmap movie_item_base::get_movie_image(const QVideoFrame& frame) const
{
if (!m_active)
{
return {};
}
if (m_movie)
{
return m_movie->currentPixmap();
}
if (!frame.isValid())
{
return {};
}
// Get image. This usually also converts the image to ARGB32.
return QPixmap::fromImage(frame.toImage());
}
void movie_item_base::call_icon_func() const
{
if (m_icon_callback)
{
m_icon_callback({});
}
}
void movie_item_base::set_icon_func(const icon_callback_t& func)
{
m_icon_callback = func;
}
void movie_item_base::call_icon_load_func(int index)
{
if (!m_icon_load_callback || m_icon_loading || m_icon_loading_aborted->load())

View file

@ -1,25 +1,16 @@
#pragma once
#include "movie_item_base.h"
#include "util/atomic.hpp"
#include "Utilities/mutex.h"
#include "qt_video_source.h"
#include <QMovie>
#include <QThread>
#include <QBuffer>
#include <QMediaPlayer>
#include <QVideoSink>
#include <QVideoFrame>
#include <QPixmap>
#include <memory>
#include <functional>
using icon_callback_t = std::function<void(const QVideoFrame&)>;
using icon_load_callback_t = std::function<void(int)>;
using size_calc_callback_t = std::function<void()>;
class movie_item_base
class movie_item_base : public qt_video_source
{
public:
movie_item_base();
@ -27,25 +18,6 @@ public:
void init_pointers();
void set_active(bool active);
[[nodiscard]] bool get_active() const
{
return m_active;
}
void set_movie_path(QString path)
{
m_movie_path = std::move(path);
}
void init_movie();
void stop_movie();
QPixmap get_movie_image(const QVideoFrame& frame) const;
void call_icon_func() const;
void set_icon_func(const icon_callback_t& func);
void call_icon_load_func(int index);
void set_icon_load_func(const icon_load_callback_t& func);
@ -77,23 +49,13 @@ public:
shared_mutex pixmap_mutex;
protected:
QString m_movie_path;
QByteArray m_movie_data{};
std::unique_ptr<QBuffer> m_movie_buffer;
std::unique_ptr<QMediaPlayer> m_media_player;
std::shared_ptr<QVideoSink> m_video_sink;
std::shared_ptr<QMovie> m_movie;
private:
std::unique_ptr<QThread> m_icon_load_thread;
std::unique_ptr<QThread> m_size_calc_thread;
bool m_active = false;
atomic_t<bool> m_size_on_disk_loading = false;
atomic_t<bool> m_icon_loading = false;
size_calc_callback_t m_size_calc_callback = nullptr;
icon_load_callback_t m_icon_load_callback = nullptr;
icon_callback_t m_icon_callback = nullptr;
std::shared_ptr<atomic_t<bool>> m_icon_loading_aborted;
std::shared_ptr<atomic_t<bool>> m_size_on_disk_loading_aborted;

View file

@ -0,0 +1,244 @@
#include "stdafx.h"
#include "Emu/System.h"
#include "qt_video_source.h"
#include <QFile>
qt_video_source::qt_video_source()
: video_source()
{
}
qt_video_source::~qt_video_source()
{
stop_movie();
}
void qt_video_source::set_video_path(const std::string& path)
{
m_video_path = QString::fromStdString(path);
}
void qt_video_source::set_active(bool active)
{
if (!m_active.exchange(active) && active)
{
start_movie();
}
}
void qt_video_source::image_change_callback() const
{
if (m_image_change_callback)
{
m_image_change_callback({});
}
}
void qt_video_source::set_image_change_callback(const std::function<void(const QVideoFrame&)>& func)
{
m_image_change_callback = func;
}
void qt_video_source::init_movie()
{
if (m_movie || m_media_player)
{
// Already initialized
return;
}
if (!m_image_change_callback || m_video_path.isEmpty() || !QFile::exists(m_video_path))
{
m_video_path.clear();
return;
}
const QString lower = m_video_path.toLower();
if (lower.endsWith(".gif"))
{
m_movie.reset(new QMovie(m_video_path));
m_video_path.clear();
if (!m_movie->isValid())
{
m_movie.reset();
return;
}
QObject::connect(m_movie.get(), &QMovie::frameChanged, m_movie.get(), [this](int)
{
m_image_change_callback({});
m_has_new = true;
});
return;
}
if (lower.endsWith(".pam"))
{
// We can't set PAM files as source of the video player, so we have to feed them as raw data.
QFile file(m_video_path);
if (!file.open(QFile::OpenModeFlag::ReadOnly))
{
return;
}
// TODO: Decode the pam properly before pushing it to the player
m_video_data = file.readAll();
if (m_video_data.isEmpty())
{
return;
}
m_video_buffer.reset(new QBuffer(&m_video_data));
m_video_buffer->open(QIODevice::ReadOnly);
}
m_video_sink.reset(new QVideoSink());
QObject::connect(m_video_sink.get(), &QVideoSink::videoFrameChanged, m_video_sink.get(), [this](const QVideoFrame& frame)
{
m_image_change_callback(frame);
m_has_new = true;
});
m_media_player.reset(new QMediaPlayer());
m_media_player->setVideoSink(m_video_sink.get());
m_media_player->setLoops(QMediaPlayer::Infinite);
if (m_video_buffer)
{
m_media_player->setSourceDevice(m_video_buffer.get());
}
else
{
m_media_player->setSource(m_video_path);
}
}
void qt_video_source::start_movie()
{
init_movie();
if (m_movie)
{
m_movie->jumpToFrame(1);
m_movie->start();
}
if (m_media_player)
{
m_media_player->play();
}
m_active = true;
}
void qt_video_source::stop_movie()
{
m_active = false;
if (m_movie)
{
m_movie->stop();
}
m_video_sink.reset();
m_media_player.reset();
m_video_buffer.reset();
m_video_data.clear();
}
QPixmap qt_video_source::get_movie_image(const QVideoFrame& frame) const
{
if (!m_active)
{
return {};
}
if (m_movie)
{
return m_movie->currentPixmap();
}
if (!frame.isValid())
{
return {};
}
// Get image. This usually also converts the image to ARGB32.
return QPixmap::fromImage(frame.toImage());
}
void qt_video_source::get_image(std::vector<u8>& data, int& w, int& h, int& ch, int& bpp)
{
if (!m_has_new.exchange(false))
{
return;
}
std::lock_guard lock(m_image_mutex);
if (m_image.isNull())
{
w = h = ch = bpp = 0;
data.clear();
return;
}
w = m_image.width();
h = m_image.height();
ch = m_image.colorCount();
bpp = m_image.depth();
data.resize(m_image.height() * m_image.bytesPerLine());
std::memcpy(data.data(), m_image.constBits(), data.size());
}
qt_video_source_wrapper::~qt_video_source_wrapper()
{
Emu.BlockingCallFromMainThread([this]()
{
m_qt_video_source.reset();
});
}
void qt_video_source_wrapper::set_video_path(const std::string& path)
{
Emu.BlockingCallFromMainThread([this, &path]()
{
m_qt_video_source = std::make_unique<qt_video_source>();
m_qt_video_source->m_image_change_callback = [this](const QVideoFrame& frame)
{
std::lock_guard lock(m_qt_video_source->m_image_mutex);
if (m_qt_video_source->m_movie)
{
m_qt_video_source->m_image = m_qt_video_source->m_movie->currentImage();
}
else if (frame.isValid())
{
// Get image. This usually also converts the image to ARGB32.
m_qt_video_source->m_image = frame.toImage();
}
else
{
return;
}
if (m_qt_video_source->m_image.format() != QImage::Format_RGBA8888)
{
m_qt_video_source->m_image.convertTo(QImage::Format_RGBA8888);
}
};
m_qt_video_source->set_video_path(path);
m_qt_video_source->set_active(true);
});
}
void qt_video_source_wrapper::get_image(std::vector<u8>& data, int& w, int& h, int& ch, int& bpp)
{
ensure(m_qt_video_source);
m_qt_video_source->get_image(data, w, h, ch, bpp);
}

View file

@ -0,0 +1,76 @@
#pragma once
#include "util/video_source.h"
#include "util/atomic.hpp"
#include "Utilities/mutex.h"
#include <QMovie>
#include <QBuffer>
#include <QMediaPlayer>
#include <QVideoSink>
#include <QVideoFrame>
#include <QPixmap>
class qt_video_source : public video_source
{
public:
qt_video_source();
virtual ~qt_video_source();
void set_video_path(const std::string& path) override;
const QString& video_path() const { return m_video_path; }
void get_image(std::vector<u8>& data, int& w, int& h, int& ch, int& bpp) override;
bool has_new() const override { return m_has_new; }
void set_active(bool active);
[[nodiscard]] bool get_active() const
{
return m_active;
}
void start_movie();
void stop_movie();
QPixmap get_movie_image(const QVideoFrame& frame) const;
void image_change_callback() const;
void set_image_change_callback(const std::function<void(const QVideoFrame&)>& func);
protected:
void init_movie();
shared_mutex m_image_mutex;
atomic_t<bool> m_active = false;
atomic_t<bool> m_has_new = false;
QString m_video_path;
QByteArray m_video_data{};
QImage m_image{};
std::vector<u8> m_image_path;
std::unique_ptr<QBuffer> m_video_buffer;
std::unique_ptr<QMediaPlayer> m_media_player;
std::shared_ptr<QVideoSink> m_video_sink;
std::shared_ptr<QMovie> m_movie;
std::function<void(const QVideoFrame&)> m_image_change_callback = nullptr;
friend class qt_video_source_wrapper;
};
// Wrapper for emulator usage
class qt_video_source_wrapper : public video_source
{
public:
qt_video_source_wrapper() : video_source() {}
virtual ~qt_video_source_wrapper();
void set_video_path(const std::string& path) override;
void get_image(std::vector<u8>& data, int& w, int& h, int& ch, int& bpp) override;
bool has_new() const override { return m_qt_video_source && m_qt_video_source->has_new(); }
private:
std::unique_ptr<qt_video_source> m_qt_video_source;
};

View file

@ -208,7 +208,7 @@ void save_manager_dialog::Init()
if (movie_item* item = static_cast<movie_item*>(m_list->item(index, SaveColumns::Icon)))
{
item->setData(SaveUserRole::PixmapScaled, pixmap);
item->call_icon_func();
item->image_change_callback();
}
});
connect(search_bar, &QLineEdit::textChanged, this, &save_manager_dialog::text_changed);
@ -344,14 +344,14 @@ void save_manager_dialog::UpdateList()
if (const std::string movie_path = dir_path + localized_movie; fs::is_file(movie_path))
{
icon_item->set_movie_path(QString::fromStdString(movie_path));
icon_item->set_video_path(movie_path);
}
else if (const std::string movie_path = dir_path + "ICON1.PAM"; fs::is_file(movie_path))
{
icon_item->set_movie_path(QString::fromStdString(movie_path));
icon_item->set_video_path(movie_path);
}
icon_item->set_icon_func([this, icon_item](const QVideoFrame& frame)
icon_item->set_image_change_callback([this, icon_item](const QVideoFrame& frame)
{
if (!icon_item)
{

20
rpcs3/util/video_source.h Normal file
View file

@ -0,0 +1,20 @@
#pragma once
#include "types.hpp"
class video_source
{
public:
video_source() {};
virtual ~video_source() {};
virtual void set_video_path(const std::string& path) { static_cast<void>(path); }
virtual bool has_new() const { return false; };
virtual void get_image(std::vector<u8>& data, int& w, int& h, int& ch, int& bpp)
{
static_cast<void>(data);
static_cast<void>(w);
static_cast<void>(h);
static_cast<void>(ch);
static_cast<void>(bpp);
}
};