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 ¶ms) 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 }