#include <mpv/client.h>
#include <mpv/opengl_cb.h>
+#include <mpv/qthelper.hpp>
#include <QLoggingCategory>
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");
{
}
};
+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);
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: {
--- /dev/null
+#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 {};
+}
--- /dev/null
+#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
pluginmanager.cpp \
videoelement.cpp \
instancemanager.cpp \
- settings.cpp
+ settings.cpp \
+ chaptermodel.cpp
HEADERS += \
player.h \
pluginmanager.h \
videoelement.h \
instancemanager.h \
- settings.h
+ settings.h \
+ chaptermodel.h
include(qtsingleapplication/qtsingleapplication.pri)
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 {
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; }
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);
loadNextFile();
}
-void Player::backendSourceChanged(QUrl source)
-{
+void Player::backendSourceChanged(QUrl source) {
if (m_currentSource == source)
return;
qCDebug(playerCategory) << "Duration changed to" << duration;
m_duration = duration;
emit durationChanged(duration);
+
+ m_chapterModel->setDuration(duration);
}
void Player::playbackPositionChanged(
void Player::streamsChanged() {}
+void Player::backendChaptersChanged(
+ const PlayerPluginInterface::ChapterList &chapters) {
+ m_chapterModel->setChapters(chapters);
+}
+
void Player::reqisterQmlTypes() {
qRegisterMetaType<TimeStamp>("TimeStamp");
qRegisterMetaType<StreamIndex>("StreamIndex");
#include <QUrl>
#include "aniplayer/backendpluginbase.h"
-#include "pluginmanager.h"
+#include "chaptermodel.h"
class Player : public QObject,
public PlayerPluginInterface,
Q_PROPERTY(
Player::SubtitleStreams availableSubtitleStreams READ
availableSubtitleStreams NOTIFY availableSubtitleStreamsChanged)
+ Q_PROPERTY(QAbstractItemModel *chapterModel READ chapterModel NOTIFY
+ chapterModelChanged)
public:
using StreamIndex = int;
double duration() const;
double position() const;
+ QAbstractItemModel *chapterModel() const;
+
signals:
void stateChanged(PlayState state);
void volumeChanged(Volume volume);
void durationChanged(double duration);
void positionChanged(double position);
+ void chapterModelChanged(QAbstractItemModel *chapterModel);
+
public slots:
// Basic Play state
void load(const QUrl &resource);
void playbackVolumeChanged(Volume) override;
void playbackMaxVolumeChanged(Volume) override;
void streamsChanged() override;
+ void backendChaptersChanged(const ChapterList &chapters) override;
public:
static void reqisterQmlTypes();
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;
}
}
SeekSlider {
- width: 200
+ width: 800
height: fullscreenButton.height
id: ss
duration: controlledPlayer ? controlledPlayer.duration : 0
import org.aptx.aniplayer 1.0
Item {
+ id: seekSlider
property double duration: 0
property double position: 0
enabled: duration > 1
+ function toPixels(timePos) {
+ return duration ? timePos / duration * seekSlider.width : 0;
+ }
+
Rectangle {
id: watched
color: "#00BB00"
anchors.top: parent.top
anchors.bottom: parent.bottom
width: {
- return duration ? position / duration * parent.width : 0;
+ return toPixels(position);
}
}
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