]> git.sur5r.net Git - minitube/blob - lib/media/src/mpv/mediampv.cpp
b7cd2361f7185c54ba7f4e0146416ef536bfc2f9
[minitube] / lib / media / src / mpv / mediampv.cpp
1 #include "mediampv.h"
2
3 #include <clocale>
4
5 #ifndef MEDIA_AUDIOONLY
6 #include "mpvwidget.h"
7 #endif
8
9 namespace {
10 void wakeup(void *ctx) {
11     // This callback is invoked from any mpv thread (but possibly also
12     // recursively from a thread that is calling the mpv API). Just notify
13     // the Qt GUI thread to wake up (so that it can process events with
14     // mpv_wait_event()), and return as quickly as possible.
15     MediaMPV *mediaMPV = (MediaMPV *)ctx;
16     emit mediaMPV->mpvEvents();
17 }
18
19 } // namespace
20
21 MediaMPV::MediaMPV(QObject *parent) : Media(parent), widget(nullptr) {
22     QThread *thread = new QThread(this);
23     thread->start();
24     moveToThread(thread);
25     connect(this, &QObject::destroyed, thread, &QThread::quit);
26
27 #ifndef Q_OS_WIN
28     // Qt sets the locale in the QApplication constructor, but libmpv requires
29     // the LC_NUMERIC category to be set to "C", so change it back.
30     std::setlocale(LC_NUMERIC, "C");
31 #endif
32
33     mpv = mpv_create();
34     if (!mpv) qFatal("Cannot create MPV instance");
35
36     mpv_set_option_string(mpv, "config", "no");
37     mpv_set_option_string(mpv, "audio-display", "no");
38     mpv_set_option_string(mpv, "gapless-audio", "weak");
39     mpv_set_option_string(mpv, "idle", "yes");
40     mpv_set_option_string(mpv, "input-default-bindings", "no");
41     mpv_set_option_string(mpv, "input-vo-keyboard", "no");
42     mpv_set_option_string(mpv, "input-cursor", "no");
43     mpv_set_option_string(mpv, "input-media-keys", "no");
44     mpv_set_option_string(mpv, "ytdl", "no");
45     mpv_set_option_string(mpv, "fs", "no");
46     mpv_set_option_string(mpv, "osd-level", "0");
47     mpv_set_option_string(mpv, "quiet", "yes");
48     mpv_set_option_string(mpv, "load-scripts", "no");
49     mpv_set_option_string(mpv, "audio-client-name",
50                           QCoreApplication::applicationName().toUtf8().data());
51     mpv_set_option_string(mpv, "hwdec", "auto");
52     mpv_set_option_string(mpv, "vo", "libmpv");
53
54     mpv_set_option_string(mpv, "cache", "no");
55     mpv_set_option_string(mpv, "demuxer-max-bytes", "10485760");
56     mpv_set_option_string(mpv, "demuxer-max-back-bytes", "10485760");
57
58 #ifdef MEDIA_MPV_WID
59     widget = new QWidget();
60     widget->setAttribute(Qt::WA_DontCreateNativeAncestors);
61     widget->setAttribute(Qt::WA_NativeWindow);
62     // If you have a HWND, use: int64_t wid = (intptr_t)hwnd;
63     int64_t wid = (intptr_t)widget->winId();
64     mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &wid);
65 #endif
66
67 #ifndef QT_NO_DEBUG_OUTPUT
68     // Request log messages
69     // They are received as MPV_EVENT_LOG_MESSAGE.
70     mpv_request_log_messages(mpv, "info");
71 #endif
72
73     // From this point on, the wakeup function will be called. The callback
74     // can come from any thread, so we use the QueuedConnection mechanism to
75     // relay the wakeup in a thread-safe way.
76     connect(this, &MediaMPV::mpvEvents, this, &MediaMPV::onMpvEvents, Qt::QueuedConnection);
77     mpv_set_wakeup_callback(mpv, wakeup, this);
78
79     if (mpv_initialize(mpv) < 0) qFatal("mpv failed to initialize");
80
81     // Let us receive property change events with MPV_EVENT_PROPERTY_CHANGE if
82     // this property changes.
83     mpv_observe_property(mpv, 0, "time-pos", MPV_FORMAT_DOUBLE);
84     mpv_observe_property(mpv, 0, "duration", MPV_FORMAT_DOUBLE);
85     mpv_observe_property(mpv, 0, "volume", MPV_FORMAT_DOUBLE);
86     mpv_observe_property(mpv, 0, "mute", MPV_FORMAT_FLAG);
87     mpv_observe_property(mpv, 0, "pause", 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->event_id << 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_PLAYBACK_RESTART: {
125         int pause;
126         mpv_get_property(mpv, "pause", MPV_FORMAT_FLAG, &pause);
127         bool paused = pause == 1;
128         if (paused)
129             setState(Media::PausedState);
130         else
131             setState(Media::PlayingState);
132         break;
133     }
134
135     case MPV_EVENT_FILE_LOADED:
136         setState(Media::PlayingState);
137         break;
138
139     case MPV_EVENT_END_FILE: {
140         struct mpv_event_end_file *eof_event = (struct mpv_event_end_file *)event->data;
141         if (eof_event->reason == MPV_END_FILE_REASON_EOF ||
142             eof_event->reason == MPV_END_FILE_REASON_ERROR) {
143             qDebug() << "Finished";
144             setState(Media::StoppedState);
145             emit finished();
146         }
147         break;
148     }
149
150     case MPV_EVENT_PROPERTY_CHANGE: {
151         mpv_event_property *prop = (mpv_event_property *)event->data;
152         qDebug() << prop->name << prop->data;
153
154         if (strcmp(prop->name, "time-pos") == 0) {
155             if (prop->format == MPV_FORMAT_DOUBLE) {
156                 double seconds = *(double *)prop->data;
157                 qint64 ms = seconds * 1000.;
158                 emit positionChanged(ms);
159                 checkAboutToFinish(ms);
160             }
161         }
162
163         else if (strcmp(prop->name, "volume") == 0) {
164             if (prop->format == MPV_FORMAT_DOUBLE) {
165                 double vol = *(double *)prop->data;
166                 emit volumeChanged(vol / 100.);
167             }
168         }
169
170         else if (strcmp(prop->name, "mute") == 0) {
171             if (prop->format == MPV_FORMAT_FLAG) {
172                 int mute = *(int *)prop->data;
173                 emit volumeMutedChanged(mute == 1);
174             }
175         }
176
177         else if (strcmp(prop->name, "pause") == 0) {
178             if (prop->format == MPV_FORMAT_FLAG) {
179                 int pause = *(int *)prop->data;
180                 bool paused = pause == 1;
181                 if (paused)
182                     setState(Media::PausedState);
183                 else {
184                     int coreIdle;
185                     mpv_get_property(mpv, "core-idle", MPV_FORMAT_FLAG, &coreIdle);
186                     if (coreIdle == 1)
187                         setState(Media::StoppedState);
188                     else
189                         setState(Media::PlayingState);
190                 }
191             }
192         }
193
194         break;
195     }
196
197     case MPV_EVENT_LOG_MESSAGE: {
198         struct mpv_event_log_message *msg = (struct mpv_event_log_message *)event->data;
199         qDebug() << "[" << msg->prefix << "] " << msg->level << ": " << msg->text;
200
201         if (msg->log_level == MPV_LOG_LEVEL_ERROR) {
202             lastErrorString = QString::fromUtf8(msg->text);
203             emit error(lastErrorString);
204         }
205
206         break;
207     }
208
209     case MPV_EVENT_SHUTDOWN: {
210         mpv_terminate_destroy(mpv);
211         mpv = nullptr;
212         break;
213     }
214
215     default:;
216         // Unhandled events
217     }
218 }
219
220 void MediaMPV::sendCommand(const char *args[]) {
221     // mpv_command_async(mpv, 0, args);
222     mpv_command(mpv, args);
223 }
224
225 void MediaMPV::setState(Media::State value) {
226     if (value != currentState) {
227         qDebug() << "State" << value;
228         currentState = value;
229         emit stateChanged(currentState);
230     }
231 }
232
233 void MediaMPV::clearTrackState() {
234     lastErrorString.clear();
235     aboutToFinishEmitted = false;
236 }
237
238 void MediaMPV::setAudioOnly(bool value) {
239     Q_UNUSED(value);
240 }
241
242 #ifndef MEDIA_AUDIOONLY
243
244 void MediaMPV::setRenderer(const QString &name) {
245     mpv_set_option_string(mpv, "vo", name.toUtf8().data());
246 }
247
248 QWidget *MediaMPV::videoWidget() {
249     if (!widget) {
250         widget = new MpvWidget(mpv);
251     }
252     return widget;
253 }
254
255 void MediaMPV::playSeparateAudioAndVideo(const QString &video, const QString &audio) {
256     const QByteArray fileUtf8 = video.toUtf8();
257     const char *args[] = {"loadfile", fileUtf8.constData(), nullptr};
258     sendCommand(args);
259
260     qApp->processEvents();
261
262     const QByteArray audioUtf8 = audio.toUtf8();
263     const char *args2[] = {"audio-add", audioUtf8.constData(), nullptr};
264     sendCommand(args2);
265
266     clearTrackState();
267 }
268
269 void MediaMPV::snapshot() {
270     if (currentState == State::StoppedState) return;
271
272     const QVariantList args = {"screenshot-raw", "video"};
273     mpv::qt::node_builder nodeBuilder(args);
274     mpv_node node;
275     const int ret = mpv_command_node(mpv, nodeBuilder.node(), &node);
276     if (ret < 0) {
277         emit error("Cannot take snapshot");
278         return;
279     }
280
281     mpv::qt::node_autofree auto_free(&node);
282     if (node.format != MPV_FORMAT_NODE_MAP) {
283         emit error("Cannot take snapshot");
284         return;
285     }
286
287     int width = 0;
288     int height = 0;
289     int stride = 0;
290     mpv_node_list *list = node.u.list;
291     uchar *data = nullptr;
292
293     for (int i = 0; i < list->num; ++i) {
294         const char *key = list->keys[i];
295         if (strcmp(key, "w") == 0) {
296             width = static_cast<int>(list->values[i].u.int64);
297         } else if (strcmp(key, "h") == 0) {
298             height = static_cast<int>(list->values[i].u.int64);
299         } else if (strcmp(key, "stride") == 0) {
300             stride = static_cast<int>(list->values[i].u.int64);
301         } else if (strcmp(key, "data") == 0) {
302             data = static_cast<uchar *>(list->values[i].u.ba->data);
303         }
304     }
305
306     if (data != nullptr) {
307         QImage img = QImage(data, width, height, stride, QImage::Format_RGB32);
308         img.bits();
309         emit snapshotReady(img);
310     }
311 }
312
313 #endif
314
315 void MediaMPV::init() {}
316
317 Media::State MediaMPV::state() const {
318     return currentState;
319 }
320
321 void MediaMPV::play(const QString &file) {
322     const QByteArray fileUtf8 = file.toUtf8();
323     const char *args[] = {"loadfile", fileUtf8.constData(), nullptr};
324     sendCommand(args);
325
326     clearTrackState();
327     if (currentState == Media::PausedState) play();
328 }
329
330 void MediaMPV::play() {
331     int flag = 0;
332     mpv_set_property_async(mpv, 0, "pause", MPV_FORMAT_FLAG, &flag);
333 }
334
335 void MediaMPV::pause() {
336     int flag = 1;
337     mpv_set_property_async(mpv, 0, "pause", MPV_FORMAT_FLAG, &flag);
338 }
339
340 void MediaMPV::stop() {
341     const char *args[] = {"stop", nullptr};
342     sendCommand(args);
343 }
344
345 void MediaMPV::seek(qint64 ms) {
346     double seconds = ms / 1000.;
347     QByteArray ba = QString::number(seconds).toUtf8();
348     const char *args[] = {"seek", ba.constData(), "absolute", nullptr};
349     sendCommand(args);
350 }
351
352 QString MediaMPV::file() const {
353     char *path;
354     mpv_get_property(mpv, "path", MPV_FORMAT_STRING, &path);
355     return QString::fromUtf8(path);
356 }
357
358 void MediaMPV::setBufferMilliseconds(qint64 value) {
359     Q_UNUSED(value);
360     // Not implemented
361 }
362
363 void MediaMPV::setUserAgent(const QString &value) {
364     mpv_set_option_string(mpv, "user-agent", value.toUtf8());
365 }
366
367 void MediaMPV::enqueue(const QString &file) {
368     const QByteArray fileUtf8 = file.toUtf8();
369     const char *args[] = {"loadfile", fileUtf8.constData(), "append", nullptr};
370     sendCommand(args);
371 }
372
373 void MediaMPV::clearQueue() {
374     const char *args[] = {"playlist-clear", nullptr};
375     sendCommand(args);
376 }
377
378 bool MediaMPV::hasQueue() const {
379     mpv_node node;
380     int r = mpv_get_property(mpv, "playlist", MPV_FORMAT_NODE, &node);
381     if (r < 0) return false;
382     QVariant v = mpv::qt::node_to_variant(&node);
383     mpv_free_node_contents(&node);
384     QVariantList list = v.toList();
385     return list.count() > 1;
386 }
387
388 qint64 MediaMPV::position() const {
389     double seconds;
390     mpv_get_property(mpv, "time-pos", MPV_FORMAT_DOUBLE, &seconds);
391     return seconds * 1000.;
392 }
393
394 qint64 MediaMPV::duration() const {
395     double seconds;
396     mpv_get_property(mpv, "duration", MPV_FORMAT_DOUBLE, &seconds);
397     return seconds * 1000.;
398 }
399
400 qint64 MediaMPV::remainingTime() const {
401     double seconds;
402     mpv_get_property(mpv, "time-remaining", MPV_FORMAT_DOUBLE, &seconds);
403     return seconds * 1000.;
404 }
405
406 qreal MediaMPV::volume() const {
407     double vol;
408     mpv_get_property(mpv, "volume", MPV_FORMAT_DOUBLE, &vol);
409     return vol / 100.;
410 }
411
412 void MediaMPV::setVolume(qreal value) {
413     double percent = value * 100.;
414     mpv_set_property_async(mpv, 0, "volume", MPV_FORMAT_DOUBLE, &percent);
415 }
416
417 bool MediaMPV::volumeMuted() const {
418     int mute;
419     mpv_get_property(mpv, "mute", MPV_FORMAT_FLAG, &mute);
420     return mute == 1;
421 }
422
423 void MediaMPV::setVolumeMuted(bool value) {
424     int mute = value ? 1 : 0;
425     mpv_set_property(mpv, "mute", MPV_FORMAT_FLAG, &mute);
426 }
427
428 QString MediaMPV::errorString() const {
429     return lastErrorString;
430 }