]> Some of my projects - aniplayer.git/commitdiff
Add support for chapters
authorAPTX <marek321@gmail.com>
Mon, 13 Mar 2017 17:40:11 +0000 (18:40 +0100)
committerAPTX <marek321@gmail.com>
Mon, 13 Mar 2017 17:48:58 +0000 (18:48 +0100)
Chapters and their start/end time is available in the chapter
model. The current chapter/chapter changes are currently not
implemented.

backendplugins/backend_mpv/backendmpv.cpp
core/chaptermodel.cpp [new file with mode: 0644]
core/chaptermodel.h [new file with mode: 0644]
core/core.pro
core/include/aniplayer/playerplugininterface.h
core/player.cpp
core/player.h
uiplugins/ui_desktop_qml_default/qml/PlayerControls.qml
uiplugins/ui_desktop_qml_default/qml/SeekSlider.qml

index 58bf9cb66931370c39f9967c0e0cdac1382e30c8..0c6c28ce2bce9e733a69d695a4a09f43a14cba92 100644 (file)
@@ -6,6 +6,7 @@
 
 #include <mpv/client.h>
 #include <mpv/opengl_cb.h>
+#include <mpv/qthelper.hpp>
 
 #include <QLoggingCategory>
 
@@ -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<MPV_FORMAT_DOUBLE> {
   }
 };
 
+template <> struct MpvProperty<MPV_FORMAT_NODE> {
+  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<mpv_node *>(property->data);
+  }
+};
+
 template <int TYPE>
 decltype(auto) readProperty(struct mpv_event_property *property) {
   return MpvProperty<TYPE>::read(property);
@@ -207,6 +219,21 @@ void MpvInstance::processMpvEvents() {
               static_cast<PlayerPluginInterface::Volume>(
                   readProperty<MPV_FORMAT_DOUBLE>(property) / 100.0));
         }
+      } else if (strcmp(property->name, "chapter-list") == 0) {
+        const auto node = readProperty<MPV_FORMAT_NODE>(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 (file)
index 0000000..15b0942
--- /dev/null
@@ -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<int, QByteArray> ChapterModel::roleNames() const {
+  static QHash<int, QByteArray> 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 (file)
index 0000000..95d31fc
--- /dev/null
@@ -0,0 +1,29 @@
+#ifndef CHAPTERMODEL_H
+#define CHAPTERMODEL_H
+
+#include "aniplayer/playerplugininterface.h"
+#include <QAbstractListModel>
+
+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<int, QByteArray> 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
index 9a045973148ac181c1f58ca08efeb8d017098a40..e971c2c2a9862fcbbe2dc5baaedbd171fc462d60 100644 (file)
@@ -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)
 
index 5dc3445878199a02d9cdbe40f65247feae7c8c00..fe7c12ecdddccedbfaa79ba8156956e73921251f 100644 (file)
@@ -46,6 +46,13 @@ public:
   virtual void playbackMaxVolumeChanged(Volume) = 0;
 
   virtual void streamsChanged() = 0;
+
+  struct Chapter {
+    QString title;
+    TimeStamp startTime;
+  };
+  using ChapterList = QList<Chapter>;
+  virtual void backendChaptersChanged(const ChapterList &chapters) = 0;
 };
 
 class PlayerRendererInterface {
index 6dd0544cfee094cdbe482f9731a1286441f70e28..a0d28bf419179ce0eef37ff1bc89cea23012099b 100644 (file)
@@ -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>("TimeStamp");
   qRegisterMetaType<StreamIndex>("StreamIndex");
index 2ad9a4c989a9a37850db1587d05f4f4bd96326da..20b264396ff804ae82ee82ab9ab9ff71fc5f1a84 100644 (file)
@@ -7,7 +7,7 @@
 #include <QUrl>
 
 #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;
index ef5a2e48ae8a55b936a8a10dce803da2be51dd79..498f1dc3351261b7e0bcc8d1c9efab4c651bf34a 100644 (file)
@@ -133,7 +133,7 @@ Row {
         }
     }
     SeekSlider {
-        width: 200
+        width: 800
         height: fullscreenButton.height
         id: ss
         duration: controlledPlayer ? controlledPlayer.duration : 0
index 596a4e8fdb61179b70b434f37b3df050be6fafae..b1b828d1b8d7297ffe3a62efa9ae355788819ae6 100644 (file)
@@ -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