diff --git a/CMakeLists.txt b/CMakeLists.txt index 33a356f..e8a2ecf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,26 +1,30 @@ cmake_minimum_required(VERSION 3.16) project(presenter) -set(KF_MIN_VERSION "5.68.0") -set(QT_MIN_VERSION "5.12.0") +include(FeatureSummary) + +set(QT5_MIN_VERSION 5.15) +set(KF5_MIN_VERSION 5.83) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) -find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE) +find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) -set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH}) include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) -include(FeatureSummary) +include(ECMSetupVersion) +include(ECMGenerateHeaders) +include(ECMPoQmTools) kde_enable_exceptions() -find_package(Qt5 ${QT_MIN_VERSION} REQUIRED NO_MODULE COMPONENTS Core Quick Test Gui QuickControls2 Widgets Sql) +find_package(Qt5 ${QT_MIN_VERSION} REQUIRED NO_MODULE COMPONENTS Core Quick Test Gui QuickControls2 Widgets Sql X11Extras) find_package(KF5 ${KF_MIN_VERSION} REQUIRED COMPONENTS Kirigami2 I18n CoreAddons) set(CMAKE_CXX_STANDARD 17) diff --git a/qhot-profile.json b/qhot-profile.json new file mode 100644 index 0000000..ba2d53b --- /dev/null +++ b/qhot-profile.json @@ -0,0 +1,14 @@ +{ + "import-path": [ + "/lib", + "/usr/lib", + "src/qml" + ], + "plugin-path": [ + "/lib", + "/usr/lib", + "/usr/lib/qt/qml/org/kde/kirigami.2/", + "src/qml" + ], + "style": "kirigami" +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3b6433a..a7250b8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,6 +4,8 @@ target_sources(presenter PRIVATE main.cpp resources.qrc songlistmodel.cpp songlistmodel.h + mpvobject.h mpvobject.cpp + qthelper.hpp mpvhelpers.h ) target_link_libraries(presenter @@ -13,8 +15,10 @@ target_link_libraries(presenter Qt5::QuickControls2 Qt5::Widgets Qt5::Sql + Qt5::X11Extras KF5::Kirigami2 KF5::I18n + mpv ) target_compile_options (presenter PUBLIC -fexceptions) diff --git a/src/main.cpp b/src/main.cpp index 188f178..5d6f8c5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,8 +9,18 @@ #include #include +#include +#include +#include +#include + +#include + +#include +#include + #include "songlistmodel.h" -// #include "mpvobject.h" +#include "mpvobject.h" int main(int argc, char *argv[]) { @@ -21,6 +31,10 @@ int main(int argc, char *argv[]) QCoreApplication::setOrganizationDomain(QStringLiteral("tfcconnection.org")); QCoreApplication::setApplicationName(QStringLiteral("Church Presenter")); + // apparently mpv needs this class set + std::setlocale(LC_NUMERIC, "C"); + qmlRegisterType("mpv", 1, 0, "MpvObject"); + SongListModel songListModel; // path = QQmlEngine::importPathList() @@ -44,3 +58,148 @@ int main(int argc, char *argv[]) return app.exec(); } + +// namespace +// { +// void on_mpv_events(void *ctx) +// { +// Q_UNUSED(ctx) +// } + +// void on_mpv_redraw(void *ctx) +// { +// MpvObject::on_update(ctx); +// } + +// static void *get_proc_address_mpv(void *ctx, const char *name) +// { +// Q_UNUSED(ctx) + +// QOpenGLContext *glctx = QOpenGLContext::currentContext(); +// if (!glctx) return nullptr; + +// return reinterpret_cast(glctx->getProcAddress(QByteArray(name))); +// } + +// } + +// class MpvRenderer : public QQuickFramebufferObject::Renderer +// { +// MpvObject *obj; + +// public: +// MpvRenderer(MpvObject *new_obj) +// : obj{new_obj} +// { +// mpv_set_wakeup_callback(obj->mpv, on_mpv_events, nullptr); +// } + +// virtual ~MpvRenderer() +// {} + +// // This function is called when a new FBO is needed. +// // This happens on the initial frame. +// QOpenGLFramebufferObject * createFramebufferObject(const QSize &size) +// { +// // init mpv_gl: +// if (!obj->mpv_gl) +// { +// mpv_opengl_init_params gl_init_params{get_proc_address_mpv, nullptr, nullptr}; +// mpv_render_param params[]{ +// {MPV_RENDER_PARAM_API_TYPE, const_cast(MPV_RENDER_API_TYPE_OPENGL)}, +// {MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params}, +// {MPV_RENDER_PARAM_INVALID, nullptr} +// }; + +// if (mpv_render_context_create(&obj->mpv_gl, obj->mpv, params) < 0) +// throw std::runtime_error("failed to initialize mpv GL context"); +// mpv_render_context_set_update_callback(obj->mpv_gl, on_mpv_redraw, obj); +// } + +// return QQuickFramebufferObject::Renderer::createFramebufferObject(size); +// } + +// void render() +// { +// obj->window()->resetOpenGLState(); + +// QOpenGLFramebufferObject *fbo = framebufferObject(); +// mpv_opengl_fbo mpfbo{.fbo = static_cast(fbo->handle()), .w = fbo->width(), .h = fbo->height(), .internal_format = 0}; +// int flip_y{0}; + +// mpv_render_param params[] = { +// // Specify the default framebuffer (0) as target. This will +// // render onto the entire screen. If you want to show the video +// // in a smaller rectangle or apply fancy transformations, you'll +// // need to render into a separate FBO and draw it manually. +// {MPV_RENDER_PARAM_OPENGL_FBO, &mpfbo}, +// // Flip rendering (needed due to flipped GL coordinate system). +// {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(obj->mpv_gl, params); + +// obj->window()->resetOpenGLState(); +// } +// }; + + +// MpvObject::MpvObject(QQuickItem * parent) +// : QQuickFramebufferObject(parent), mpv{mpv_create()}, mpv_gl(nullptr) +// { +// 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=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"); + +// connect(this, &MpvObject::onUpdate, this, &MpvObject::doUpdate, +// Qt::QueuedConnection); +// } + +// MpvObject::~MpvObject() +// { +// if (mpv_gl) // only initialized if something got drawn +// { +// mpv_render_context_free(mpv_gl); +// } + +// mpv_terminate_destroy(mpv); +// } + +// void MpvObject::on_update(void *ctx) +// { +// MpvObject *self = (MpvObject *)ctx; +// emit self->onUpdate(); +// } + +// // connected to onUpdate(); signal makes sure it runs on the GUI thread +// void MpvObject::doUpdate() +// { +// update(); +// } + +// void MpvObject::command(const QVariant& params) +// { +// mpv::qt::command_variant(mpv, params); +// } + +// void MpvObject::setProperty(const QString& name, const QVariant& value) +// { +// mpv::qt::set_property_variant(mpv, name, value); +// } + +// QQuickFramebufferObject::Renderer *MpvObject::createRenderer() const +// { +// window()->setPersistentOpenGLContext(true); +// window()->setPersistentSceneGraph(true); +// return new MpvRenderer(const_cast(this)); +// } diff --git a/src/mpvhelpers.h b/src/mpvhelpers.h new file mode 100644 index 0000000..302708b --- /dev/null +++ b/src/mpvhelpers.h @@ -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); \ + } diff --git a/src/mpvobject.cpp b/src/mpvobject.cpp new file mode 100644 index 0000000..0fc4ae3 --- /dev/null +++ b/src/mpvobject.cpp @@ -0,0 +1,539 @@ +#include "mpvobject.h" + +// std +#include +#include + +// Qt +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include + +#include + +// libmpv +#include +#include + +// 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(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(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(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); + } +} + diff --git a/src/mpvobject.h b/src/mpvobject.h new file mode 100644 index 0000000..f410b77 --- /dev/null +++ b/src/mpvobject.h @@ -0,0 +1,202 @@ +#pragma once + +#include "mpvhelpers.h" + +// Qt +#include +#include + +// libmpv +#include +#include + +// 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; +}; diff --git a/src/qml/presenter/Library.qml b/src/qml/presenter/Library.qml index db13b0e..b3a2b8b 100644 --- a/src/qml/presenter/Library.qml +++ b/src/qml/presenter/Library.qml @@ -85,6 +85,7 @@ Item { hoverEnabled: true onClicked: { ListView.view.currentIndex = index + song = ListView.view.selected songTitle = title songLyrics = lyrics songAuthor = author diff --git a/src/qml/presenter/MainWindow.qml b/src/qml/presenter/MainWindow.qml index 3ea2476..2a4ae35 100644 --- a/src/qml/presenter/MainWindow.qml +++ b/src/qml/presenter/MainWindow.qml @@ -11,7 +11,9 @@ import "./" as Presenter Controls.Page { id: mainPage padding: 0 - property url background: "" + property url imageBackground: "" + property url videoBackground: "" + property var song property string songTitle: "" property string songLyrics: "" property string songAuthor: "" @@ -79,27 +81,22 @@ Controls.Page { } Presenter.Slide { id: presentationSlide - imageSource: "../../assets/parallel.jpg" + imageSource: imageBackground + videoSource: videoBackground } } } FileDialog { - id: fileDialog + id: videoFileDialog title: "Please choose a background" folder: shortcuts.home selectMultiple: false - nameFilters: ["Video files (*.mp4 *.mkv *.mov *.wmv *.avi *.MP4 *.MOV *.MKV)", - "Image files (*.jpg *.jpeg *.png *.JPG *.JPEG *.PNG)"] + nameFilters: ["Video files (*.mp4 *.mkv *.mov *.wmv *.avi *.MP4 *.MOV *.MKV)"] onAccepted: { - print("You chose: " + fileDialog.fileUrls); - videoBackground = fileDialog.fileUrl; - print(videoBackground); - - str = videoBackground.toString(); - if (str.endsWith("mp4")) - videoBackground = fileDialog.fileUrl; print("WE DID IT!!"); - + imageBackground = "" + videoBackground = videoFileDialog.fileUrls[0] + print("video background = " + videoFileDialog.fileUrl) } onRejected: { print("Canceled") @@ -108,8 +105,20 @@ Controls.Page { } - function endsWith(str, suffix) { - return str.indexOf(suffix, str.length - suffix.length) !== -1; - } + FileDialog { + id: imageFileDialog + title: "Please choose a background" + folder: shortcuts.home + selectMultiple: false + nameFilters: ["Image files (*.jpg *.jpeg *.png *.JPG *.JPEG *.PNG)"] + onAccepted: { + videoBackground = "" + imageBackground = imageFileDialog.fileUrls + } + onRejected: { + print("Canceled") + /* Qt.quit() */ + } + } } diff --git a/src/qml/presenter/Slide.qml b/src/qml/presenter/Slide.qml index 9f6c731..9893881 100644 --- a/src/qml/presenter/Slide.qml +++ b/src/qml/presenter/Slide.qml @@ -1,11 +1,12 @@ import QtQuick 2.13 import QtQuick.Controls 2.15 as Controls import QtQuick.Layouts 1.2 -import QtMultimedia 5.15 +/* import QtMultimedia 5.15 */ import QtAudioEngine 1.15 import QtGraphicalEffects 1.15 import org.kde.kirigami 2.13 as Kirigami import "./" as Presenter +import mpv 1.0 Item { id: root @@ -14,32 +15,66 @@ Item { property real textSize: 50 property bool editMode: false property bool dropShadow: false - property url imageSource: "" - property url videoSource: "" + property url imageSource: imageBackground + property url videoSource: videoBackground property string chosenFont: "Quicksand" + property color backgroundColor Rectangle { id: basePrColor anchors.fill: parent color: "black" - MediaPlayer { - id: videoPlayer - source: videoSource - loops: MediaPlayer.Infinite - autoPlay: true - notifyInterval: 100 + /* MediaPlayer { */ + /* id: mediaPlayer */ + /* source: videoSource */ + /* loops: MediaPlayer.Infinite */ + /* autoPlay: editMode ? false : true */ + /* notifyInterval: 100 */ + /* } */ + + /* VideoOutput { */ + /* id: videoPlayer */ + /* anchors.fill: parent */ + /* source: mediaPlayer */ + /* /\* flushMode: VideoOutput.LastFrame *\/ */ + /* MouseArea { */ + /* id: playArea */ + /* anchors.fill: parent */ + /* onPressed: mediaPlayer.play(); */ + /* } */ + /* } */ + + MpvObject { + id: mpv + objectName: "mpv" + anchors.fill: parent + useHwdec: true + Component.onCompleted: mpvLoadingTimer.start() + onFileLoaded: { + print(videoSource + " has been loaded"); + mpv.setProperty("loop", "inf"); + print(mpv.getProperty("loop")); + } + + MouseArea { + id: playArea + anchors.fill: parent + onPressed: mpv.loadFile(videoSource.toString()); + } + + /* Controls.ProgressBar { */ + /* anchors.centerIn: parent */ + /* width: parent.width - 400 */ + /* value: mpv.position */ + /* to: mpv.duration */ + /* } */ } - VideoOutput { - id: videoOutput - anchors.fill: parent - source: videoPlayer - } - MouseArea { - id: playArea - anchors.fill: parent - onPressed: videoPlayer.play(); + Timer { + id: mpvLoadingTimer + interval: 100 + onTriggered: mpv.loadFile(videoSource.toString()) } Image { @@ -48,13 +83,14 @@ Item { source: imageSource fillMode: Image.PreserveAspectCrop clip: true + visible: false } FastBlur { id: imageBlue anchors.fill: parent - source: backgroundImage + source: imageSource == "" ? mpv : backgroundImage radius: blurRadius Controls.Label { @@ -77,7 +113,6 @@ Item { color: "#80000000" visible: true } - } } } diff --git a/src/qml/presenter/SlideEditor.qml b/src/qml/presenter/SlideEditor.qml index 7561eeb..cbc486e 100644 --- a/src/qml/presenter/SlideEditor.qml +++ b/src/qml/presenter/SlideEditor.qml @@ -13,6 +13,6 @@ Item { Presenter.Slide { id: representation - + editMode: true } } diff --git a/src/qml/presenter/SongEditor.qml b/src/qml/presenter/SongEditor.qml index 5a82f87..d0970ed 100644 --- a/src/qml/presenter/SongEditor.qml +++ b/src/qml/presenter/SongEditor.qml @@ -55,11 +55,38 @@ Item { onClicked: {} } Controls.ToolButton { + id: backgroundButton text: "Background" icon.name: "fileopen" - onClicked: { - print("Action button in buttons page clicked"); - fileDialog.open() + onClicked: backgroundType.open() + } + + Controls.Popup { + id: backgroundType + x: backgroundButton.x + y: backgroundButton.y + backgroundButton.height + 20 + width: 200 + height: 100 + modal: true + focus: true + dim: false + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent + ColumnLayout { + anchors.fill: parent + Controls.ToolButton { + Layout.fillHeight: true + Layout.fillWidth: true + text: "Video" + icon.name: "emblem-videos-symbolic" + onClicked: videoFileDialog.open() & backgroundType.close() + } + Controls.ToolButton { + Layout.fillWidth: true + Layout.fillHeight: true + text: "Image" + icon.name: "folder-pictures-symbolic" + onClicked: imageFileDialog.open() & backgroundType.close() + } } } } @@ -106,7 +133,7 @@ Item { text: songLyrics textFormat: TextEdit.MarkdownText padding: 10 - onEditingFinished: editorTimer.running = false + onEditingFinished: song.lyricsSlides(text) onPressed: editorTimer.running = true } } diff --git a/src/qthelper.hpp b/src/qthelper.hpp new file mode 100644 index 0000000..27d2827 --- /dev/null +++ b/src/qthelper.hpp @@ -0,0 +1,370 @@ +#pragma once + +// libmpv +#include + +// std +#include + +// Qt +#include +#include +#include +#include +#include +#include + +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 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(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(node->u.flag)); + case MPV_FORMAT_INT64: + return QVariant(static_cast(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(v.type()) == static_cast(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 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 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()) + return 0; + return v.value().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) diff --git a/src/songlistmodel.cpp b/src/songlistmodel.cpp index 3c82608..a79c30c 100644 --- a/src/songlistmodel.cpp +++ b/src/songlistmodel.cpp @@ -1,4 +1,6 @@ #include "songlistmodel.h" +#include +#include SongListModel::SongListModel(QObject *parent) : QAbstractListModel(parent) @@ -59,3 +61,10 @@ QHash SongListModel::roleNames() const return mapping; } + +void SongListModel::lyricsSlides(QString lyrics) +{ + QTextStream stream(&lyrics); + QString line = stream.readLine(); + qDebug() << line; +} diff --git a/src/songlistmodel.h b/src/songlistmodel.h index ea2fb3d..94e3286 100644 --- a/src/songlistmodel.h +++ b/src/songlistmodel.h @@ -36,9 +36,12 @@ public: QHash roleNames() const override; + private: QVector< Data > m_data; +public slots: + void lyricsSlides(QString lyrics); }; #endif // SONGLISTMODEL_H diff --git a/src/songtext.cpp b/src/songtext.cpp new file mode 100644 index 0000000..ec4565a --- /dev/null +++ b/src/songtext.cpp @@ -0,0 +1,25 @@ +#include "songtext.h" + +SongText::SongText(QObject *parent) : + QObject(parent) +{ +} + +QString SongText::songText() +{ + return m_songText; +} + +void SongText::setSongText(const QString &songText) +{ + if (songText == m_songText) + return; + + QTextStream stream(&songText); + QString line = stream.readLine(); + qDebug() << line; + + m_songText = songText; + emit songTextChanged(); +} + diff --git a/src/songtext.h b/src/songtext.h new file mode 100644 index 0000000..dcbc56d --- /dev/null +++ b/src/songtext.h @@ -0,0 +1,27 @@ +#ifndef SONGTEXT_H +#define SONGTEXT_H + +#include +#include +#include + +class SongText : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString songText READ songText WRITE setSongText NOTIFY songTextChanged) + QML_ELEMENT + +public: + explicit SongText(QObject *parent = nullptr); + + QString songText(); + void setSongText(const QString &lyrics); + +signals: + void songTextChanged(); + +private: + QString m_songText; +}; + +#endif // SONGTEXT_H