mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2024-11-22 09:15:18 +00:00
Move playback logic to C++ side
This commit is contained in:
parent
5ddd5e8e2e
commit
a244c27b1a
|
@ -39,11 +39,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
#include "credentialmanager.h"
|
#include "credentialmanager.h"
|
||||||
#include "jellyfindeviceprofile.h"
|
#include "jellyfindeviceprofile.h"
|
||||||
#include "jellyfinitem.h"
|
#include "jellyfinitem.h"
|
||||||
|
#include "jellyfinplaybackmanager.h"
|
||||||
#include "jellyfinwebsocket.h"
|
#include "jellyfinwebsocket.h"
|
||||||
|
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
class MediaSource;
|
class MediaSource;
|
||||||
class WebSocket;
|
class WebSocket;
|
||||||
|
class PlaybackManager;
|
||||||
/**
|
/**
|
||||||
* @brief An Api client for Jellyfin. Handles requests and authentication.
|
* @brief An Api client for Jellyfin. Handles requests and authentication.
|
||||||
*
|
*
|
||||||
|
@ -71,6 +73,7 @@ class WebSocket;
|
||||||
*/
|
*/
|
||||||
class ApiClient : public QObject {
|
class ApiClient : public QObject {
|
||||||
friend class WebSocket;
|
friend class WebSocket;
|
||||||
|
friend class PlaybackManager;
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit ApiClient(QObject *parent = nullptr);
|
explicit ApiClient(QObject *parent = nullptr);
|
||||||
|
|
|
@ -23,6 +23,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QtGlobal>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
|
||||||
#include <QUrlQuery>
|
#include <QUrlQuery>
|
||||||
|
@ -31,11 +32,22 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
|
|
||||||
#include "jellyfinapiclient.h"
|
#include "jellyfinapiclient.h"
|
||||||
|
#include "jellyfinitem.h"
|
||||||
|
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
|
|
||||||
class PlaybackManager : public QObject {
|
// Forward declaration of Jellyfin::Item found in jellyfinitem.h
|
||||||
|
class Item;
|
||||||
|
// Forward declaration of Jellyfin::ApiClient found in jellyfinapiclient.h
|
||||||
|
class ApiClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The PlaybackManager class manages the playback of Jellyfin items. It fetches streams based on Jellyfin items, posts
|
||||||
|
* the current playback state to the Jellyfin Server and so on.
|
||||||
|
*/
|
||||||
|
class PlaybackManager : public QObject, public QQmlParserStatus {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
Q_INTERFACES(QQmlParserStatus)
|
||||||
public:
|
public:
|
||||||
enum PlayMethod {
|
enum PlayMethod {
|
||||||
Transcode,
|
Transcode,
|
||||||
|
@ -46,58 +58,71 @@ public:
|
||||||
|
|
||||||
explicit PlaybackManager(QObject *parent = nullptr);
|
explicit PlaybackManager(QObject *parent = nullptr);
|
||||||
Q_PROPERTY(ApiClient *apiClient MEMBER m_apiClient)
|
Q_PROPERTY(ApiClient *apiClient MEMBER m_apiClient)
|
||||||
Q_PROPERTY(QString itemId READ itemId WRITE setItemId NOTIFY itemIdChanged)
|
Q_PROPERTY(Item *item READ item WRITE setItem NOTIFY itemChanged)
|
||||||
Q_PROPERTY(QString streamUrl READ streamUrl NOTIFY streamUrlChanged)
|
Q_PROPERTY(QString streamUrl READ streamUrl NOTIFY streamUrlChanged)
|
||||||
Q_PROPERTY(bool autoOpen MEMBER m_autoOpen NOTIFY autoOpenChanged)
|
Q_PROPERTY(bool autoOpen MEMBER m_autoOpen NOTIFY autoOpenChanged)
|
||||||
Q_PROPERTY(int audioIndex MEMBER m_audioIndex NOTIFY audioIndexChanged)
|
Q_PROPERTY(int audioIndex MEMBER m_audioIndex NOTIFY audioIndexChanged)
|
||||||
Q_PROPERTY(int subtitleIndex MEMBER m_subtitleIndex NOTIFY subtitleIndexChanged)
|
Q_PROPERTY(int subtitleIndex MEMBER m_subtitleIndex NOTIFY subtitleIndexChanged)
|
||||||
Q_PROPERTY(qint64 position MEMBER m_position WRITE setPosition NOTIFY positionChanged)
|
Q_PROPERTY(bool resumePlayback MEMBER m_resumePlayback NOTIFY resumePlaybackChanged)
|
||||||
Q_PROPERTY(QMediaPlayer::State state READ state WRITE setState NOTIFY stateChanged)
|
Q_PROPERTY(QObject* mediaPlayer READ mediaPlayer WRITE setMediaPlayer NOTIFY mediaPlayerChanged)
|
||||||
|
|
||||||
QString itemId() const { return m_itemId; }
|
Item *item() const { return m_item; }
|
||||||
void setItemId(const QString &newItemId);
|
void setItem(Item *newItem);
|
||||||
|
|
||||||
QMediaPlayer::State state() const { return m_state; }
|
QObject *mediaPlayer() const {
|
||||||
void setState(QMediaPlayer::State newState);
|
return m_qmlMediaPlayer;
|
||||||
|
}
|
||||||
void setPosition(qint64 position);
|
void setMediaPlayer(QObject *qmlMediaPlayer);
|
||||||
|
|
||||||
QString streamUrl() const { return m_streamUrl; }
|
QString streamUrl() const { return m_streamUrl; }
|
||||||
signals:
|
signals:
|
||||||
void itemIdChanged(const QString &newItemId);
|
void itemChanged(Item *newItemId);
|
||||||
void streamUrlChanged(const QString &newStreamUrl);
|
void streamUrlChanged(const QString &newStreamUrl);
|
||||||
void autoOpenChanged(bool autoOpen);
|
void autoOpenChanged(bool autoOpen);
|
||||||
void audioIndexChanged(int audioIndex);
|
void audioIndexChanged(int audioIndex);
|
||||||
void subtitleIndexChanged(int subtitleIndex);
|
void subtitleIndexChanged(int subtitleIndex);
|
||||||
void positionChanged(qint64 position);
|
void mediaPlayerChanged(QObject *newMediaPlayer);
|
||||||
void stateChanged(QMediaPlayer::State state);
|
void resumePlaybackChanged(bool newResumePlayback);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void updatePlaybackInfo();
|
void updatePlaybackInfo();
|
||||||
|
private slots:
|
||||||
|
void mediaPlayerStateChanged(QMediaPlayer::State newState);
|
||||||
|
void mediaPlayerPositionChanged(qint64 position);
|
||||||
|
void mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus newStatus);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QTimer m_updateTimer;
|
QTimer m_updateTimer;
|
||||||
ApiClient *m_apiClient = nullptr;
|
ApiClient *m_apiClient = nullptr;
|
||||||
QString m_itemId;
|
Item *m_item = nullptr;
|
||||||
QString m_streamUrl;
|
QString m_streamUrl;
|
||||||
QString m_playSessionId;
|
QString m_playSessionId;
|
||||||
int m_audioIndex = 0;
|
int m_audioIndex = 0;
|
||||||
int m_subtitleIndex = -1;
|
int m_subtitleIndex = -1;
|
||||||
qint64 m_position = 0;
|
qint64 m_resumePosition = 0;
|
||||||
|
qint64 m_oldPosition = 0;
|
||||||
qint64 m_stopPosition = 0;
|
qint64 m_stopPosition = 0;
|
||||||
|
QMediaPlayer::State m_oldState = QMediaPlayer::StoppedState;
|
||||||
PlayMethod m_playMethod;
|
PlayMethod m_playMethod;
|
||||||
QMediaPlayer::State m_state = QMediaPlayer::StoppedState;
|
QObject *m_qmlMediaPlayer = nullptr;
|
||||||
|
QMediaPlayer * m_mediaPlayer = nullptr;
|
||||||
|
bool m_resumePlayback = true;
|
||||||
|
|
||||||
|
bool m_qmlIsParsingComponent = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Whether to automatically open the livestream of the item;
|
* @brief Whether to automatically open the livestream of the item;
|
||||||
*/
|
*/
|
||||||
bool m_autoOpen = false;
|
bool m_autoOpen = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Retrieves the URL of the stream to open.
|
||||||
|
*/
|
||||||
void fetchStreamUrl();
|
void fetchStreamUrl();
|
||||||
void setStreamUrl(const QString &streamUrl);
|
void setStreamUrl(const QString &streamUrl);
|
||||||
|
|
||||||
// Factor to multiply with when converting from milliseconds to ticks.
|
// Factor to multiply with when converting from milliseconds to ticks.
|
||||||
const int MS_TICK_FACTOR = 10000;
|
const static int MS_TICK_FACTOR = 10000;
|
||||||
|
|
||||||
enum PlaybackInfoType { Started, Stopped, Progress };
|
enum PlaybackInfoType { Started, Stopped, Progress };
|
||||||
|
|
||||||
|
@ -105,6 +130,11 @@ private:
|
||||||
* @brief Posts the playback information
|
* @brief Posts the playback information
|
||||||
*/
|
*/
|
||||||
void postPlaybackInfo(PlaybackInfoType type);
|
void postPlaybackInfo(PlaybackInfoType type);
|
||||||
|
|
||||||
|
void classBegin() override {
|
||||||
|
m_qmlIsParsingComponent = true;
|
||||||
|
}
|
||||||
|
void componentComplete() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,19 +29,27 @@ PlaybackManager::PlaybackManager(QObject *parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::fetchStreamUrl() {
|
void PlaybackManager::fetchStreamUrl() {
|
||||||
|
if (m_item == nullptr || m_apiClient == nullptr) return;
|
||||||
|
m_resumePosition = 0;
|
||||||
|
if (m_resumePlayback && !m_item->property("userData").isNull()) {
|
||||||
|
UserData* userData = qvariant_cast<UserData *>(m_item->property("userData"));
|
||||||
|
if (userData != nullptr) {
|
||||||
|
m_resumePosition = userData->playbackPositionTicks();
|
||||||
|
}
|
||||||
|
}
|
||||||
QUrlQuery params;
|
QUrlQuery params;
|
||||||
params.addQueryItem("UserId", m_apiClient->userId());
|
params.addQueryItem("UserId", m_apiClient->userId());
|
||||||
params.addQueryItem("StartTimeTicks", QString::number(m_position));
|
params.addQueryItem("StartTimeTicks", QString::number(m_resumePosition));
|
||||||
params.addQueryItem("IsPlayback", "true");
|
params.addQueryItem("IsPlayback", "true");
|
||||||
params.addQueryItem("AutoOpenLiveStream", this->m_autoOpen ? "true" : "false");
|
params.addQueryItem("AutoOpenLiveStream", this->m_autoOpen ? "true" : "false");
|
||||||
params.addQueryItem("MediaSourceId", this->m_itemId);
|
params.addQueryItem("MediaSourceId", this->m_item->jellyfinId());
|
||||||
params.addQueryItem("SubtitleStreamIndex", QString::number(m_subtitleIndex));
|
params.addQueryItem("SubtitleStreamIndex", QString::number(m_subtitleIndex));
|
||||||
params.addQueryItem("AudioStreamIndex", QString::number(m_audioIndex));
|
params.addQueryItem("AudioStreamIndex", QString::number(m_audioIndex));
|
||||||
|
|
||||||
QJsonObject root;
|
QJsonObject root;
|
||||||
root["DeviceProfile"] = m_apiClient->playbackDeviceProfile();
|
root["DeviceProfile"] = m_apiClient->playbackDeviceProfile();
|
||||||
|
|
||||||
QNetworkReply *rep = m_apiClient->post("/Items/" + this->m_itemId + "/PlaybackInfo", QJsonDocument(root), params);
|
QNetworkReply *rep = m_apiClient->post("/Items/" + this->m_item->jellyfinId() + "/PlaybackInfo", QJsonDocument(root), params);
|
||||||
connect(rep, &QNetworkReply::finished, this, [this, rep]() {
|
connect(rep, &QNetworkReply::finished, this, [this, rep]() {
|
||||||
QJsonObject root = QJsonDocument::fromJson(rep->readAll()).object();
|
QJsonObject root = QJsonDocument::fromJson(rep->readAll()).object();
|
||||||
this->m_playSessionId = root["PlaySessionId"].toString();
|
this->m_playSessionId = root["PlaySessionId"].toString();
|
||||||
|
@ -50,11 +58,11 @@ void PlaybackManager::fetchStreamUrl() {
|
||||||
if (this->m_autoOpen) {
|
if (this->m_autoOpen) {
|
||||||
QJsonArray mediaSources = root["MediaSources"].toArray();
|
QJsonArray mediaSources = root["MediaSources"].toArray();
|
||||||
//FIXME: relies on the fact that the returned transcode url always has a query!
|
//FIXME: relies on the fact that the returned transcode url always has a query!
|
||||||
this->m_streamUrl = this->m_apiClient->baseUrl()
|
QString streamUrl = this->m_apiClient->baseUrl()
|
||||||
+ mediaSources[0].toObject()["TranscodingUrl"].toString();
|
+ mediaSources[0].toObject()["TranscodingUrl"].toString();
|
||||||
|
|
||||||
this->m_playMethod = Transcode;
|
this->m_playMethod = Transcode;
|
||||||
emit this->streamUrlChanged(this->m_streamUrl);
|
setStreamUrl(streamUrl);
|
||||||
qDebug() << "Found stream url: " << this->m_streamUrl;
|
qDebug() << "Found stream url: " << this->m_streamUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,39 +70,46 @@ void PlaybackManager::fetchStreamUrl() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::setItemId(const QString &newItemId) {
|
void PlaybackManager::setItem(Item *newItem) {
|
||||||
|
this->m_item = newItem;
|
||||||
|
// Don't try to start fetching when we're not completely parsed yet.
|
||||||
|
if (m_qmlIsParsingComponent) return;
|
||||||
|
|
||||||
if (m_apiClient == nullptr) {
|
if (m_apiClient == nullptr) {
|
||||||
qWarning() << "apiClient is not set on this MediaSource instance! Aborting.";
|
qWarning() << "apiClient is not set on this MediaSource instance! Aborting.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this->m_itemId = newItemId;
|
|
||||||
// Deinitialize the streamUrl
|
// Deinitialize the streamUrl
|
||||||
setStreamUrl("");
|
setStreamUrl("");
|
||||||
if (!newItemId.isEmpty()) {
|
if (newItem != nullptr && !newItem->jellyfinId().isEmpty()) {
|
||||||
fetchStreamUrl();
|
fetchStreamUrl();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::setStreamUrl(const QString &streamUrl) {
|
void PlaybackManager::setStreamUrl(const QString &streamUrl) {
|
||||||
this->m_streamUrl = streamUrl;
|
this->m_streamUrl = streamUrl;
|
||||||
|
// Inspired by PHP naming schemes
|
||||||
|
QUrl realStreamUrl(streamUrl);
|
||||||
|
Q_ASSERT_X(realStreamUrl.isValid(), "setStreamUrl", "StreamURL Jellyfin returned is not valid");
|
||||||
|
if (m_mediaPlayer != nullptr) {
|
||||||
|
m_mediaPlayer->setMedia(QMediaContent(realStreamUrl));
|
||||||
|
}
|
||||||
emit streamUrlChanged(streamUrl);
|
emit streamUrlChanged(streamUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::setPosition(qint64 position) {
|
void PlaybackManager::mediaPlayerPositionChanged(qint64 position) {
|
||||||
if (position == 0 && m_position != 0) {
|
if (position == 0 && m_oldPosition != 0) {
|
||||||
// Save the old position when stop gets called. The QMediaPlayer will try to set
|
// Save the old position when stop gets called. The QMediaPlayer will try to set
|
||||||
// position to 0 when stopped, but we don't want to report that to Jellyfin. We
|
// position to 0 when stopped, but we don't want to report that to Jellyfin. We
|
||||||
// want the old position.
|
// want the old position.
|
||||||
m_stopPosition = m_position;
|
m_stopPosition = m_oldPosition;
|
||||||
}
|
}
|
||||||
m_position = position;
|
m_oldPosition = position;
|
||||||
emit positionChanged(position);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::setState(QMediaPlayer::State newState) {
|
void PlaybackManager::mediaPlayerStateChanged(QMediaPlayer::State newState) {
|
||||||
if (m_state == newState) return;
|
if (m_oldState == newState) return;
|
||||||
if (m_state == QMediaPlayer::StoppedState) {
|
if (m_oldState == QMediaPlayer::StoppedState) {
|
||||||
// We're transitioning from stopped to either playing or paused.
|
// We're transitioning from stopped to either playing or paused.
|
||||||
// Set up the recurring timer
|
// Set up the recurring timer
|
||||||
m_updateTimer.start();
|
m_updateTimer.start();
|
||||||
|
@ -106,10 +121,37 @@ void PlaybackManager::setState(QMediaPlayer::State newState) {
|
||||||
} else {
|
} else {
|
||||||
postPlaybackInfo(Progress);
|
postPlaybackInfo(Progress);
|
||||||
}
|
}
|
||||||
|
m_oldState = newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlaybackManager::mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus newStatus) {
|
||||||
|
if (newStatus == QMediaPlayer::LoadedMedia) {
|
||||||
|
m_mediaPlayer->play();
|
||||||
|
if (m_resumePlayback) {
|
||||||
|
qDebug() << "Resuming playback by seeking to " << (m_resumePosition / MS_TICK_FACTOR);
|
||||||
|
m_mediaPlayer->setPosition(m_resumePosition / MS_TICK_FACTOR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m_state = newState;
|
void PlaybackManager::setMediaPlayer(QObject *qmlMediaPlayer) {
|
||||||
emit this->stateChanged(newState);
|
if (m_mediaPlayer != nullptr) {
|
||||||
|
// Clean up the old media player.
|
||||||
|
disconnect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
|
||||||
|
disconnect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged);
|
||||||
|
disconnect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_qmlMediaPlayer = qmlMediaPlayer;
|
||||||
|
if (qmlMediaPlayer != nullptr) {
|
||||||
|
m_mediaPlayer = qvariant_cast<QMediaPlayer *>(qmlMediaPlayer->property("mediaObject"));
|
||||||
|
Q_ASSERT_X(m_mediaPlayer != nullptr, "setMediaPlayer", "The mediaPlayer property must contain a qml MediaPlayer with the mediaObject property");
|
||||||
|
|
||||||
|
// Connect signals from the new media player
|
||||||
|
connect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
|
||||||
|
connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged);
|
||||||
|
connect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::updatePlaybackInfo() {
|
void PlaybackManager::updatePlaybackInfo() {
|
||||||
|
@ -119,24 +161,24 @@ void PlaybackManager::updatePlaybackInfo() {
|
||||||
void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
|
void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
|
||||||
QJsonObject root;
|
QJsonObject root;
|
||||||
|
|
||||||
root["ItemId"] = m_itemId;
|
root["ItemId"] = m_item->jellyfinId();
|
||||||
root["SessionId"] = m_playSessionId;
|
root["SessionId"] = m_playSessionId;
|
||||||
|
|
||||||
switch(type) {
|
switch(type) {
|
||||||
case Started: // FALLTHROUGH
|
case Started: // FALLTHROUGH
|
||||||
case Progress:
|
case Progress:
|
||||||
|
|
||||||
root["IsPaused"] = m_state != QMediaPlayer::PlayingState;
|
root["IsPaused"] = m_mediaPlayer->state() != QMediaPlayer::PlayingState;
|
||||||
root["IsMuted"] = false;
|
root["IsMuted"] = false;
|
||||||
|
|
||||||
root["AudioStreamIndex"] = m_audioIndex;
|
root["AudioStreamIndex"] = m_audioIndex;
|
||||||
root["SubtitleStreamIndex"] = m_subtitleIndex;
|
root["SubtitleStreamIndex"] = m_subtitleIndex;
|
||||||
|
|
||||||
root["PlayMethod"] = QVariant::fromValue(m_playMethod).toString();
|
root["PlayMethod"] = QVariant::fromValue(m_playMethod).toString();
|
||||||
root["PositionTicks"] = m_position;
|
root["PositionTicks"] = m_mediaPlayer->position() * MS_TICK_FACTOR;
|
||||||
break;
|
break;
|
||||||
case Stopped:
|
case Stopped:
|
||||||
root["PositionTicks"] = m_stopPosition;
|
root["PositionTicks"] = m_stopPosition * MS_TICK_FACTOR;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,4 +202,17 @@ void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
|
||||||
m_apiClient->setDefaultErrorHandler(rep);
|
m_apiClient->setDefaultErrorHandler(rep);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlaybackManager::componentComplete() {
|
||||||
|
if (m_apiClient == nullptr) qWarning() << "No ApiClient set for PlaybackManager";
|
||||||
|
if (m_item != nullptr) {
|
||||||
|
if (m_item->status() == RemoteData::Ready) {
|
||||||
|
fetchStreamUrl();
|
||||||
|
} else {
|
||||||
|
connect(m_item, &RemoteData::ready, [this]() -> void {
|
||||||
|
fetchStreamUrl();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ Column {
|
||||||
property real playProgress: 0.0
|
property real playProgress: 0.0
|
||||||
property bool favourited: false
|
property bool favourited: false
|
||||||
property alias imageBlurhash: playImage.blurhash
|
property alias imageBlurhash: playImage.blurhash
|
||||||
signal playPressed(bool startFromBeginning)
|
signal playPressed(bool resume)
|
||||||
spacing: Theme.paddingLarge
|
spacing: Theme.paddingLarge
|
||||||
|
|
||||||
BackgroundItem {
|
BackgroundItem {
|
||||||
|
@ -57,7 +57,7 @@ Column {
|
||||||
color: Theme.highlightColor
|
color: Theme.highlightColor
|
||||||
width: parent.width * playProgress
|
width: parent.width * playProgress
|
||||||
}
|
}
|
||||||
onClicked: playPressed(false)
|
onClicked: playPressed(true)
|
||||||
}
|
}
|
||||||
Row {
|
Row {
|
||||||
anchors {
|
anchors {
|
||||||
|
@ -71,7 +71,7 @@ Column {
|
||||||
id: playFromBeginning
|
id: playFromBeginning
|
||||||
icon.source: "image://theme/icon-m-backup"
|
icon.source: "image://theme/icon-m-backup"
|
||||||
visible: playProgress > 0
|
visible: playProgress > 0
|
||||||
onClicked: playPressed(true)
|
onClicked: playPressed(false)
|
||||||
}
|
}
|
||||||
IconButton {
|
IconButton {
|
||||||
id: favouriteButton
|
id: favouriteButton
|
||||||
|
|
|
@ -31,15 +31,15 @@ import "../"
|
||||||
|
|
||||||
SilicaItem {
|
SilicaItem {
|
||||||
id: playerRoot
|
id: playerRoot
|
||||||
property string itemId
|
property alias item : mediaSource.item
|
||||||
property string title
|
property string title: item.name
|
||||||
|
property alias resume: mediaSource.resumePlayback
|
||||||
property int progress
|
property int progress
|
||||||
readonly property bool landscape: videoOutput.contentRect.width > videoOutput.contentRect.height
|
readonly property bool landscape: videoOutput.contentRect.width > videoOutput.contentRect.height
|
||||||
property MediaPlayer player
|
property MediaPlayer player
|
||||||
readonly property bool hudVisible: !hud.hidden || player.error !== MediaPlayer.NoError
|
readonly property bool hudVisible: !hud.hidden || player.error !== MediaPlayer.NoError
|
||||||
property alias audioTrack: mediaSource.audioIndex
|
property alias audioTrack: mediaSource.audioIndex
|
||||||
property alias subtitleTrack: mediaSource.subtitleIndex
|
property alias subtitleTrack: mediaSource.subtitleIndex
|
||||||
property real startTicks: 0
|
|
||||||
|
|
||||||
// Blackground to prevent the ambience from leaking through
|
// Blackground to prevent the ambience from leaking through
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
@ -50,22 +50,10 @@ SilicaItem {
|
||||||
PlaybackManager {
|
PlaybackManager {
|
||||||
id: mediaSource
|
id: mediaSource
|
||||||
apiClient: ApiClient
|
apiClient: ApiClient
|
||||||
itemId: playerRoot.itemId
|
mediaPlayer: player
|
||||||
autoOpen: true
|
autoOpen: true
|
||||||
onStreamUrlChanged: {
|
|
||||||
if (mediaSource.streamUrl != "") {
|
|
||||||
player.source = streamUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: player
|
|
||||||
onPlaybackStateChanged: mediaSource.state = player.playbackState
|
|
||||||
onPositionChanged: mediaSource.position = Utils.msToTicks(player.position)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
VideoOutput {
|
VideoOutput {
|
||||||
id: videoOutput
|
id: videoOutput
|
||||||
source: player
|
source: player
|
||||||
|
@ -81,7 +69,8 @@ SilicaItem {
|
||||||
Label {
|
Label {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Theme.horizontalPageMargin
|
anchors.margins: Theme.horizontalPageMargin
|
||||||
text: itemId + "\n" + mediaSource.streamUrl + "\n"
|
text: item.jellyfinId + "\n" + mediaSource.streamUrl + "\n"
|
||||||
|
+ player.position + "\n"
|
||||||
+ player.status + "\n"
|
+ player.status + "\n"
|
||||||
+ player.bufferProgress + "\n"
|
+ player.bufferProgress + "\n"
|
||||||
+ player.metaData.videoCodec + "@" + player.metaData.videoFrameRate + "(" + player.metaData.videoBitRate + ")" + "\n"
|
+ player.metaData.videoCodec + "@" + player.metaData.videoFrameRate + "(" + player.metaData.videoBitRate + ")" + "\n"
|
||||||
|
@ -89,7 +78,7 @@ SilicaItem {
|
||||||
+ player.errorString + "\n"
|
+ player.errorString + "\n"
|
||||||
font.pixelSize: Theme.fontSizeExtraSmall
|
font.pixelSize: Theme.fontSizeExtraSmall
|
||||||
wrapMode: "WordWrap"
|
wrapMode: "WordWrap"
|
||||||
visible: false
|
visible: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,18 +90,4 @@ SilicaItem {
|
||||||
function stop() {
|
function stop() {
|
||||||
player.stop()
|
player.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
|
||||||
property bool enabled: true
|
|
||||||
id: playerReadyToSeek
|
|
||||||
target: player
|
|
||||||
onPlaybackStateChanged: {
|
|
||||||
if (!enabled) return;
|
|
||||||
if (startTicks > 0 && player.playbackState == MediaPlayer.PlayingState) {
|
|
||||||
console.log("Seeking to " + Utils.ticksToMs(startTicks))
|
|
||||||
player.seek(Utils.ticksToMs(startTicks))
|
|
||||||
playerReadyToSeek.enabled = false // Only seek the first time this property changes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,8 @@ import Sailfish.Silica 1.0
|
||||||
|
|
||||||
import "../components"
|
import "../components"
|
||||||
|
|
||||||
|
import nl.netsoj.chris.Jellyfin 1.0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page only containing a video player.
|
* Page only containing a video player.
|
||||||
*
|
*
|
||||||
|
@ -29,11 +31,10 @@ import "../components"
|
||||||
|
|
||||||
Page {
|
Page {
|
||||||
id: videoPage
|
id: videoPage
|
||||||
property string itemId
|
property JellyfinItem itemData
|
||||||
property var itemData
|
|
||||||
property int audioTrack
|
property int audioTrack
|
||||||
property int subtitleTrack
|
property int subtitleTrack
|
||||||
property real startTicks: 0 // Why is this a real? Because an integer only goes to 3:44 when the ticks are converted to doubles
|
property bool resume: true
|
||||||
|
|
||||||
allowedOrientations: Orientation.All
|
allowedOrientations: Orientation.All
|
||||||
showNavigationIndicator: videoPlayer.hudVisible
|
showNavigationIndicator: videoPlayer.hudVisible
|
||||||
|
@ -41,12 +42,12 @@ Page {
|
||||||
VideoPlayer {
|
VideoPlayer {
|
||||||
id: videoPlayer
|
id: videoPlayer
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
itemId: videoPage.itemId
|
|
||||||
player: appWindow.mediaPlayer
|
player: appWindow.mediaPlayer
|
||||||
title: itemData.name
|
title: itemData.name
|
||||||
audioTrack: videoPage.audioTrack
|
audioTrack: videoPage.audioTrack
|
||||||
subtitleTrack: videoPage.subtitleTrack
|
subtitleTrack: videoPage.subtitleTrack
|
||||||
startTicks: videoPage.startTicks
|
resume: videoPage.resume
|
||||||
|
item: itemData
|
||||||
|
|
||||||
onLandscapeChanged: {
|
onLandscapeChanged: {
|
||||||
console.log("Is landscape: " + landscape)
|
console.log("Is landscape: " + landscape)
|
||||||
|
|
|
@ -62,7 +62,7 @@ Page {
|
||||||
id: backdropBackground
|
id: backdropBackground
|
||||||
ThemeBackground {
|
ThemeBackground {
|
||||||
sourceItem: backdrop
|
sourceItem: backdrop
|
||||||
backgroundMaterial: Materials.blur
|
backgroundMaterial: "blur"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,11 +60,10 @@ BaseDetailPage {
|
||||||
favourited: itemData.userData.isFavorite
|
favourited: itemData.userData.isFavorite
|
||||||
playProgress: itemData.userData.playedPercentage / 100
|
playProgress: itemData.userData.playedPercentage / 100
|
||||||
onPlayPressed: pageStack.push(Qt.resolvedUrl("../VideoPage.qml"),
|
onPlayPressed: pageStack.push(Qt.resolvedUrl("../VideoPage.qml"),
|
||||||
{"itemId": itemId, "itemData": itemData,
|
{"itemData": itemData,
|
||||||
"audioTrack": trackSelector.audioTrack,
|
"audioTrack": trackSelector.audioTrack,
|
||||||
"subtitleTrack": trackSelector.subtitleTrack,
|
"subtitleTrack": trackSelector.subtitleTrack,
|
||||||
"startTicks": startFromBeginning ? 0.0
|
"resume": resume})
|
||||||
: _playbackProsition })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VideoTrackSelector {
|
VideoTrackSelector {
|
||||||
|
|
Loading…
Reference in a new issue