#include "mpvwidget.hh" #include <stdexcept> #include <QtGui/QOpenGLContext> #include <QtCore/QMetaObject> #include <QApplication> #include <QKeyEvent> #include <QString> #include "qthelper.hh" #include "../mpv.h" #include <lektor/mkv.h> PRIVATE_FUNCTION void wakeup(void *ctx) { QMetaObject::invokeMethod(static_cast<MpvWidget *>(ctx), "on_mpv_events", Qt::QueuedConnection); } PRIVATE_FUNCTION void * get_proc_address(void *ctx, const char *name) { Q_UNUSED(ctx); QOpenGLContext *glctx = QOpenGLContext::currentContext(); if (!glctx) return nullptr; return reinterpret_cast<void *>(glctx->getProcAddress(QByteArray(name))); } MpvWidget::MpvWidget(queue *queue, lkt_db *db, module_reg *reg, bool *launched, QWidget *parent) : QOpenGLWidget(parent) , m_queue(queue) , m_db(db) , m_reg(reg) , m_launched(launched) { mpv = mpv_create(); if (!mpv) throw std::runtime_error("could not create mpv context"); setFocusPolicy(Qt::StrongFocus); mpv_set_option_string(mpv, "osc", "yes"); if (mpv_initialize(mpv) < 0) throw std::runtime_error("could not initialize mpv context"); // Request hw decoding, just for testing. mpv_set_option_string(mpv, "hwdec", "auto"); mpv_observe_property(mpv, 0, "duration", MPV_FORMAT_DOUBLE); mpv_observe_property(mpv, 0, "time-pos", MPV_FORMAT_DOUBLE); mpv_observe_property(mpv, 0, "pause", MPV_FORMAT_FLAG); mpv_observe_property(mpv, 0, "unpause", MPV_FORMAT_FLAG); mpv_observe_property(mpv, 0, "idle-active", MPV_FORMAT_FLAG); mpv_set_wakeup_callback(mpv, wakeup, this); } MpvWidget::~MpvWidget() { makeCurrent(); if (mpv_gl) mpv_render_context_free(mpv_gl); mpv_terminate_destroy(mpv); } void MpvWidget::command(const QVariant ¶ms) { mpv::qt::command(mpv, params); } void MpvWidget::setProperty(const QString &name, const QVariant &value) { mpv::qt::set_property(mpv, name, value); } QVariant MpvWidget::getProperty(const QString &name) const { return mpv::qt::get_property(mpv, name); } void MpvWidget::initializeGL() { mpv_opengl_init_params gl_init_params{ get_proc_address, nullptr, nullptr }; 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"); mpv_render_context_set_update_callback(mpv_gl, MpvWidget::on_update, reinterpret_cast<void *>(this)); } void MpvWidget::paintGL() { 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 MpvWidget::on_mpv_events() { // Process all events, until the event queue is empty. while (mpv) { mpv_event *event = mpv_wait_event(mpv, 0); if (event->event_id == MPV_EVENT_NONE) { break; } handle_mpv_event(event); } } void MpvWidget::handle_mpv_event(mpv_event *event) { size_t ao_volume; mpv_event_property *prop; (void)ao_volume; (void)prop; switch (event->event_id) { case MPV_EVENT_SHUTDOWN: lkt_queue_send(m_queue, LKT_EVENT_PLAY_TOGGLE, LKT_PLAY_STOP); *m_launched = false; reg_call(m_reg, "close", 1); break; case MPV_EVENT_START_FILE: m_inhib = false; LOG_DEBUG("WINDOW", "Start of file!"); break; case MPV_EVENT_END_FILE: LOG_DEBUG("WINDOW", "End of file!"); if (!m_inhib && m_state != STOP) lkt_queue_send(m_queue, LKT_EVENT_PLAY_NEXT, nullptr); break; case MPV_EVENT_PROPERTY_CHANGE: { prop = static_cast<mpv_event_property *>(event->data); if (strcmp(prop->name, "time-pos") == 0) { if (prop->format == MPV_FORMAT_DOUBLE) { m_position = static_cast<int>(*reinterpret_cast<double *>(prop->data)); } } else if (strcmp(prop->name, "duration") == 0) { if (prop->format == MPV_FORMAT_DOUBLE) { m_duration = static_cast<int>(*reinterpret_cast<double *>(prop->data)); } } else if (strcmp(prop->name, "pause") == 0) { LOG_DEBUG("WINDOW", "Detected pause"); if (prop->format == MPV_FORMAT_FLAG) { if (*static_cast<int*>(prop->data)) goto apply_pause; else goto apply_unpause; } } else if (strcmp(prop->name, "unpause") == 0) { LOG_DEBUG("WINDOW", "Detected unpause"); if (prop->format == MPV_FORMAT_FLAG) { if (*static_cast<int*>(prop->data)) goto apply_unpause; else goto apply_pause; } } else if (strcmp(prop->name, "idle-active") == 0) { LOG_DEBUG("WINDOW", "Detected idle"); if (prop->format == MPV_FORMAT_FLAG && *static_cast<int*>(prop->data)) { LOG_DEBUG("WINDOW", "Applying idle"); lkt_queue_make_available(m_queue, static_cast<LKT_EVENT_TYPE>(LKT_EVENT_PLAY)); lkt_queue_make_available(m_queue, static_cast<LKT_EVENT_TYPE>(LKT_EVENT_PROP)); *m_launched = true; emit titleChanged("[Lektord] Stopped"); } } break; apply_pause: LOG_DEBUG("WINDOW", "Applying pause"); lkt_queue_send(m_queue, LKT_EVENT_PLAY_TOGGLE, LKT_PLAY_PAUSE); break; apply_unpause: LOG_DEBUG("WINDOW", "Applying unpause"); lkt_queue_send(m_queue, LKT_EVENT_PLAY_TOGGLE, LKT_PLAY_PLAY); break; } case MPV_EVENT_LOG_MESSAGE: case MPV_EVENT_GET_PROPERTY_REPLY: case MPV_EVENT_SET_PROPERTY_REPLY: case MPV_EVENT_NONE: case MPV_EVENT_COMMAND_REPLY: case MPV_EVENT_FILE_LOADED: case MPV_EVENT_CLIENT_MESSAGE: case MPV_EVENT_VIDEO_RECONFIG: case MPV_EVENT_AUDIO_RECONFIG: case MPV_EVENT_SEEK: case MPV_EVENT_PLAYBACK_RESTART: case MPV_EVENT_QUEUE_OVERFLOW: case MPV_EVENT_HOOK: break; default: LOG_ERROR("WINDOW", "Should not get event %s (id is %d)", mpv_event_name(event->event_id), event->event_id ); break; } } // Make Qt invoke mpv_render_context_render() to draw a new/updated video frame. void MpvWidget::maybeUpdate() { // 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 MpvWidget::on_update(void *ctx) { QMetaObject::invokeMethod(static_cast<MpvWidget *>(ctx), "maybeUpdate"); } bool MpvWidget::get_elapsed(int UNUSED *elapsed_sec) { *elapsed_sec = m_position; return true; } bool MpvWidget::get_duration(int UNUSED *dur_sec) { *dur_sec = m_duration; return true; } bool MpvWidget::set_paused(int paused) { const char *cmd[] = { "set", "pause", paused == 1 ? "yes" : "no", nullptr }; mpv_command_async(mpv, 0, cmd); return true; } bool MpvWidget::set_volume(int UNUSED vol) { return true; } bool MpvWidget::set_position(int sec) { return lmpv_set_position(mpv, sec); } bool MpvWidget::load_file(const char *filepath) { const bool ret = !lmpv_load_file(mpv, filepath); m_inhib = true; if (ret) { LOG_DEBUG("WINDOW", "Loaded file: %s", filepath); m_state = NONSTOPPED; update_window_title(); } else { LOG_ERROR("WINDOW", "Failed to load kara with path: %s", filepath); } return ret; } void MpvWidget::update_window_title() { kara_metadata kara_mdt; int changed_kara = 0; char *kara_title = nullptr; char window_title[LKT_LINE_MAX]; if (database_queue_current_kara(m_db, &kara_mdt, &changed_kara)) { mdtcat(&kara_mdt, &kara_title); safe_snprintf(window_title, LKT_LINE_MAX, "[Lektord] %d: %s", changed_kara, kara_title); LOG_DEBUG("WINDOW", "Set window title to: %s", window_title); titleChanged(QString::fromLocal8Bit(window_title)); free(kara_title); } else { LOG_ERROR("WINDOW", "Failed to get current kara, can't change window title"); } } bool MpvWidget::toggle_pause() { const char *cmd[] = { "cycle", "pause", nullptr }; mpv_command_async(mpv, 0, cmd); return true; } bool MpvWidget::stop() { m_state = STOP; const char *cmd[] = { "stop", nullptr }; mpv_command_async(mpv, 0, cmd); emit titleChanged("[Lektord] Stopped"); return true; } #define MPV_SEND_COMMAND_ASYNC(...) \ { \ const char *cmd[] = { __VA_ARGS__ }; \ mpv_command_async(mpv, 0, cmd); \ break; \ } void MpvWidget::keyPressEvent(QKeyEvent *event) { if (m_state == STOP) return QOpenGLWidget::keyPressEvent(event); switch (event->modifiers()) { /* SHIFTED */ case Qt::ShiftModifier: switch (event->key()) { case Qt::Key_J: MPV_SEND_COMMAND_ASYNC("osd-msg", "cycle", "sub", nullptr); case Qt::Key_Period: MPV_SEND_COMMAND_ASYNC("osd-msg", "frame-step", nullptr); case Qt::Key_Z: MPV_SEND_COMMAND_ASYNC("osd-msg", "add", "sub-delay", "+0.1", nullptr); case Qt::Key_G: MPV_SEND_COMMAND_ASYNC("osd-msg", "add", "sub-scale", "+0.1", nullptr); case Qt::Key_F: MPV_SEND_COMMAND_ASYNC("osd-msg", "add", "sub-scale", "-0.1", nullptr); } break; /* UN-SHIFTED */ default: switch (event->key()) { /* Playback */ case Qt::Key_Space: lmpv_toggle_pause(mpv); //lkt_queue_send(m_queue, LKT_EVENT_PLAY_TOGGLE, LKT_PLAY_TOGGLE); break; case Qt::Key_Return: case Qt::Key_Greater: lkt_queue_send(m_queue, LKT_EVENT_PLAY_NEXT, nullptr); break; case Qt::Key_Less: lkt_queue_send(m_queue, LKT_EVENT_PLAY_PREV, nullptr); break; case Qt::Key_Left: MPV_SEND_COMMAND_ASYNC("osd-msg-bar", "seek", "-5", "relative", nullptr); case Qt::Key_Right: MPV_SEND_COMMAND_ASYNC("osd-msg-bar", "seek", "+5", "relative", nullptr); case Qt::Key_Down: MPV_SEND_COMMAND_ASYNC("osd-msg-bar", "seek", "-60", "relative", nullptr); case Qt::Key_Up: MPV_SEND_COMMAND_ASYNC("osd-msg-bar", "seek", "+60", "relative", nullptr); case Qt::Key_L: MPV_SEND_COMMAND_ASYNC("osd-msg", "ab-loop", nullptr); case Qt::Key_O: MPV_SEND_COMMAND_ASYNC("osd-msg-bar", "show-progress", nullptr); case Qt::Key_BracketLeft: MPV_SEND_COMMAND_ASYNC("osd-msg", "multiply", "speed", "1/1.1", nullptr); case Qt::Key_BracketRight: MPV_SEND_COMMAND_ASYNC("osd-msg", "multiply", "speed", "1.1", nullptr); case Qt::Key_BraceLeft: MPV_SEND_COMMAND_ASYNC("osd-msg", "multiply", "speed", "0.5", nullptr); case Qt::Key_BraceRight: MPV_SEND_COMMAND_ASYNC("osd-msg", "multiply", "speed", "2", nullptr); case Qt::Key_Backspace: MPV_SEND_COMMAND_ASYNC("osd-msg", "set", "speed", "1.0", nullptr); case Qt::Key_Semicolon: MPV_SEND_COMMAND_ASYNC("osd-msg", "frame-step", nullptr); case Qt::Key_Comma: MPV_SEND_COMMAND_ASYNC("osd-msg", "frame-back-step", nullptr); /* Track management */ case Qt::Key_NumberSign: MPV_SEND_COMMAND_ASYNC("osd-msg", "cycle", "audio", nullptr); case Qt::Key_J: MPV_SEND_COMMAND_ASYNC("osd-msg", "cycle", "sub", "down", nullptr); case Qt::Key_Underscore: MPV_SEND_COMMAND_ASYNC("osd-msg", "cycle", "video", nullptr); /* Misc */ case Qt::Key_I: MPV_SEND_COMMAND_ASYNC("script-binding", "stats/display-stats", nullptr); case Qt::Key_Delete: MPV_SEND_COMMAND_ASYNC("script-message", "osc-visibility", (m_oscVisible = !m_oscVisible) ? "always" : "never", nullptr); case Qt::Key_Z: MPV_SEND_COMMAND_ASYNC("osd-msg", "add", "sub-delay", "-0.1", nullptr); case Qt::Key_M: MPV_SEND_COMMAND_ASYNC("osd-msg", "cycle", "mute", nullptr); default: break; } return QOpenGLWidget::keyPressEvent(event); } }