diff --git a/src/PreCompiledHeaders.hh b/src/PreCompiledHeaders.hh index b8aec3cf8594b5e49afe13880268536dabada109..e39b970f195aa0697806625c68e465f0efb5d653 100644 --- a/src/PreCompiledHeaders.hh +++ b/src/PreCompiledHeaders.hh @@ -30,6 +30,7 @@ /* EXT_INC_PRIVATE */ #include <mpv/client.h> +#include <mpv/render_gl.h> /* QT_STRUCT_INC */ #include <qglobal.h> @@ -65,6 +66,7 @@ #include <QJsonArray> #include <QJsonDocument> #include <QJsonValue> +#include <QOpenGLWidget> /* QT_WIDGET_INC */ #include <QCoreApplication> diff --git a/src/UI/DocumentViews/MpvContainer.cc b/src/UI/DocumentViews/MpvContainer.cc index f52c629eedc74d2813b8790ca38047e7b433cef9..104463b87d8c8216ea6a32147b775f1b94634226 100644 --- a/src/UI/DocumentViews/MpvContainer.cc +++ b/src/UI/DocumentViews/MpvContainer.cc @@ -1,18 +1,31 @@ #include "PreCompiledHeaders.hh" #include "UI/DocumentViews/MpvContainer.hh" +#include <QtGui/QOpenGLContext> +#include <QtCore/QMetaObject> +#include <mpv/client.h> +#include <mpv/render_gl.h> using namespace Vivy; using namespace std::string_literals; +static void * +get_proc_address(void *ctx, const char *name) +{ + QOpenGLContext *glctx = QOpenGLContext::currentContext(); + if (!glctx) + return nullptr; + return reinterpret_cast<void *>(glctx->getProcAddress(QByteArray(name))); +} + void MpvContainer::mpvEventWakeUpCB(void *user) noexcept { - MpvContainer *container = reinterpret_cast<MpvContainer *>(user); - emit container->mpvEvent(); + QMetaObject::invokeMethod(reinterpret_cast<MpvContainer *>(user), "mpvEvent", + Qt::QueuedConnection); } MpvContainer::MpvContainer(QWidget *parent) - : QWidget(parent) + : QOpenGLWidget(parent) , mpv(mpv_create()) { if (mpv == nullptr) @@ -21,7 +34,7 @@ MpvContainer::MpvContainer(QWidget *parent) setAttribute(Qt::WA_DontCreateNativeAncestors); setAttribute(Qt::WA_NativeWindow); - logWarning() << "Don't init MPV by default, you should use the reload button!"; + initializeMpv(); } void @@ -31,8 +44,6 @@ MpvContainer::initializeMpv() throw std::logic_error("MPV is already initialized!"); isMpvAlreadyInitialized = true; - quint64 wid = winId(); - mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &wid); mpv_set_option_string(mpv, "idle", "yes"); mpv_set_option_string(mpv, "loop-file", "inf"); mpv_set_option_string(mpv, "no-config", "yes"); @@ -56,14 +67,42 @@ MpvContainer::initializeMpv() } void -MpvContainer::reCreateMpvContext() +MpvContainer::initializeGL() { - closeMpv(); - mpv = mpv_create(); - if (mpv == nullptr) - throw std::runtime_error("Failed to create the MPV context"); - initializeMpv(); - loadFile(previousLoadedFile); + mpv_opengl_init_params gl_init_params; + memset(&gl_init_params, 0, sizeof(mpv_opengl_init_params)); + gl_init_params.get_proc_address = get_proc_address; + mpv_render_param params[]{ { MPV_RENDER_PARAM_API_TYPE, + const_cast<char *>(MPV_RENDER_API_TYPE_OPENGL) }, + { MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params }, + { MPV_RENDER_PARAM_INVALID, nullptr } }; + + if (mpv_render_context_create(&mpv_gl, mpv, params) < 0) { + throw std::runtime_error("failed to initialize mpv GL context"); + } else { + mpv_render_context_set_update_callback(mpv_gl, MpvContainer::onUpdate, + reinterpret_cast<void *>(this)); + } +} + +void +MpvContainer::paintGL() noexcept +{ + mpv_opengl_fbo mpfbo{ static_cast<int>(defaultFramebufferObject()), width(), height(), 0 }; + int flip_y{ 1 }; + + mpv_render_param params[] = { { MPV_RENDER_PARAM_OPENGL_FBO, &mpfbo }, + { MPV_RENDER_PARAM_FLIP_Y, &flip_y }, + { MPV_RENDER_PARAM_INVALID, nullptr } }; + // See render_gl.h on what OpenGL environment mpv expects, and + // other API details. + mpv_render_context_render(mpv_gl, params); +} + +void +MpvContainer::onUpdate(void *ctx) noexcept +{ + QMetaObject::invokeMethod(reinterpret_cast<MpvContainer *>(ctx), "maybeUpdate"); } void @@ -82,24 +121,24 @@ MpvContainer::registerMpvDurationCallback(std::function<void(double)> callback) mpvDurationCallback = callback; } -void -MpvContainer::closeMpv() noexcept +MpvContainer::~MpvContainer() noexcept { + makeCurrent(); + logDebug() << "Closing the MPV context"; + if (mpv_gl) { + mpv_render_context_free(mpv_gl); + } if (mpv) { - logDebug() << "Closing the MPV context"; asyncCommand(AsyncCmdType::None, { "quit", nullptr }); + mpv_wait_async_requests(mpv); registerMpvTimeCallback(nullptr); registerMpvDurationCallback(nullptr); - asyncCommand(AsyncCmdType::None, { "quit", nullptr }); - mpv_wait_async_requests(mpv); mpv_destroy(mpv); mpv = nullptr; // Stop all other callbacks here isMpvAlreadyInitialized = false; // De-init } } -MpvContainer::~MpvContainer() noexcept { closeMpv(); } - void MpvContainer::handleMpvEvent(const mpv_event *const event) noexcept { @@ -117,7 +156,7 @@ MpvContainer::handleMpvEvent(const mpv_event *const event) noexcept }; switch (event->event_id) { - case MPV_EVENT_SHUTDOWN: closeMpv(); break; + case MPV_EVENT_SHUTDOWN: logWarning() << "Ignore shutdown event for now..."; break; case MPV_EVENT_LOG_MESSAGE: msg = reinterpret_cast<mpv_event_log_message *>(event->data); @@ -233,6 +272,27 @@ MpvContainer::onMpvEvent() noexcept } } +// Make Qt invoke mpv_render_context_render() to draw a new/updated video frame. +void +MpvContainer::maybeUpdate() noexcept +{ + // If the Qt window is not visible, Qt's update() will just skip rendering. + // This confuses mpv's render API, and may lead to small occasional + // freezes due to video rendering timing out. + // Handle this by manually redrawing. + // Note: Qt doesn't seem to provide a way to query whether update() will + // be skipped, and the following code still fails when e.g. switching + // to a different workspace with a reparenting window manager. + if (window()->isMinimized()) { + makeCurrent(); + paintGL(); + context()->swapBuffers(context()->surface()); + doneCurrent(); + } else { + update(); + } +} + void MpvContainer::loadFile(const QString &filename) noexcept { @@ -268,7 +328,7 @@ MpvContainer::printMpvError(int rc) const noexcept if (rc == MPV_ERROR_SUCCESS) return; - logError() << "MPV error:" << mpv_error_string(rc); + logError() << "MPV error: " << mpv_error_string(rc); } void @@ -288,10 +348,6 @@ MpvContainer::mpvPause() noexcept void MpvContainer::mpvTogglePlayback() noexcept { - if (!isMpvAlreadyInitialized) { - reCreateMpvContext(); - } - logDebug() << "MPV: Toggling the playback"; asyncCommand(AsyncCmdType::TogglePlayback, { "cycle", "pause", "up", nullptr }); } diff --git a/src/UI/DocumentViews/MpvContainer.hh b/src/UI/DocumentViews/MpvContainer.hh index b83e6f9d65ccb9c6b4ecdd552da7982516e45a39..1649ede2c47e6abd91d4ae79b0a1208f9c04d450 100644 --- a/src/UI/DocumentViews/MpvContainer.hh +++ b/src/UI/DocumentViews/MpvContainer.hh @@ -9,14 +9,9 @@ #include "Lib/Utils.hh" #include "Lib/Log.hh" -extern "C" { -struct mpv_handle; -struct mpv_event; -} - namespace Vivy { -class MpvContainer final : public QWidget { +class MpvContainer final : public QOpenGLWidget { Q_OBJECT VIVY_UNMOVABLE_OBJECT(MpvContainer) VIVY_APP_LOGGABLE_OBJECT(MpvContainer, logger) @@ -49,19 +44,22 @@ public: private: void initializeMpv(); void handleMpvEvent(const mpv_event *const) noexcept; - void closeMpv() noexcept; void printMpvError(int) const noexcept; void asyncCommand(const AsyncCmdType, std::initializer_list<const char *>) noexcept; void unloadAssFile() noexcept; void handleMpvEventCommandReply(const AsyncCmdType) noexcept; int getAssSid() const noexcept; - // Must be static to be passed as a function ptr + void initializeGL() override; + void paintGL() noexcept override; + + static void onUpdate(void *) noexcept; static void mpvEventWakeUpCB(void *) noexcept; bool isPlaybackPaused{ true }; bool isMpvAlreadyInitialized{ false }; - mpv_handle *volatile mpv{ nullptr }; + mpv_handle *mpv{ nullptr }; + mpv_render_context *mpv_gl{ nullptr }; qint64 sid{ -1 }; QString previousLoadedFile{}; std::function<void(double)> mpvTimeCallback{ nullptr }; @@ -71,7 +69,7 @@ public slots: void mpvPlay() noexcept; void mpvPause() noexcept; void mpvTogglePlayback() noexcept; - void reCreateMpvContext(); + void maybeUpdate() noexcept; private slots: void onMpvEvent() noexcept;