3 #include "qthelper.hpp"
6 #ifndef MEDIA_AUDIOONLY
11 void wakeup(void *ctx) {
12 // This callback is invoked from any mpv thread (but possibly also
13 // recursively from a thread that is calling the mpv API). Just notify
14 // the Qt GUI thread to wake up (so that it can process events with
15 // mpv_wait_event()), and return as quickly as possible.
16 MediaMPV *mediaMPV = (MediaMPV *)ctx;
17 emit mediaMPV->mpvEvents();
22 MediaMPV::MediaMPV(QObject *parent) : Media(parent), widget(nullptr) {
23 QThread *thread = new QThread(this);
26 connect(this, &QObject::destroyed, thread, &QThread::quit);
29 // Qt sets the locale in the QApplication constructor, but libmpv requires
30 // the LC_NUMERIC category to be set to "C", so change it back.
31 std::setlocale(LC_NUMERIC, "C");
35 if (!mpv) qFatal("Cannot create MPV instance");
37 mpv_set_option_string(mpv, "config", "no");
38 mpv_set_option_string(mpv, "audio-display", "no");
39 mpv_set_option_string(mpv, "gapless-audio", "weak");
40 mpv_set_option_string(mpv, "idle", "yes");
41 mpv_set_option_string(mpv, "input-default-bindings", "no");
42 mpv_set_option_string(mpv, "input-vo-keyboard", "no");
43 mpv_set_option_string(mpv, "input-cursor", "no");
44 mpv_set_option_string(mpv, "input-media-keys", "no");
45 mpv_set_option_string(mpv, "ytdl", "no");
46 mpv_set_option_string(mpv, "fs", "no");
47 mpv_set_option_string(mpv, "osd-level", "0");
48 mpv_set_option_string(mpv, "quiet", "yes");
49 mpv_set_option_string(mpv, "load-scripts", "no");
50 mpv_set_option_string(mpv, "audio-client-name",
51 QCoreApplication::applicationName().toUtf8().data());
52 mpv_set_option_string(mpv, "hwdec", "auto");
53 mpv_set_option_string(mpv, "vo", "libmpv");
55 mpv_set_option_string(mpv, "cache", "no");
56 mpv_set_option_string(mpv, "demuxer-max-bytes", "10485760");
57 mpv_set_option_string(mpv, "demuxer-max-back-bytes", "10485760");
60 widget = new QWidget();
61 widget->setAttribute(Qt::WA_DontCreateNativeAncestors);
62 widget->setAttribute(Qt::WA_NativeWindow);
63 // If you have a HWND, use: int64_t wid = (intptr_t)hwnd;
64 int64_t wid = (intptr_t)widget->winId();
65 mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &wid);
68 #ifndef QT_NO_DEBUG_OUTPUT
69 // Request log messages
70 // They are received as MPV_EVENT_LOG_MESSAGE.
71 mpv_request_log_messages(mpv, "info");
74 // From this point on, the wakeup function will be called. The callback
75 // can come from any thread, so we use the QueuedConnection mechanism to
76 // relay the wakeup in a thread-safe way.
77 connect(this, &MediaMPV::mpvEvents, this, &MediaMPV::onMpvEvents, Qt::QueuedConnection);
78 mpv_set_wakeup_callback(mpv, wakeup, this);
80 if (mpv_initialize(mpv) < 0) qFatal("mpv failed to initialize");
82 // Let us receive property change events with MPV_EVENT_PROPERTY_CHANGE if
83 // this property changes.
84 mpv_observe_property(mpv, 0, "time-pos", MPV_FORMAT_DOUBLE);
85 mpv_observe_property(mpv, 0, "duration", MPV_FORMAT_DOUBLE);
86 mpv_observe_property(mpv, 0, "volume", MPV_FORMAT_DOUBLE);
87 mpv_observe_property(mpv, 0, "mute", MPV_FORMAT_FLAG);
88 mpv_observe_property(mpv, 0, "pause", MPV_FORMAT_FLAG);
91 // This slot is invoked by wakeup() (through the mpvEvents signal).
92 void MediaMPV::onMpvEvents() {
93 // Process all events, until the event queue is empty.
95 mpv_event *event = mpv_wait_event(mpv, 0);
96 if (event->event_id == MPV_EVENT_NONE) break;
97 handleMpvEvent(event);
101 void MediaMPV::checkAboutToFinish(qint64 position) {
102 if (!aboutToFinishEmitted && currentState == Media::PlayingState) {
103 const qint64 dur = duration();
104 if (dur > 0 && dur - position < 5000) {
105 aboutToFinishEmitted = true;
106 qDebug() << "aboutToFinish" << position << dur;
107 emit aboutToFinish();
112 void MediaMPV::handleMpvEvent(mpv_event *event) {
113 // qDebug() << event->event_id << event->data;
114 switch (event->event_id) {
115 case MPV_EVENT_START_FILE:
117 emit sourceChanged();
118 setState(Media::LoadingState);
122 setState(Media::BufferingState);
125 case MPV_EVENT_PLAYBACK_RESTART: {
127 mpv_get_property(mpv, "pause", MPV_FORMAT_FLAG, &pause);
128 bool paused = pause == 1;
130 setState(Media::PausedState);
132 setState(Media::PlayingState);
136 case MPV_EVENT_FILE_LOADED:
137 // Add separate audiofile if there is any
138 if (!audioFileToAdd.isEmpty())
140 const QByteArray audioUtf8 = audioFileToAdd.toUtf8();
141 const char *args2[] = {"audio-add", audioUtf8.constData(), nullptr};
143 audioFileToAdd.clear();
145 setState(Media::PlayingState);
148 case MPV_EVENT_END_FILE: {
149 struct mpv_event_end_file *eof_event = (struct mpv_event_end_file *)event->data;
150 if (eof_event->reason == MPV_END_FILE_REASON_EOF ||
151 eof_event->reason == MPV_END_FILE_REASON_ERROR) {
152 qDebug() << "Finished";
153 setState(Media::StoppedState);
159 case MPV_EVENT_PROPERTY_CHANGE: {
160 mpv_event_property *prop = (mpv_event_property *)event->data;
161 // qDebug() << prop->name << prop->data;
163 if (strcmp(prop->name, "time-pos") == 0) {
164 if (prop->format == MPV_FORMAT_DOUBLE) {
165 double seconds = *(double *)prop->data;
166 qint64 ms = seconds * 1000.;
167 emit positionChanged(ms);
168 checkAboutToFinish(ms);
172 else if (strcmp(prop->name, "volume") == 0) {
173 if (prop->format == MPV_FORMAT_DOUBLE) {
174 double vol = *(double *)prop->data;
175 emit volumeChanged(vol / 100.);
179 else if (strcmp(prop->name, "mute") == 0) {
180 if (prop->format == MPV_FORMAT_FLAG) {
181 int mute = *(int *)prop->data;
182 emit volumeMutedChanged(mute == 1);
186 else if (strcmp(prop->name, "pause") == 0) {
187 if (prop->format == MPV_FORMAT_FLAG) {
188 int pause = *(int *)prop->data;
189 bool paused = pause == 1;
191 setState(Media::PausedState);
194 mpv_get_property(mpv, "core-idle", MPV_FORMAT_FLAG, &coreIdle);
196 setState(Media::StoppedState);
198 setState(Media::PlayingState);
206 case MPV_EVENT_LOG_MESSAGE: {
207 struct mpv_event_log_message *msg = (struct mpv_event_log_message *)event->data;
208 qDebug() << "[" << msg->prefix << "] " << msg->level << ": " << msg->text;
210 if (msg->log_level == MPV_LOG_LEVEL_ERROR) {
211 lastErrorString = QString::fromUtf8(msg->text);
212 emit error(lastErrorString);
218 case MPV_EVENT_SHUTDOWN: {
219 mpv_terminate_destroy(mpv);
229 void MediaMPV::sendCommand(const char *args[]) {
230 // mpv_command_async(mpv, 0, args);
231 mpv_command(mpv, args);
234 void MediaMPV::setState(Media::State value) {
235 if (value != currentState) {
236 qDebug() << "State" << value;
237 currentState = value;
238 emit stateChanged(currentState);
242 void MediaMPV::clearTrackState() {
243 lastErrorString.clear();
244 aboutToFinishEmitted = false;
247 void MediaMPV::setAudioOnly(bool value) {
251 #ifndef MEDIA_AUDIOONLY
253 void MediaMPV::setRenderer(const QString &name) {
254 mpv_set_option_string(mpv, "vo", name.toUtf8().data());
257 QWidget *MediaMPV::videoWidget() {
259 widget = new MpvWidget(mpv);
264 void MediaMPV::playSeparateAudioAndVideo(const QString &video, const QString &audio) {
265 const QByteArray fileUtf8 = video.toUtf8();
266 const char *args[] = {"loadfile", fileUtf8.constData(), nullptr};
269 // We are playing audio as separate file. The add audio command must executed when the main file is loaded
270 // Otherwise the audio file doesn't gets played
271 audioFileToAdd = audio;
273 qApp->processEvents();
277 void MediaMPV::snapshot() {
278 if (currentState == State::StoppedState) return;
280 const QVariantList args = {"screenshot-raw", "video"};
281 mpv::qt::node_builder nodeBuilder(args);
283 const int ret = mpv_command_node(mpv, nodeBuilder.node(), &node);
285 emit error("Cannot take snapshot");
289 mpv::qt::node_autofree auto_free(&node);
290 if (node.format != MPV_FORMAT_NODE_MAP) {
291 emit error("Cannot take snapshot");
298 mpv_node_list *list = node.u.list;
299 uchar *data = nullptr;
301 for (int i = 0; i < list->num; ++i) {
302 const char *key = list->keys[i];
303 if (strcmp(key, "w") == 0) {
304 width = static_cast<int>(list->values[i].u.int64);
305 } else if (strcmp(key, "h") == 0) {
306 height = static_cast<int>(list->values[i].u.int64);
307 } else if (strcmp(key, "stride") == 0) {
308 stride = static_cast<int>(list->values[i].u.int64);
309 } else if (strcmp(key, "data") == 0) {
310 data = static_cast<uchar *>(list->values[i].u.ba->data);
314 if (data != nullptr) {
315 QImage img = QImage(data, width, height, stride, QImage::Format_RGB32);
317 emit snapshotReady(img);
323 void MediaMPV::init() {}
325 Media::State MediaMPV::state() const {
329 void MediaMPV::play(const QString &file) {
330 audioFileToAdd.clear();
332 const QByteArray fileUtf8 = file.toUtf8();
333 const char *args[] = {"loadfile", fileUtf8.constData(), nullptr};
337 if (currentState == Media::PausedState) play();
340 void MediaMPV::play() {
342 mpv_set_property_async(mpv, 0, "pause", MPV_FORMAT_FLAG, &flag);
345 void MediaMPV::pause() {
347 mpv_set_property_async(mpv, 0, "pause", MPV_FORMAT_FLAG, &flag);
350 void MediaMPV::stop() {
351 const char *args[] = {"stop", nullptr};
355 void MediaMPV::seek(qint64 ms) {
356 double seconds = ms / 1000.;
357 QByteArray ba = QString::number(seconds).toUtf8();
358 const char *args[] = {"seek", ba.constData(), "absolute", nullptr};
362 QString MediaMPV::file() const {
364 mpv_get_property(mpv, "path", MPV_FORMAT_STRING, &path);
365 return QString::fromUtf8(path);
368 void MediaMPV::setBufferMilliseconds(qint64 value) {
373 void MediaMPV::setUserAgent(const QString &value) {
374 mpv_set_option_string(mpv, "user-agent", value.toUtf8());
377 void MediaMPV::enqueue(const QString &file) {
378 audioFileToAdd.clear();
379 const QByteArray fileUtf8 = file.toUtf8();
380 const char *args[] = {"loadfile", fileUtf8.constData(), "append", nullptr};
384 void MediaMPV::clearQueue() {
385 const char *args[] = {"playlist-clear", nullptr};
389 bool MediaMPV::hasQueue() const {
391 int r = mpv_get_property(mpv, "playlist", MPV_FORMAT_NODE, &node);
392 if (r < 0) return false;
393 QVariant v = mpv::qt::node_to_variant(&node);
394 mpv_free_node_contents(&node);
395 QVariantList list = v.toList();
396 return list.count() > 1;
399 qint64 MediaMPV::position() const {
401 mpv_get_property(mpv, "time-pos", MPV_FORMAT_DOUBLE, &seconds);
402 return seconds * 1000.;
405 qint64 MediaMPV::duration() const {
407 mpv_get_property(mpv, "duration", MPV_FORMAT_DOUBLE, &seconds);
408 return seconds * 1000.;
411 qint64 MediaMPV::remainingTime() const {
413 mpv_get_property(mpv, "time-remaining", MPV_FORMAT_DOUBLE, &seconds);
414 return seconds * 1000.;
417 qreal MediaMPV::volume() const {
419 mpv_get_property(mpv, "volume", MPV_FORMAT_DOUBLE, &vol);
423 void MediaMPV::setVolume(qreal value) {
424 double percent = value * 100.;
425 mpv_set_property_async(mpv, 0, "volume", MPV_FORMAT_DOUBLE, &percent);
428 bool MediaMPV::volumeMuted() const {
430 mpv_get_property(mpv, "mute", MPV_FORMAT_FLAG, &mute);
434 void MediaMPV::setVolumeMuted(bool value) {
435 int mute = value ? 1 : 0;
436 mpv_set_property(mpv, "mute", MPV_FORMAT_FLAG, &mute);
439 QString MediaMPV::errorString() const {
440 return lastErrorString;