File indexing completed on 2024-05-12 16:28:12

0001 // SPDX-FileCopyrightText: 2019 Linus Jahn <lnj@kaidan.im>
0002 // SPDX-FileCopyrightText: 2022 Devin Lin <devin@kde.org>
0003 // SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
0004 //
0005 // SPDX-License-Identifier: GPL-3.0-or-later
0006 
0007 #include "mpvplayer.h"
0008 
0009 #include <QOpenGLContext>
0010 #include <QOpenGLFramebufferObject>
0011 #include <QQuickWindow>
0012 #include <QStandardPaths>
0013 #include <stdexcept>
0014 
0015 namespace
0016 {
0017 void on_mpv_events(void *ctx)
0018 {
0019     QMetaObject::invokeMethod((MpvPlayer *)ctx, "onMpvEvents", Qt::QueuedConnection);
0020 }
0021 
0022 void on_mpv_redraw(void *ctx)
0023 {
0024     MpvPlayer::on_update(ctx);
0025 }
0026 
0027 void *get_proc_address_mpv(void *ctx, const char *name)
0028 {
0029     Q_UNUSED(ctx)
0030 
0031     QOpenGLContext *glctx = QOpenGLContext::currentContext();
0032     if (!glctx)
0033         return nullptr;
0034 
0035     return reinterpret_cast<void *>(glctx->getProcAddress(QByteArray(name)));
0036 }
0037 
0038 }
0039 
0040 class MpvRenderer : public QQuickFramebufferObject::Renderer
0041 {
0042 public:
0043     explicit MpvRenderer(MpvPlayer *new_obj)
0044         : obj{new_obj}
0045     {
0046     }
0047 
0048     // This function is called when a new FBO is needed.
0049     // This happens on the initial frame.
0050     QOpenGLFramebufferObject *createFramebufferObject(const QSize &size) override
0051     {
0052         // init mpv_gl:
0053         if (!obj->mpv_gl) {
0054             mpv_opengl_init_params gl_init_params{get_proc_address_mpv, nullptr};
0055             mpv_render_param params[]{{MPV_RENDER_PARAM_API_TYPE, const_cast<char *>(MPV_RENDER_API_TYPE_OPENGL)},
0056                                       {MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params},
0057                                       {MPV_RENDER_PARAM_INVALID, nullptr}};
0058 
0059             if (mpv_render_context_create(&obj->mpv_gl, obj->mpv, params) < 0)
0060                 throw std::runtime_error("failed to initialize mpv GL context");
0061             mpv_render_context_set_update_callback(obj->mpv_gl, on_mpv_redraw, obj);
0062         }
0063 
0064         return QQuickFramebufferObject::Renderer::createFramebufferObject(size);
0065     }
0066 
0067     void render() override
0068     {
0069 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0070         if (obj->window() != nullptr) {
0071             obj->window()->resetOpenGLState();
0072         }
0073 #endif
0074 
0075         QOpenGLFramebufferObject *fbo = framebufferObject();
0076         mpv_opengl_fbo mpfbo;
0077         mpfbo.fbo = static_cast<int>(fbo->handle());
0078         mpfbo.w = fbo->width();
0079         mpfbo.h = fbo->height();
0080         mpfbo.internal_format = 0;
0081 
0082         int flip_y = 0;
0083 
0084         mpv_render_param params[] = {// Specify the default framebuffer (0) as target. This will
0085                                      // render onto the entire screen. If you want to show the video
0086                                      // in a smaller rectangle or apply fancy transformations, you'll
0087                                      // need to render into a separate FBO and draw it manually.
0088                                      {MPV_RENDER_PARAM_OPENGL_FBO, &mpfbo},
0089                                      // Flip rendering (needed due to flipped GL coordinate system).
0090                                      {MPV_RENDER_PARAM_FLIP_Y, &flip_y},
0091                                      {MPV_RENDER_PARAM_INVALID, nullptr}};
0092         // See render_gl.h on what OpenGL environment mpv expects, and
0093         // other API details.
0094         mpv_render_context_render(obj->mpv_gl, params);
0095 
0096 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0097         if (obj->window() != nullptr) {
0098             obj->window()->resetOpenGLState();
0099         }
0100 #endif
0101     }
0102 
0103 private:
0104     MpvPlayer *obj = nullptr;
0105 };
0106 
0107 MpvPlayer::MpvPlayer(QQuickItem *parent)
0108     : QQuickFramebufferObject(parent)
0109     , mpv{mpv_create()}
0110     , mpv_gl(nullptr)
0111 {
0112     if (!mpv)
0113         throw std::runtime_error("could not create mpv context");
0114 
0115     // enable console output
0116     setProperty("terminal", "yes");
0117 
0118     // don't load user scripts or configs
0119     setProperty("config", "no");
0120     setProperty("load-scripts", "no");
0121 
0122     // force vo to libmpv (which it should be set to anyway)
0123     setProperty("vo", "libmpv");
0124 
0125     // use safe hardware acceleration
0126     setProperty("hwdec", "auto-safe");
0127 
0128     // disable OSD and fonts
0129     setProperty("osd-level", "0");
0130     setProperty("embeddedfonts", "no");
0131 
0132     // disable input
0133     setProperty("input-builtin-bindings", "no");
0134     setProperty("input-default-bindings", "no");
0135     setProperty("input-vo-keyboard", "no");
0136 
0137     if (mpv_initialize(mpv) < 0)
0138         throw std::runtime_error("could not initialize mpv context");
0139 
0140     mpv_observe_property(mpv, 0, "duration", MPV_FORMAT_DOUBLE);
0141     mpv_observe_property(mpv, 0, "time-pos", MPV_FORMAT_DOUBLE);
0142     mpv_observe_property(mpv, 0, "pause", MPV_FORMAT_FLAG);
0143 
0144     mpv_set_wakeup_callback(mpv, on_mpv_events, this);
0145 
0146     connect(this, &MpvPlayer::onUpdate, this, &MpvPlayer::doUpdate, Qt::QueuedConnection);
0147 }
0148 
0149 MpvPlayer::~MpvPlayer()
0150 {
0151     if (mpv_gl) // only initialized if something got drawn
0152     {
0153         mpv_render_context_free(mpv_gl);
0154     }
0155 
0156     mpv_terminate_destroy(mpv);
0157 }
0158 
0159 qreal MpvPlayer::position() const
0160 {
0161     return m_position;
0162 }
0163 
0164 qreal MpvPlayer::duration() const
0165 {
0166     return m_duration;
0167 }
0168 
0169 bool MpvPlayer::paused() const
0170 {
0171     return m_paused;
0172 }
0173 
0174 QSize MpvPlayer::sourceSize() const
0175 {
0176     return m_sourceSize;
0177 }
0178 
0179 QString MpvPlayer::source() const
0180 {
0181     return m_source;
0182 }
0183 
0184 bool MpvPlayer::loading() const
0185 {
0186     return m_loading;
0187 }
0188 
0189 bool MpvPlayer::looping() const
0190 {
0191     return m_looping;
0192 }
0193 
0194 bool MpvPlayer::autoPlay() const
0195 {
0196     return m_autoPlay;
0197 }
0198 
0199 bool MpvPlayer::stopped() const
0200 {
0201     return m_currentSource.isEmpty();
0202 }
0203 
0204 void MpvPlayer::setSource(const QString &source)
0205 {
0206     if (m_source == source) {
0207         return;
0208     }
0209 
0210     m_source = source;
0211 
0212     if (m_autoPlay) {
0213         play();
0214     }
0215 
0216     Q_EMIT sourceChanged();
0217 }
0218 
0219 void MpvPlayer::setLooping(bool loop)
0220 {
0221     if (m_looping == loop) {
0222         return;
0223     }
0224 
0225     m_looping = loop;
0226     mpv_set_option_string(mpv, "loop-file", loop ? "inf" : "no");
0227 
0228     Q_EMIT loopingChanged();
0229 }
0230 
0231 void MpvPlayer::setAutoPlay(bool autoPlay)
0232 {
0233     if (m_autoPlay == autoPlay) {
0234         return;
0235     }
0236 
0237     m_autoPlay = autoPlay;
0238 
0239     Q_EMIT autoPlayChanged();
0240 }
0241 
0242 void MpvPlayer::play()
0243 {
0244     if (!paused() || m_source.isEmpty()) {
0245         return;
0246     }
0247 
0248     // setSource doesn't actually load the file into MPV, because the player is always created
0249     // regardless of autoPlay. This incurs a very visible overhead in the UI, so we want to delay
0250     // the loadfile command until the last possible moment (when the user requests to play it).
0251     if (m_currentSource != m_source) {
0252         m_loading = true;
0253         Q_EMIT loadingChanged();
0254 
0255         command(QStringList{QStringLiteral("loadfile"), m_source});
0256 
0257         m_currentSource = m_source;
0258         m_paused = false;
0259 
0260         Q_EMIT stoppedChanged();
0261         Q_EMIT pausedChanged();
0262     }
0263 
0264     setProperty("pause", false);
0265 }
0266 
0267 void MpvPlayer::pause()
0268 {
0269     if (paused()) {
0270         return;
0271     }
0272     setProperty("pause", true);
0273 }
0274 
0275 void MpvPlayer::stop()
0276 {
0277     setPosition(0);
0278     setProperty("pause", true);
0279 }
0280 
0281 void MpvPlayer::setPosition(double value)
0282 {
0283     if (value == position()) {
0284         return;
0285     }
0286     setProperty("time-pos", value);
0287 }
0288 
0289 void MpvPlayer::seek(qreal offset)
0290 {
0291     command(QStringList() << "add"
0292                           << "time-pos" << QString::number(offset));
0293 }
0294 
0295 void MpvPlayer::on_update(void *ctx)
0296 {
0297     auto self = static_cast<MpvPlayer *>(ctx);
0298     Q_EMIT self->onUpdate();
0299 }
0300 
0301 // connected to onUpdate(); signal makes sure it runs on the GUI thread
0302 void MpvPlayer::doUpdate()
0303 {
0304     update();
0305 }
0306 
0307 void MpvPlayer::command(const QVariant &params)
0308 {
0309     mpv::qt::command(mpv, params);
0310 }
0311 
0312 void MpvPlayer::setOption(const QString &name, const QVariant &value)
0313 {
0314     mpv::qt::set_option_variant(mpv, name, value);
0315 }
0316 
0317 void MpvPlayer::setProperty(const QString &name, const QVariant &value)
0318 {
0319     mpv::qt::set_property(mpv, name, value);
0320 }
0321 
0322 QVariant MpvPlayer::getProperty(const QString &name)
0323 {
0324     return mpv::qt::get_property(mpv, name);
0325 }
0326 
0327 QQuickFramebufferObject::Renderer *MpvPlayer::createRenderer() const
0328 {
0329     if (window() == nullptr) {
0330         return nullptr;
0331     }
0332 
0333 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0334     window()->setPersistentOpenGLContext(true);
0335 #endif
0336     window()->setPersistentSceneGraph(true);
0337     return new MpvRenderer(const_cast<MpvPlayer *>(this));
0338 }
0339 
0340 void MpvPlayer::onMpvEvents()
0341 {
0342     // Process all events, until the event queue is empty.
0343     while (mpv) {
0344         mpv_event *event = mpv_wait_event(mpv, 0);
0345         if (event->event_id == MPV_EVENT_NONE) {
0346             break;
0347         }
0348 
0349         switch (event->event_id) {
0350         case MPV_EVENT_PROPERTY_CHANGE: {
0351             const auto prop = static_cast<mpv_event_property *>(event->data);
0352             if (strcmp(prop->name, "time-pos") == 0) {
0353                 if (prop->format == MPV_FORMAT_DOUBLE) {
0354                     double time = *(double *)prop->data;
0355                     m_position = time;
0356                     Q_EMIT positionChanged();
0357                 }
0358             } else if (strcmp(prop->name, "duration") == 0) {
0359                 if (prop->format == MPV_FORMAT_DOUBLE) {
0360                     double time = *(double *)prop->data;
0361                     m_duration = time;
0362                     Q_EMIT durationChanged();
0363                 }
0364             } else if (strcmp(prop->name, "pause") == 0) {
0365                 if (prop->format == MPV_FORMAT_FLAG) {
0366                     // pause is expected to be false if there's no currently loaded media, so skip it
0367                     if (!m_currentSource.isEmpty()) {
0368                         m_paused = *(bool *)prop->data;
0369                         Q_EMIT pausedChanged();
0370                     }
0371                 }
0372             }
0373             break;
0374         }
0375         case MPV_EVENT_VIDEO_RECONFIG: {
0376             int64_t w, h;
0377             if (mpv_get_property(mpv, "dwidth", MPV_FORMAT_INT64, &w) >= 0 && mpv_get_property(mpv, "dheight", MPV_FORMAT_INT64, &h) >= 0 && w > 0 && h > 0) {
0378                 const QSize newSize(w, h);
0379                 if (newSize != m_sourceSize) {
0380                     m_sourceSize = newSize;
0381                     Q_EMIT sourceSizeChanged();
0382                 }
0383             }
0384         } break;
0385         case MPV_EVENT_PLAYBACK_RESTART: {
0386             m_loading = false;
0387             Q_EMIT loadingChanged();
0388         } break;
0389         default:;
0390             // Ignore uninteresting or unknown events.
0391         }
0392     }
0393 }