]> git.sur5r.net Git - minitube/blob - lib/media/src/mpv/mediampv.cpp
New upstream version 3.1
[minitube] / lib / media / src / mpv / mediampv.cpp
1 #include "mediampv.h"
2
3 #include <clocale>
4 #include <mpv/qthelper.hpp>
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 }
89
90 // This slot is invoked by wakeup() (through the mpvEvents signal).
91 void MediaMPV::onMpvEvents() {
92     // Process all events, until the event queue is empty.
93     while (mpv) {
94         mpv_event *event = mpv_wait_event(mpv, 0);
95         if (event->event_id == MPV_EVENT_NONE) break;
96         handleMpvEvent(event);
97     }
98 }
99
100 void MediaMPV::checkAboutToFinish(qint64 position) {
101     if (!aboutToFinishEmitted && currentState == Media::PlayingState) {
102         const qint64 dur = duration();
103         if (dur > 0 && dur - position < 5000) {
104             aboutToFinishEmitted = true;
105             qDebug() << "aboutToFinish" << position << dur;
106             emit aboutToFinish();
107         }
108     }
109 }
110
111 void MediaMPV::handleMpvEvent(mpv_event *event) {
112     // qDebug() << event->data;
113     switch (event->event_id) {
114     case MPV_EVENT_START_FILE:
115         clearTrackState();
116         emit sourceChanged();
117         setState(Media::LoadingState);
118         break;
119
120     case MPV_EVENT_SEEK:
121         setState(Media::BufferingState);
122         break;
123
124     case MPV_EVENT_FILE_LOADED:
125         setState(Media::PlayingState);
126         break;
127
128     case MPV_EVENT_PLAYBACK_RESTART:
129     case MPV_EVENT_UNPAUSE:
130         setState(Media::PlayingState);
131         break;
132
133     case MPV_EVENT_END_FILE: {
134         struct mpv_event_end_file *eof_event = (struct mpv_event_end_file *)event->data;
135         if (eof_event->reason == MPV_END_FILE_REASON_EOF ||
136             eof_event->reason == MPV_END_FILE_REASON_ERROR) {
137             qDebug() << "Finished";
138             setState(Media::StoppedState);
139             emit finished();
140         }
141         break;
142     }
143
144     case MPV_EVENT_PAUSE:
145         setState(Media::PausedState);
146         break;
147
148     case MPV_EVENT_PROPERTY_CHANGE: {
149         mpv_event_property *prop = (mpv_event_property *)event->data;
150         // qDebug() << prop->name << prop->data;
151
152         if (strcmp(prop->name, "time-pos") == 0) {
153             if (prop->format == MPV_FORMAT_DOUBLE) {
154                 double seconds = *(double *)prop->data;
155                 qint64 ms = seconds * 1000.;
156                 emit positionChanged(ms);
157                 checkAboutToFinish(ms);
158             }
159         }
160
161         else if (strcmp(prop->name, "volume") == 0) {
162             if (prop->format == MPV_FORMAT_DOUBLE) {
163                 double vol = *(double *)prop->data;
164                 emit volumeChanged(vol / 100.);
165             }
166         }
167
168         else if (strcmp(prop->name, "mute") == 0) {
169             if (prop->format == MPV_FORMAT_FLAG) {
170                 int mute = *(int *)prop->data;
171                 emit volumeMutedChanged(mute == 1);
172             }
173         }
174
175         break;
176     }
177
178     case MPV_EVENT_LOG_MESSAGE: {
179         struct mpv_event_log_message *msg = (struct mpv_event_log_message *)event->data;
180         qDebug() << "[" << msg->prefix << "] " << msg->level << ": " << msg->text;
181
182         if (msg->log_level == MPV_LOG_LEVEL_ERROR) {
183             lastErrorString = QString::fromUtf8(msg->text);
184             emit error(lastErrorString);
185         }
186
187         break;
188     }
189
190     case MPV_EVENT_SHUTDOWN: {
191         mpv_terminate_destroy(mpv);
192         mpv = nullptr;
193         break;
194     }
195
196     default:;
197         // Unhandled events
198     }
199 }
200
201 void MediaMPV::sendCommand(const char *args[]) {
202     // mpv_command_async(mpv, 0, args);
203     mpv_command(mpv, args);
204 }
205
206 void MediaMPV::setState(Media::State value) {
207     if (value != currentState) {
208         currentState = value;
209         emit stateChanged(currentState);
210     }
211 }
212
213 void MediaMPV::clearTrackState() {
214     lastErrorString.clear();
215     aboutToFinishEmitted = false;
216 }
217
218 void MediaMPV::setAudioOnly(bool value) {
219     Q_UNUSED(value);
220 }
221
222 #ifndef MEDIA_AUDIOONLY
223
224 void MediaMPV::setRenderer(const QString &name) {
225     mpv_set_option_string(mpv, "vo", name.toUtf8().data());
226 }
227
228 QWidget *MediaMPV::videoWidget() {
229     if (!widget) {
230         widget = new MpvWidget(mpv);
231     }
232     return widget;
233 }
234
235 void MediaMPV::playSeparateAudioAndVideo(const QString &video, const QString &audio) {
236     const QByteArray fileUtf8 = video.toUtf8();
237     const char *args[] = {"loadfile", fileUtf8.constData(), nullptr};
238     sendCommand(args);
239
240     qApp->processEvents();
241
242     const QByteArray audioUtf8 = audio.toUtf8();
243     const char *args2[] = {"audio-add", audioUtf8.constData(), nullptr};
244     sendCommand(args2);
245
246     clearTrackState();
247 }
248
249 void MediaMPV::snapshot() {
250     if (currentState == State::StoppedState) return;
251
252     const QVariantList args = {"screenshot-raw", "video"};
253     mpv::qt::node_builder nodeBuilder(args);
254     mpv_node node;
255     const int ret = mpv_command_node(mpv, nodeBuilder.node(), &node);
256     if (ret < 0) {
257         emit error("Cannot take snapshot");
258         return;
259     }
260
261     mpv::qt::node_autofree auto_free(&node);
262     if (node.format != MPV_FORMAT_NODE_MAP) {
263         emit error("Cannot take snapshot");
264         return;
265     }
266
267     int width = 0;
268     int height = 0;
269     int stride = 0;
270     mpv_node_list *list = node.u.list;
271     uchar *data = nullptr;
272
273     for (int i = 0; i < list->num; ++i) {
274         const char *key = list->keys[i];
275         if (strcmp(key, "w") == 0) {
276             width = static_cast<int>(list->values[i].u.int64);
277         } else if (strcmp(key, "h") == 0) {
278             height = static_cast<int>(list->values[i].u.int64);
279         } else if (strcmp(key, "stride") == 0) {
280             stride = static_cast<int>(list->values[i].u.int64);
281         } else if (strcmp(key, "data") == 0) {
282             data = static_cast<uchar *>(list->values[i].u.ba->data);
283         }
284     }
285
286     if (data != nullptr) {
287         QImage img = QImage(data, width, height, stride, QImage::Format_RGB32);
288         img.bits();
289         emit snapshotReady(img);
290     }
291 }
292
293 #endif
294
295 void MediaMPV::init() {}
296
297 Media::State MediaMPV::state() const {
298     return currentState;
299 }
300
301 void MediaMPV::play(const QString &file) {
302     const QByteArray fileUtf8 = file.toUtf8();
303     const char *args[] = {"loadfile", fileUtf8.constData(), nullptr};
304     sendCommand(args);
305
306     clearTrackState();
307     if (currentState == Media::PausedState) play();
308 }
309
310 void MediaMPV::play() {
311     int flag = 0;
312     mpv_set_property_async(mpv, 0, "pause", MPV_FORMAT_FLAG, &flag);
313 }
314
315 void MediaMPV::pause() {
316     int flag = 1;
317     mpv_set_property_async(mpv, 0, "pause", MPV_FORMAT_FLAG, &flag);
318 }
319
320 void MediaMPV::stop() {
321     const char *args[] = {"stop", nullptr};
322     sendCommand(args);
323 }
324
325 void MediaMPV::seek(qint64 ms) {
326     double seconds = ms / 1000.;
327     QByteArray ba = QString::number(seconds).toUtf8();
328     const char *args[] = {"seek", ba.constData(), "absolute", nullptr};
329     sendCommand(args);
330 }
331
332 QString MediaMPV::file() const {
333     char *path;
334     mpv_get_property(mpv, "path", MPV_FORMAT_STRING, &path);
335     return QString::fromUtf8(path);
336 }
337
338 void MediaMPV::setBufferMilliseconds(qint64 value) {
339     Q_UNUSED(value);
340     // Not implemented
341 }
342
343 void MediaMPV::setUserAgent(const QString &value) {
344     mpv_set_option_string(mpv, "user-agent", value.toUtf8());
345 }
346
347 void MediaMPV::enqueue(const QString &file) {
348     const QByteArray fileUtf8 = file.toUtf8();
349     const char *args[] = {"loadfile", fileUtf8.constData(), "append", nullptr};
350     sendCommand(args);
351 }
352
353 void MediaMPV::clearQueue() {
354     const char *args[] = {"playlist-clear", nullptr};
355     sendCommand(args);
356 }
357
358 bool MediaMPV::hasQueue() const {
359     mpv_node node;
360     int r = mpv_get_property(mpv, "playlist", MPV_FORMAT_NODE, &node);
361     if (r < 0) return false;
362     QVariant v = mpv::qt::node_to_variant(&node);
363     mpv_free_node_contents(&node);
364     QVariantList list = v.toList();
365     return list.count() > 1;
366 }
367
368 qint64 MediaMPV::position() const {
369     double seconds;
370     mpv_get_property(mpv, "time-pos", MPV_FORMAT_DOUBLE, &seconds);
371     return seconds * 1000.;
372 }
373
374 qint64 MediaMPV::duration() const {
375     double seconds;
376     mpv_get_property(mpv, "duration", MPV_FORMAT_DOUBLE, &seconds);
377     return seconds * 1000.;
378 }
379
380 qint64 MediaMPV::remainingTime() const {
381     double seconds;
382     mpv_get_property(mpv, "time-remaining", MPV_FORMAT_DOUBLE, &seconds);
383     return seconds * 1000.;
384 }
385
386 qreal MediaMPV::volume() const {
387     double vol;
388     mpv_get_property(mpv, "volume", MPV_FORMAT_DOUBLE, &vol);
389     return vol / 100.;
390 }
391
392 void MediaMPV::setVolume(qreal value) {
393     double percent = value * 100.;
394     mpv_set_property_async(mpv, 0, "volume", MPV_FORMAT_DOUBLE, &percent);
395 }
396
397 bool MediaMPV::volumeMuted() const {
398     int mute;
399     mpv_get_property(mpv, "mute", MPV_FORMAT_FLAG, &mute);
400     return mute == 1;
401 }
402
403 void MediaMPV::setVolumeMuted(bool value) {
404     int mute = value ? 1 : 0;
405     mpv_set_property(mpv, "mute", MPV_FORMAT_FLAG, &mute);
406 }
407
408 QString MediaMPV::errorString() const {
409     return lastErrorString;
410 }