diff --git a/orion.pro b/orion.pro index a1ff40d..1861a48 100644 --- a/orion.pro +++ b/orion.pro @@ -96,11 +96,8 @@ mpv { #DEFINES += DEBUG_LIBMPV DEFINES += MPV_PLAYER DEFINES += PLAYER_BACKEND=\\\"mpv\\\" - SOURCES += src/player/mpvrenderer.cpp \ - src/player/mpvobject.cpp - - HEADERS += src/player/mpvobject.h \ - src/player/mpvrenderer.h + SOURCES += src/player/mpvobject.cpp + HEADERS += src/player/mpvobject.h LIBS += -lmpv } diff --git a/src/main.cpp b/src/main.cpp index 15f481e..6a49fbe 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -40,7 +40,7 @@ #endif #ifdef MPV_PLAYER -#include "player/mpvrenderer.h" +#include "player/mpvobject.h" #endif inline void noisyFailureMsgHandler(QtMsgType /*type*/, const QMessageLogContext &/*context*/, const QString &/*msg*/) diff --git a/src/model/vod.cpp b/src/model/vod.cpp index 2d6cacd..a64cc69 100644 --- a/src/model/vod.cpp +++ b/src/model/vod.cpp @@ -14,27 +14,7 @@ #include "vod.h" -Vod::Vod() -{ - title = ""; - id = ""; - game = ""; - duration = 0; - views = 0; - preview = ""; - createdAt = ""; -} - -Vod::Vod(Vod &other) -{ - title = other.title; - id = other.id; - game = other.game; - views = other.views; - preview = other.preview; - duration = other.duration; - createdAt = other.createdAt; -} +Vod::Vod() { } QString Vod::getPreview() const { @@ -105,3 +85,13 @@ void Vod::setCreatedAt(const QString &value) { createdAt = value; } + +QString Vod::getSeekPreviews() const +{ + return seekPreviews; +} + +void Vod::setSeekPreviews(const QString &value) +{ + seekPreviews = value; +} diff --git a/src/model/vod.h b/src/model/vod.h index 9d3d35c..16230d5 100644 --- a/src/model/vod.h +++ b/src/model/vod.h @@ -20,17 +20,18 @@ class Vod { - QString title; - QString id; - QString game; - quint32 duration; - quint64 views; - QString preview; - QString createdAt; + QString title = ""; + QString id = ""; + QString game = ""; + quint32 duration = 0; + quint64 views = 0; + QString preview = ""; + QString createdAt = ""; + QString seekPreviews = ""; public: Vod(); - Vod(Vod &other); + Vod(Vod &other) = default; ~Vod(){}; QString getPreview() const; @@ -47,6 +48,8 @@ class Vod void setTitle(const QString &value); QString getCreatedAt() const; void setCreatedAt(const QString &value); + QString getSeekPreviews() const; + void setSeekPreviews(const QString &value); }; #endif // VOD_H diff --git a/src/model/vodlistmodel.cpp b/src/model/vodlistmodel.cpp index ef13ecf..4df7334 100644 --- a/src/model/vodlistmodel.cpp +++ b/src/model/vodlistmodel.cpp @@ -68,6 +68,11 @@ QVariant VodListModel::data(const QModelIndex &index, int role) const case CreatedAt: var.setValue(vod->getCreatedAt()); + break; + + case SeekPreviews: + var.setValue(vod->getSeekPreviews()); + break; } } @@ -89,6 +94,7 @@ QHash VodListModel::roleNames() const roles[Duration] = "duration"; roles[Views] = "views"; roles[CreatedAt] = "createdAt"; + roles[SeekPreviews] = "seekPreviews"; return roles; } diff --git a/src/model/vodlistmodel.h b/src/model/vodlistmodel.h index 709f002..d004534 100644 --- a/src/model/vodlistmodel.h +++ b/src/model/vodlistmodel.h @@ -36,7 +36,8 @@ class VodListModel: public QAbstractListModel Game, Duration, Views, - CreatedAt + CreatedAt, + SeekPreviews, }; Qt::ItemFlags flags(const QModelIndex &index) const; diff --git a/src/player/mpvobject.cpp b/src/player/mpvobject.cpp index 4bf6230..2c30462 100644 --- a/src/player/mpvobject.cpp +++ b/src/player/mpvobject.cpp @@ -1,75 +1,137 @@ #include "mpvobject.h" -#include "mpvrenderer.h" +#include +#include +#include +#include #include #include +#include +#include +#include + +#include +#include +#include + +namespace +{ static void wakeup(void *ctx) { MpvObject *mpvhandler = (MpvObject*)ctx; QCoreApplication::postEvent(mpvhandler, new QEvent(QEvent::User)); } -MpvObject::MpvObject(QQuickItem * parent) - : QQuickFramebufferObject(parent), mpv_gl(0) +void on_mpv_redraw(void *ctx) { - std::setlocale(LC_NUMERIC, "C"); + 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} + { + + } + + virtual ~MpvRenderer() + {} - mpv = mpv::qt::Handle::FromRawHandle(mpv_create()); + // 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{static_cast(fbo->handle()), fbo->width(), fbo->height(), 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"); #ifdef DEBUG_LIBMPV - //Enable for debugging mpv_set_option_string(mpv, "terminal", "yes"); mpv_set_option_string(mpv, "msg-level", "all=v"); #endif - // Make use of the MPV_SUB_API_OPENGL_CB API. - mpv::qt::set_option_variant(mpv, "gpu-context", "angle"); - mpv::qt::set_option_variant(mpv, "vo", "opengl-cb"); - //mpv::qt::set_option_variant(mpv, "input-cursor", "no"); - - // Request hw decoding, just for testing. - mpv::qt::set_option_variant(mpv, "hwdec", "auto"); - - //Cache - //mpv::qt::set_option_variant(mpv, "cache", 8192); - if (mpv_initialize(mpv) < 0) throw std::runtime_error("could not initialize mpv context"); - // 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. - mpv_gl = (mpv_opengl_cb_context *)mpv_get_sub_api(mpv, MPV_SUB_API_OPENGL_CB); - - if (!mpv_gl) - throw std::runtime_error("OpenGL not compiled in"); - - mpv_opengl_cb_set_update_callback(mpv_gl, MpvObject::on_update, (void *)this); - connect(this, &MpvObject::onUpdate, this, &MpvObject::doUpdate, - Qt::QueuedConnection); - - //Restore volume to 100 - //setProperty("volume", QVariant::fromValue(100)); - - //Set observe properties - mpv_observe_property(mpv, 0, "core-idle", MPV_FORMAT_FLAG); - //mpv_observe_property(mpv, 0, "volume", MPV_FORMAT_DOUBLE); - mpv_observe_property(mpv, 0, "cache-buffering-state", MPV_FORMAT_INT64); - mpv_observe_property(mpv, 0, "playback-time", MPV_FORMAT_DOUBLE); + // Request hw decoding, just for testing. + mpv::qt::set_option_variant(mpv, "hwdec", "auto"); - // setup callback event handling mpv_set_wakeup_callback(mpv, wakeup, this); - time = 0; + connect(this, &MpvObject::onUpdate, this, &MpvObject::doUpdate, Qt::QueuedConnection); } MpvObject::~MpvObject() { - if (mpv_gl) - mpv_opengl_cb_set_update_callback(mpv_gl, NULL, NULL); + 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) @@ -99,29 +161,47 @@ void MpvObject::setOption(const QString &name, const QVariant &value) mpv::qt::set_option_variant(mpv, name, value); } -QQuickFramebufferObject::Renderer *MpvObject::createRenderer() const +bool MpvObject::observeProperty(const QString &name, const QJSValue &callback) { - window()->setPersistentOpenGLContext(true); - window()->setPersistentSceneGraph(true); - return new MpvRenderer(this); + if (!callback.isCallable()) return false; + callbacks.emplace_back(std::make_unique(callback)); + QJSValue *pCallback = callbacks[callbacks.size() - 1].get(); + if (mpv_observe_property(mpv, (uint64_t)(pCallback), name.toLatin1().data(), MPV_FORMAT_NODE) >= 0) { + connect(callback.engine(), &QJSEngine::destroyed, this, [this, pCallback](QObject*){ + callbacks.erase(std::remove_if(callbacks.begin(), callbacks.end(), [&](auto const& cb){ + if (cb.get() == pCallback) { + mpv_unobserve_property(mpv, (uint64_t)pCallback); + return true; + } + return false; + })); + }); + return true; + } else { + callbacks.pop_back(); + return false; + } } -void MpvObject::pause() +bool MpvObject::unobserveProperty(const QJSValue &callback) { - time = QDateTime::currentMSecsSinceEpoch(); - QStringList args = (QStringList() << "set" << "pause" << "yes"); - mpv::qt::command_variant(mpv, args); + bool erased = false; + callbacks.erase(std::remove_if(callbacks.begin(), callbacks.end(), [&](auto const& cb){ + if (cb->equals(callback)) { + mpv_unobserve_property(mpv, (uint64_t)cb.get()); + erased = true; + return true; + } + return false; + })); + return erased; } -void MpvObject::play(bool autoReload) +QQuickFramebufferObject::Renderer *MpvObject::createRenderer() const { - if (autoReload && QDateTime::currentMSecsSinceEpoch() - time > 5000){ - qDebug() << "Waited too long, resetting playback" << mpv::qt::get_property_variant(mpv, "path"); - mpv::qt::command_variant(mpv, (QStringList() << "loadfile" << mpv::qt::get_property_variant(mpv, "path").toString())); - } - - QStringList args = (QStringList() << "set" << "pause" << "no"); - mpv::qt::command_variant(mpv, args); + window()->setPersistentOpenGLContext(true); + window()->setPersistentSceneGraph(true); + return new MpvRenderer(const_cast(this)); } QVariant MpvObject::getProperty(const QString &name) @@ -129,8 +209,6 @@ QVariant MpvObject::getProperty(const QString &name) return mpv::qt::get_property_variant(mpv, name); } - - bool MpvObject::event(QEvent *event) { if(event->type() == QEvent::User) @@ -148,68 +226,26 @@ bool MpvObject::event(QEvent *event) { case MPV_EVENT_PROPERTY_CHANGE: { - mpv_event_property *prop = (mpv_event_property*)event->data; - if(QString(prop->name) == "playback-time") // playback-time does the same thing as time-pos but works for streaming media - { - if(prop->format == MPV_FORMAT_DOUBLE) - { - int pos = (int)*(double *)prop->data; - emit positionChanged(pos); - } - } - else if(QString(prop->name) == "volume") - { - if(prop->format == MPV_FORMAT_DOUBLE) - emit volumeChanged(*(double*)prop->data); - } - else if(QString(prop->name) == "sid") - { -// if(prop->format == MPV_FORMAT_INT64) -// setSid(*(int*)prop->data); - } - else if(QString(prop->name) == "aid") - { -// if(prop->format == MPV_FORMAT_INT64) -// setAid(*(int*)prop->data); - } - else if(QString(prop->name) == "sub-visibility") - { -// if(prop->format == MPV_FORMAT_FLAG) -// setSubtitleVisibility((bool)*(unsigned*)prop->data); - } - else if(QString(prop->name) == "mute") - { -// if(prop->format == MPV_FORMAT_FLAG) -// setMute((bool)*(unsigned*)prop->data); - } - else if(QString(prop->name) == "core-idle") - { - if(prop->format == MPV_FORMAT_FLAG) - { - if((bool)*(unsigned*)prop->data){ - emit playingPaused(); - } else { - emit playingResumed(); - } - } - } - else if(QString(prop->name) == "cache-buffering-state") - { - if(prop->format == MPV_FORMAT_INT64) - { - if ((int) *(int*)prop->data < 100) - emit bufferingStarted(); + mpv_event_property *prop = reinterpret_cast(event->data); + if (prop->format == MPV_FORMAT_NODE && event->reply_userdata) { + QJSValue& callback = *reinterpret_cast(event->reply_userdata); + mpv_node* node = reinterpret_cast(prop->data); + mpv::qt::node_autofree f(node); + + if (callback.isCallable()) { + QJSEngine *engine = callback.engine(); + callback.call({ + engine->toScriptValue(mpv::qt::node_to_variant(node)), + engine->toScriptValue(QString(prop->name)) + }); } } break; } case MPV_EVENT_IDLE: - emit playingStopped(); break; - // these two look like they're reversed but they aren't. the names are misleading. case MPV_EVENT_START_FILE: break; - case MPV_EVENT_UNPAUSE: break; case MPV_EVENT_PAUSE: diff --git a/src/player/mpvobject.h b/src/player/mpvobject.h index 849dc06..0ec0b7a 100644 --- a/src/player/mpvobject.h +++ b/src/player/mpvobject.h @@ -1,58 +1,52 @@ #ifndef MPVOBJECT_H #define MPVOBJECT_H +#include +#include + #include + #include -#include +#include #include -#include -#include -#include -#include -#include -#include "../power/power.h" + +class MpvRenderer; class MpvObject : public QQuickFramebufferObject { Q_OBJECT - mpv::qt::Handle mpv; - mpv_opengl_cb_context *mpv_gl; + mpv_handle *mpv; + mpv_render_context *mpv_gl; + std::vector> callbacks; friend class MpvRenderer; public: + static void on_update(void *ctx); + MpvObject(QQuickItem * parent = 0); virtual ~MpvObject(); virtual Renderer *createRenderer() const; - Q_INVOKABLE void pause(); - Q_INVOKABLE void play(bool autoReload = true); Q_INVOKABLE QVariant getProperty(const QString &name); public slots: void command(const QVariant& params); void setProperty(const QString& name, const QVariant& value); void setOption(const QString& name, const QVariant& value); + bool observeProperty(const QString& name, const QJSValue& callback); + bool unobserveProperty(const QJSValue& callback); signals: void onUpdate(); - void playingPaused(); - void playingStopped(); - void playingResumed(); - void bufferingStarted(); - - void volumeChanged(double volume); - void positionChanged(int position); private slots: void doUpdate(); private: qint64 time; - static void on_update(void *ctx); bool event(QEvent *event); }; - #endif // MPVOBJECT_H diff --git a/src/player/mpvrenderer.cpp b/src/player/mpvrenderer.cpp deleted file mode 100644 index 557b89e..0000000 --- a/src/player/mpvrenderer.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "mpvrenderer.h" - - -void *MpvRenderer::get_proc_address(void *ctx, const char *name) { - (void)ctx; - QOpenGLContext *glctx = QOpenGLContext::currentContext(); - if (!glctx) - return NULL; - - void *res = (void *)glctx->getProcAddress(QByteArray(name)); -#ifdef Q_OS_WIN - if (!res) - { - HMODULE oglmod = GetModuleHandleA("opengl32.dll"); - return (void *) GetProcAddress(oglmod, name); - } -#endif - return res; -} - -MpvRenderer::MpvRenderer(const MpvObject *obj) - : mpv(obj->mpv), window(obj->window()), mpv_gl(obj->mpv_gl) -{ - int r = mpv_opengl_cb_init_gl(mpv_gl, NULL, get_proc_address, NULL); - if (r < 0) - throw std::runtime_error("could not initialize OpenGL"); -} - - -MpvRenderer::~MpvRenderer() -{ - // Until this call is done, we need to make sure the player remains - // alive. This is done implicitly with the mpv::qt::Handle instance - // in this class. - mpv_opengl_cb_uninit_gl(mpv_gl); -} - -void MpvRenderer::render() -{ - QOpenGLFramebufferObject *fbo = framebufferObject(); - window->resetOpenGLState(); - mpv_opengl_cb_draw(mpv_gl, fbo->handle(), fbo->width(), fbo->height()); - window->resetOpenGLState(); -} diff --git a/src/player/mpvrenderer.h b/src/player/mpvrenderer.h deleted file mode 100644 index 9a96589..0000000 --- a/src/player/mpvrenderer.h +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef MPVRENDERER_H_ -#define MPVRENDERER_H_ - - -#include "mpvobject.h" -#include -#include - -#include -#include -#include -#include -#include - -#ifdef Q_OS_WIN - #include -#endif - -#include - -class MpvRenderer : public QQuickFramebufferObject::Renderer, protected QOpenGLFunctions -{ - static void *get_proc_address(void *ctx, const char *name); - - mpv::qt::Handle mpv; - QQuickWindow *window; - mpv_opengl_cb_context *mpv_gl; - -public: - MpvRenderer(const MpvObject *obj); - - virtual ~MpvRenderer(); - - void render(); -}; - - -#endif diff --git a/src/qml/MpvBackend.qml b/src/qml/MpvBackend.qml index ba20448..a74d9ea 100644 --- a/src/qml/MpvBackend.qml +++ b/src/qml/MpvBackend.qml @@ -14,6 +14,7 @@ import QtQuick 2.5 import mpv 1.0 +import "util.js" as Util /* Interface for backend Mpv @@ -53,25 +54,21 @@ Item { if (start >= 0) { position = start renderer.setOption("start", "+" + position) - lastStartPosition = position; - streamOffsetCalibrated = false; - streamOffset = 0; } renderer.setOption("audio-client-name", "Orion"); renderer.setOption("title", description); - renderer.command(["loadfile", src]) - + renderer.command(["loadfile", src, "replace"]) resume() } function resume() { - renderer.play(false) + renderer.command(["set", "pause", "no"]) } function pause() { - renderer.pause() + renderer.command(["set", "pause", "yes"]) } function stop() { @@ -87,31 +84,25 @@ Item { } function seekTo(sec) { - var adjustedSec = sec; - if (streamOffsetCalibrated) { - adjustedSec += streamOffset; - } - renderer.setProperty("playback-time", adjustedSec) + status = "BUFFERING" + renderer.setProperty("playback-time", sec) root.position = sec; } function setVolume(vol) { - if (Qt.platform.os === "linux") - volume = Math.round(Math.log(vol) / Math.log(100)) - else - volume = Math.round(vol) + volume = Math.round(vol) } function getDecoder() { var defaultDecoders = [] if (Qt.platform.os == "windows") { - defaultDecoders = [ "dxva2-copy", "d3d11va-copy", "cuda-copy", "no" ] + defaultDecoders = [ "dxva2-copy", "d3d11va-copy", "cuda-copy", "nvdec-copy", "no" ] } else if (Qt.platform.os == "osx" || Qt.platform.os == "ios") { defaultDecoders = [ "videotoolbox", "no" ] } else if(Qt.platform.os == "android") { defaultDecoders = [ "mediacodec_embed", "no" ] } else if (Qt.platform.os == "linux") { - defaultDecoders = [ "vaapi-copy", "vdpau-copy", "no" ] + defaultDecoders = [ "vaapi-copy", "vdpau-copy", "cuda-copy", "nvdec-copy", "no" ] } return [ "auto" ].concat(defaultDecoders) } @@ -142,77 +133,63 @@ Item { } property int position: 0 - onPositionChanged: { - //console.log("Position", position) - - } - - property int lastStartPosition; - property bool streamOffsetCalibrated: false; - property int streamOffset: 0; property double volume: 100 onVolumeChanged: { - //console.log("Volume", volume) renderer.setProperty("volume", volume) } - Timer { - id: positionTimer - interval: 1000 - running: false - repeat: true - onTriggered: { - if (root.status === "PLAYING") - root.position += 1 - } - } - MpvObject { id: renderer anchors.fill: parent - onBufferingStarted: { - root.status = "BUFFERING" - } - - onPlayingStopped: { - root.status = "STOPPED" - positionTimer.stop() - } - - onPlayingPaused: { - root.status = "PAUSED" - positionTimer.stop() - } - - onPlayingResumed: { - root.status = "PLAYING" - positionTimer.start() + function updateStatus() { + if (idleActive) { + if (root.status !== "STOPPED") root.playingStopped() + root.status = "STOPPED" + } else if (bufferingState >= 100 && !seeking) { + if (!getProperty("pause")) { + if (root.status != "PLAYING") root.playingResumed() + root.status = "PLAYING" + } else if (coreIdle) { + if (root.status === "BUFFERING") command(["frame-step"]) + if (root.status !== "PAUSED") root.playingPaused() + root.status = "PAUSED" + } else { + root.status = "BUFFERING" + } + } else { + root.status = "BUFFERING" + } } - onPositionChanged: { - var adjustedPosition = position; + onPlaybackTimeChanged: root.position = playbackTime + onCoreIdleChanged: Qt.callLater(updateStatus) + onBufferingStateChanged: Qt.callLater(updateStatus) + onIdleActiveChanged: Qt.callLater(updateStatus) + onSeekingChanged: Qt.callLater(updateStatus) + onVolumeChanged: root.volumeChangedInternally() - if (!root.streamOffsetCalibrated && status == "PLAYING") { - root.streamOffset = position - lastStartPosition; - root.streamOffsetCalibrated = true; - console.log("MpvBackend stream offset", root.streamOffset); - } + property real bufferingState: 0 + property bool coreIdle: true + property bool idleActive: true + property bool seeking: false + property real volume: getProperty("volume") + property real playbackTime: 0 - if (root.streamOffsetCalibrated) { - adjustedPosition -= root.streamOffset; - } + Component.onCompleted: { + renderer.observeProperty("cache-buffering-state", function(value) { bufferingState = value }); + renderer.observeProperty("core-idle", function(value) { coreIdle = value }); + renderer.observeProperty("idle-active", function(value) { idleActive = value }); + renderer.observeProperty("seeking", function(value) { seeking = value }); + renderer.observeProperty("volume", function(value) { volume = value }); + renderer.observeProperty("playback-time", function(value) { playbackTime = value }); - if (root.position !== adjustedPosition) - root.position = adjustedPosition + // https://github.com/mpv-player/mpv/issues/4195 + Util.setInterval(function() { renderer.playbackTime = renderer.getProperty("playback-time") }, 1000) - positionTimer.restart() - } - - Component.onCompleted: { - root.setVolume(Math.round(renderer.getProperty("volume"))) + root.setVolume(Math.round(volume)) root.volumeChangedInternally() } } diff --git a/src/qml/MultimediaBackend.qml b/src/qml/MultimediaBackend.qml index 73c6bfc..d2675cd 100644 --- a/src/qml/MultimediaBackend.qml +++ b/src/qml/MultimediaBackend.qml @@ -80,6 +80,8 @@ Item { } function seekTo(pos) { + if (status !== "STOPPING") + status = "BUFFERING" renderer.seek(pos * 1000) root.position = pos } @@ -135,11 +137,21 @@ Item { console.error(errorString) } - onStatusChanged: { - if (status === MediaPlayer.Buffering) + function updateStatus() { + if (status === MediaPlayer.Buffering) { root.status = "BUFFERING" + } else if (playbackState === MediaPlayer.PlayingState) { + root.status = "PLAYING" + } else if (playbackState === MediaPlayer.PausedState) { + root.status = "PAUSED" + } else if (playbackState === MediaPlayer.StoppedState) { + root.status = "STOPPED" + } } + onStatusChanged: updateStatus() + onPlaybackStateChanged: updateStatus() + onStopped: { root.status = "STOPPED" root.playingStopped() @@ -161,8 +173,10 @@ Item { return; } var pos = position / 1000 - if (root.position !== pos) + if (root.position !== pos) { root.position = pos + updateStatus() + } } } diff --git a/src/qml/PlayerView.qml b/src/qml/PlayerView.qml index 81d1a59..69d8f08 100644 --- a/src/qml/PlayerView.qml +++ b/src/qml/PlayerView.qml @@ -17,6 +17,7 @@ import QtQuick.Controls 2.1 import QtQuick.Controls.Material 2.1 import QtQuick.Layouts 1.1 import "components" +import "util.js" as Util import app.orion 1.0 @@ -34,9 +35,9 @@ Page { onHeadersVisibleChanged: { if (root.visible) { - pArea.hoverEnabled = false + pArea.hoverEnabled = false topbar.visible = headersVisible - disableTimer.restart() + disableTimer.restart() } } @@ -176,12 +177,13 @@ Page { "_id": channel._id, "name": channel.name, "game": isVod ? vod.game : channel.game, - "title": isVod ? vod.title : channel.title, - "online": channel.online, - "favourite": channel.favourite || ChannelManager.containsFavourite(channel._id), - "viewers": channel.viewers, - "logo": channel.logo, - "preview": channel.preview + "title": isVod ? vod.title : channel.title, + "online": channel.online, + "favourite": channel.favourite || ChannelManager.containsFavourite(channel._id), + "viewers": channel.viewers, + "logo": channel.logo, + "preview": channel.preview, + "seekPreviews": isVod ? vod.seekPreviews : "", } favBtn.update() @@ -307,13 +309,16 @@ Page { } } - Shortcut { - sequence: "0" - context: Qt.ApplicationShortcut - onActivated: { - reloadStream() - pArea.refreshHeaders() - } + Repeater { + model: ["0", "F5"] + delegate: Item { Shortcut { + sequence: modelData + context: Qt.ApplicationShortcut + onActivated: { + reloadStream() + clickRect.show("\ue5d5") + } + } } } Shortcut { @@ -337,7 +342,7 @@ Page { context: Qt.ApplicationShortcut onActivated: { volumeBtn.toggleMute() - pArea.refreshHeaders() + clickRect.show(volumeSlider.value > 0 ? "\ue050" : "\ue04f") } } @@ -345,8 +350,10 @@ Page { sequence: "Up" context: Qt.ApplicationShortcut onActivated: { - volumeSlider.value += 5 - pArea.refreshHeaders() + if (volumeSlider.value < volumeSlider.to) { + volumeSlider.value += 5 + clickRect.show("\ue050") + } } } @@ -354,8 +361,10 @@ Page { sequence: "Down" context: Qt.ApplicationShortcut onActivated: { - volumeSlider.value -= 5 - pArea.refreshHeaders() + if (volumeSlider.value > volumeSlider.from) { + volumeSlider.value -= 5 + clickRect.show("\ue04d") + } } } @@ -365,7 +374,8 @@ Page { onActivated: { if (!isVod || seekBar.pressed) return seekBar.seek(seekBar.value + 5) - pArea.refreshHeaders() + var totalSeek = seekBar.value - renderer.position + clickRect.show((totalSeek > 0 ? "+" : "") + totalSeek + "s") } } @@ -375,7 +385,8 @@ Page { onActivated: { if (!isVod || seekBar.pressed) return seekBar.seek(seekBar.value - 5) - pArea.refreshHeaders() + var totalSeek = seekBar.value - renderer.position + clickRect.show((totalSeek > 0 ? "+" : "") + totalSeek + "s") } } @@ -406,6 +417,41 @@ Page { } } + SeekPreview { + id: preview + anchors.fill: parent + blur: 2 + visible: opacity > 0 + opacity: 0 + Behavior on opacity { PropertyAnimation { easing: Easing.InOutCubic } } + + Connections { + target: seekBar + onValueChanged: preview.value = seekBar.value + onPressedChanged: { + if (seekBar.pressed) { + preview.opacity = 1 + //todo: stop player while seeking + } + } + } + Connections { + target: renderer + onStatusChanged: { + if (preview.visible && !seekBar.pressed && renderer.status !== "BUFFERING") { + Util.setTimeout(function() { + if (!seekBar.pressed) preview.opacity = 0 + }, 100) + } + } + } + Connections { + target: root + onCurrentChannelChanged: preview.source = currentChannel.seekPreviews + onDurationChanged: preview.to = duration + } + } + BusyIndicator { anchors.centerIn: parent running: renderer.status === "BUFFERING" @@ -422,14 +468,14 @@ Page { hideTimer.restart() } - Timer { - id: disableTimer - interval: 200 - running: false - onTriggered:{ - pArea.hoverEnabled = true - } - } + Timer { + id: disableTimer + interval: 200 + running: false + onTriggered:{ + pArea.hoverEnabled = true + } + } onVisibleChanged: refreshHeaders() onPositionChanged: refreshHeaders() @@ -446,27 +492,22 @@ Page { id: clickRectIcon text: "" anchors.centerIn: parent - font.family: "Material Icons" - font.pointSize: parent.width * 0.5 + font.family: /^[\x00-\x7F]*$/.test(text) ? "Helvetica" : "Material Icons" + font.pointSize: parent.width * 0.5 / text.length } ParallelAnimation { id: _anim running: false - onStarted: { - clickRectIcon.text = renderer.status !== "PLAYING" ? "\ue037" : "\ue034" - } - onStopped: { clickRect.opacity = 0 - clickRect.width = 0 } NumberAnimation { target: clickRect property: "width" - from: 0 + from: pArea.width * 0.1 to: pArea.width * 0.6 duration: 1500 easing.type: Easing.OutCubic @@ -481,8 +522,13 @@ Page { } } + function show(text) { + clickRectIcon.text = text + _anim.restart() + } + function run() { - _anim.restart() + show(renderer.status !== "PLAYING" ? "\ue037" : "\ue034") } function abort() { @@ -710,6 +756,81 @@ Page { value = val seekTimer.restart() } + + MouseArea { + id: seekBarMouseArea + anchors.fill: seekBar + hoverEnabled: true + propagateComposedEvents: true + onClicked: mouse.accepted = false + onPressed: mouse.accepted = false + } + + } + + Rectangle { + color: root.Material.background + radius: 5 + implicitHeight: seekTooltip.implicitHeight + implicitWidth: seekTooltip.implicitWidth + + anchors.bottom: seekBar.top + x: Math.min(seekBar.width-width-5, Math.max(5, (seekBar.pressed ? (seekBar.handle.x + seekBar.handle.width / 2) : seekBarMouseArea.mouseX) - width / 2)) + + visible: opacity > 0 + opacity: (seekBar.hovered || seekBar.pressed) ? 1 : 0 + Behavior on opacity { PropertyAnimation { easing: Easing.InCubic } } + + ColumnLayout { + id: seekTooltip + anchors.fill: parent + spacing: 0 + + function timeAtMouse() { + if (seekBar.pressed && seekBar.live) return seekBar.value + var seekWidth = seekBar.width - seekBar.handle.width + var seekX = seekBarMouseArea.mouseX - seekBar.handle.width / 2 + seekX = Math.min(seekWidth, Math.max(0, seekX)) + return seekBar.valueAt(seekX / seekWidth) + } + + SeekPreview { + id: seekPreview + + Layout.topMargin: 5 + Layout.leftMargin: 5 + Layout.rightMargin: 5 + + Layout.preferredWidth: implicitWidth + Layout.preferredHeight: implicitHeight + + visible: !seekBar.pressed && implicitWidth > 0 + blur: 1 + + Connections { + target: seekBar + onValueChanged: seekPreview.value = seekTooltip.timeAtMouse() + } + Connections { + target: seekBarMouseArea + onPositionChanged: seekPreview.value = seekTooltip.timeAtMouse() + } + Connections { + target: root + onCurrentChannelChanged: seekPreview.source = currentChannel.seekPreviews + onDurationChanged: seekPreview.to = duration + } + + } + + Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + padding: 5 + text: Util.formatTime(seekTooltip.timeAtMouse()) + } + } + } RowLayout { @@ -813,13 +934,7 @@ Page { clip: true function updateText() { if (!isVod) return "" - var formatTime = function(seconds) { - var d = new Date() - d.setTime(seconds * 1000) - d.setMinutes(d.getMinutes() + d.getTimezoneOffset()) - return d.toTimeString() - } - text = formatTime(seekBar.value) + "/" + formatTime(duration) + text = Util.formatTime(seekBar.value) + "/" + Util.formatTime(duration) } Connections { target: seekBar diff --git a/src/qml/VodsView.qml b/src/qml/VodsView.qml index ff425cc..0de4f6e 100644 --- a/src/qml/VodsView.qml +++ b/src/qml/VodsView.qml @@ -96,6 +96,7 @@ Item{ position: channelVodPositions[model.id] || 0 game: model.game createdAt: model.createdAt + seekPreviews: model.seekPreviews width: vodgrid.cellWidth } diff --git a/src/qml/components/SeekPreview.qml b/src/qml/components/SeekPreview.qml new file mode 100644 index 0000000..19ddd68 --- /dev/null +++ b/src/qml/components/SeekPreview.qml @@ -0,0 +1,134 @@ +import QtQuick 2.5 +import QtGraphicalEffects 1.0 +import "../util.js" as Util + +Item { + id: root + property string source + property real from: 0 + property real to: d.duration + property real value: 0 + property int fillMode: Image.PreserveAspectFit + property real blur: 0 + property var color: "black" + + implicitWidth: image.implicitWidth + implicitHeight: image.implicitHeight + + QtObject { + id: d + + property string baseUrl + + property int index: 0 + + property int width: 0 + property int height: 0 + property int cols: 0 + property int rows: 0 + property int count: 0 + + property real duration: 0 + property var images: [] + + function updateImage() { + index = Math.min(count - 1, Math.round((value - root.from) / (root.to - root.from) * count)) + } + + function updateInfo() { + duration = 0 + width = 0 + height = 0 + count = 0 + images = [] + if (!source) return + baseUrl = source.substring(0, source.lastIndexOf("/")) + Util.requestJSON(root.source, function(resp) { + var info = resp[0] + for(var i = 1; i < resp.length; i++) { + if (resp[i].width > info.width) { + info = resp[i] + } + } + duration = info.count * info.interval + width = info.width + height = info.height + rows = info.rows + cols = info.cols + count = info.count + images = info.images + updateImage() + }) + } + } + + Connections { + target: root + onSourceChanged: d.updateInfo() + onFromChanged: d.updateImage() + onToChanged: d.updateImage() + onValueChanged: d.updateImage() + } + + Rectangle { + visible: root.color && root.color != "transparent" + color: root.color + anchors.fill: parent + } + + Item { + id: image + visible: d.count > 0 + + implicitWidth: d.width + implicitHeight: d.height + + property real fitToWidth: root.fillMode === Image.PreserveAspectFit ? implicitWidth * root.height > root.width * implicitHeight : true + property real fitToHeight: root.fillMode === Image.PreserveAspectFit ? !fitToWidth : true + + property real paintedWidth: fitToWidth ? root.width : paintedHeight / implicitHeight * implicitWidth + property real paintedHeight: fitToHeight ? root.height : paintedWidth / implicitWidth * implicitHeight + + x: (root.width - paintedWidth) / 2 + y: (root.height - paintedHeight) / 2 + + transform: Scale { + xScale: image.paintedWidth / image.implicitWidth + yScale: image.paintedHeight / image.implicitHeight + } + + Repeater { + model: d.count + delegate: Item { + visible: index === d.index + clip: true + width: d.width + height: d.height + + property int fullRow: Math.floor(index / d.cols) + property int column: index % d.cols + property int page: Math.floor(fullRow / d.rows) + property int row: fullRow % d.rows + + Image { + x: -parent.width * parent.column + y: -parent.height * parent.row + source: d.baseUrl + "/" + d.images[parent.page] + asynchronous: true + cache: true + } + + GaussianBlur { + visible: root.blur && root.blur > 0 + anchors.fill: parent + // according to docs setting source of GaussianBlur to the parent is not allowed, but it works and is + // better than adding a second repeater and trying find the source via Repeater.itemAt + source: parent + radius: root.blur + samples: Math.floor(radius * 2 + 1) + cached: true + } + } + } + } +} diff --git a/src/qml/components/Video.qml b/src/qml/components/Video.qml index c5ff6e2..f8cf79c 100644 --- a/src/qml/components/Video.qml +++ b/src/qml/components/Video.qml @@ -22,6 +22,7 @@ Channel { property int duration property int position property string createdAt + property string seekPreviews online: true Label { diff --git a/src/qml/main.qml b/src/qml/main.qml index f8b2106..9b22ff9 100644 --- a/src/qml/main.qml +++ b/src/qml/main.qml @@ -36,7 +36,15 @@ ApplicationWindow { Material.theme: Settings.lightTheme ? Material.Light : Material.Dark title: "Orion" - visibility: appFullScreen ? Window.FullScreen : Window.AutomaticVisibility + visibility: Window.AutomaticVisibility + + property int restoredVisibility: Window.AutomaticVisibility + onAppFullScreenChanged: { + if (visibility != Window.FullScreen) { + restoredVisibility = visibility + } + visibility = appFullScreen ? Window.FullScreen : restoredVisibility + } property variant rootWindow: root property variant g_tooltip diff --git a/src/qml/qml.qrc b/src/qml/qml.qrc index e4d2aa5..9e96a6c 100644 --- a/src/qml/qml.qrc +++ b/src/qml/qml.qrc @@ -42,5 +42,6 @@ components/RoundImage.qml components/OptionButton.qml components/RotatingButton.qml + components/SeekPreview.qml diff --git a/src/qml/util.js b/src/qml/util.js index 65b6ba4..02b722d 100644 --- a/src/qml/util.js +++ b/src/qml/util.js @@ -269,3 +269,68 @@ function objectAssign() { } return target; } + +function formatTime(seconds) { + seconds = Math.floor(seconds) + var hours = Math.floor(seconds / 3600) + var minutes = Math.floor(seconds / 60) % 60 + seconds = seconds % 60 + hours = hours < 10 ? '0' + hours : hours + minutes = minutes < 10 ? '0' + minutes : minutes + seconds = seconds < 10 ? '0' + seconds : seconds + return hours + ":" + minutes + ":" + seconds +} + +function requestJSON(url, callback) { + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = (function(xhr) { + return function() { + if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { + callback(JSON.parse(xhr.responseText)); + } + } + })(xhr); + xhr.open('GET', url, true); + xhr.send(''); +} + +var timerComponent = Qt.createQmlObject('import QtQuick 2.5; Component { Timer {} }', Qt.application); +var freeTimers = []; +function setTimeout(callback, timeout) { + var timer = freeTimers.length > 0 ? freeTimers.pop() : timerComponent.createObject(Qt.application); + timer.interval = timeout || 0; + var onTriggered = function() { + timer.triggered.disconnect(onTriggered); + timer.stop(); + freeTimers.push(timer); + callback(); + } + timer.triggered.connect(onTriggered); + timer.start(); +} + +var intervalTimerIndex = 0; +var intervalTimer = {}; +function setInterval(callback, timeout) { + var timer = freeTimers.length > 0 ? freeTimers.pop() : timerComponent.createObject(Qt.application); + timer.interval = Math.max(10, timeout || 0); + timer.repeat = true; + timer.triggered.connect(callback); + timer.start(); + intervalTimer[intervalTimerIndex] = { + timer: timer, + callback: callback + } + return intervalTimerIndex++; +} + +function clearInterval(val) { + if (!intervalTimer[val]) return; + var timer = intervalTimer[val].timer; + var callback = intervalTimer[val].callback; + timer.triggered.disconnect(callback); + timer.stop(); + timer.repeat = false; + freeTimers.push(timer); + delete intervalTimer[val]; +} diff --git a/src/util/jsonparser.cpp b/src/util/jsonparser.cpp index 8fe8ba1..6e23fd0 100644 --- a/src/util/jsonparser.cpp +++ b/src/util/jsonparser.cpp @@ -224,13 +224,16 @@ Vod *JsonParser::parseVod(const QJsonObject &json) vod->setPreview(preview.toString()); } else if (preview.isObject()) { - const QJsonValue & previewUrl = preview.toObject()["medium"]; + const QJsonValue & previewUrl = preview.toObject()["large"]; if (previewUrl.isString()) { vod->setPreview(previewUrl.toString()); } } } + if (!json["seek_previews_url"].isNull()) + vod->setSeekPreviews(json["seek_previews_url"].toString()); + if (!json["title"].isNull()) vod->setTitle(json["title"].toString());