From ef653527fb6ea751ae7f66c203cd352d6bd6ecba Mon Sep 17 00:00:00 2001
From: Elliu <elliu@hashi.re>
Date: Sun, 19 Dec 2021 21:43:21 +0100
Subject: [PATCH] WIP: first take at qt_window player module

---
 CMakeLists.txt                               |  42 ++-
 src/module/module_qt_window.c                | 114 ++++++
 src/module/qt_window/mpvwidget.cpp           | 160 +++++++++
 src/module/qt_window/mpvwidget.hh            |  42 +++
 src/module/qt_window/mpvwidget_interface.cpp |   7 +
 src/module/qt_window/mpvwidget_interface.h   |  18 +
 src/module/qt_window/qthelper.cpp            | 353 +++++++++++++++++++
 7 files changed, 734 insertions(+), 2 deletions(-)
 create mode 100644 src/module/module_qt_window.c
 create mode 100644 src/module/qt_window/mpvwidget.cpp
 create mode 100644 src/module/qt_window/mpvwidget.hh
 create mode 100644 src/module/qt_window/mpvwidget_interface.cpp
 create mode 100644 src/module/qt_window/mpvwidget_interface.h
 create mode 100644 src/module/qt_window/qthelper.cpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index b1c20ba1..d9d8d8d2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -6,7 +6,7 @@ if(CMAKE_BUILD_TYPE STREQUAL "Release")
     set(CMAKE_BUILD_TYPE RelWithDebInfo)
 endif()
 
-project(lektor VERSION ${LKT_VERSION} LANGUAGES C)
+project(lektor VERSION ${LKT_VERSION} LANGUAGES C CXX)
 
 cmake_policy(SET CMP0100 NEW)
 cmake_policy(SET CMP0009 NEW)
@@ -68,6 +68,14 @@ find_package(SDL2                           REQUIRED version>=2.0)      # tested
 find_package(SDL2_image                     REQUIRED version>=2.0)      # same as above ^
 find_package(CURL                           REQUIRED HTTP HTTPS)        # tested with 7.74.0
 
+# For Qt
+set(CMAKE_AUTOUIC ON)
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTORCC ON)
+# Find Qt dependencies
+find_package(QT NAMES Qt6 Qt5      COMPONENTS Widgets REQUIRED)
+find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED)
+
 find_program(MKVPROPEDIT        mkvpropedit REQUIRED)   # To DL karas and apply metadata
 find_program(XXD                xxd         REQUIRED)   # To embed the sqlite db schemas into the binary
 
