Skip to content
Extraits de code Groupes Projets
Vérifiée Valider ef653527 rédigé par Elliu's avatar Elliu
Parcourir les fichiers

WIP: first take at qt_window player module

parent 6bf46b95
Branches
Étiquettes
1 requête de fusion!186Add the Qt window module as an alternative to the SDL2 module
......@@ -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()
#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);
}
#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");
}
#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__
#include "mpvwidget_interface.h"
#include "mpvwidget.hh"
MpvWidget*
___create_mpv_widget(struct queue *queue, lkt_db* db){
return new MpvWidget(queue, db);
}
#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
#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
0% Chargement en cours ou .
You are about to add 0 people to the discussion. Proceed with caution.
Veuillez vous inscrire ou vous pour commenter