Making some more functional ui

This commit is contained in:
Chris Cochrun 2022-02-18 16:53:27 -06:00
parent ef1cb70d10
commit 2496f6708a
15 changed files with 218 additions and 236 deletions

154
src/mpv/mpvhelpers.h Normal file
View file

@ -0,0 +1,154 @@
#pragma once
// MpvObject definition
#define READONLY_PROP_BOOL(p, varName) \
public: \
Q_PROPERTY(bool varName READ varName NOTIFY varName##Changed) \
public Q_SLOTS: \
bool varName() const { return getProperty(p).toBool(); } \
Q_SIGNALS: \
void varName##Changed(bool value);
#define WRITABLE_PROP_BOOL(p, varName) \
public: \
Q_PROPERTY(bool varName READ varName WRITE set_##varName NOTIFY varName##Changed) \
public Q_SLOTS: \
bool varName() const { return getProperty(p).toBool(); } \
void set_##varName(bool value) { setProperty(p, value); } \
Q_SIGNALS: \
void varName##Changed(bool value);
#define READONLY_PROP_INT(p, varName) \
public: \
Q_PROPERTY(int varName READ varName NOTIFY varName##Changed) \
public Q_SLOTS: \
int varName() { return getProperty(p).toInt(); } \
Q_SIGNALS: \
void varName##Changed(int value);
#define WRITABLE_PROP_INT(p, varName) \
public: \
Q_PROPERTY(int varName READ varName WRITE set_##varName NOTIFY varName##Changed) \
public Q_SLOTS: \
int varName() { return getProperty(p).toInt(); } \
void set_##varName(int value) { setProperty(p, value); } \
Q_SIGNALS: \
void varName##Changed(int value);
#define READONLY_PROP_DOUBLE(p, varName) \
public: \
Q_PROPERTY(double varName READ varName NOTIFY varName##Changed) \
public Q_SLOTS: \
double varName() { return getProperty(p).toDouble(); } \
Q_SIGNALS: \
void varName##Changed(double value);
#define WRITABLE_PROP_DOUBLE(p, varName) \
public: \
Q_PROPERTY(double varName READ varName WRITE set_##varName NOTIFY varName##Changed) \
public Q_SLOTS: \
double varName() { return getProperty(p).toDouble(); } \
void set_##varName(double value) { setProperty(p, value); } \
Q_SIGNALS: \
void varName##Changed(double value);
#define READONLY_PROP_STRING(p, varName) \
public: \
Q_PROPERTY(QString varName READ varName NOTIFY varName##Changed) \
public Q_SLOTS: \
QString varName() { return getProperty(p).toString(); } \
Q_SIGNALS: \
void varName##Changed(QString value);
#define WRITABLE_PROP_STRING(p, varName) \
public: \
Q_PROPERTY(QString varName READ varName WRITE set_##varName NOTIFY varName##Changed) \
public Q_SLOTS: \
QString varName() { return getProperty(p).toString(); } \
void set_##varName(QString value) { setProperty(p, value); } \
Q_SIGNALS: \
void varName##Changed(QString value);
#define READONLY_PROP_ARRAY(p, varName) \
public: \
Q_PROPERTY(QVariantList varName READ varName NOTIFY varName##Changed) \
public Q_SLOTS: \
QVariantList varName() { return getProperty(p).toList(); } \
Q_SIGNALS: \
void varName##Changed(QVariantList value);
#define WRITABLE_PROP_ARRAY(p, varName) \
public: \
Q_PROPERTY(QVariantList varName READ varName WRITE set_##varName NOTIFY varName##Changed) \
public Q_SLOTS: \
QVariantList varName() { return getProperty(p).toList(); } \
void set_##varName(QVariantList value) { setProperty(p, value); } \
Q_SIGNALS: \
void varName##Changed(QVariantList value);
#define READONLY_PROP_MAP(p, varName) \
public: \
Q_PROPERTY(QVariantMap varName READ varName NOTIFY varName##Changed) \
public Q_SLOTS: \
QVariantMap varName() { return getProperty(p).toMap(); } \
Q_SIGNALS: \
void varName##Changed(QVariantMap value);
#define WRITABLE_PROP_MAP(p, varName) \
public: \
Q_PROPERTY(QVariantMap varName READ varName WRITE set_##varName NOTIFY varName##Changed) \
public Q_SLOTS: \
QVariantMap varName() { return getProperty(p).toMap(); } \
void set_##varName(QVariantMap value) { setProperty(p, value); } \
Q_SIGNALS: \
void varName##Changed(QVariantMap value);
// MpvObject() constructor
#define WATCH_PROP_BOOL(p) \
mpv_observe_property(mpv, 0, p, MPV_FORMAT_FLAG);
#define WATCH_PROP_DOUBLE(p) \
mpv_observe_property(mpv, 0, p, MPV_FORMAT_DOUBLE);
#define WATCH_PROP_INT(p) \
mpv_observe_property(mpv, 0, p, MPV_FORMAT_INT64);
#define WATCH_PROP_STRING(p) \
mpv_observe_property(mpv, 0, p, MPV_FORMAT_STRING);
#define WATCH_PROP_ARRAY(p) \
mpv_observe_property(mpv, 0, p, MPV_FORMAT_NODE_ARRAY);
#define WATCH_PROP_MAP(p) \
mpv_observe_property(mpv, 0, p, MPV_FORMAT_NODE_MAP);
// MpvObject::handle_mpv_event()
#define HANDLE_PROP_NONE(p, varName) \
(strcmp(prop->name, p) == 0) { \
int64_t value = 0; \
Q_EMIT varName##Changed(value); \
}
#define HANDLE_PROP_BOOL(p, varName) \
(strcmp(prop->name, p) == 0) { \
bool value = *(bool *)prop->data; \
Q_EMIT varName##Changed(value); \
}
#define HANDLE_PROP_INT(p, varName) \
(strcmp(prop->name, p) == 0) { \
int64_t value = *(int64_t *)prop->data; \
Q_EMIT varName##Changed(value); \
}
#define HANDLE_PROP_DOUBLE(p, varName) \
(strcmp(prop->name, p) == 0) { \
double value = *(double *)prop->data; \
Q_EMIT varName##Changed(value); \
}
#define HANDLE_PROP_STRING(p, varName) \
(strcmp(prop->name, p) == 0) { \
char* charValue = *(char**)prop->data; \
QString value = QString::fromUtf8(charValue); \
Q_EMIT varName##Changed(value); \
}
#define HANDLE_PROP_ARRAY(p, varName) \
(strcmp(prop->name, p) == 0) { \
QVariantList value = getProperty(p).toList(); \
Q_EMIT varName##Changed(value); \
}
#define HANDLE_PROP_MAP(p, varName) \
(strcmp(prop->name, p) == 0) { \
QVariantMap value = getProperty(p).toMap(); \
Q_EMIT varName##Changed(value); \
}

539
src/mpv/mpvobject.cpp Normal file
View file

@ -0,0 +1,539 @@
#include "mpvobject.h"
// std
#include <stdexcept>
#include <clocale>
// Qt
#include <QObject>
#include <QtGlobal>
#include <QOpenGLContext>
#include <QGuiApplication>
#include <QtQuick/QQuickWindow>
#include <QtQuick/QQuickView>
#include <QtQuick/QQuickFramebufferObject>
#include <QtGui/QOpenGLFramebufferObject>
#include <QtX11Extras/QX11Info>
#include <QDebug>
// libmpv
#include <mpv/client.h>
#include <mpv/render_gl.h>
// own
#include "qthelper.hpp"
//--- MpvRenderer
void* MpvRenderer::get_proc_address(void *ctx, const char *name) {
(void)ctx;
QOpenGLContext *glctx = QOpenGLContext::currentContext();
if (!glctx)
return nullptr;
return reinterpret_cast<void *>(glctx->getProcAddress(QByteArray(name)));
}
MpvRenderer::MpvRenderer(const MpvObject *obj)
: obj(obj)
, mpv_gl(nullptr)
{
// https://github.com/mpv-player/mpv/blob/master/libmpv/render_gl.h#L106
#if MPV_CLIENT_API_VERSION >= MPV_MAKE_VERSION(2, 0)
mpv_opengl_init_params gl_init_params{
get_proc_address,
nullptr // get_proc_address_ctx
};
#else
mpv_opengl_init_params gl_init_params{
get_proc_address,
nullptr, // get_proc_address_ctx
nullptr // extra_exts (deprecated)
};
#endif
mpv_render_param display{
MPV_RENDER_PARAM_INVALID,
nullptr
};
if (QX11Info::isPlatformX11()) {
display.type = MPV_RENDER_PARAM_X11_DISPLAY;
display.data = QX11Info::display();
}
mpv_render_param params[]{
{ MPV_RENDER_PARAM_API_TYPE, const_cast<char *>(MPV_RENDER_API_TYPE_OPENGL) },
{ MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params },
display,
{ MPV_RENDER_PARAM_INVALID, nullptr }
};
if (mpv_render_context_create(&mpv_gl, obj->mpv, params) < 0)
throw std::runtime_error("failed to initialize mpv GL context");
mpv_render_context_set_update_callback(mpv_gl, MpvObject::on_update, (void *)obj);
}
MpvRenderer::~MpvRenderer() {
if (mpv_gl)
mpv_render_context_free(mpv_gl);
mpv_terminate_destroy(obj->mpv);
}
void MpvRenderer::render() {
QOpenGLFramebufferObject *fbo = framebufferObject();
// fbo->bind();
obj->window()->resetOpenGLState();
// https://github.com/mpv-player/mpv/blob/master/libmpv/render_gl.h#L133
mpv_opengl_fbo mpfbo{
.fbo = static_cast<int>(fbo->handle()),
.w = fbo->width(),
.h = fbo->height(),
.internal_format = 0 // 0=unknown
};
mpv_render_param params[] = {
{ MPV_RENDER_PARAM_OPENGL_FBO, &mpfbo },
{ MPV_RENDER_PARAM_INVALID, nullptr }
};
mpv_render_context_render(mpv_gl, params);
obj->window()->resetOpenGLState();
// fbo->release();
}
//--- MpvObject
static void wakeup(void *ctx)
{
QMetaObject::invokeMethod((MpvObject*)ctx, "onMpvEvents", Qt::QueuedConnection);
}
MpvObject::MpvObject(QQuickItem *parent)
: QQuickFramebufferObject(parent)
, m_enableAudio(true)
, m_useHwdec(false)
, m_duration(0)
, m_position(0)
, m_isPlaying(false)
{
mpv = mpv_create();
if (!mpv)
throw std::runtime_error("could not create mpv context");
mpv_set_option_string(mpv, "terminal", "yes");
// mpv_set_option_string(mpv, "msg-level", "all=warn,ao/alsa=error");
// mpv_set_option_string(mpv, "msg-level", "all=debug");
//--- Hardware Decoding
mpv::qt::set_option_variant(mpv, "hwdec-codecs", "all");
if (mpv_initialize(mpv) < 0)
throw std::runtime_error("could not initialize mpv context");
mpv::qt::set_option_variant(mpv, "audio-client-name", "mpvz");
mpv_request_log_messages(mpv, "terminal-default");
//--- 60fps Interpolation
mpv::qt::set_option_variant(mpv, "interpolation", "yes");
mpv::qt::set_option_variant(mpv, "video-sync", "display-resample");
// mpv::qt::set_option_variant(mpv, "vf", "lavfi=\"fps=fps=60:round=down\"");
// mpv::qt::set_option_variant(mpv, "override-display-fps", "60");
//--- ytdl 1080p max
mpv::qt::set_option_variant(mpv, "ytdl-format", "ytdl-format=bestvideo[width<=?720]+bestaudio/best");
mpv::qt::set_option_variant(mpv, "quiet", "yes");
// Setup the callback that will make QtQuick update and redraw if there
// is a new video frame. Use a queued connection: this makes sure the
// doUpdate() function is run on the GUI thread.
// * MpvRender binds mpv_gl update function to MpvObject::on_update
// * MpvObject::on_update will emit MpvObject::mpvUpdated
connect(this, &MpvObject::mpvUpdated,
this, &MpvObject::doUpdate,
Qt::QueuedConnection);
WATCH_PROP_BOOL("idle-active")
WATCH_PROP_BOOL("mute")
WATCH_PROP_BOOL("pause")
WATCH_PROP_BOOL("paused-for-cache")
WATCH_PROP_BOOL("seekable")
WATCH_PROP_INT("chapter")
WATCH_PROP_INT("chapter-list/count")
WATCH_PROP_INT("decoder-frame-drop-count")
WATCH_PROP_INT("dheight")
WATCH_PROP_INT("dwidth")
WATCH_PROP_INT("estimated-frame-count")
WATCH_PROP_INT("estimated-frame-number")
WATCH_PROP_INT("frame-drop-count")
WATCH_PROP_INT("playlist-pos")
WATCH_PROP_INT("playlist/count")
WATCH_PROP_INT("vo-delayed-frame-count")
WATCH_PROP_INT("volume")
WATCH_PROP_INT("vid")
WATCH_PROP_INT("aid")
WATCH_PROP_INT("sid")
WATCH_PROP_INT("audio-params/channel-count")
WATCH_PROP_INT("audio-params/samplerate")
WATCH_PROP_INT("track-list/count")
WATCH_PROP_INT("contrast")
WATCH_PROP_INT("brightness")
WATCH_PROP_INT("gamma")
WATCH_PROP_INT("saturation")
WATCH_PROP_INT("sub-margin-y")
WATCH_PROP_DOUBLE("audio-bitrate")
WATCH_PROP_DOUBLE("avsync")
WATCH_PROP_DOUBLE("container-fps")
WATCH_PROP_DOUBLE("demuxer-cache-duration")
WATCH_PROP_DOUBLE("display-fps")
WATCH_PROP_DOUBLE("duration")
WATCH_PROP_DOUBLE("estimated-display-fps")
WATCH_PROP_DOUBLE("estimated-vf-fps")
WATCH_PROP_DOUBLE("fps")
WATCH_PROP_DOUBLE("speed")
WATCH_PROP_DOUBLE("time-pos")
WATCH_PROP_DOUBLE("video-bitrate")
WATCH_PROP_DOUBLE("video-params/aspect")
WATCH_PROP_DOUBLE("video-out-params/aspect")
WATCH_PROP_DOUBLE("window-scale")
WATCH_PROP_DOUBLE("current-window-scale")
WATCH_PROP_STRING("audio-codec")
WATCH_PROP_STRING("audio-codec-name")
WATCH_PROP_STRING("audio-params/format")
WATCH_PROP_STRING("filename")
WATCH_PROP_STRING("file-format")
WATCH_PROP_STRING("file-size")
WATCH_PROP_STRING("audio-format")
WATCH_PROP_STRING("hwdec")
WATCH_PROP_STRING("hwdec-current")
WATCH_PROP_STRING("hwdec-interop")
WATCH_PROP_STRING("media-title")
WATCH_PROP_STRING("path")
WATCH_PROP_STRING("video-codec")
WATCH_PROP_STRING("video-format")
WATCH_PROP_STRING("video-params/pixelformat")
WATCH_PROP_STRING("video-out-params/pixelformat")
WATCH_PROP_STRING("ytdl-format")
WATCH_PROP_MAP("demuxer-cache-state")
connect(this, &MpvObject::idleActiveChanged,
this, &MpvObject::updateState);
connect(this, &MpvObject::pausedChanged,
this, &MpvObject::updateState);
mpv_set_wakeup_callback(mpv, wakeup, this);
}
MpvObject::~MpvObject()
{
}
QQuickFramebufferObject::Renderer *MpvObject::createRenderer() const
{
window()->setPersistentOpenGLContext(true);
window()->setPersistentSceneGraph(true);
return new MpvRenderer(this);
}
void MpvObject::on_update(void *ctx)
{
MpvObject *self = (MpvObject *)ctx;
emit self->mpvUpdated();
}
// connected to mpvUpdated(); signal makes sure it runs on the GUI thread
void MpvObject::doUpdate()
{
update();
}
void MpvObject::command(const QVariant& params)
{
mpv::qt::command(mpv, params);
}
void MpvObject::commandAsync(const QVariant& params)
{
mpv::qt::command_async(mpv, params);
}
void MpvObject::setProperty(const QString& name, const QVariant& value)
{
mpv::qt::set_property_variant(mpv, name, value);
}
QVariant MpvObject::getProperty(const QString &name) const
{
return mpv::qt::get_property_variant(mpv, name);
}
void MpvObject::setOption(const QString& name, const QVariant& value)
{
mpv::qt::set_option_variant(mpv, name, value);
}
void MpvObject::onMpvEvents()
{
// Process all events, until the event queue is empty.
while (mpv) {
mpv_event *event = mpv_wait_event(mpv, 0);
if (event->event_id == MPV_EVENT_NONE) {
break;
}
handle_mpv_event(event);
}
}
void MpvObject::logPropChange(mpv_event_property *prop)
{
switch (prop->format) {
case MPV_FORMAT_NONE:
qDebug() << objectName() << "none" << prop->name << 0;
break;
case MPV_FORMAT_STRING:
qDebug() << objectName() << "str " << prop->name << *(char**)prop->data;
break;
case MPV_FORMAT_FLAG:
qDebug() << objectName() << "bool" << prop->name << *(bool *)prop->data;
break;
case MPV_FORMAT_INT64:
qDebug() << objectName() << "int " << prop->name << *(int64_t *)prop->data;
break;
case MPV_FORMAT_DOUBLE:
qDebug() << objectName() << "doub" << prop->name << *(double *)prop->data;
break;
case MPV_FORMAT_NODE_ARRAY:
qDebug() << objectName() << "arr " << prop->name; // TODO
break;
case MPV_FORMAT_NODE_MAP:
qDebug() << objectName() << "map " << prop->name; // TODO
break;
default:
qDebug() << objectName() << "prop(format=" << prop->format << ")" << prop->name;
break;
}
}
void MpvObject::handle_mpv_event(mpv_event *event)
{
// See: https://github.com/mpv-player/mpv/blob/master/libmpv/client.h
// See: https://github.com/mpv-player/mpv/blob/master/player/lua.c#L471
switch (event->event_id) {
case MPV_EVENT_LOG_MESSAGE: {
mpv_event_log_message *logData = (mpv_event_log_message *)event->data;
Q_EMIT logMessage(
QString(logData->prefix),
QString(logData->level),
QString(logData->text)
);
break;
}
case MPV_EVENT_START_FILE: {
Q_EMIT fileStarted();
break;
}
case MPV_EVENT_END_FILE: {
mpv_event_end_file *eef = (mpv_event_end_file *)event->data;
const char *reason;
switch (eef->reason) {
case MPV_END_FILE_REASON_EOF: reason = "eof"; break;
case MPV_END_FILE_REASON_STOP: reason = "stop"; break;
case MPV_END_FILE_REASON_QUIT: reason = "quit"; break;
case MPV_END_FILE_REASON_ERROR: reason = "error"; break;
case MPV_END_FILE_REASON_REDIRECT: reason = "redirect"; break;
default:
reason = "unknown";
}
Q_EMIT fileEnded(QString(reason));
break;
}
case MPV_EVENT_FILE_LOADED: {
Q_EMIT fileLoaded();
break;
}
case MPV_EVENT_PROPERTY_CHANGE: {
mpv_event_property *prop = (mpv_event_property *)event->data;
// logPropChange(prop);
if (prop->format == MPV_FORMAT_NONE) {
if HANDLE_PROP_NONE("vid", vid)
else if HANDLE_PROP_NONE("aid", aid)
else if HANDLE_PROP_NONE("sid", sid)
else if HANDLE_PROP_NONE("track-list/count", trackListCount)
} else if (prop->format == MPV_FORMAT_DOUBLE) {
if (strcmp(prop->name, "time-pos") == 0) {
double time = *(double *)prop->data;
m_position = time;
Q_EMIT positionChanged(time);
} else if (strcmp(prop->name, "duration") == 0) {
double time = *(double *)prop->data;
m_duration = time;
Q_EMIT durationChanged(time);
}
else if HANDLE_PROP_DOUBLE("audio-bitrate", audioBitrate)
else if HANDLE_PROP_DOUBLE("avsync", avsync)
else if HANDLE_PROP_DOUBLE("container-fps", containerFps)
else if HANDLE_PROP_DOUBLE("demuxer-cache-duration", demuxerCacheDuration)
else if HANDLE_PROP_DOUBLE("display-fps", displayFps)
else if HANDLE_PROP_DOUBLE("estimated-display-fps", estimatedDisplayFps)
else if HANDLE_PROP_DOUBLE("estimated-vf-fps", estimatedVfFps)
else if HANDLE_PROP_DOUBLE("fps", fps)
else if HANDLE_PROP_DOUBLE("speed", speed)
else if HANDLE_PROP_DOUBLE("video-bitrate", videoBitrate)
else if HANDLE_PROP_DOUBLE("video-params/aspect", videoParamsAspect)
else if HANDLE_PROP_DOUBLE("video-out-params/aspect", videoOutParamsAspect)
else if HANDLE_PROP_DOUBLE("window-scale", windowScale)
else if HANDLE_PROP_DOUBLE("current-window-scale", currentWindowScale)
} else if (prop->format == MPV_FORMAT_FLAG) {
if HANDLE_PROP_BOOL("idle-active", idleActive)
else if HANDLE_PROP_BOOL("mute", muted)
else if HANDLE_PROP_BOOL("pause", paused)
else if HANDLE_PROP_BOOL("paused-for-cache", pausedForCache)
else if HANDLE_PROP_BOOL("seekable", seekable)
} else if (prop->format == MPV_FORMAT_STRING) {
if HANDLE_PROP_STRING("audio-codec", audioCodec)
else if HANDLE_PROP_STRING("audio-codec-name", audioCodecName)
else if HANDLE_PROP_STRING("audio-params/format", audioParamsFormat)
else if HANDLE_PROP_STRING("filename", filename)
else if HANDLE_PROP_STRING("file-format", fileFormat)
else if HANDLE_PROP_STRING("file-size", fileSize)
else if HANDLE_PROP_STRING("audio-format", audioFormat)
else if HANDLE_PROP_STRING("hwdec", hwdec)
else if HANDLE_PROP_STRING("hwdec-current", hwdecCurrent)
else if HANDLE_PROP_STRING("hwdec-interop", hwdecInterop)
else if HANDLE_PROP_STRING("media-title", mediaTitle)
else if HANDLE_PROP_STRING("path", path)
else if HANDLE_PROP_STRING("video-codec", videoCodec)
else if HANDLE_PROP_STRING("video-format", videoFormat)
else if HANDLE_PROP_STRING("video-params/pixelformat", videoParamsPixelformat)
else if HANDLE_PROP_STRING("video-out-params/pixelformat", videoOutParamsPixelformat)
else if HANDLE_PROP_STRING("ytdl-format", ytdlFormat)
} else if (prop->format == MPV_FORMAT_INT64) {
if HANDLE_PROP_INT("chapter", chapter)
else if HANDLE_PROP_INT("chapter-list/count", chapterListCount)
else if HANDLE_PROP_INT("decoder-frame-drop-count", decoderFrameDropCount)
else if HANDLE_PROP_INT("dwidth", dwidth)
else if HANDLE_PROP_INT("dheight", dheight)
else if HANDLE_PROP_INT("estimated-frame-count", estimatedFrameCount)
else if HANDLE_PROP_INT("estimated-frame-number", estimatedFrameNumber)
else if HANDLE_PROP_INT("frame-drop-count", frameDropCount)
else if HANDLE_PROP_INT("playlist-pos", playlistPos)
else if HANDLE_PROP_INT("playlist/count", playlistCount)
else if HANDLE_PROP_INT("vo-delayed-frame-count", voDelayedFrameCount)
else if HANDLE_PROP_INT("volume", volume)
else if HANDLE_PROP_INT("vid", vid)
else if HANDLE_PROP_INT("aid", aid)
else if HANDLE_PROP_INT("sid", sid)
else if HANDLE_PROP_INT("audio-params/channel-count", audioParamsChannelCount)
else if HANDLE_PROP_INT("audio-params/samplerate", audioParamsSampleRate)
else if HANDLE_PROP_INT("track-list/count", trackListCount)
else if HANDLE_PROP_INT("contrast", contrast)
else if HANDLE_PROP_INT("brightness", brightness)
else if HANDLE_PROP_INT("gamma", gamma)
else if HANDLE_PROP_INT("saturation", saturation)
else if HANDLE_PROP_INT("sub-margin-y", subMarginY)
} else if (prop->format == MPV_FORMAT_NODE_MAP) {
if HANDLE_PROP_MAP("demuxer-cache-state", demuxerCacheState)
}
break;
}
default: ;
// Ignore uninteresting or unknown events.
}
}
void MpvObject::play()
{
// qDebug() << "play";
if (idleActive() && playlistCount() >= 1) { // File has finished playing.
// qDebug() << "\treload";
set_playlistPos(playlistPos()); // Reload and play file again.
}
if (!isPlaying()) {
// qDebug() << "\t!isPlaying";
set_paused(false);
}
}
void MpvObject::pause()
{
// qDebug() << "pause";
if (isPlaying()) {
// qDebug() << "!isPlaying";
set_paused(true);
}
}
void MpvObject::playPause()
{
if (isPlaying()) {
pause();
} else {
play();
}
}
void MpvObject::stop()
{
command(QVariantList() << "stop" << "keep-playlist");
}
void MpvObject::stepBackward()
{
command(QVariantList() << "frame-back-step");
}
void MpvObject::stepForward()
{
command(QVariantList() << "frame-step");
}
void MpvObject::seek(double pos)
{
// qDebug() << "seek" << pos;
pos = qMax(0.0, qMin(pos, m_duration));
commandAsync(QVariantList() << "seek" << pos << "absolute");
}
void MpvObject::loadFile(QVariant urls)
{
qDebug() << "Url being loaded: " << urls;
command(QVariantList() << "loadfile" << urls);
}
void MpvObject::subAdd(QVariant urls)
{
command(QVariantList() << "sub-add" << urls);
}
void MpvObject::updateState()
{
bool isNowPlaying = !idleActive() && !paused();
if (m_isPlaying != isNowPlaying) {
m_isPlaying = isNowPlaying;
emit isPlayingChanged(m_isPlaying);
}
}

202
src/mpv/mpvobject.h Normal file
View file

@ -0,0 +1,202 @@
#pragma once
#include "mpvhelpers.h"
// Qt
#include <QtQuick/QQuickItem>
#include <QtQuick/QQuickFramebufferObject>
// libmpv
#include <mpv/client.h>
#include <mpv/render_gl.h>
// own
#include "qthelper.hpp"
class MpvObject;
class MpvRenderer;
class MpvRenderer : public QQuickFramebufferObject::Renderer
{
static void* get_proc_address(void *ctx, const char *name);
public:
MpvRenderer(const MpvObject *obj);
virtual ~MpvRenderer();
void render();
private:
const MpvObject *obj;
mpv_render_context *mpv_gl;
};
class MpvObject : public QQuickFramebufferObject
{
Q_OBJECT
friend class MpvRenderer;
Q_PROPERTY(bool enableAudio READ enableAudio WRITE setEnableAudio NOTIFY enableAudioChanged)
Q_PROPERTY(bool useHwdec READ useHwdec WRITE setUseHwdec NOTIFY useHwdecChanged)
READONLY_PROP_BOOL("idle-active", idleActive)
WRITABLE_PROP_BOOL("mute", muted)
WRITABLE_PROP_BOOL("pause", paused)
READONLY_PROP_BOOL("paused-for-cache", pausedForCache)
READONLY_PROP_BOOL("seekable", seekable)
READONLY_PROP_INT("chapter", chapter)
READONLY_PROP_INT("chapter-list/count", chapterListCount) // OR "chapters"
READONLY_PROP_INT("decoder-frame-drop-count", decoderFrameDropCount)
READONLY_PROP_INT("dheight", dheight)
READONLY_PROP_INT("dwidth", dwidth)
READONLY_PROP_INT("estimated-frame-count", estimatedFrameCount)
READONLY_PROP_INT("estimated-frame-number", estimatedFrameNumber)
READONLY_PROP_INT("frame-drop-count", frameDropCount)
WRITABLE_PROP_INT("playlist-pos", playlistPos)
READONLY_PROP_INT("playlist/count", playlistCount)
WRITABLE_PROP_INT("vo-delayed-frame-count", voDelayedFrameCount)
WRITABLE_PROP_INT("volume", volume)
WRITABLE_PROP_INT("contrast", contrast)
WRITABLE_PROP_INT("brightness", brightness)
WRITABLE_PROP_INT("gamma", gamma)
WRITABLE_PROP_INT("saturation", saturation)
WRITABLE_PROP_INT("sub-margin-y", subMarginY)
READONLY_PROP_INT("vid", vid)
READONLY_PROP_INT("aid", aid)
READONLY_PROP_INT("sid", sid)
READONLY_PROP_INT("audio-params/channel-count", audioParamsChannelCount)
READONLY_PROP_INT("audio-params/samplerate", audioParamsSampleRate)
READONLY_PROP_INT("track-list/count", trackListCount)
READONLY_PROP_DOUBLE("audio-bitrate", audioBitrate)
READONLY_PROP_DOUBLE("avsync", avsync)
READONLY_PROP_DOUBLE("container-fps", containerFps)
READONLY_PROP_DOUBLE("demuxer-cache-duration", demuxerCacheDuration)
READONLY_PROP_DOUBLE("display-fps", displayFps)
READONLY_PROP_DOUBLE("estimated-display-fps", estimatedDisplayFps)
READONLY_PROP_DOUBLE("estimated-vf-fps", estimatedVfFps)
READONLY_PROP_DOUBLE("fps", fps) // Deprecated, use "container-fps"
WRITABLE_PROP_DOUBLE("speed", speed)
READONLY_PROP_DOUBLE("video-bitrate", videoBitrate)
READONLY_PROP_DOUBLE("video-params/aspect", videoParamsAspect)
READONLY_PROP_DOUBLE("video-out-params/aspect", videoOutParamsAspect)
WRITABLE_PROP_DOUBLE("window-scale", windowScale)
READONLY_PROP_DOUBLE("current-window-scale", currentWindowScale)
READONLY_PROP_STRING("audio-params/format", audioParamsFormat)
READONLY_PROP_STRING("audio-codec", audioCodec)
READONLY_PROP_STRING("audio-codec-name", audioCodecName)
READONLY_PROP_STRING("filename", filename)
READONLY_PROP_STRING("file-format", fileFormat)
READONLY_PROP_STRING("file-size", fileSize)
READONLY_PROP_STRING("audio-format", audioFormat)
WRITABLE_PROP_STRING("hwdec", hwdec)
READONLY_PROP_STRING("hwdec-current", hwdecCurrent)
READONLY_PROP_STRING("hwdec-interop", hwdecInterop)
READONLY_PROP_STRING("media-title", mediaTitle)
READONLY_PROP_STRING("path", path)
READONLY_PROP_STRING("video-codec", videoCodec)
READONLY_PROP_STRING("video-format", videoFormat)
READONLY_PROP_STRING("video-params/pixelformat", videoParamsPixelformat)
READONLY_PROP_STRING("video-out-params/pixelformat", videoOutParamsPixelformat)
READONLY_PROP_STRING("ytdl-format", ytdlFormat)
READONLY_PROP_MAP("demuxer-cache-state", demuxerCacheState)
public:
Q_PROPERTY(bool isPlaying READ isPlaying NOTIFY isPlayingChanged)
Q_PROPERTY(double duration READ duration NOTIFY durationChanged)
Q_PROPERTY(double position READ position NOTIFY positionChanged)
public:
MpvObject(QQuickItem *parent = nullptr);
virtual ~MpvObject();
virtual Renderer *createRenderer() const;
Q_INVOKABLE void setProperty(const QString& name, const QVariant& value);
Q_INVOKABLE QVariant getProperty(const QString& name) const;
Q_INVOKABLE void setOption(const QString& name, const QVariant& value);
Q_INVOKABLE QString getPlaylistFilename(int playlistIndex) const { return getProperty(QString("playlist/%1/filename").arg(playlistIndex)).toString(); }
Q_INVOKABLE QString getPlaylistTitle(int playlistIndex) const { return getProperty(QString("playlist/%1/title").arg(playlistIndex)).toString(); }
Q_INVOKABLE QString getChapterTitle(int chapterIndex) const { return getProperty(QString("chapter-list/%1/title").arg(chapterIndex)).toString(); }
Q_INVOKABLE double getChapterTime(int chapterIndex) const { return getProperty(QString("chapter-list/%1/time").arg(chapterIndex)).toDouble(); }
public slots:
void command(const QVariant& params);
void commandAsync(const QVariant& params);
void playPause();
void play();
void pause();
void stop();
void stepBackward();
void stepForward();
void seek(double pos);
void loadFile(QVariant urls);
void subAdd(QVariant urls);
bool enableAudio() const { return m_enableAudio; }
void setEnableAudio(bool value) {
if (m_enableAudio != value) {
m_enableAudio = value;
Q_EMIT enableAudioChanged(value);
}
if (!m_enableAudio) {
mpv::qt::set_option_variant(mpv, "ao", "null");
}
}
bool useHwdec() const { return m_useHwdec; }
void setUseHwdec(bool value) {
if (m_useHwdec != value) {
m_useHwdec = value;
if (m_useHwdec) {
mpv::qt::set_option_variant(mpv, "hwdec", "auto-copy");
} else {
mpv::qt::set_option_variant(mpv, "hwdec", "no");
}
Q_EMIT useHwdecChanged(value);
}
}
bool isPlaying() const { return m_isPlaying; }
void updateState();
double duration() const { return m_duration; }
double position() const { return m_position; }
signals:
void enableAudioChanged(bool value);
void useHwdecChanged(bool value);
void isPlayingChanged(bool value);
void durationChanged(double value); // Unit: seconds
void positionChanged(double value); // Unit: seconds
void mpvUpdated();
void logMessage(QString prefix, QString level, QString text);
void fileStarted();
void fileEnded(QString reason);
void fileLoaded();
private slots:
void onMpvEvents();
void doUpdate();
protected:
mpv_handle *mpv;
private:
void logPropChange(mpv_event_property *prop);
void handle_mpv_event(mpv_event *event);
static void on_update(void *ctx);
bool m_enableAudio;
bool m_useHwdec;
double m_duration;
double m_position;
bool m_isPlaying;
};

370
src/mpv/qthelper.hpp Normal file
View file

@ -0,0 +1,370 @@
#pragma once
// libmpv
#include <mpv/client.h>
// std
#include <cstring>
// Qt
#include <QVariant>
#include <QString>
#include <QList>
#include <QHash>
#include <QSharedPointer>
#include <QMetaType>
namespace mpv {
namespace qt {
// Wrapper around mpv_handle. Does refcounting under the hood.
class Handle
{
struct container {
container(mpv_handle *h) : mpv(h) {}
~container() { mpv_terminate_destroy(mpv); }
mpv_handle *mpv;
};
QSharedPointer<container> sptr;
public:
// Construct a new Handle from a raw mpv_handle with refcount 1. If the
// last Handle goes out of scope, the mpv_handle will be destroyed with
// mpv_terminate_destroy().
// Never destroy the mpv_handle manually when using this wrapper. You
// will create dangling pointers. Just let the wrapper take care of
// destroying the mpv_handle.
// Never create multiple wrappers from the same raw mpv_handle; copy the
// wrapper instead (that's what it's for).
static Handle FromRawHandle(mpv_handle *handle) {
Handle h;
h.sptr = QSharedPointer<container>(new container(handle));
return h;
}
// Return the raw handle; for use with the libmpv C API.
operator mpv_handle*() const { return sptr ? (*sptr).mpv : 0; }
};
static inline QVariant node_to_variant(const mpv_node *node)
{
switch (node->format) {
case MPV_FORMAT_STRING:
return QVariant(QString::fromUtf8(node->u.string));
case MPV_FORMAT_FLAG:
return QVariant(static_cast<bool>(node->u.flag));
case MPV_FORMAT_INT64:
return QVariant(static_cast<qlonglong>(node->u.int64));
case MPV_FORMAT_DOUBLE:
return QVariant(node->u.double_);
case MPV_FORMAT_NODE_ARRAY: {
mpv_node_list *list = node->u.list;
QVariantList qlist;
for (int n = 0; n < list->num; n++)
qlist.append(node_to_variant(&list->values[n]));
return QVariant(qlist);
}
case MPV_FORMAT_NODE_MAP: {
mpv_node_list *list = node->u.list;
QVariantMap qmap;
for (int n = 0; n < list->num; n++) {
qmap.insert(QString::fromUtf8(list->keys[n]),
node_to_variant(&list->values[n]));
}
return QVariant(qmap);
}
default: // MPV_FORMAT_NONE, unknown values (e.g. future extensions)
return QVariant();
}
}
struct node_builder {
node_builder(const QVariant& v) {
set(&node_, v);
}
~node_builder() {
free_node(&node_);
}
mpv_node *node() { return &node_; }
private:
Q_DISABLE_COPY(node_builder)
mpv_node node_;
mpv_node_list *create_list(mpv_node *dst, bool is_map, int num) {
dst->format = is_map ? MPV_FORMAT_NODE_MAP : MPV_FORMAT_NODE_ARRAY;
mpv_node_list *list = new mpv_node_list();
dst->u.list = list;
if (!list)
goto err;
list->values = new mpv_node[num]();
if (!list->values)
goto err;
if (is_map) {
list->keys = new char*[num]();
if (!list->keys)
goto err;
}
return list;
err:
free_node(dst);
return NULL;
}
char *dup_qstring(const QString &s) {
QByteArray b = s.toUtf8();
char *r = new char[b.size() + 1];
if (r)
std::memcpy(r, b.data(), b.size() + 1);
return r;
}
bool test_type(const QVariant &v, QMetaType::Type t) {
// The Qt docs say: "Although this function is declared as returning
// "QVariant::Type(obsolete), the return value should be interpreted
// as QMetaType::Type."
// So a cast really seems to be needed to avoid warnings (urgh).
return static_cast<int>(v.type()) == static_cast<int>(t);
}
void set(mpv_node *dst, const QVariant &src) {
if (test_type(src, QMetaType::QString)) {
dst->format = MPV_FORMAT_STRING;
dst->u.string = dup_qstring(src.toString());
if (!dst->u.string)
goto fail;
} else if (test_type(src, QMetaType::Bool)) {
dst->format = MPV_FORMAT_FLAG;
dst->u.flag = src.toBool() ? 1 : 0;
} else if (test_type(src, QMetaType::Int) ||
test_type(src, QMetaType::LongLong) ||
test_type(src, QMetaType::UInt) ||
test_type(src, QMetaType::ULongLong))
{
dst->format = MPV_FORMAT_INT64;
dst->u.int64 = src.toLongLong();
} else if (test_type(src, QMetaType::Double)) {
dst->format = MPV_FORMAT_DOUBLE;
dst->u.double_ = src.toDouble();
} else if (src.canConvert<QVariantList>()) {
QVariantList qlist = src.toList();
mpv_node_list *list = create_list(dst, false, qlist.size());
if (!list)
goto fail;
list->num = qlist.size();
for (int n = 0; n < qlist.size(); n++)
set(&list->values[n], qlist[n]);
} else if (src.canConvert<QVariantMap>()) {
QVariantMap qmap = src.toMap();
mpv_node_list *list = create_list(dst, true, qmap.size());
if (!list)
goto fail;
list->num = qmap.size();
for (int n = 0; n < qmap.size(); n++) {
list->keys[n] = dup_qstring(qmap.keys()[n]);
if (!list->keys[n]) {
free_node(dst);
goto fail;
}
set(&list->values[n], qmap.values()[n]);
}
} else {
goto fail;
}
return;
fail:
dst->format = MPV_FORMAT_NONE;
}
void free_node(mpv_node *dst) {
switch (dst->format) {
case MPV_FORMAT_STRING:
delete[] dst->u.string;
break;
case MPV_FORMAT_NODE_ARRAY:
case MPV_FORMAT_NODE_MAP: {
mpv_node_list *list = dst->u.list;
if (list) {
for (int n = 0; n < list->num; n++) {
if (list->keys)
delete[] list->keys[n];
if (list->values)
free_node(&list->values[n]);
}
delete[] list->keys;
delete[] list->values;
}
delete list;
break;
}
default: ;
}
dst->format = MPV_FORMAT_NONE;
}
};
/**
* RAII wrapper that calls mpv_free_node_contents() on the pointer.
*/
struct node_autofree {
mpv_node *ptr;
node_autofree(mpv_node *a_ptr) : ptr(a_ptr) {}
~node_autofree() { mpv_free_node_contents(ptr); }
};
/**
* Return the given property as mpv_node converted to QVariant, or QVariant()
* on error.
*
* @deprecated use get_property() instead
*
* @param name the property name
*/
static inline QVariant get_property_variant(mpv_handle *ctx, const QString &name)
{
mpv_node node;
if (mpv_get_property(ctx, name.toUtf8().data(), MPV_FORMAT_NODE, &node) < 0)
return QVariant();
node_autofree f(&node);
return node_to_variant(&node);
}
/**
* Set the given property as mpv_node converted from the QVariant argument.
* @deprecated use set_property() instead
*/
static inline int set_property_variant(mpv_handle *ctx, const QString &name,
const QVariant &v)
{
node_builder node(v);
return mpv_set_property(ctx, name.toUtf8().data(), MPV_FORMAT_NODE, node.node());
}
/**
* Set the given option as mpv_node converted from the QVariant argument.
*
* @deprecated use set_property() instead
*/
static inline int set_option_variant(mpv_handle *ctx, const QString &name,
const QVariant &v)
{
node_builder node(v);
return mpv_set_option(ctx, name.toUtf8().data(), MPV_FORMAT_NODE, node.node());
}
/**
* mpv_command_node() equivalent. Returns QVariant() on error (and
* unfortunately, the same on success).
*
* @deprecated use command() instead
*/
static inline QVariant command_variant(mpv_handle *ctx, const QVariant &args)
{
node_builder node(args);
mpv_node res;
if (mpv_command_node(ctx, node.node(), &res) < 0)
return QVariant();
node_autofree f(&res);
return node_to_variant(&res);
}
/**
* This is used to return error codes wrapped in QVariant for functions which
* return QVariant.
*
* You can use get_error() or is_error() to extract the error status from a
* QVariant value.
*/
struct ErrorReturn
{
/**
* enum mpv_error value (or a value outside of it if ABI was extended)
*/
int error;
ErrorReturn() : error(0) {}
explicit ErrorReturn(int err) : error(err) {}
};
/**
* Return the mpv error code packed into a QVariant, or 0 (success) if it's not
* an error value.
*
* @return error code (<0) or success (>=0)
*/
static inline int get_error(const QVariant &v)
{
if (!v.canConvert<ErrorReturn>())
return 0;
return v.value<ErrorReturn>().error;
}
/**
* Return whether the QVariant carries a mpv error code.
*/
static inline bool is_error(const QVariant &v)
{
return get_error(v) < 0;
}
/**
* Return the given property as mpv_node converted to QVariant, or QVariant()
* on error.
*
* @param name the property name
* @return the property value, or an ErrorReturn with the error code
*/
static inline QVariant get_property(mpv_handle *ctx, const QString &name)
{
mpv_node node;
int err = mpv_get_property(ctx, name.toUtf8().data(), MPV_FORMAT_NODE, &node);
if (err < 0)
return QVariant::fromValue(ErrorReturn(err));
node_autofree f(&node);
return node_to_variant(&node);
}
/**
* Set the given property as mpv_node converted from the QVariant argument.
*
* @return mpv error code (<0 on error, >= 0 on success)
*/
static inline int set_property(mpv_handle *ctx, const QString &name,
const QVariant &v)
{
node_builder node(v);
return mpv_set_property(ctx, name.toUtf8().data(), MPV_FORMAT_NODE, node.node());
}
/**
* mpv_command_node() equivalent.
*
* @param args command arguments, with args[0] being the command name as string
* @return the property value, or an ErrorReturn with the error code
*/
static inline QVariant command(mpv_handle *ctx, const QVariant &args)
{
node_builder node(args);
mpv_node res;
int err = mpv_command_node(ctx, node.node(), &res);
if (err < 0)
return QVariant::fromValue(ErrorReturn(err));
node_autofree f(&res);
return node_to_variant(&res);
}
/**
* mpv_command_node_async() equivalent.
*
* @param args command arguments, with args[0] being the command name as string
* @return empty QVariant, or an ErrorReturn with the error code
*/
static inline QVariant command_async(mpv_handle *ctx, const QVariant &args)
{
node_builder node(args);
quint64 replyUserdata = 0; // TODO: Bomi casted args[0] to int. Bomi's args was a QByteArray however.
int err = mpv_command_node_async(ctx, replyUserdata, node.node());
if (err < 0)
return QVariant::fromValue(ErrorReturn(err));
// TODO: Return unique replyUserdata so the app can wait for
// the result in an MPV_EVENT_COMMAND_REPLY event.
return QVariant();
}
}
}
Q_DECLARE_METATYPE(mpv::qt::ErrorReturn)