From: APTX Date: Mon, 13 Mar 2017 17:40:11 +0000 (+0100) Subject: Add support for chapters X-Git-Url: https://gitweb.tyo.aptx.org/?a=commitdiff_plain;h=e461d7ef12fdb174caa6506e9630e0ec3adbf123;p=aniplayer.git Add support for chapters Chapters and their start/end time is available in the chapter model. The current chapter/chapter changes are currently not implemented. --- diff --git a/backendplugins/backend_mpv/backendmpv.cpp b/backendplugins/backend_mpv/backendmpv.cpp index 58bf9cb..0c6c28c 100644 --- a/backendplugins/backend_mpv/backendmpv.cpp +++ b/backendplugins/backend_mpv/backendmpv.cpp @@ -6,6 +6,7 @@ #include #include +#include #include @@ -57,7 +58,9 @@ MpvInstance::MpvInstance(PlayerPluginInterface *playerInterface, qCDebug(mpvBackend) << "register" << VOLUME << mpv_observe_property(m_handle, 0, VOLUME, MPV_FORMAT_DOUBLE); - + qCDebug(mpvBackend) << "register chapter-list" + << mpv_observe_property(m_handle, 0, "chapter-list", + MPV_FORMAT_NODE); qCDebug(mpvBackend) << "request log messages" << mpv_request_log_messages(m_handle, "info"); { @@ -159,6 +162,15 @@ template <> struct MpvProperty { } }; +template <> struct MpvProperty { + static mpv_node *read(struct mpv_event_property *property) { + Q_ASSERT(property->format == MPV_FORMAT_NODE); + if (!property->data) + qWarning("Property data data is null"); + return static_cast(property->data); + } +}; + template decltype(auto) readProperty(struct mpv_event_property *property) { return MpvProperty::read(property); @@ -207,6 +219,21 @@ void MpvInstance::processMpvEvents() { static_cast( readProperty(property) / 100.0)); } + } else if (strcmp(property->name, "chapter-list") == 0) { + const auto node = readProperty(property); + const auto variant = mpv::qt::node_to_variant(node); + qCDebug(mpvBackend) << "CHAPTERS" << variant; + + PlayerPluginInterface::ChapterList chapters; + for (const auto &v : variant.toList()) { + const auto map = v.toMap(); + chapters << PlayerPluginInterface::Chapter{map["title"].toString(), + map["time"].toDouble()}; + } + m_player->backendChaptersChanged(chapters); + } else { + qCWarning(mpvBackend) + << "Change notification for not handled property" << property->name; } } break; case MPV_EVENT_LOG_MESSAGE: { diff --git a/core/chaptermodel.cpp b/core/chaptermodel.cpp new file mode 100644 index 0000000..15b0942 --- /dev/null +++ b/core/chaptermodel.cpp @@ -0,0 +1,45 @@ +#include "chaptermodel.h" + +ChapterModel::ChapterModel(QObject *parent) : QAbstractListModel{parent} {} + +void ChapterModel::setChapters(const PlayerPluginInterface::ChapterList &data) { + beginResetModel(); + m_data = data; + endResetModel(); +} + +void ChapterModel::setDuration(PlayerPluginInterface::TimeStamp duration) { + m_duration = duration; + const auto idx = index(rowCount(), 0, {}); + emit dataChanged(idx, idx); +} + +QHash ChapterModel::roleNames() const { + static QHash roles{ + {TitleRole, "title"}, + {StartTimeRole, "startTime"}, + {EndTimeRole, "endTime"}, + }; + return roles; +} + +int ChapterModel::rowCount(const QModelIndex &parent) const { + if (parent.isValid()) + return 0; + return m_data.size(); +} + +QVariant ChapterModel::data(const QModelIndex &index, int role) const { + switch (role) { + case TitleRole: + case Qt::DisplayRole: + return m_data[index.row()].title; + case StartTimeRole: + return m_data[index.row()].startTime; + case EndTimeRole: + if (index.row() + 1 == m_data.size()) + return m_duration; + return m_data[index.row() + 1].startTime; + } + return {}; +} diff --git a/core/chaptermodel.h b/core/chaptermodel.h new file mode 100644 index 0000000..95d31fc --- /dev/null +++ b/core/chaptermodel.h @@ -0,0 +1,29 @@ +#ifndef CHAPTERMODEL_H +#define CHAPTERMODEL_H + +#include "aniplayer/playerplugininterface.h" +#include + +class ChapterModel : public QAbstractListModel { +public: + enum ChapterRoles { + TitleRole = Qt::UserRole + 1, + StartTimeRole, + EndTimeRole + }; + + ChapterModel(QObject *parent = nullptr); + + void setChapters(const PlayerPluginInterface::ChapterList &); + void setDuration(PlayerPluginInterface::TimeStamp duration); + + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex{}) const override; + QVariant data(const QModelIndex &index, int role) const override; + +private: + PlayerPluginInterface::ChapterList m_data; + PlayerPluginInterface::TimeStamp m_duration; +}; + +#endif // CHAPTERMODEL_H diff --git a/core/core.pro b/core/core.pro index 9a04597..e971c2c 100644 --- a/core/core.pro +++ b/core/core.pro @@ -11,7 +11,8 @@ SOURCES += main.cpp \ pluginmanager.cpp \ videoelement.cpp \ instancemanager.cpp \ - settings.cpp + settings.cpp \ + chaptermodel.cpp HEADERS += \ player.h \ @@ -22,7 +23,8 @@ HEADERS += \ pluginmanager.h \ videoelement.h \ instancemanager.h \ - settings.h + settings.h \ + chaptermodel.h include(qtsingleapplication/qtsingleapplication.pri) diff --git a/core/include/aniplayer/playerplugininterface.h b/core/include/aniplayer/playerplugininterface.h index 5dc3445..fe7c12e 100644 --- a/core/include/aniplayer/playerplugininterface.h +++ b/core/include/aniplayer/playerplugininterface.h @@ -46,6 +46,13 @@ public: virtual void playbackMaxVolumeChanged(Volume) = 0; virtual void streamsChanged() = 0; + + struct Chapter { + QString title; + TimeStamp startTime; + }; + using ChapterList = QList; + virtual void backendChaptersChanged(const ChapterList &chapters) = 0; }; class PlayerRendererInterface { diff --git a/core/player.cpp b/core/player.cpp index 6dd0544..a0d28bf 100644 --- a/core/player.cpp +++ b/core/player.cpp @@ -15,6 +15,9 @@ Player::Player(BackendPluginBase *backendPlugin, QObject *parent) qCDebug(playerCategory) << "Creating player" << this; m_backend = backendPlugin->createInstance(this); Q_CHECK_PTR(m_backend); + + m_chapterModel = new ChapterModel{this}; + Q_CHECK_PTR(m_chapterModel); } Player::~Player() { qCDebug(playerCategory) << "Destroying player" << this; } @@ -63,6 +66,8 @@ double Player::duration() const { return m_duration; } double Player::position() const { return m_position; } +QAbstractItemModel *Player::chapterModel() const { return m_chapterModel; } + void Player::load(const QUrl &resource) { if (canLoadVideoNow()) m_backend->open(resource); @@ -149,8 +154,7 @@ void Player::backendReadyToPlay() { loadNextFile(); } -void Player::backendSourceChanged(QUrl source) -{ +void Player::backendSourceChanged(QUrl source) { if (m_currentSource == source) return; @@ -187,6 +191,8 @@ void Player::playbackDurationChanged( qCDebug(playerCategory) << "Duration changed to" << duration; m_duration = duration; emit durationChanged(duration); + + m_chapterModel->setDuration(duration); } void Player::playbackPositionChanged( @@ -219,6 +225,11 @@ void Player::playbackMaxVolumeChanged(Player::Volume volume) { void Player::streamsChanged() {} +void Player::backendChaptersChanged( + const PlayerPluginInterface::ChapterList &chapters) { + m_chapterModel->setChapters(chapters); +} + void Player::reqisterQmlTypes() { qRegisterMetaType("TimeStamp"); qRegisterMetaType("StreamIndex"); diff --git a/core/player.h b/core/player.h index 2ad9a4c..20b2643 100644 --- a/core/player.h +++ b/core/player.h @@ -7,7 +7,7 @@ #include #include "aniplayer/backendpluginbase.h" -#include "pluginmanager.h" +#include "chaptermodel.h" class Player : public QObject, public PlayerPluginInterface, @@ -41,6 +41,8 @@ class Player : public QObject, Q_PROPERTY( Player::SubtitleStreams availableSubtitleStreams READ availableSubtitleStreams NOTIFY availableSubtitleStreamsChanged) + Q_PROPERTY(QAbstractItemModel *chapterModel READ chapterModel NOTIFY + chapterModelChanged) public: using StreamIndex = int; @@ -85,6 +87,8 @@ public: double duration() const; double position() const; + QAbstractItemModel *chapterModel() const; + signals: void stateChanged(PlayState state); void volumeChanged(Volume volume); @@ -106,6 +110,8 @@ signals: void durationChanged(double duration); void positionChanged(double position); + void chapterModelChanged(QAbstractItemModel *chapterModel); + public slots: // Basic Play state void load(const QUrl &resource); @@ -144,6 +150,7 @@ protected: void playbackVolumeChanged(Volume) override; void playbackMaxVolumeChanged(Volume) override; void streamsChanged() override; + void backendChaptersChanged(const ChapterList &chapters) override; public: static void reqisterQmlTypes(); @@ -167,6 +174,7 @@ private: Player::TimeStamp m_duration = 0; Player::TimeStamp m_position = 0; VideoUpdateInterface *m_renderer = nullptr; + ChapterModel *m_chapterModel; bool m_muted = false; bool m_backendInstanceReady = false; bool m_rendererReady = false; diff --git a/uiplugins/ui_desktop_qml_default/qml/PlayerControls.qml b/uiplugins/ui_desktop_qml_default/qml/PlayerControls.qml index ef5a2e4..498f1dc 100644 --- a/uiplugins/ui_desktop_qml_default/qml/PlayerControls.qml +++ b/uiplugins/ui_desktop_qml_default/qml/PlayerControls.qml @@ -133,7 +133,7 @@ Row { } } SeekSlider { - width: 200 + width: 800 height: fullscreenButton.height id: ss duration: controlledPlayer ? controlledPlayer.duration : 0 diff --git a/uiplugins/ui_desktop_qml_default/qml/SeekSlider.qml b/uiplugins/ui_desktop_qml_default/qml/SeekSlider.qml index 596a4e8..b1b828d 100644 --- a/uiplugins/ui_desktop_qml_default/qml/SeekSlider.qml +++ b/uiplugins/ui_desktop_qml_default/qml/SeekSlider.qml @@ -2,6 +2,7 @@ import QtQuick 2.0 import org.aptx.aniplayer 1.0 Item { + id: seekSlider property double duration: 0 property double position: 0 @@ -9,6 +10,10 @@ Item { enabled: duration > 1 + function toPixels(timePos) { + return duration ? timePos / duration * seekSlider.width : 0; + } + Rectangle { id: watched color: "#00BB00" @@ -16,7 +21,7 @@ Item { anchors.top: parent.top anchors.bottom: parent.bottom width: { - return duration ? position / duration * parent.width : 0; + return toPixels(position); } } @@ -29,6 +34,20 @@ Item { anchors.bottom: parent.bottom } + Repeater { + model: player.chapterModel + anchors.fill: parent + delegate: Text { + y: 0 + x: { + console.log("CHAPTER", title, 0, toPixels(startTime - 0), startTime); + return toPixels(startTime - 0); + } + width: toPixels(endTime - startTime) + text: title + } + } + MouseArea { id: ma anchors.fill: parent