]> git.sur5r.net Git - minitube/blob - lib/media/src/mpv/mediampv.cpp
New upstream version 3.8
[minitube] / lib / media / src / mpv / mediampv.cpp
1 #include "mediampv.h"
2
3 #include "qthelper.hpp"
4 #include <clocale>
5
6 #ifndef MEDIA_AUDIOONLY
7 #include "mpvwidget.h"
8 #endif
9
10 namespace {
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();
18 }
19
20 } // namespace
21
22 MediaMPV::MediaMPV(QObject *parent) : Media(parent), widget(nullptr) {
23     QThread *thread = new QThread(this);
24     thread->start();
25     moveToThread(thread);
26     connect(this, &QObject::destroyed, thread, &QThread::quit);
27
28 #ifndef Q_OS_WIN
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");
32 #endif
33
34     mpv = mpv_create();
35     if (!mpv) qFatal("Cannot create MPV instance");
36
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");
54
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");
58
59 #ifdef MEDIA_MPV_WID
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);
66 #endif
67
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");
72 #endif
73
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);
79
80     if (mpv_initialize(mpv) < 0) qFatal("mpv failed to initialize");
81
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);
89 }
90
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.
94     while (mpv) {
95         mpv_event *event = mpv_wait_event(mpv, 0);
96         if (event->event_id == MPV_EVENT_NONE) break;
97         handleMpvEvent(event);
98     }
99 }
100
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();
108         }
109     }
110 }
111
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:
116         clearTrackState();
117         emit sourceChanged();
118         setState(Media::LoadingState);
119         break;
120
121     case MPV_EVENT_SEEK:
122         setState(Media::BufferingState);
123         break;
124
125     case MPV_EVENT_PLAYBACK_RESTART: {
126         int pause;
127         mpv_get_property(mpv, "pause", MPV_FORMAT_FLAG, &pause);
128         bool paused = pause == 1;
129         if (paused)
130             setState(Media::PausedState);
131         else
132             setState(Media::PlayingState);
133         break;
134     }
135
136     case MPV_EVENT_FILE_LOADED:
137         // Add separate audiofile if there is any
138         if (!audioFileToAdd.isEmpty())
139         {
140             const QByteArray audioUtf8 = audioFileToAdd.toUtf8();
141             const char *args2[] = {"audio-add", audioUtf8.constData(), nullptr};
142             sendCommand(args2);
143             audioFileToAdd.clear();
144         }
145         setState(Media::PlayingState);
146         break;
147
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);
154             emit finished();
155         }
156         break;
157     }
158
159     case MPV_EVENT_PROPERTY_CHANGE: {
160         mpv_event_property *prop = (mpv_event_property *)event->data;
161         // qDebug() << prop->name << prop->data;
162
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);
169             }
170         }
171
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.);
176             }
177         }
178
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);
183             }
184         }
185
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;
190                 if (paused)
191                     setState(Media::PausedState);
192                 else {
193                     int coreIdle;
194                     mpv_get_property(mpv, "core-idle", MPV_FORMAT_FLAG, &coreIdle);
195                     if (coreIdle == 1)
196                         setState(Media::StoppedState);
197                     else
198                         setState(Media::PlayingState);
199                 }
200             }
201         }
202
203         break;
204     }
205
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;
209
210         if (msg->log_level == MPV_LOG_LEVEL_ERROR) {
211             lastErrorString = QString::fromUtf8(msg->text);
212             emit error(lastErrorString);
213         }
214
215         break;
216     }
217
218     case MPV_EVENT_SHUTDOWN: {
219         mpv_terminate_destroy(mpv);
220         mpv = nullptr;
221         break;
222     }
223
224     default:;
225         // Unhandled events
226     }
227 }
228
229 void MediaMPV::sendCommand(const char *args[]) {
230     // mpv_command_async(mpv, 0, args);
231     mpv_command(mpv, args);
232 }
233
234 void MediaMPV::setState(Media::State value) {
235     if (value != currentState) {
236         qDebug() << "State" << value;
237         currentState = value;
238         emit stateChanged(currentState);
239     }
240 }
241
242 void MediaMPV::clearTrackState() {
243     lastErrorString.clear();
244     aboutToFinishEmitted = false;
245 }
246
247 void MediaMPV::setAudioOnly(bool value) {
248     Q_UNUSED(value);
249 }
250
251 #ifndef MEDIA_AUDIOONLY
252
253 void MediaMPV::setRenderer(const QString &name) {
254     mpv_set_option_string(mpv, "vo", name.toUtf8().data());
255 }
256
257 QWidget *MediaMPV::videoWidget() {
258     if (!widget) {
259         widget = new MpvWidget(mpv);
260     }
261     return widget;
262 }
263
264 void MediaMPV::playSeparateAudioAndVideo(const QString &video, const QString &audio) {
265     const QByteArray fileUtf8 = video.toUtf8();
266     const char *args[] = {"loadfile", fileUtf8.constData(), nullptr};
267     sendCommand(args);
268
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;
272
273     qApp->processEvents();
274     clearTrackState();
275 }
276
277 void MediaMPV::snapshot() {
278     if (currentState == State::StoppedState) return;
279
280     const QVariantList args = {"screenshot-raw", "video"};
281     mpv::qt::node_builder nodeBuilder(args);
282     mpv_node node;
283     const int ret = mpv_command_node(mpv, nodeBuilder.node(), &node);
284     if (ret < 0) {
285         emit error("Cannot take snapshot");
286         return;
287     }
288
289     mpv::qt::node_autofree auto_free(&node);
290     if (node.format != MPV_FORMAT_NODE_MAP) {
291         emit error("Cannot take snapshot");
292         return;
293     }
294
295     int width = 0;
296     int height = 0;
297     int stride = 0;
298     mpv_node_list *list = node.u.list;
299     uchar *data = nullptr;
300
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);
311         }
312     }
313
314     if (data != nullptr) {
315         QImage img = QImage(data, width, height, stride, QImage::Format_RGB32);
316         img.bits();
317         emit snapshotReady(img);
318     }
319 }
320
321 #endif
322
323 void MediaMPV::init() {}
324
325 Media::State MediaMPV::state() const {
326     return currentState;
327 }
328
329 void MediaMPV::play(const QString &file) {
330     audioFileToAdd.clear();
331
332     const QByteArray fileUtf8 = file.toUtf8();
333     const char *args[] = {"loadfile", fileUtf8.constData(), nullptr};
334     sendCommand(args);
335
336     clearTrackState();
337     if (currentState == Media::PausedState) play();
338 }
339
340 void MediaMPV::play() {
341     int flag = 0;
342     mpv_set_property_async(mpv, 0, "pause", MPV_FORMAT_FLAG, &flag);
343 }
344
345 void MediaMPV::pause() {
346     int flag = 1;
347     mpv_set_property_async(mpv, 0, "pause", MPV_FORMAT_FLAG, &flag);
348 }
349
350 void MediaMPV::stop() {
351     const char *args[] = {"stop", nullptr};
352     sendCommand(args);
353 }
354
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};
359     sendCommand(args);
360 }
361
362 void MediaMPV::relativeSeek(qint64 ms) {
363     double seconds = ms / 1000.;
364     QByteArray ba = QString::number(seconds).toUtf8();
365     const char *args[] = {"seek", ba.constData(), "relative", nullptr};
366     sendCommand(args);
367 }
368
369 QString MediaMPV::file() const {
370     char *path = nullptr;
371     mpv_get_property(mpv, "path", MPV_FORMAT_STRING, &path);
372     if (!path) return QString();
373     return QString::fromUtf8(path);
374 }
375
376 void MediaMPV::setBufferMilliseconds(qint64 value) {
377     Q_UNUSED(value);
378     // Not implemented
379 }
380
381 void MediaMPV::setUserAgent(const QString &value) {
382     mpv_set_option_string(mpv, "user-agent", value.toUtf8());
383 }
384
385 void MediaMPV::enqueue(const QString &file) {
386     audioFileToAdd.clear();
387     const QByteArray fileUtf8 = file.toUtf8();
388     const char *args[] = {"loadfile", fileUtf8.constData(), "append", nullptr};
389     sendCommand(args);
390 }
391
392 void MediaMPV::clearQueue() {
393     const char *args[] = {"playlist-clear", nullptr};
394     sendCommand(args);
395 }
396
397 bool MediaMPV::hasQueue() const {
398     mpv_node node;
399     int r = mpv_get_property(mpv, "playlist", MPV_FORMAT_NODE, &node);
400     if (r < 0) return false;
401     QVariant v = mpv::qt::node_to_variant(&node);
402     mpv_free_node_contents(&node);
403     QVariantList list = v.toList();
404     return list.count() > 1;
405 }
406
407 qint64 MediaMPV::position() const {
408     double seconds;
409     mpv_get_property(mpv, "time-pos", MPV_FORMAT_DOUBLE, &seconds);
410     return seconds * 1000.;
411 }
412
413 qint64 MediaMPV::duration() const {
414     double seconds;
415     mpv_get_property(mpv, "duration", MPV_FORMAT_DOUBLE, &seconds);
416     return seconds * 1000.;
417 }
418
419 qint64 MediaMPV::remainingTime() const {
420     double seconds;
421     mpv_get_property(mpv, "time-remaining", MPV_FORMAT_DOUBLE, &seconds);
422     return seconds * 1000.;
423 }
424
425 qreal MediaMPV::volume() const {
426     double vol;
427     mpv_get_property(mpv, "volume", MPV_FORMAT_DOUBLE, &vol);
428     return vol / 100.;
429 }
430
431 void MediaMPV::setVolume(qreal value) {
432     double percent = value * 100.;
433     mpv_set_property_async(mpv, 0, "volume", MPV_FORMAT_DOUBLE, &percent);
434 }
435
436 bool MediaMPV::volumeMuted() const {
437     int mute;
438     mpv_get_property(mpv, "mute", MPV_FORMAT_FLAG, &mute);
439     return mute == 1;
440 }
441
442 void MediaMPV::setVolumeMuted(bool value) {
443     int mute = value ? 1 : 0;
444     mpv_set_property(mpv, "mute", MPV_FORMAT_FLAG, &mute);
445 }
446
447 QString MediaMPV::errorString() const {
448     return lastErrorString;
449 }