@@ -138,6 +146,10 @@ set(lektor_module_SOURCES
     src/module/module_repo.c
     src/module/mpv.c
     src/module/module_sdl2.c
+    src/module/module_qt_window.c
+    src/module/qt_window/mpvwidget.cpp
+    src/module/qt_window/mpvwidget_interface.cpp
+    src/module/qt_window/qthelper.cpp
 )
 
 set(lektor_mkv_SOURCES
@@ -227,7 +239,18 @@ add_custom_command(OUTPUT ${SQL_GENERATED_FILE}
     COMMENT "Generating SQL included files to embed them"
 )
 
-add_executable(lektord ${lektord_SOURCES} ${SQL_GENERATED_FILE})
+if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
+    qt_add_executable(lektord
+        MANUAL_FINALIZATION
+        ${lektord_SOURCES}
+        ${SQL_GENERATED_FILE}
+    )
+else()
+    add_executable(lektord
+        ${lektord_SOURCES}
+        ${SQL_GENERATED_FILE}
+    )
+endif()
 add_executable(lkt     ${lkt_SOURCES})
 add_executable(luka    ${luka_SOURCES} ${SQL_GENERATED_FILE})
 
@@ -255,6 +278,7 @@ target_link_libraries(lektord PRIVATE
     ${SDL2_LIBRARIES}
     ${CURL_LIBRARIES}
     ${SDL2_IMAGE_LIBRARIES}
+    Qt${QT_VERSION_MAJOR}::Widgets
 )
 target_link_libraries(luka PRIVATE
     ${MPV_LIBRARY}
@@ -263,6 +287,7 @@ target_link_libraries(luka PRIVATE
     ${SDL2_LIBRARIES}
     ${CURL_LIBRARIES}
     ${SDL2_IMAGE_LIBRARIES}
+    Qt${QT_VERSION_MAJOR}::Widgets
 )
 
 target_include_directories(lkt  PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/inc)
@@ -279,6 +304,7 @@ target_include_directories(lektord PRIVATE
     ${CURL_INCLUDE_DIRS}
 )
 
+target_link_libraries(lektord PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)
 target_link_libraries(lektord PRIVATE -fopenmp)
 target_link_libraries(luka    PRIVATE -fopenmp)
 
@@ -292,6 +318,14 @@ target_compile_definitions(luka    PRIVATE ${common_DEFINITIONS})
 # target_precompile_headers(luka     PRIVATE ${common_HEADERS})
 
 target_compile_options(lektord PRIVATE ${COMMON_C_FLAGS} ${${CMAKE_C_COMPILER_ID}_C_FLAGS})
+# Prepare for Qt6
+target_compile_definitions(lektord PRIVATE
+    QT_DISABLE_DEPRECATED_BEFORE=0x050F00
+    QT_NO_CAST_TO_ASCII
+    QT_RESTRICTED_CAST_FROM_ASCII
+    QTCREATOR_UTILS_STATIC_LIB
+)
+
 target_compile_options(lkt     PRIVATE ${COMMON_C_FLAGS} ${${CMAKE_C_COMPILER_ID}_C_FLAGS})
 target_compile_options(luka    PRIVATE ${COMMON_C_FLAGS} ${${CMAKE_C_COMPILER_ID}_C_FLAGS})
 
@@ -320,3 +354,7 @@ foreach(CMD IN LISTS MANPAGE_COMMANDS)
         DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 # A cat 1 manpage
     )
 endforeach()
+
+if(QT_VERSION_MAJOR EQUAL 6)
+    qt_finalize_executable(lektord)
+endif()
diff --git a/src/module/module_qt_window.c b/src/module/module_qt_window.c
new file mode 100644
index 00000000..0284b0ed
--- /dev/null
+++ b/src/module/module_qt_window.c
@@ -0,0 +1,114 @@
+#define __LKT_MODULE_MAIN_SOURCE__
+#include <lektor/lktmodule.h>
+#include "qt_window/mpvwidget_interface.h"
+
+struct module_qt_window_s {
+    MpvWidget* mpv_widget;
+};
+
+/************************
+ * Function definitions *
+ ************************/
+
+static void module_qt_window_free(struct module_qt_window_s *);
+static void module_qt_window_close(struct module_qt_window_s *);
+static bool module_qt_window_new(struct module_qt_window_s **, struct queue *, lkt_db *);
+
+/*********************
+ * Private functions *
+ *********************/
+
+
+/********************************
+ * va_list version of functions *
+ ********************************/
+
+static int
+mod_new(va_list *va)
+{
+    va_list copy;
+    struct module_qt_window_s **win;
+    va_copy(copy, *va);
+    win                 = (struct module_qt_window_s **)va_arg(copy, void **);
+    struct queue *queue = va_arg(copy, struct queue *);
+    lkt_db *db          = va_arg(copy, lkt_db *);
+    bool ret            = module_qt_window_new(win, queue, db);
+    va_end(copy);
+    return !ret;
+}
+
+static int
+mod_close(va_list *va)
+{
+    va_list copy;
+    va_copy(copy, *va);
+    struct module_qt_window_s **win = (struct module_qt_window_s **)va_arg(copy, void **);
+    module_qt_window_close(*win);
+    va_end(copy);
+    return 0;
+}
+
+static int
+mod_free(va_list *va)
+{
+    va_list copy;
+    va_copy(copy, *va);
+    struct module_qt_window_s **win = (struct module_qt_window_s **)va_arg(copy, void **);
+    module_qt_window_free(*win);
+    va_end(copy);
+    return 0;
+}
+
+/********************
+ * The module stuff *
+ ********************/
+
+REG_BEGIN(qt_window_reg)
+REG_ADD_NAMED("new", mod_new)
+REG_ADD_NAMED("free", mod_free)
+REG_ADD_NAMED("close", mod_close)
+REG_END()
+#if !defined(LKT_STATIC_MODULE)
+REG_EXPORT(qt_window_reg)
+#endif
+
+/****************************
+ * Private helper functions *
+ ****************************/
+
+
+/***************************
+ * Function implementation *
+ ***************************/
+
+static bool
+module_qt_window_new(struct module_qt_window_s **win, struct queue *queue, lkt_db *db)
+{
+    (void)mod_new;
+    (void)mod_free;
+    (void)mod_close;
+    RETURN_UNLESS(win, "Invalid arguments", false);
+
+    if (*win == NULL) {
+        *win = (struct module_qt_window_s*)calloc(1, sizeof(struct module_qt_window_s));
+        RETURN_UNLESS(*win, "Out of memory", false);
+        memset(*win, 0, sizeof(struct module_qt_window_s));
+
+        (*win)->mpv_widget = ___create_mpv_widget(queue, db);
+    }
+
+    return true;
+}
+
+static void
+module_qt_window_close(struct module_qt_window_s *win)
+{
+    RETURN_UNLESS(win && win->mpv_widget, "Invalid arguments", NOTHING);
+}
+
+static void
+module_qt_window_free(struct module_qt_window_s *win)
+{
+    RETURN_UNLESS(win, "Invalid arguments", NOTHING);
+    module_qt_window_close(win);
+}
diff --git a/src/module/qt_window/mpvwidget.cpp b/src/module/qt_window/mpvwidget.cpp
new file mode 100644
index 00000000..201bfa3d
--- /dev/null
+++ b/src/module/qt_window/mpvwidget.cpp
@@ -0,0 +1,160 @@
+#include "mpvwidget.hh"
+
+#include <stdexcept>
+#include <QtGui/QOpenGLContext>
+#include <QtCore/QMetaObject>
+
+#include "qthelper.cpp"
+
+static void wakeup(void *ctx)
+{
+    QMetaObject::invokeMethod((MpvWidget*)ctx, "on_mpv_events", Qt::QueuedConnection);
+}
+
+static void *get_proc_address(void *ctx, const char *name) {
+    Q_UNUSED(ctx);
+    QOpenGLContext *glctx = QOpenGLContext::currentContext();
+    if (!glctx)
+        return nullptr;
+    return reinterpret_cast<void *>(glctx->getProcAddress(QByteArray(name)));
+}
+
+MpvWidget::MpvWidget(struct queue *queue, lkt_db *db)
+    : QOpenGLWidget(),
+    m_queue(queue),
+    m_db(db)
+{
+    setFocusPolicy(Qt::StrongFocus);
+    mpv = mpv_create();
+    if (!mpv)
+        throw std::runtime_error("could not create mpv context");
+
+    mpv_set_option_string(mpv, "input-default-bindings", "yes");
+    mpv_set_option_string(mpv, "input-vo-keyboard", "yes");
+    int val = 1;
+    mpv_set_option(mpv, "osc", MPV_FORMAT_FLAG, &val);
+
+    mpv_set_option_string(mpv, "terminal", "yes");
+    mpv_set_option_string(mpv, "msg-level", "all=v");
+    if (mpv_initialize(mpv) < 0)
+        throw std::runtime_error("could not initialize mpv context");
+
+    // Request hw decoding, just for testing.
+    mpv::qt::set_option_variant(mpv, "hwdec", "auto");
+
+    mpv_observe_property(mpv, 0, "duration", MPV_FORMAT_DOUBLE);
+    mpv_observe_property(mpv, 0, "time-pos", MPV_FORMAT_DOUBLE);
+    mpv_set_wakeup_callback(mpv, wakeup, this);
+}
+
+MpvWidget::~MpvWidget()
+{
+    makeCurrent();
+    if (mpv_gl)
+        mpv_render_context_free(mpv_gl);
+    mpv_terminate_destroy(mpv);
+}
+
+void MpvWidget::command(const QVariant& params)
+{
+    mpv::qt::command_variant(mpv, params);
+}
+
+void MpvWidget::setProperty(const QString& name, const QVariant& value)
+{
+    mpv::qt::set_property_variant(mpv, name, value);
+}
+
+QVariant MpvWidget::getProperty(const QString &name) const
+{
+    return mpv::qt::get_property_variant(mpv, name);
+}
+
+void MpvWidget::initializeGL()
+{
+    mpv_opengl_init_params gl_init_params{get_proc_address, nullptr, nullptr};
+    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},
+        {MPV_RENDER_PARAM_INVALID, nullptr}
+    };
+
+    if (mpv_render_context_create(&mpv_gl, mpv, params) < 0)
+        throw std::runtime_error("failed to initialize mpv GL context");
+    mpv_render_context_set_update_callback(mpv_gl, MpvWidget::on_update, reinterpret_cast<void *>(this));
+}
+
+void MpvWidget::paintGL()
+{
+    mpv_opengl_fbo mpfbo{static_cast<int>(defaultFramebufferObject()), width(), height(), 0};
+    int flip_y{1};
+
+    mpv_render_param params[] = {
+        {MPV_RENDER_PARAM_OPENGL_FBO, &mpfbo},
+        {MPV_RENDER_PARAM_FLIP_Y, &flip_y},
+        {MPV_RENDER_PARAM_INVALID, nullptr}
+    };
+    // See render_gl.h on what OpenGL environment mpv expects, and
+    // other API details.
+    mpv_render_context_render(mpv_gl, params);
+}
+
+void MpvWidget::on_mpv_events()
+{
+    // 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 MpvWidget::handle_mpv_event(mpv_event *event)
+{
+    switch (event->event_id) {
+    case MPV_EVENT_PROPERTY_CHANGE: {
+        mpv_event_property *prop = (mpv_event_property *)event->data;
+        if (strcmp(prop->name, "time-pos") == 0) {
+            if (prop->format == MPV_FORMAT_DOUBLE) {
+                double time = *(double *)prop->data;
+                Q_EMIT positionChanged((int)time);
+            }
+        } else if (strcmp(prop->name, "duration") == 0) {
+            if (prop->format == MPV_FORMAT_DOUBLE) {
+                double time = *(double *)prop->data;
+                Q_EMIT durationChanged((int)time);
+            }
+        }
+        break;
+    }
+    default: ;
+        // Ignore uninteresting or unknown events.
+    }
+}
+
+// Make Qt invoke mpv_render_context_render() to draw a new/updated video frame.
+void MpvWidget::maybeUpdate()
+{
+    // If the Qt window is not visible, Qt's update() will just skip rendering.
+    // This confuses mpv's render API, and may lead to small occasional
+    // freezes due to video rendering timing out.
+    // Handle this by manually redrawing.
+    // Note: Qt doesn't seem to provide a way to query whether update() will
+    //       be skipped, and the following code still fails when e.g. switching
+    //       to a different workspace with a reparenting window manager.
+    if (window()->isMinimized()) {
+        makeCurrent();
+        paintGL();
+        context()->swapBuffers(context()->surface());
+        doneCurrent();
+    } else {
+        update();
+    }
+}
+
+void MpvWidget::on_update(void *ctx)
+{
+    QMetaObject::invokeMethod((MpvWidget*)ctx, "maybeUpdate");
+}
diff --git a/src/module/qt_window/mpvwidget.hh b/src/module/qt_window/mpvwidget.hh
new file mode 100644
index 00000000..54a1f997
--- /dev/null
+++ b/src/module/qt_window/mpvwidget.hh
@@ -0,0 +1,42 @@
+#ifndef __LKT_MODULE_QT_WINDOW__
+#define __LKT_MODULE_QT_WINDOW__
+
+#include <QtWidgets/QOpenGLWidget>
+#include <mpv/client.h>
+#include <mpv/render_gl.h>
+#include <QtGui>
+
+#include <lektor/common.h>
+
+class MpvWidget Q_DECL_FINAL: public QOpenGLWidget
+{
+    Q_OBJECT
+public:
+    MpvWidget(struct queue *queue, lkt_db *db);
+    ~MpvWidget();
+    void command(const QVariant& params);
+    void setProperty(const QString& name, const QVariant& value);
+    QVariant getProperty(const QString& name) const;
+    QSize sizeHint() const { return QSize(480, 270);}
+Q_SIGNALS:
+    void durationChanged(int value);
+    void positionChanged(int value);
+protected:
+    void initializeGL() Q_DECL_OVERRIDE;
+    void paintGL() Q_DECL_OVERRIDE;
+private Q_SLOTS:
+    void on_mpv_events();
+    void maybeUpdate();
+public:
+    void handle_mpv_event(mpv_event *event);
+    static void on_update(void *ctx);
+
+    mpv_handle *mpv;
+    mpv_render_context *mpv_gl;
+
+private:
+    struct queue *m_queue;
+    lkt_db *m_db;
+};
+
+#endif // __LKT_MODULE_QT_WINDOW__
diff --git a/src/module/qt_window/mpvwidget_interface.cpp b/src/module/qt_window/mpvwidget_interface.cpp
new file mode 100644
index 00000000..5c6cd9c8
--- /dev/null
+++ b/src/module/qt_window/mpvwidget_interface.cpp
@@ -0,0 +1,7 @@
+#include "mpvwidget_interface.h"
+#include "mpvwidget.hh"
+
+MpvWidget*
+___create_mpv_widget(struct queue *queue, lkt_db* db){
+    return new MpvWidget(queue, db);
+}
diff --git a/src/module/qt_window/mpvwidget_interface.h b/src/module/qt_window/mpvwidget_interface.h
new file mode 100644
index 00000000..fa62dd73
--- /dev/null
+++ b/src/module/qt_window/mpvwidget_interface.h
@@ -0,0 +1,18 @@
+#ifndef __LKT_MODULE_QT_WINDOW_MPVWIDGET_INTERFACE_H__
+#define __LKT_MODULE_QT_WINDOW_MPVWIDGET_INTERFACE_H__
+
+#if defined( __cplusplus)
+extern "C" {
+class MpvWidget;
+#else
+typedef struct MpvWidget MpvWidget;
+#endif
+
+#include <lektor/common.h>
+
+MpvWidget* ___create_mpv_widget(struct queue *queue, lkt_db* db);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/src/module/qt_window/qthelper.cpp b/src/module/qt_window/qthelper.cpp
new file mode 100644
index 00000000..09c28c10
--- /dev/null
+++ b/src/module/qt_window/qthelper.cpp
@@ -0,0 +1,353 @@
+#ifndef LIBMPV_QTHELPER_H_
+#define LIBMPV_QTHELPER_H_
+
+#include <mpv/client.h>
+
+#include <cstring>
+
+#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(), (size_t)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);
+}
+
+}
+}
+
+Q_DECLARE_METATYPE(mpv::qt::ErrorReturn)
+
+#endif
-- 
GitLab