MPV is working now
This commit is contained in:
parent
6aab0acd27
commit
ef1cb70d10
17 changed files with 1630 additions and 48 deletions
|
@ -1,26 +1,30 @@
|
||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
project(presenter)
|
project(presenter)
|
||||||
|
|
||||||
set(KF_MIN_VERSION "5.68.0")
|
include(FeatureSummary)
|
||||||
set(QT_MIN_VERSION "5.12.0")
|
|
||||||
|
set(QT5_MIN_VERSION 5.15)
|
||||||
|
set(KF5_MIN_VERSION 5.83)
|
||||||
|
|
||||||
set(CMAKE_AUTOMOC ON)
|
set(CMAKE_AUTOMOC ON)
|
||||||
set(CMAKE_AUTORCC ON)
|
set(CMAKE_AUTORCC ON)
|
||||||
|
|
||||||
set(CMAKE_INCLUDE_CURRENT_DIR 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(KDEInstallDirs)
|
||||||
include(KDECMakeSettings)
|
include(KDECMakeSettings)
|
||||||
include(KDECompilerSettings NO_POLICY_SCOPE)
|
include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||||
include(FeatureSummary)
|
include(ECMSetupVersion)
|
||||||
|
include(ECMGenerateHeaders)
|
||||||
|
include(ECMPoQmTools)
|
||||||
|
|
||||||
kde_enable_exceptions()
|
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)
|
find_package(KF5 ${KF_MIN_VERSION} REQUIRED COMPONENTS Kirigami2 I18n CoreAddons)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
|
14
qhot-profile.json
Normal file
14
qhot-profile.json
Normal file
|
@ -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"
|
||||||
|
}
|
|
@ -4,6 +4,8 @@ target_sources(presenter
|
||||||
PRIVATE
|
PRIVATE
|
||||||
main.cpp resources.qrc
|
main.cpp resources.qrc
|
||||||
songlistmodel.cpp songlistmodel.h
|
songlistmodel.cpp songlistmodel.h
|
||||||
|
mpvobject.h mpvobject.cpp
|
||||||
|
qthelper.hpp mpvhelpers.h
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(presenter
|
target_link_libraries(presenter
|
||||||
|
@ -13,8 +15,10 @@ target_link_libraries(presenter
|
||||||
Qt5::QuickControls2
|
Qt5::QuickControls2
|
||||||
Qt5::Widgets
|
Qt5::Widgets
|
||||||
Qt5::Sql
|
Qt5::Sql
|
||||||
|
Qt5::X11Extras
|
||||||
KF5::Kirigami2
|
KF5::Kirigami2
|
||||||
KF5::I18n
|
KF5::I18n
|
||||||
|
mpv
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_options (presenter PUBLIC -fexceptions)
|
target_compile_options (presenter PUBLIC -fexceptions)
|
||||||
|
|
161
src/main.cpp
161
src/main.cpp
|
@ -9,8 +9,18 @@
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QtGlobal>
|
||||||
|
#include <QOpenGLContext>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
|
||||||
|
#include <QtGui/QOpenGLFramebufferObject>
|
||||||
|
|
||||||
|
#include <QtQuick/QQuickWindow>
|
||||||
|
#include <QtQuick/QQuickView>
|
||||||
|
|
||||||
#include "songlistmodel.h"
|
#include "songlistmodel.h"
|
||||||
// #include "mpvobject.h"
|
#include "mpvobject.h"
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
|
@ -21,6 +31,10 @@ int main(int argc, char *argv[])
|
||||||
QCoreApplication::setOrganizationDomain(QStringLiteral("tfcconnection.org"));
|
QCoreApplication::setOrganizationDomain(QStringLiteral("tfcconnection.org"));
|
||||||
QCoreApplication::setApplicationName(QStringLiteral("Church Presenter"));
|
QCoreApplication::setApplicationName(QStringLiteral("Church Presenter"));
|
||||||
|
|
||||||
|
// apparently mpv needs this class set
|
||||||
|
std::setlocale(LC_NUMERIC, "C");
|
||||||
|
qmlRegisterType<MpvObject>("mpv", 1, 0, "MpvObject");
|
||||||
|
|
||||||
SongListModel songListModel;
|
SongListModel songListModel;
|
||||||
|
|
||||||
// path = QQmlEngine::importPathList()
|
// path = QQmlEngine::importPathList()
|
||||||
|
@ -44,3 +58,148 @@ int main(int argc, char *argv[])
|
||||||
|
|
||||||
return app.exec();
|
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<void *>(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<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(&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<int>(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<MpvObject *>(this));
|
||||||
|
// }
|
||||||
|
|
154
src/mpvhelpers.h
Normal file
154
src/mpvhelpers.h
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// MpvObject definition
|
||||||
|
#define READONLY_PROP_BOOL(p, varName) \
|
||||||
|
public: \
|
||||||
|
Q_PROPERTY(bool varName READ varName NOTIFY varName##Changed) \
|
||||||
|
public Q_SLOTS: \
|
||||||
|
bool varName() const { return getProperty(p).toBool(); } \
|
||||||
|
Q_SIGNALS: \
|
||||||
|
void varName##Changed(bool value);
|
||||||
|
#define WRITABLE_PROP_BOOL(p, varName) \
|
||||||
|
public: \
|
||||||
|
Q_PROPERTY(bool varName READ varName WRITE set_##varName NOTIFY varName##Changed) \
|
||||||
|
public Q_SLOTS: \
|
||||||
|
bool varName() const { return getProperty(p).toBool(); } \
|
||||||
|
void set_##varName(bool value) { setProperty(p, value); } \
|
||||||
|
Q_SIGNALS: \
|
||||||
|
void varName##Changed(bool value);
|
||||||
|
|
||||||
|
#define READONLY_PROP_INT(p, varName) \
|
||||||
|
public: \
|
||||||
|
Q_PROPERTY(int varName READ varName NOTIFY varName##Changed) \
|
||||||
|
public Q_SLOTS: \
|
||||||
|
int varName() { return getProperty(p).toInt(); } \
|
||||||
|
Q_SIGNALS: \
|
||||||
|
void varName##Changed(int value);
|
||||||
|
#define WRITABLE_PROP_INT(p, varName) \
|
||||||
|
public: \
|
||||||
|
Q_PROPERTY(int varName READ varName WRITE set_##varName NOTIFY varName##Changed) \
|
||||||
|
public Q_SLOTS: \
|
||||||
|
int varName() { return getProperty(p).toInt(); } \
|
||||||
|
void set_##varName(int value) { setProperty(p, value); } \
|
||||||
|
Q_SIGNALS: \
|
||||||
|
void varName##Changed(int value);
|
||||||
|
|
||||||
|
#define READONLY_PROP_DOUBLE(p, varName) \
|
||||||
|
public: \
|
||||||
|
Q_PROPERTY(double varName READ varName NOTIFY varName##Changed) \
|
||||||
|
public Q_SLOTS: \
|
||||||
|
double varName() { return getProperty(p).toDouble(); } \
|
||||||
|
Q_SIGNALS: \
|
||||||
|
void varName##Changed(double value);
|
||||||
|
#define WRITABLE_PROP_DOUBLE(p, varName) \
|
||||||
|
public: \
|
||||||
|
Q_PROPERTY(double varName READ varName WRITE set_##varName NOTIFY varName##Changed) \
|
||||||
|
public Q_SLOTS: \
|
||||||
|
double varName() { return getProperty(p).toDouble(); } \
|
||||||
|
void set_##varName(double value) { setProperty(p, value); } \
|
||||||
|
Q_SIGNALS: \
|
||||||
|
void varName##Changed(double value);
|
||||||
|
|
||||||
|
#define READONLY_PROP_STRING(p, varName) \
|
||||||
|
public: \
|
||||||
|
Q_PROPERTY(QString varName READ varName NOTIFY varName##Changed) \
|
||||||
|
public Q_SLOTS: \
|
||||||
|
QString varName() { return getProperty(p).toString(); } \
|
||||||
|
Q_SIGNALS: \
|
||||||
|
void varName##Changed(QString value);
|
||||||
|
#define WRITABLE_PROP_STRING(p, varName) \
|
||||||
|
public: \
|
||||||
|
Q_PROPERTY(QString varName READ varName WRITE set_##varName NOTIFY varName##Changed) \
|
||||||
|
public Q_SLOTS: \
|
||||||
|
QString varName() { return getProperty(p).toString(); } \
|
||||||
|
void set_##varName(QString value) { setProperty(p, value); } \
|
||||||
|
Q_SIGNALS: \
|
||||||
|
void varName##Changed(QString value);
|
||||||
|
|
||||||
|
#define READONLY_PROP_ARRAY(p, varName) \
|
||||||
|
public: \
|
||||||
|
Q_PROPERTY(QVariantList varName READ varName NOTIFY varName##Changed) \
|
||||||
|
public Q_SLOTS: \
|
||||||
|
QVariantList varName() { return getProperty(p).toList(); } \
|
||||||
|
Q_SIGNALS: \
|
||||||
|
void varName##Changed(QVariantList value);
|
||||||
|
#define WRITABLE_PROP_ARRAY(p, varName) \
|
||||||
|
public: \
|
||||||
|
Q_PROPERTY(QVariantList varName READ varName WRITE set_##varName NOTIFY varName##Changed) \
|
||||||
|
public Q_SLOTS: \
|
||||||
|
QVariantList varName() { return getProperty(p).toList(); } \
|
||||||
|
void set_##varName(QVariantList value) { setProperty(p, value); } \
|
||||||
|
Q_SIGNALS: \
|
||||||
|
void varName##Changed(QVariantList value);
|
||||||
|
|
||||||
|
#define READONLY_PROP_MAP(p, varName) \
|
||||||
|
public: \
|
||||||
|
Q_PROPERTY(QVariantMap varName READ varName NOTIFY varName##Changed) \
|
||||||
|
public Q_SLOTS: \
|
||||||
|
QVariantMap varName() { return getProperty(p).toMap(); } \
|
||||||
|
Q_SIGNALS: \
|
||||||
|
void varName##Changed(QVariantMap value);
|
||||||
|
#define WRITABLE_PROP_MAP(p, varName) \
|
||||||
|
public: \
|
||||||
|
Q_PROPERTY(QVariantMap varName READ varName WRITE set_##varName NOTIFY varName##Changed) \
|
||||||
|
public Q_SLOTS: \
|
||||||
|
QVariantMap varName() { return getProperty(p).toMap(); } \
|
||||||
|
void set_##varName(QVariantMap value) { setProperty(p, value); } \
|
||||||
|
Q_SIGNALS: \
|
||||||
|
void varName##Changed(QVariantMap value);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// MpvObject() constructor
|
||||||
|
#define WATCH_PROP_BOOL(p) \
|
||||||
|
mpv_observe_property(mpv, 0, p, MPV_FORMAT_FLAG);
|
||||||
|
#define WATCH_PROP_DOUBLE(p) \
|
||||||
|
mpv_observe_property(mpv, 0, p, MPV_FORMAT_DOUBLE);
|
||||||
|
#define WATCH_PROP_INT(p) \
|
||||||
|
mpv_observe_property(mpv, 0, p, MPV_FORMAT_INT64);
|
||||||
|
#define WATCH_PROP_STRING(p) \
|
||||||
|
mpv_observe_property(mpv, 0, p, MPV_FORMAT_STRING);
|
||||||
|
#define WATCH_PROP_ARRAY(p) \
|
||||||
|
mpv_observe_property(mpv, 0, p, MPV_FORMAT_NODE_ARRAY);
|
||||||
|
#define WATCH_PROP_MAP(p) \
|
||||||
|
mpv_observe_property(mpv, 0, p, MPV_FORMAT_NODE_MAP);
|
||||||
|
|
||||||
|
|
||||||
|
// MpvObject::handle_mpv_event()
|
||||||
|
#define HANDLE_PROP_NONE(p, varName) \
|
||||||
|
(strcmp(prop->name, p) == 0) { \
|
||||||
|
int64_t value = 0; \
|
||||||
|
Q_EMIT varName##Changed(value); \
|
||||||
|
}
|
||||||
|
#define HANDLE_PROP_BOOL(p, varName) \
|
||||||
|
(strcmp(prop->name, p) == 0) { \
|
||||||
|
bool value = *(bool *)prop->data; \
|
||||||
|
Q_EMIT varName##Changed(value); \
|
||||||
|
}
|
||||||
|
#define HANDLE_PROP_INT(p, varName) \
|
||||||
|
(strcmp(prop->name, p) == 0) { \
|
||||||
|
int64_t value = *(int64_t *)prop->data; \
|
||||||
|
Q_EMIT varName##Changed(value); \
|
||||||
|
}
|
||||||
|
#define HANDLE_PROP_DOUBLE(p, varName) \
|
||||||
|
(strcmp(prop->name, p) == 0) { \
|
||||||
|
double value = *(double *)prop->data; \
|
||||||
|
Q_EMIT varName##Changed(value); \
|
||||||
|
}
|
||||||
|
#define HANDLE_PROP_STRING(p, varName) \
|
||||||
|
(strcmp(prop->name, p) == 0) { \
|
||||||
|
char* charValue = *(char**)prop->data; \
|
||||||
|
QString value = QString::fromUtf8(charValue); \
|
||||||
|
Q_EMIT varName##Changed(value); \
|
||||||
|
}
|
||||||
|
#define HANDLE_PROP_ARRAY(p, varName) \
|
||||||
|
(strcmp(prop->name, p) == 0) { \
|
||||||
|
QVariantList value = getProperty(p).toList(); \
|
||||||
|
Q_EMIT varName##Changed(value); \
|
||||||
|
}
|
||||||
|
#define HANDLE_PROP_MAP(p, varName) \
|
||||||
|
(strcmp(prop->name, p) == 0) { \
|
||||||
|
QVariantMap value = getProperty(p).toMap(); \
|
||||||
|
Q_EMIT varName##Changed(value); \
|
||||||
|
}
|
539
src/mpvobject.cpp
Normal file
539
src/mpvobject.cpp
Normal file
|
@ -0,0 +1,539 @@
|
||||||
|
#include "mpvobject.h"
|
||||||
|
|
||||||
|
// std
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <clocale>
|
||||||
|
|
||||||
|
// Qt
|
||||||
|
#include <QObject>
|
||||||
|
#include <QtGlobal>
|
||||||
|
#include <QOpenGLContext>
|
||||||
|
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QtQuick/QQuickWindow>
|
||||||
|
#include <QtQuick/QQuickView>
|
||||||
|
|
||||||
|
#include <QtQuick/QQuickFramebufferObject>
|
||||||
|
#include <QtGui/QOpenGLFramebufferObject>
|
||||||
|
|
||||||
|
#include <QtX11Extras/QX11Info>
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
// libmpv
|
||||||
|
#include <mpv/client.h>
|
||||||
|
#include <mpv/render_gl.h>
|
||||||
|
|
||||||
|
// own
|
||||||
|
#include "qthelper.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//--- MpvRenderer
|
||||||
|
void* MpvRenderer::get_proc_address(void *ctx, const char *name) {
|
||||||
|
(void)ctx;
|
||||||
|
QOpenGLContext *glctx = QOpenGLContext::currentContext();
|
||||||
|
if (!glctx)
|
||||||
|
return nullptr;
|
||||||
|
return reinterpret_cast<void *>(glctx->getProcAddress(QByteArray(name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
MpvRenderer::MpvRenderer(const MpvObject *obj)
|
||||||
|
: obj(obj)
|
||||||
|
, mpv_gl(nullptr)
|
||||||
|
{
|
||||||
|
|
||||||
|
// https://github.com/mpv-player/mpv/blob/master/libmpv/render_gl.h#L106
|
||||||
|
#if MPV_CLIENT_API_VERSION >= MPV_MAKE_VERSION(2, 0)
|
||||||
|
mpv_opengl_init_params gl_init_params{
|
||||||
|
get_proc_address,
|
||||||
|
nullptr // get_proc_address_ctx
|
||||||
|
};
|
||||||
|
#else
|
||||||
|
mpv_opengl_init_params gl_init_params{
|
||||||
|
get_proc_address,
|
||||||
|
nullptr, // get_proc_address_ctx
|
||||||
|
nullptr // extra_exts (deprecated)
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
mpv_render_param display{
|
||||||
|
MPV_RENDER_PARAM_INVALID,
|
||||||
|
nullptr
|
||||||
|
};
|
||||||
|
if (QX11Info::isPlatformX11()) {
|
||||||
|
display.type = MPV_RENDER_PARAM_X11_DISPLAY;
|
||||||
|
display.data = QX11Info::display();
|
||||||
|
}
|
||||||
|
mpv_render_param params[]{
|
||||||
|
{ MPV_RENDER_PARAM_API_TYPE, const_cast<char *>(MPV_RENDER_API_TYPE_OPENGL) },
|
||||||
|
{ MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params },
|
||||||
|
display,
|
||||||
|
{ MPV_RENDER_PARAM_INVALID, nullptr }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mpv_render_context_create(&mpv_gl, obj->mpv, params) < 0)
|
||||||
|
throw std::runtime_error("failed to initialize mpv GL context");
|
||||||
|
|
||||||
|
mpv_render_context_set_update_callback(mpv_gl, MpvObject::on_update, (void *)obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
MpvRenderer::~MpvRenderer() {
|
||||||
|
if (mpv_gl)
|
||||||
|
mpv_render_context_free(mpv_gl);
|
||||||
|
|
||||||
|
mpv_terminate_destroy(obj->mpv);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvRenderer::render() {
|
||||||
|
QOpenGLFramebufferObject *fbo = framebufferObject();
|
||||||
|
// fbo->bind();
|
||||||
|
obj->window()->resetOpenGLState();
|
||||||
|
|
||||||
|
// https://github.com/mpv-player/mpv/blob/master/libmpv/render_gl.h#L133
|
||||||
|
mpv_opengl_fbo mpfbo{
|
||||||
|
.fbo = static_cast<int>(fbo->handle()),
|
||||||
|
.w = fbo->width(),
|
||||||
|
.h = fbo->height(),
|
||||||
|
.internal_format = 0 // 0=unknown
|
||||||
|
};
|
||||||
|
mpv_render_param params[] = {
|
||||||
|
{ MPV_RENDER_PARAM_OPENGL_FBO, &mpfbo },
|
||||||
|
{ MPV_RENDER_PARAM_INVALID, nullptr }
|
||||||
|
};
|
||||||
|
mpv_render_context_render(mpv_gl, params);
|
||||||
|
|
||||||
|
|
||||||
|
obj->window()->resetOpenGLState();
|
||||||
|
// fbo->release();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//--- MpvObject
|
||||||
|
static void wakeup(void *ctx)
|
||||||
|
{
|
||||||
|
QMetaObject::invokeMethod((MpvObject*)ctx, "onMpvEvents", Qt::QueuedConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
MpvObject::MpvObject(QQuickItem *parent)
|
||||||
|
: QQuickFramebufferObject(parent)
|
||||||
|
, m_enableAudio(true)
|
||||||
|
, m_useHwdec(false)
|
||||||
|
, m_duration(0)
|
||||||
|
, m_position(0)
|
||||||
|
, m_isPlaying(false)
|
||||||
|
{
|
||||||
|
mpv = mpv_create();
|
||||||
|
if (!mpv)
|
||||||
|
throw std::runtime_error("could not create mpv context");
|
||||||
|
|
||||||
|
mpv_set_option_string(mpv, "terminal", "yes");
|
||||||
|
// mpv_set_option_string(mpv, "msg-level", "all=warn,ao/alsa=error");
|
||||||
|
// mpv_set_option_string(mpv, "msg-level", "all=debug");
|
||||||
|
|
||||||
|
//--- Hardware Decoding
|
||||||
|
mpv::qt::set_option_variant(mpv, "hwdec-codecs", "all");
|
||||||
|
|
||||||
|
if (mpv_initialize(mpv) < 0)
|
||||||
|
throw std::runtime_error("could not initialize mpv context");
|
||||||
|
|
||||||
|
|
||||||
|
mpv::qt::set_option_variant(mpv, "audio-client-name", "mpvz");
|
||||||
|
|
||||||
|
mpv_request_log_messages(mpv, "terminal-default");
|
||||||
|
|
||||||
|
//--- 60fps Interpolation
|
||||||
|
mpv::qt::set_option_variant(mpv, "interpolation", "yes");
|
||||||
|
mpv::qt::set_option_variant(mpv, "video-sync", "display-resample");
|
||||||
|
// mpv::qt::set_option_variant(mpv, "vf", "lavfi=\"fps=fps=60:round=down\"");
|
||||||
|
// mpv::qt::set_option_variant(mpv, "override-display-fps", "60");
|
||||||
|
|
||||||
|
//--- ytdl 1080p max
|
||||||
|
mpv::qt::set_option_variant(mpv, "ytdl-format", "ytdl-format=bestvideo[width<=?720]+bestaudio/best");
|
||||||
|
|
||||||
|
|
||||||
|
mpv::qt::set_option_variant(mpv, "quiet", "yes");
|
||||||
|
|
||||||
|
// Setup the callback that will make QtQuick update and redraw if there
|
||||||
|
// is a new video frame. Use a queued connection: this makes sure the
|
||||||
|
// doUpdate() function is run on the GUI thread.
|
||||||
|
// * MpvRender binds mpv_gl update function to MpvObject::on_update
|
||||||
|
// * MpvObject::on_update will emit MpvObject::mpvUpdated
|
||||||
|
connect(this, &MpvObject::mpvUpdated,
|
||||||
|
this, &MpvObject::doUpdate,
|
||||||
|
Qt::QueuedConnection);
|
||||||
|
|
||||||
|
WATCH_PROP_BOOL("idle-active")
|
||||||
|
WATCH_PROP_BOOL("mute")
|
||||||
|
WATCH_PROP_BOOL("pause")
|
||||||
|
WATCH_PROP_BOOL("paused-for-cache")
|
||||||
|
WATCH_PROP_BOOL("seekable")
|
||||||
|
WATCH_PROP_INT("chapter")
|
||||||
|
WATCH_PROP_INT("chapter-list/count")
|
||||||
|
WATCH_PROP_INT("decoder-frame-drop-count")
|
||||||
|
WATCH_PROP_INT("dheight")
|
||||||
|
WATCH_PROP_INT("dwidth")
|
||||||
|
WATCH_PROP_INT("estimated-frame-count")
|
||||||
|
WATCH_PROP_INT("estimated-frame-number")
|
||||||
|
WATCH_PROP_INT("frame-drop-count")
|
||||||
|
WATCH_PROP_INT("playlist-pos")
|
||||||
|
WATCH_PROP_INT("playlist/count")
|
||||||
|
WATCH_PROP_INT("vo-delayed-frame-count")
|
||||||
|
WATCH_PROP_INT("volume")
|
||||||
|
WATCH_PROP_INT("vid")
|
||||||
|
WATCH_PROP_INT("aid")
|
||||||
|
WATCH_PROP_INT("sid")
|
||||||
|
WATCH_PROP_INT("audio-params/channel-count")
|
||||||
|
WATCH_PROP_INT("audio-params/samplerate")
|
||||||
|
WATCH_PROP_INT("track-list/count")
|
||||||
|
WATCH_PROP_INT("contrast")
|
||||||
|
WATCH_PROP_INT("brightness")
|
||||||
|
WATCH_PROP_INT("gamma")
|
||||||
|
WATCH_PROP_INT("saturation")
|
||||||
|
WATCH_PROP_INT("sub-margin-y")
|
||||||
|
WATCH_PROP_DOUBLE("audio-bitrate")
|
||||||
|
WATCH_PROP_DOUBLE("avsync")
|
||||||
|
WATCH_PROP_DOUBLE("container-fps")
|
||||||
|
WATCH_PROP_DOUBLE("demuxer-cache-duration")
|
||||||
|
WATCH_PROP_DOUBLE("display-fps")
|
||||||
|
WATCH_PROP_DOUBLE("duration")
|
||||||
|
WATCH_PROP_DOUBLE("estimated-display-fps")
|
||||||
|
WATCH_PROP_DOUBLE("estimated-vf-fps")
|
||||||
|
WATCH_PROP_DOUBLE("fps")
|
||||||
|
WATCH_PROP_DOUBLE("speed")
|
||||||
|
WATCH_PROP_DOUBLE("time-pos")
|
||||||
|
WATCH_PROP_DOUBLE("video-bitrate")
|
||||||
|
WATCH_PROP_DOUBLE("video-params/aspect")
|
||||||
|
WATCH_PROP_DOUBLE("video-out-params/aspect")
|
||||||
|
WATCH_PROP_DOUBLE("window-scale")
|
||||||
|
WATCH_PROP_DOUBLE("current-window-scale")
|
||||||
|
WATCH_PROP_STRING("audio-codec")
|
||||||
|
WATCH_PROP_STRING("audio-codec-name")
|
||||||
|
WATCH_PROP_STRING("audio-params/format")
|
||||||
|
WATCH_PROP_STRING("filename")
|
||||||
|
WATCH_PROP_STRING("file-format")
|
||||||
|
WATCH_PROP_STRING("file-size")
|
||||||
|
WATCH_PROP_STRING("audio-format")
|
||||||
|
WATCH_PROP_STRING("hwdec")
|
||||||
|
WATCH_PROP_STRING("hwdec-current")
|
||||||
|
WATCH_PROP_STRING("hwdec-interop")
|
||||||
|
WATCH_PROP_STRING("media-title")
|
||||||
|
WATCH_PROP_STRING("path")
|
||||||
|
WATCH_PROP_STRING("video-codec")
|
||||||
|
WATCH_PROP_STRING("video-format")
|
||||||
|
WATCH_PROP_STRING("video-params/pixelformat")
|
||||||
|
WATCH_PROP_STRING("video-out-params/pixelformat")
|
||||||
|
WATCH_PROP_STRING("ytdl-format")
|
||||||
|
WATCH_PROP_MAP("demuxer-cache-state")
|
||||||
|
|
||||||
|
connect(this, &MpvObject::idleActiveChanged,
|
||||||
|
this, &MpvObject::updateState);
|
||||||
|
connect(this, &MpvObject::pausedChanged,
|
||||||
|
this, &MpvObject::updateState);
|
||||||
|
|
||||||
|
mpv_set_wakeup_callback(mpv, wakeup, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
MpvObject::~MpvObject()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QQuickFramebufferObject::Renderer *MpvObject::createRenderer() const
|
||||||
|
{
|
||||||
|
window()->setPersistentOpenGLContext(true);
|
||||||
|
window()->setPersistentSceneGraph(true);
|
||||||
|
return new MpvRenderer(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvObject::on_update(void *ctx)
|
||||||
|
{
|
||||||
|
MpvObject *self = (MpvObject *)ctx;
|
||||||
|
emit self->mpvUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
// connected to mpvUpdated(); signal makes sure it runs on the GUI thread
|
||||||
|
void MpvObject::doUpdate()
|
||||||
|
{
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvObject::command(const QVariant& params)
|
||||||
|
{
|
||||||
|
mpv::qt::command(mpv, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvObject::commandAsync(const QVariant& params)
|
||||||
|
{
|
||||||
|
mpv::qt::command_async(mpv, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvObject::setProperty(const QString& name, const QVariant& value)
|
||||||
|
{
|
||||||
|
mpv::qt::set_property_variant(mpv, name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant MpvObject::getProperty(const QString &name) const
|
||||||
|
{
|
||||||
|
return mpv::qt::get_property_variant(mpv, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvObject::setOption(const QString& name, const QVariant& value)
|
||||||
|
{
|
||||||
|
mpv::qt::set_option_variant(mpv, name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void MpvObject::onMpvEvents()
|
||||||
|
{
|
||||||
|
// Process all events, until the event queue is empty.
|
||||||
|
while (mpv) {
|
||||||
|
mpv_event *event = mpv_wait_event(mpv, 0);
|
||||||
|
if (event->event_id == MPV_EVENT_NONE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
handle_mpv_event(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvObject::logPropChange(mpv_event_property *prop)
|
||||||
|
{
|
||||||
|
switch (prop->format) {
|
||||||
|
case MPV_FORMAT_NONE:
|
||||||
|
qDebug() << objectName() << "none" << prop->name << 0;
|
||||||
|
break;
|
||||||
|
case MPV_FORMAT_STRING:
|
||||||
|
qDebug() << objectName() << "str " << prop->name << *(char**)prop->data;
|
||||||
|
break;
|
||||||
|
case MPV_FORMAT_FLAG:
|
||||||
|
qDebug() << objectName() << "bool" << prop->name << *(bool *)prop->data;
|
||||||
|
break;
|
||||||
|
case MPV_FORMAT_INT64:
|
||||||
|
qDebug() << objectName() << "int " << prop->name << *(int64_t *)prop->data;
|
||||||
|
break;
|
||||||
|
case MPV_FORMAT_DOUBLE:
|
||||||
|
qDebug() << objectName() << "doub" << prop->name << *(double *)prop->data;
|
||||||
|
break;
|
||||||
|
case MPV_FORMAT_NODE_ARRAY:
|
||||||
|
qDebug() << objectName() << "arr " << prop->name; // TODO
|
||||||
|
break;
|
||||||
|
case MPV_FORMAT_NODE_MAP:
|
||||||
|
qDebug() << objectName() << "map " << prop->name; // TODO
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
qDebug() << objectName() << "prop(format=" << prop->format << ")" << prop->name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvObject::handle_mpv_event(mpv_event *event)
|
||||||
|
{
|
||||||
|
// See: https://github.com/mpv-player/mpv/blob/master/libmpv/client.h
|
||||||
|
// See: https://github.com/mpv-player/mpv/blob/master/player/lua.c#L471
|
||||||
|
|
||||||
|
switch (event->event_id) {
|
||||||
|
case MPV_EVENT_LOG_MESSAGE: {
|
||||||
|
mpv_event_log_message *logData = (mpv_event_log_message *)event->data;
|
||||||
|
Q_EMIT logMessage(
|
||||||
|
QString(logData->prefix),
|
||||||
|
QString(logData->level),
|
||||||
|
QString(logData->text)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MPV_EVENT_START_FILE: {
|
||||||
|
Q_EMIT fileStarted();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MPV_EVENT_END_FILE: {
|
||||||
|
mpv_event_end_file *eef = (mpv_event_end_file *)event->data;
|
||||||
|
const char *reason;
|
||||||
|
switch (eef->reason) {
|
||||||
|
case MPV_END_FILE_REASON_EOF: reason = "eof"; break;
|
||||||
|
case MPV_END_FILE_REASON_STOP: reason = "stop"; break;
|
||||||
|
case MPV_END_FILE_REASON_QUIT: reason = "quit"; break;
|
||||||
|
case MPV_END_FILE_REASON_ERROR: reason = "error"; break;
|
||||||
|
case MPV_END_FILE_REASON_REDIRECT: reason = "redirect"; break;
|
||||||
|
default:
|
||||||
|
reason = "unknown";
|
||||||
|
}
|
||||||
|
Q_EMIT fileEnded(QString(reason));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MPV_EVENT_FILE_LOADED: {
|
||||||
|
Q_EMIT fileLoaded();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MPV_EVENT_PROPERTY_CHANGE: {
|
||||||
|
mpv_event_property *prop = (mpv_event_property *)event->data;
|
||||||
|
// logPropChange(prop);
|
||||||
|
|
||||||
|
if (prop->format == MPV_FORMAT_NONE) {
|
||||||
|
if HANDLE_PROP_NONE("vid", vid)
|
||||||
|
else if HANDLE_PROP_NONE("aid", aid)
|
||||||
|
else if HANDLE_PROP_NONE("sid", sid)
|
||||||
|
else if HANDLE_PROP_NONE("track-list/count", trackListCount)
|
||||||
|
|
||||||
|
} else if (prop->format == MPV_FORMAT_DOUBLE) {
|
||||||
|
if (strcmp(prop->name, "time-pos") == 0) {
|
||||||
|
double time = *(double *)prop->data;
|
||||||
|
m_position = time;
|
||||||
|
Q_EMIT positionChanged(time);
|
||||||
|
} else if (strcmp(prop->name, "duration") == 0) {
|
||||||
|
double time = *(double *)prop->data;
|
||||||
|
m_duration = time;
|
||||||
|
Q_EMIT durationChanged(time);
|
||||||
|
}
|
||||||
|
else if HANDLE_PROP_DOUBLE("audio-bitrate", audioBitrate)
|
||||||
|
else if HANDLE_PROP_DOUBLE("avsync", avsync)
|
||||||
|
else if HANDLE_PROP_DOUBLE("container-fps", containerFps)
|
||||||
|
else if HANDLE_PROP_DOUBLE("demuxer-cache-duration", demuxerCacheDuration)
|
||||||
|
else if HANDLE_PROP_DOUBLE("display-fps", displayFps)
|
||||||
|
else if HANDLE_PROP_DOUBLE("estimated-display-fps", estimatedDisplayFps)
|
||||||
|
else if HANDLE_PROP_DOUBLE("estimated-vf-fps", estimatedVfFps)
|
||||||
|
else if HANDLE_PROP_DOUBLE("fps", fps)
|
||||||
|
else if HANDLE_PROP_DOUBLE("speed", speed)
|
||||||
|
else if HANDLE_PROP_DOUBLE("video-bitrate", videoBitrate)
|
||||||
|
else if HANDLE_PROP_DOUBLE("video-params/aspect", videoParamsAspect)
|
||||||
|
else if HANDLE_PROP_DOUBLE("video-out-params/aspect", videoOutParamsAspect)
|
||||||
|
else if HANDLE_PROP_DOUBLE("window-scale", windowScale)
|
||||||
|
else if HANDLE_PROP_DOUBLE("current-window-scale", currentWindowScale)
|
||||||
|
|
||||||
|
} else if (prop->format == MPV_FORMAT_FLAG) {
|
||||||
|
if HANDLE_PROP_BOOL("idle-active", idleActive)
|
||||||
|
else if HANDLE_PROP_BOOL("mute", muted)
|
||||||
|
else if HANDLE_PROP_BOOL("pause", paused)
|
||||||
|
else if HANDLE_PROP_BOOL("paused-for-cache", pausedForCache)
|
||||||
|
else if HANDLE_PROP_BOOL("seekable", seekable)
|
||||||
|
|
||||||
|
} else if (prop->format == MPV_FORMAT_STRING) {
|
||||||
|
if HANDLE_PROP_STRING("audio-codec", audioCodec)
|
||||||
|
else if HANDLE_PROP_STRING("audio-codec-name", audioCodecName)
|
||||||
|
else if HANDLE_PROP_STRING("audio-params/format", audioParamsFormat)
|
||||||
|
else if HANDLE_PROP_STRING("filename", filename)
|
||||||
|
else if HANDLE_PROP_STRING("file-format", fileFormat)
|
||||||
|
else if HANDLE_PROP_STRING("file-size", fileSize)
|
||||||
|
else if HANDLE_PROP_STRING("audio-format", audioFormat)
|
||||||
|
else if HANDLE_PROP_STRING("hwdec", hwdec)
|
||||||
|
else if HANDLE_PROP_STRING("hwdec-current", hwdecCurrent)
|
||||||
|
else if HANDLE_PROP_STRING("hwdec-interop", hwdecInterop)
|
||||||
|
else if HANDLE_PROP_STRING("media-title", mediaTitle)
|
||||||
|
else if HANDLE_PROP_STRING("path", path)
|
||||||
|
else if HANDLE_PROP_STRING("video-codec", videoCodec)
|
||||||
|
else if HANDLE_PROP_STRING("video-format", videoFormat)
|
||||||
|
else if HANDLE_PROP_STRING("video-params/pixelformat", videoParamsPixelformat)
|
||||||
|
else if HANDLE_PROP_STRING("video-out-params/pixelformat", videoOutParamsPixelformat)
|
||||||
|
else if HANDLE_PROP_STRING("ytdl-format", ytdlFormat)
|
||||||
|
|
||||||
|
|
||||||
|
} else if (prop->format == MPV_FORMAT_INT64) {
|
||||||
|
if HANDLE_PROP_INT("chapter", chapter)
|
||||||
|
else if HANDLE_PROP_INT("chapter-list/count", chapterListCount)
|
||||||
|
else if HANDLE_PROP_INT("decoder-frame-drop-count", decoderFrameDropCount)
|
||||||
|
else if HANDLE_PROP_INT("dwidth", dwidth)
|
||||||
|
else if HANDLE_PROP_INT("dheight", dheight)
|
||||||
|
else if HANDLE_PROP_INT("estimated-frame-count", estimatedFrameCount)
|
||||||
|
else if HANDLE_PROP_INT("estimated-frame-number", estimatedFrameNumber)
|
||||||
|
else if HANDLE_PROP_INT("frame-drop-count", frameDropCount)
|
||||||
|
else if HANDLE_PROP_INT("playlist-pos", playlistPos)
|
||||||
|
else if HANDLE_PROP_INT("playlist/count", playlistCount)
|
||||||
|
else if HANDLE_PROP_INT("vo-delayed-frame-count", voDelayedFrameCount)
|
||||||
|
else if HANDLE_PROP_INT("volume", volume)
|
||||||
|
else if HANDLE_PROP_INT("vid", vid)
|
||||||
|
else if HANDLE_PROP_INT("aid", aid)
|
||||||
|
else if HANDLE_PROP_INT("sid", sid)
|
||||||
|
else if HANDLE_PROP_INT("audio-params/channel-count", audioParamsChannelCount)
|
||||||
|
else if HANDLE_PROP_INT("audio-params/samplerate", audioParamsSampleRate)
|
||||||
|
else if HANDLE_PROP_INT("track-list/count", trackListCount)
|
||||||
|
else if HANDLE_PROP_INT("contrast", contrast)
|
||||||
|
else if HANDLE_PROP_INT("brightness", brightness)
|
||||||
|
else if HANDLE_PROP_INT("gamma", gamma)
|
||||||
|
else if HANDLE_PROP_INT("saturation", saturation)
|
||||||
|
else if HANDLE_PROP_INT("sub-margin-y", subMarginY)
|
||||||
|
|
||||||
|
|
||||||
|
} else if (prop->format == MPV_FORMAT_NODE_MAP) {
|
||||||
|
if HANDLE_PROP_MAP("demuxer-cache-state", demuxerCacheState)
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: ;
|
||||||
|
// Ignore uninteresting or unknown events.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvObject::play()
|
||||||
|
{
|
||||||
|
// qDebug() << "play";
|
||||||
|
if (idleActive() && playlistCount() >= 1) { // File has finished playing.
|
||||||
|
// qDebug() << "\treload";
|
||||||
|
set_playlistPos(playlistPos()); // Reload and play file again.
|
||||||
|
}
|
||||||
|
if (!isPlaying()) {
|
||||||
|
// qDebug() << "\t!isPlaying";
|
||||||
|
set_paused(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvObject::pause()
|
||||||
|
{
|
||||||
|
// qDebug() << "pause";
|
||||||
|
if (isPlaying()) {
|
||||||
|
// qDebug() << "!isPlaying";
|
||||||
|
set_paused(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvObject::playPause()
|
||||||
|
{
|
||||||
|
if (isPlaying()) {
|
||||||
|
pause();
|
||||||
|
} else {
|
||||||
|
play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvObject::stop()
|
||||||
|
{
|
||||||
|
command(QVariantList() << "stop" << "keep-playlist");
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvObject::stepBackward()
|
||||||
|
{
|
||||||
|
command(QVariantList() << "frame-back-step");
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvObject::stepForward()
|
||||||
|
{
|
||||||
|
command(QVariantList() << "frame-step");
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvObject::seek(double pos)
|
||||||
|
{
|
||||||
|
// qDebug() << "seek" << pos;
|
||||||
|
pos = qMax(0.0, qMin(pos, m_duration));
|
||||||
|
commandAsync(QVariantList() << "seek" << pos << "absolute");
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvObject::loadFile(QVariant urls)
|
||||||
|
{
|
||||||
|
qDebug() << "Url being loaded: " << urls;
|
||||||
|
command(QVariantList() << "loadfile" << urls);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvObject::subAdd(QVariant urls)
|
||||||
|
{
|
||||||
|
command(QVariantList() << "sub-add" << urls);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void MpvObject::updateState()
|
||||||
|
{
|
||||||
|
bool isNowPlaying = !idleActive() && !paused();
|
||||||
|
if (m_isPlaying != isNowPlaying) {
|
||||||
|
m_isPlaying = isNowPlaying;
|
||||||
|
emit isPlayingChanged(m_isPlaying);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
202
src/mpvobject.h
Normal file
202
src/mpvobject.h
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "mpvhelpers.h"
|
||||||
|
|
||||||
|
// Qt
|
||||||
|
#include <QtQuick/QQuickItem>
|
||||||
|
#include <QtQuick/QQuickFramebufferObject>
|
||||||
|
|
||||||
|
// libmpv
|
||||||
|
#include <mpv/client.h>
|
||||||
|
#include <mpv/render_gl.h>
|
||||||
|
|
||||||
|
// own
|
||||||
|
#include "qthelper.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
class MpvObject;
|
||||||
|
class MpvRenderer;
|
||||||
|
|
||||||
|
class MpvRenderer : public QQuickFramebufferObject::Renderer
|
||||||
|
{
|
||||||
|
static void* get_proc_address(void *ctx, const char *name);
|
||||||
|
|
||||||
|
public:
|
||||||
|
MpvRenderer(const MpvObject *obj);
|
||||||
|
virtual ~MpvRenderer();
|
||||||
|
|
||||||
|
void render();
|
||||||
|
|
||||||
|
private:
|
||||||
|
const MpvObject *obj;
|
||||||
|
mpv_render_context *mpv_gl;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class MpvObject : public QQuickFramebufferObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
friend class MpvRenderer;
|
||||||
|
|
||||||
|
Q_PROPERTY(bool enableAudio READ enableAudio WRITE setEnableAudio NOTIFY enableAudioChanged)
|
||||||
|
Q_PROPERTY(bool useHwdec READ useHwdec WRITE setUseHwdec NOTIFY useHwdecChanged)
|
||||||
|
|
||||||
|
READONLY_PROP_BOOL("idle-active", idleActive)
|
||||||
|
WRITABLE_PROP_BOOL("mute", muted)
|
||||||
|
WRITABLE_PROP_BOOL("pause", paused)
|
||||||
|
READONLY_PROP_BOOL("paused-for-cache", pausedForCache)
|
||||||
|
READONLY_PROP_BOOL("seekable", seekable)
|
||||||
|
READONLY_PROP_INT("chapter", chapter)
|
||||||
|
READONLY_PROP_INT("chapter-list/count", chapterListCount) // OR "chapters"
|
||||||
|
READONLY_PROP_INT("decoder-frame-drop-count", decoderFrameDropCount)
|
||||||
|
READONLY_PROP_INT("dheight", dheight)
|
||||||
|
READONLY_PROP_INT("dwidth", dwidth)
|
||||||
|
READONLY_PROP_INT("estimated-frame-count", estimatedFrameCount)
|
||||||
|
READONLY_PROP_INT("estimated-frame-number", estimatedFrameNumber)
|
||||||
|
READONLY_PROP_INT("frame-drop-count", frameDropCount)
|
||||||
|
WRITABLE_PROP_INT("playlist-pos", playlistPos)
|
||||||
|
READONLY_PROP_INT("playlist/count", playlistCount)
|
||||||
|
WRITABLE_PROP_INT("vo-delayed-frame-count", voDelayedFrameCount)
|
||||||
|
WRITABLE_PROP_INT("volume", volume)
|
||||||
|
WRITABLE_PROP_INT("contrast", contrast)
|
||||||
|
WRITABLE_PROP_INT("brightness", brightness)
|
||||||
|
WRITABLE_PROP_INT("gamma", gamma)
|
||||||
|
WRITABLE_PROP_INT("saturation", saturation)
|
||||||
|
WRITABLE_PROP_INT("sub-margin-y", subMarginY)
|
||||||
|
READONLY_PROP_INT("vid", vid)
|
||||||
|
READONLY_PROP_INT("aid", aid)
|
||||||
|
READONLY_PROP_INT("sid", sid)
|
||||||
|
READONLY_PROP_INT("audio-params/channel-count", audioParamsChannelCount)
|
||||||
|
READONLY_PROP_INT("audio-params/samplerate", audioParamsSampleRate)
|
||||||
|
READONLY_PROP_INT("track-list/count", trackListCount)
|
||||||
|
READONLY_PROP_DOUBLE("audio-bitrate", audioBitrate)
|
||||||
|
READONLY_PROP_DOUBLE("avsync", avsync)
|
||||||
|
READONLY_PROP_DOUBLE("container-fps", containerFps)
|
||||||
|
READONLY_PROP_DOUBLE("demuxer-cache-duration", demuxerCacheDuration)
|
||||||
|
READONLY_PROP_DOUBLE("display-fps", displayFps)
|
||||||
|
READONLY_PROP_DOUBLE("estimated-display-fps", estimatedDisplayFps)
|
||||||
|
READONLY_PROP_DOUBLE("estimated-vf-fps", estimatedVfFps)
|
||||||
|
READONLY_PROP_DOUBLE("fps", fps) // Deprecated, use "container-fps"
|
||||||
|
WRITABLE_PROP_DOUBLE("speed", speed)
|
||||||
|
READONLY_PROP_DOUBLE("video-bitrate", videoBitrate)
|
||||||
|
READONLY_PROP_DOUBLE("video-params/aspect", videoParamsAspect)
|
||||||
|
READONLY_PROP_DOUBLE("video-out-params/aspect", videoOutParamsAspect)
|
||||||
|
WRITABLE_PROP_DOUBLE("window-scale", windowScale)
|
||||||
|
READONLY_PROP_DOUBLE("current-window-scale", currentWindowScale)
|
||||||
|
READONLY_PROP_STRING("audio-params/format", audioParamsFormat)
|
||||||
|
READONLY_PROP_STRING("audio-codec", audioCodec)
|
||||||
|
READONLY_PROP_STRING("audio-codec-name", audioCodecName)
|
||||||
|
READONLY_PROP_STRING("filename", filename)
|
||||||
|
READONLY_PROP_STRING("file-format", fileFormat)
|
||||||
|
READONLY_PROP_STRING("file-size", fileSize)
|
||||||
|
READONLY_PROP_STRING("audio-format", audioFormat)
|
||||||
|
WRITABLE_PROP_STRING("hwdec", hwdec)
|
||||||
|
READONLY_PROP_STRING("hwdec-current", hwdecCurrent)
|
||||||
|
READONLY_PROP_STRING("hwdec-interop", hwdecInterop)
|
||||||
|
READONLY_PROP_STRING("media-title", mediaTitle)
|
||||||
|
READONLY_PROP_STRING("path", path)
|
||||||
|
READONLY_PROP_STRING("video-codec", videoCodec)
|
||||||
|
READONLY_PROP_STRING("video-format", videoFormat)
|
||||||
|
READONLY_PROP_STRING("video-params/pixelformat", videoParamsPixelformat)
|
||||||
|
READONLY_PROP_STRING("video-out-params/pixelformat", videoOutParamsPixelformat)
|
||||||
|
READONLY_PROP_STRING("ytdl-format", ytdlFormat)
|
||||||
|
READONLY_PROP_MAP("demuxer-cache-state", demuxerCacheState)
|
||||||
|
|
||||||
|
public:
|
||||||
|
Q_PROPERTY(bool isPlaying READ isPlaying NOTIFY isPlayingChanged)
|
||||||
|
Q_PROPERTY(double duration READ duration NOTIFY durationChanged)
|
||||||
|
Q_PROPERTY(double position READ position NOTIFY positionChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
MpvObject(QQuickItem *parent = nullptr);
|
||||||
|
virtual ~MpvObject();
|
||||||
|
|
||||||
|
virtual Renderer *createRenderer() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE void setProperty(const QString& name, const QVariant& value);
|
||||||
|
Q_INVOKABLE QVariant getProperty(const QString& name) const;
|
||||||
|
Q_INVOKABLE void setOption(const QString& name, const QVariant& value);
|
||||||
|
|
||||||
|
Q_INVOKABLE QString getPlaylistFilename(int playlistIndex) const { return getProperty(QString("playlist/%1/filename").arg(playlistIndex)).toString(); }
|
||||||
|
Q_INVOKABLE QString getPlaylistTitle(int playlistIndex) const { return getProperty(QString("playlist/%1/title").arg(playlistIndex)).toString(); }
|
||||||
|
Q_INVOKABLE QString getChapterTitle(int chapterIndex) const { return getProperty(QString("chapter-list/%1/title").arg(chapterIndex)).toString(); }
|
||||||
|
Q_INVOKABLE double getChapterTime(int chapterIndex) const { return getProperty(QString("chapter-list/%1/time").arg(chapterIndex)).toDouble(); }
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void command(const QVariant& params);
|
||||||
|
void commandAsync(const QVariant& params);
|
||||||
|
|
||||||
|
void playPause();
|
||||||
|
void play();
|
||||||
|
void pause();
|
||||||
|
void stop();
|
||||||
|
void stepBackward();
|
||||||
|
void stepForward();
|
||||||
|
void seek(double pos);
|
||||||
|
void loadFile(QVariant urls);
|
||||||
|
void subAdd(QVariant urls);
|
||||||
|
|
||||||
|
bool enableAudio() const { return m_enableAudio; }
|
||||||
|
void setEnableAudio(bool value) {
|
||||||
|
if (m_enableAudio != value) {
|
||||||
|
m_enableAudio = value;
|
||||||
|
Q_EMIT enableAudioChanged(value);
|
||||||
|
}
|
||||||
|
if (!m_enableAudio) {
|
||||||
|
mpv::qt::set_option_variant(mpv, "ao", "null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool useHwdec() const { return m_useHwdec; }
|
||||||
|
void setUseHwdec(bool value) {
|
||||||
|
if (m_useHwdec != value) {
|
||||||
|
m_useHwdec = value;
|
||||||
|
if (m_useHwdec) {
|
||||||
|
mpv::qt::set_option_variant(mpv, "hwdec", "auto-copy");
|
||||||
|
} else {
|
||||||
|
mpv::qt::set_option_variant(mpv, "hwdec", "no");
|
||||||
|
}
|
||||||
|
Q_EMIT useHwdecChanged(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isPlaying() const { return m_isPlaying; }
|
||||||
|
void updateState();
|
||||||
|
|
||||||
|
double duration() const { return m_duration; }
|
||||||
|
double position() const { return m_position; }
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void enableAudioChanged(bool value);
|
||||||
|
void useHwdecChanged(bool value);
|
||||||
|
void isPlayingChanged(bool value);
|
||||||
|
|
||||||
|
void durationChanged(double value); // Unit: seconds
|
||||||
|
void positionChanged(double value); // Unit: seconds
|
||||||
|
|
||||||
|
void mpvUpdated();
|
||||||
|
|
||||||
|
void logMessage(QString prefix, QString level, QString text);
|
||||||
|
void fileStarted();
|
||||||
|
void fileEnded(QString reason);
|
||||||
|
void fileLoaded();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onMpvEvents();
|
||||||
|
void doUpdate();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
mpv_handle *mpv;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void logPropChange(mpv_event_property *prop);
|
||||||
|
void handle_mpv_event(mpv_event *event);
|
||||||
|
static void on_update(void *ctx);
|
||||||
|
|
||||||
|
bool m_enableAudio;
|
||||||
|
bool m_useHwdec;
|
||||||
|
double m_duration;
|
||||||
|
double m_position;
|
||||||
|
bool m_isPlaying;
|
||||||
|
};
|
|
@ -85,6 +85,7 @@ Item {
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
onClicked: {
|
onClicked: {
|
||||||
ListView.view.currentIndex = index
|
ListView.view.currentIndex = index
|
||||||
|
song = ListView.view.selected
|
||||||
songTitle = title
|
songTitle = title
|
||||||
songLyrics = lyrics
|
songLyrics = lyrics
|
||||||
songAuthor = author
|
songAuthor = author
|
||||||
|
|
|
@ -11,7 +11,9 @@ import "./" as Presenter
|
||||||
Controls.Page {
|
Controls.Page {
|
||||||
id: mainPage
|
id: mainPage
|
||||||
padding: 0
|
padding: 0
|
||||||
property url background: ""
|
property url imageBackground: ""
|
||||||
|
property url videoBackground: ""
|
||||||
|
property var song
|
||||||
property string songTitle: ""
|
property string songTitle: ""
|
||||||
property string songLyrics: ""
|
property string songLyrics: ""
|
||||||
property string songAuthor: ""
|
property string songAuthor: ""
|
||||||
|
@ -79,27 +81,22 @@ Controls.Page {
|
||||||
}
|
}
|
||||||
Presenter.Slide {
|
Presenter.Slide {
|
||||||
id: presentationSlide
|
id: presentationSlide
|
||||||
imageSource: "../../assets/parallel.jpg"
|
imageSource: imageBackground
|
||||||
|
videoSource: videoBackground
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FileDialog {
|
FileDialog {
|
||||||
id: fileDialog
|
id: videoFileDialog
|
||||||
title: "Please choose a background"
|
title: "Please choose a background"
|
||||||
folder: shortcuts.home
|
folder: shortcuts.home
|
||||||
selectMultiple: false
|
selectMultiple: false
|
||||||
nameFilters: ["Video files (*.mp4 *.mkv *.mov *.wmv *.avi *.MP4 *.MOV *.MKV)",
|
nameFilters: ["Video files (*.mp4 *.mkv *.mov *.wmv *.avi *.MP4 *.MOV *.MKV)"]
|
||||||
"Image files (*.jpg *.jpeg *.png *.JPG *.JPEG *.PNG)"]
|
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
print("You chose: " + fileDialog.fileUrls);
|
imageBackground = ""
|
||||||
videoBackground = fileDialog.fileUrl;
|
videoBackground = videoFileDialog.fileUrls[0]
|
||||||
print(videoBackground);
|
print("video background = " + videoFileDialog.fileUrl)
|
||||||
|
|
||||||
str = videoBackground.toString();
|
|
||||||
if (str.endsWith("mp4"))
|
|
||||||
videoBackground = fileDialog.fileUrl; print("WE DID IT!!");
|
|
||||||
|
|
||||||
}
|
}
|
||||||
onRejected: {
|
onRejected: {
|
||||||
print("Canceled")
|
print("Canceled")
|
||||||
|
@ -108,8 +105,20 @@ Controls.Page {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function endsWith(str, suffix) {
|
FileDialog {
|
||||||
return str.indexOf(suffix, str.length - suffix.length) !== -1;
|
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() */
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import QtQuick 2.13
|
import QtQuick 2.13
|
||||||
import QtQuick.Controls 2.15 as Controls
|
import QtQuick.Controls 2.15 as Controls
|
||||||
import QtQuick.Layouts 1.2
|
import QtQuick.Layouts 1.2
|
||||||
import QtMultimedia 5.15
|
/* import QtMultimedia 5.15 */
|
||||||
import QtAudioEngine 1.15
|
import QtAudioEngine 1.15
|
||||||
import QtGraphicalEffects 1.15
|
import QtGraphicalEffects 1.15
|
||||||
import org.kde.kirigami 2.13 as Kirigami
|
import org.kde.kirigami 2.13 as Kirigami
|
||||||
import "./" as Presenter
|
import "./" as Presenter
|
||||||
|
import mpv 1.0
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
@ -14,32 +15,66 @@ Item {
|
||||||
property real textSize: 50
|
property real textSize: 50
|
||||||
property bool editMode: false
|
property bool editMode: false
|
||||||
property bool dropShadow: false
|
property bool dropShadow: false
|
||||||
property url imageSource: ""
|
property url imageSource: imageBackground
|
||||||
property url videoSource: ""
|
property url videoSource: videoBackground
|
||||||
property string chosenFont: "Quicksand"
|
property string chosenFont: "Quicksand"
|
||||||
|
property color backgroundColor
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: basePrColor
|
id: basePrColor
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: "black"
|
color: "black"
|
||||||
|
|
||||||
MediaPlayer {
|
/* MediaPlayer { */
|
||||||
id: videoPlayer
|
/* id: mediaPlayer */
|
||||||
source: videoSource
|
/* source: videoSource */
|
||||||
loops: MediaPlayer.Infinite
|
/* loops: MediaPlayer.Infinite */
|
||||||
autoPlay: true
|
/* autoPlay: editMode ? false : true */
|
||||||
notifyInterval: 100
|
/* 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"));
|
||||||
}
|
}
|
||||||
|
|
||||||
VideoOutput {
|
|
||||||
id: videoOutput
|
|
||||||
anchors.fill: parent
|
|
||||||
source: videoPlayer
|
|
||||||
}
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: playArea
|
id: playArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onPressed: videoPlayer.play();
|
onPressed: mpv.loadFile(videoSource.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Controls.ProgressBar { */
|
||||||
|
/* anchors.centerIn: parent */
|
||||||
|
/* width: parent.width - 400 */
|
||||||
|
/* value: mpv.position */
|
||||||
|
/* to: mpv.duration */
|
||||||
|
/* } */
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: mpvLoadingTimer
|
||||||
|
interval: 100
|
||||||
|
onTriggered: mpv.loadFile(videoSource.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
|
@ -48,13 +83,14 @@ Item {
|
||||||
source: imageSource
|
source: imageSource
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
clip: true
|
clip: true
|
||||||
|
visible: false
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FastBlur {
|
FastBlur {
|
||||||
id: imageBlue
|
id: imageBlue
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: backgroundImage
|
source: imageSource == "" ? mpv : backgroundImage
|
||||||
radius: blurRadius
|
radius: blurRadius
|
||||||
|
|
||||||
Controls.Label {
|
Controls.Label {
|
||||||
|
@ -77,7 +113,6 @@ Item {
|
||||||
color: "#80000000"
|
color: "#80000000"
|
||||||
visible: true
|
visible: true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,6 @@ Item {
|
||||||
|
|
||||||
Presenter.Slide {
|
Presenter.Slide {
|
||||||
id: representation
|
id: representation
|
||||||
|
editMode: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,11 +55,38 @@ Item {
|
||||||
onClicked: {}
|
onClicked: {}
|
||||||
}
|
}
|
||||||
Controls.ToolButton {
|
Controls.ToolButton {
|
||||||
|
id: backgroundButton
|
||||||
text: "Background"
|
text: "Background"
|
||||||
icon.name: "fileopen"
|
icon.name: "fileopen"
|
||||||
onClicked: {
|
onClicked: backgroundType.open()
|
||||||
print("Action button in buttons page clicked");
|
}
|
||||||
fileDialog.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
|
text: songLyrics
|
||||||
textFormat: TextEdit.MarkdownText
|
textFormat: TextEdit.MarkdownText
|
||||||
padding: 10
|
padding: 10
|
||||||
onEditingFinished: editorTimer.running = false
|
onEditingFinished: song.lyricsSlides(text)
|
||||||
onPressed: editorTimer.running = true
|
onPressed: editorTimer.running = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
370
src/qthelper.hpp
Normal file
370
src/qthelper.hpp
Normal file
|
@ -0,0 +1,370 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// libmpv
|
||||||
|
#include <mpv/client.h>
|
||||||
|
|
||||||
|
// std
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
// Qt
|
||||||
|
#include <QVariant>
|
||||||
|
#include <QString>
|
||||||
|
#include <QList>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QSharedPointer>
|
||||||
|
#include <QMetaType>
|
||||||
|
|
||||||
|
namespace mpv {
|
||||||
|
namespace qt {
|
||||||
|
|
||||||
|
// Wrapper around mpv_handle. Does refcounting under the hood.
|
||||||
|
class Handle
|
||||||
|
{
|
||||||
|
struct container {
|
||||||
|
container(mpv_handle *h) : mpv(h) {}
|
||||||
|
~container() { mpv_terminate_destroy(mpv); }
|
||||||
|
mpv_handle *mpv;
|
||||||
|
};
|
||||||
|
QSharedPointer<container> sptr;
|
||||||
|
public:
|
||||||
|
// Construct a new Handle from a raw mpv_handle with refcount 1. If the
|
||||||
|
// last Handle goes out of scope, the mpv_handle will be destroyed with
|
||||||
|
// mpv_terminate_destroy().
|
||||||
|
// Never destroy the mpv_handle manually when using this wrapper. You
|
||||||
|
// will create dangling pointers. Just let the wrapper take care of
|
||||||
|
// destroying the mpv_handle.
|
||||||
|
// Never create multiple wrappers from the same raw mpv_handle; copy the
|
||||||
|
// wrapper instead (that's what it's for).
|
||||||
|
static Handle FromRawHandle(mpv_handle *handle) {
|
||||||
|
Handle h;
|
||||||
|
h.sptr = QSharedPointer<container>(new container(handle));
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the raw handle; for use with the libmpv C API.
|
||||||
|
operator mpv_handle*() const { return sptr ? (*sptr).mpv : 0; }
|
||||||
|
};
|
||||||
|
|
||||||
|
static inline QVariant node_to_variant(const mpv_node *node)
|
||||||
|
{
|
||||||
|
switch (node->format) {
|
||||||
|
case MPV_FORMAT_STRING:
|
||||||
|
return QVariant(QString::fromUtf8(node->u.string));
|
||||||
|
case MPV_FORMAT_FLAG:
|
||||||
|
return QVariant(static_cast<bool>(node->u.flag));
|
||||||
|
case MPV_FORMAT_INT64:
|
||||||
|
return QVariant(static_cast<qlonglong>(node->u.int64));
|
||||||
|
case MPV_FORMAT_DOUBLE:
|
||||||
|
return QVariant(node->u.double_);
|
||||||
|
case MPV_FORMAT_NODE_ARRAY: {
|
||||||
|
mpv_node_list *list = node->u.list;
|
||||||
|
QVariantList qlist;
|
||||||
|
for (int n = 0; n < list->num; n++)
|
||||||
|
qlist.append(node_to_variant(&list->values[n]));
|
||||||
|
return QVariant(qlist);
|
||||||
|
}
|
||||||
|
case MPV_FORMAT_NODE_MAP: {
|
||||||
|
mpv_node_list *list = node->u.list;
|
||||||
|
QVariantMap qmap;
|
||||||
|
for (int n = 0; n < list->num; n++) {
|
||||||
|
qmap.insert(QString::fromUtf8(list->keys[n]),
|
||||||
|
node_to_variant(&list->values[n]));
|
||||||
|
}
|
||||||
|
return QVariant(qmap);
|
||||||
|
}
|
||||||
|
default: // MPV_FORMAT_NONE, unknown values (e.g. future extensions)
|
||||||
|
return QVariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct node_builder {
|
||||||
|
node_builder(const QVariant& v) {
|
||||||
|
set(&node_, v);
|
||||||
|
}
|
||||||
|
~node_builder() {
|
||||||
|
free_node(&node_);
|
||||||
|
}
|
||||||
|
mpv_node *node() { return &node_; }
|
||||||
|
private:
|
||||||
|
Q_DISABLE_COPY(node_builder)
|
||||||
|
mpv_node node_;
|
||||||
|
mpv_node_list *create_list(mpv_node *dst, bool is_map, int num) {
|
||||||
|
dst->format = is_map ? MPV_FORMAT_NODE_MAP : MPV_FORMAT_NODE_ARRAY;
|
||||||
|
mpv_node_list *list = new mpv_node_list();
|
||||||
|
dst->u.list = list;
|
||||||
|
if (!list)
|
||||||
|
goto err;
|
||||||
|
list->values = new mpv_node[num]();
|
||||||
|
if (!list->values)
|
||||||
|
goto err;
|
||||||
|
if (is_map) {
|
||||||
|
list->keys = new char*[num]();
|
||||||
|
if (!list->keys)
|
||||||
|
goto err;
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
err:
|
||||||
|
free_node(dst);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
char *dup_qstring(const QString &s) {
|
||||||
|
QByteArray b = s.toUtf8();
|
||||||
|
char *r = new char[b.size() + 1];
|
||||||
|
if (r)
|
||||||
|
std::memcpy(r, b.data(), b.size() + 1);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
bool test_type(const QVariant &v, QMetaType::Type t) {
|
||||||
|
// The Qt docs say: "Although this function is declared as returning
|
||||||
|
// "QVariant::Type(obsolete), the return value should be interpreted
|
||||||
|
// as QMetaType::Type."
|
||||||
|
// So a cast really seems to be needed to avoid warnings (urgh).
|
||||||
|
return static_cast<int>(v.type()) == static_cast<int>(t);
|
||||||
|
}
|
||||||
|
void set(mpv_node *dst, const QVariant &src) {
|
||||||
|
if (test_type(src, QMetaType::QString)) {
|
||||||
|
dst->format = MPV_FORMAT_STRING;
|
||||||
|
dst->u.string = dup_qstring(src.toString());
|
||||||
|
if (!dst->u.string)
|
||||||
|
goto fail;
|
||||||
|
} else if (test_type(src, QMetaType::Bool)) {
|
||||||
|
dst->format = MPV_FORMAT_FLAG;
|
||||||
|
dst->u.flag = src.toBool() ? 1 : 0;
|
||||||
|
} else if (test_type(src, QMetaType::Int) ||
|
||||||
|
test_type(src, QMetaType::LongLong) ||
|
||||||
|
test_type(src, QMetaType::UInt) ||
|
||||||
|
test_type(src, QMetaType::ULongLong))
|
||||||
|
{
|
||||||
|
dst->format = MPV_FORMAT_INT64;
|
||||||
|
dst->u.int64 = src.toLongLong();
|
||||||
|
} else if (test_type(src, QMetaType::Double)) {
|
||||||
|
dst->format = MPV_FORMAT_DOUBLE;
|
||||||
|
dst->u.double_ = src.toDouble();
|
||||||
|
} else if (src.canConvert<QVariantList>()) {
|
||||||
|
QVariantList qlist = src.toList();
|
||||||
|
mpv_node_list *list = create_list(dst, false, qlist.size());
|
||||||
|
if (!list)
|
||||||
|
goto fail;
|
||||||
|
list->num = qlist.size();
|
||||||
|
for (int n = 0; n < qlist.size(); n++)
|
||||||
|
set(&list->values[n], qlist[n]);
|
||||||
|
} else if (src.canConvert<QVariantMap>()) {
|
||||||
|
QVariantMap qmap = src.toMap();
|
||||||
|
mpv_node_list *list = create_list(dst, true, qmap.size());
|
||||||
|
if (!list)
|
||||||
|
goto fail;
|
||||||
|
list->num = qmap.size();
|
||||||
|
for (int n = 0; n < qmap.size(); n++) {
|
||||||
|
list->keys[n] = dup_qstring(qmap.keys()[n]);
|
||||||
|
if (!list->keys[n]) {
|
||||||
|
free_node(dst);
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
set(&list->values[n], qmap.values()[n]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
fail:
|
||||||
|
dst->format = MPV_FORMAT_NONE;
|
||||||
|
}
|
||||||
|
void free_node(mpv_node *dst) {
|
||||||
|
switch (dst->format) {
|
||||||
|
case MPV_FORMAT_STRING:
|
||||||
|
delete[] dst->u.string;
|
||||||
|
break;
|
||||||
|
case MPV_FORMAT_NODE_ARRAY:
|
||||||
|
case MPV_FORMAT_NODE_MAP: {
|
||||||
|
mpv_node_list *list = dst->u.list;
|
||||||
|
if (list) {
|
||||||
|
for (int n = 0; n < list->num; n++) {
|
||||||
|
if (list->keys)
|
||||||
|
delete[] list->keys[n];
|
||||||
|
if (list->values)
|
||||||
|
free_node(&list->values[n]);
|
||||||
|
}
|
||||||
|
delete[] list->keys;
|
||||||
|
delete[] list->values;
|
||||||
|
}
|
||||||
|
delete list;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: ;
|
||||||
|
}
|
||||||
|
dst->format = MPV_FORMAT_NONE;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RAII wrapper that calls mpv_free_node_contents() on the pointer.
|
||||||
|
*/
|
||||||
|
struct node_autofree {
|
||||||
|
mpv_node *ptr;
|
||||||
|
node_autofree(mpv_node *a_ptr) : ptr(a_ptr) {}
|
||||||
|
~node_autofree() { mpv_free_node_contents(ptr); }
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the given property as mpv_node converted to QVariant, or QVariant()
|
||||||
|
* on error.
|
||||||
|
*
|
||||||
|
* @deprecated use get_property() instead
|
||||||
|
*
|
||||||
|
* @param name the property name
|
||||||
|
*/
|
||||||
|
static inline QVariant get_property_variant(mpv_handle *ctx, const QString &name)
|
||||||
|
{
|
||||||
|
mpv_node node;
|
||||||
|
if (mpv_get_property(ctx, name.toUtf8().data(), MPV_FORMAT_NODE, &node) < 0)
|
||||||
|
return QVariant();
|
||||||
|
node_autofree f(&node);
|
||||||
|
return node_to_variant(&node);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the given property as mpv_node converted from the QVariant argument.
|
||||||
|
* @deprecated use set_property() instead
|
||||||
|
*/
|
||||||
|
static inline int set_property_variant(mpv_handle *ctx, const QString &name,
|
||||||
|
const QVariant &v)
|
||||||
|
{
|
||||||
|
node_builder node(v);
|
||||||
|
return mpv_set_property(ctx, name.toUtf8().data(), MPV_FORMAT_NODE, node.node());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the given option as mpv_node converted from the QVariant argument.
|
||||||
|
*
|
||||||
|
* @deprecated use set_property() instead
|
||||||
|
*/
|
||||||
|
static inline int set_option_variant(mpv_handle *ctx, const QString &name,
|
||||||
|
const QVariant &v)
|
||||||
|
{
|
||||||
|
node_builder node(v);
|
||||||
|
return mpv_set_option(ctx, name.toUtf8().data(), MPV_FORMAT_NODE, node.node());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mpv_command_node() equivalent. Returns QVariant() on error (and
|
||||||
|
* unfortunately, the same on success).
|
||||||
|
*
|
||||||
|
* @deprecated use command() instead
|
||||||
|
*/
|
||||||
|
static inline QVariant command_variant(mpv_handle *ctx, const QVariant &args)
|
||||||
|
{
|
||||||
|
node_builder node(args);
|
||||||
|
mpv_node res;
|
||||||
|
if (mpv_command_node(ctx, node.node(), &res) < 0)
|
||||||
|
return QVariant();
|
||||||
|
node_autofree f(&res);
|
||||||
|
return node_to_variant(&res);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is used to return error codes wrapped in QVariant for functions which
|
||||||
|
* return QVariant.
|
||||||
|
*
|
||||||
|
* You can use get_error() or is_error() to extract the error status from a
|
||||||
|
* QVariant value.
|
||||||
|
*/
|
||||||
|
struct ErrorReturn
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* enum mpv_error value (or a value outside of it if ABI was extended)
|
||||||
|
*/
|
||||||
|
int error;
|
||||||
|
|
||||||
|
ErrorReturn() : error(0) {}
|
||||||
|
explicit ErrorReturn(int err) : error(err) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the mpv error code packed into a QVariant, or 0 (success) if it's not
|
||||||
|
* an error value.
|
||||||
|
*
|
||||||
|
* @return error code (<0) or success (>=0)
|
||||||
|
*/
|
||||||
|
static inline int get_error(const QVariant &v)
|
||||||
|
{
|
||||||
|
if (!v.canConvert<ErrorReturn>())
|
||||||
|
return 0;
|
||||||
|
return v.value<ErrorReturn>().error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether the QVariant carries a mpv error code.
|
||||||
|
*/
|
||||||
|
static inline bool is_error(const QVariant &v)
|
||||||
|
{
|
||||||
|
return get_error(v) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the given property as mpv_node converted to QVariant, or QVariant()
|
||||||
|
* on error.
|
||||||
|
*
|
||||||
|
* @param name the property name
|
||||||
|
* @return the property value, or an ErrorReturn with the error code
|
||||||
|
*/
|
||||||
|
static inline QVariant get_property(mpv_handle *ctx, const QString &name)
|
||||||
|
{
|
||||||
|
mpv_node node;
|
||||||
|
int err = mpv_get_property(ctx, name.toUtf8().data(), MPV_FORMAT_NODE, &node);
|
||||||
|
if (err < 0)
|
||||||
|
return QVariant::fromValue(ErrorReturn(err));
|
||||||
|
node_autofree f(&node);
|
||||||
|
return node_to_variant(&node);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the given property as mpv_node converted from the QVariant argument.
|
||||||
|
*
|
||||||
|
* @return mpv error code (<0 on error, >= 0 on success)
|
||||||
|
*/
|
||||||
|
static inline int set_property(mpv_handle *ctx, const QString &name,
|
||||||
|
const QVariant &v)
|
||||||
|
{
|
||||||
|
node_builder node(v);
|
||||||
|
return mpv_set_property(ctx, name.toUtf8().data(), MPV_FORMAT_NODE, node.node());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mpv_command_node() equivalent.
|
||||||
|
*
|
||||||
|
* @param args command arguments, with args[0] being the command name as string
|
||||||
|
* @return the property value, or an ErrorReturn with the error code
|
||||||
|
*/
|
||||||
|
static inline QVariant command(mpv_handle *ctx, const QVariant &args)
|
||||||
|
{
|
||||||
|
node_builder node(args);
|
||||||
|
mpv_node res;
|
||||||
|
int err = mpv_command_node(ctx, node.node(), &res);
|
||||||
|
if (err < 0)
|
||||||
|
return QVariant::fromValue(ErrorReturn(err));
|
||||||
|
node_autofree f(&res);
|
||||||
|
return node_to_variant(&res);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mpv_command_node_async() equivalent.
|
||||||
|
*
|
||||||
|
* @param args command arguments, with args[0] being the command name as string
|
||||||
|
* @return empty QVariant, or an ErrorReturn with the error code
|
||||||
|
*/
|
||||||
|
static inline QVariant command_async(mpv_handle *ctx, const QVariant &args)
|
||||||
|
{
|
||||||
|
node_builder node(args);
|
||||||
|
quint64 replyUserdata = 0; // TODO: Bomi casted args[0] to int. Bomi's args was a QByteArray however.
|
||||||
|
int err = mpv_command_node_async(ctx, replyUserdata, node.node());
|
||||||
|
if (err < 0)
|
||||||
|
return QVariant::fromValue(ErrorReturn(err));
|
||||||
|
// TODO: Return unique replyUserdata so the app can wait for
|
||||||
|
// the result in an MPV_EVENT_COMMAND_REPLY event.
|
||||||
|
return QVariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_DECLARE_METATYPE(mpv::qt::ErrorReturn)
|
|
@ -1,4 +1,6 @@
|
||||||
#include "songlistmodel.h"
|
#include "songlistmodel.h"
|
||||||
|
#include <QTextStream>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
SongListModel::SongListModel(QObject *parent)
|
SongListModel::SongListModel(QObject *parent)
|
||||||
: QAbstractListModel(parent)
|
: QAbstractListModel(parent)
|
||||||
|
@ -59,3 +61,10 @@ QHash<int, QByteArray> SongListModel::roleNames() const
|
||||||
|
|
||||||
return mapping;
|
return mapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SongListModel::lyricsSlides(QString lyrics)
|
||||||
|
{
|
||||||
|
QTextStream stream(&lyrics);
|
||||||
|
QString line = stream.readLine();
|
||||||
|
qDebug() << line;
|
||||||
|
}
|
||||||
|
|
|
@ -36,9 +36,12 @@ public:
|
||||||
|
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QVector< Data > m_data;
|
QVector< Data > m_data;
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void lyricsSlides(QString lyrics);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // SONGLISTMODEL_H
|
#endif // SONGLISTMODEL_H
|
||||||
|
|
25
src/songtext.cpp
Normal file
25
src/songtext.cpp
Normal file
|
@ -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();
|
||||||
|
}
|
||||||
|
|
27
src/songtext.h
Normal file
27
src/songtext.h
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
#ifndef SONGTEXT_H
|
||||||
|
#define SONGTEXT_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <qqml.h>
|
||||||
|
|
||||||
|
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
|
Loading…
Add table
Add a link
Reference in a new issue