1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2024-11-22 09:15:18 +00:00

Added Direct Play and websocket improvements

* [backend]: Websocket now automatically tries to reconnect if connection was lost, up to 3 times.
* [backend]: Move more playback and resume logic to the backend, to avoid having it in multiple places within the QML. Regression: pausing playback sometimes halts the video player for an unknown reason.
* [playback]: Sailfin will try to play without the server transcoding, if possible.
* [ui]: added a debug page in the settings
This commit is contained in:
Chris Josten 2021-02-14 13:29:30 +01:00
parent a244c27b1a
commit 7e77abc173
13 changed files with 204 additions and 45 deletions

View file

@ -82,6 +82,7 @@ public:
Q_PROPERTY(QString userId READ userId NOTIFY userIdChanged) Q_PROPERTY(QString userId READ userId NOTIFY userIdChanged)
Q_PROPERTY(QJsonObject deviceProfile READ deviceProfile NOTIFY deviceProfileChanged) Q_PROPERTY(QJsonObject deviceProfile READ deviceProfile NOTIFY deviceProfileChanged)
Q_PROPERTY(QString version READ version) Q_PROPERTY(QString version READ version)
Q_PROPERTY(WebSocket *websocket READ websocket NOTIFY websocketChanged)
/*QNetworkReply *handleRequest(QString path, QStringList sort, Pagination *pagination, /*QNetworkReply *handleRequest(QString path, QStringList sort, Pagination *pagination,
QVariantMap filters, QStringList fields, QStringList expand, QString id);*/ QVariantMap filters, QStringList fields, QStringList expand, QString id);*/
@ -111,6 +112,7 @@ public:
QJsonObject &deviceProfile() { return m_deviceProfile; } QJsonObject &deviceProfile() { return m_deviceProfile; }
QJsonObject &playbackDeviceProfile() { return m_playbackDeviceProfile; } QJsonObject &playbackDeviceProfile() { return m_playbackDeviceProfile; }
QString version() const; QString version() const;
WebSocket *websocket() const { return m_webSocket; }
/** /**
* @brief Sets the error handler of a reply to this classes default error handler * @brief Sets the error handler of a reply to this classes default error handler
@ -146,10 +148,8 @@ signals:
void userIdChanged(QString userId); void userIdChanged(QString userId);
void itemFetched(const QString &itemId, const QJsonObject &result);
void itemFetchFailed(const QString &itemId, const QNetworkReply::NetworkError error);
void deviceProfileChanged(); void deviceProfileChanged();
void websocketChanged(WebSocket *newWebsocket);
/** /**
* @brief onUserDataChanged Emitted when the user data of an item is changed on the server. * @brief onUserDataChanged Emitted when the user data of an item is changed on the server.
@ -179,8 +179,6 @@ public slots:
*/ */
void deleteSession(); void deleteSession();
void fetchItem(const QString &id);
/** /**
* @brief Shares the capabilities of this device to the server. * @brief Shares the capabilities of this device to the server.
*/ */

View file

@ -367,6 +367,7 @@ public:
Q_PROPERTY(QJsonObject imageTags MEMBER m_imageTags NOTIFY imageTagsChanged) Q_PROPERTY(QJsonObject imageTags MEMBER m_imageTags NOTIFY imageTagsChanged)
Q_PROPERTY(QStringList backdropImageTags MEMBER m_backdropImageTags NOTIFY backdropImageTagsChanged) Q_PROPERTY(QStringList backdropImageTags MEMBER m_backdropImageTags NOTIFY backdropImageTagsChanged)
Q_PROPERTY(QJsonObject imageBlurHashes MEMBER m_imageBlurHashes NOTIFY imageBlurHashesChanged) Q_PROPERTY(QJsonObject imageBlurHashes MEMBER m_imageBlurHashes NOTIFY imageBlurHashesChanged)
Q_PROPERTY(QString mediaType MEMBER m_mediaType READ mediaType NOTIFY mediaTypeChanged)
Q_PROPERTY(int width MEMBER m_width NOTIFY widthChanged) Q_PROPERTY(int width MEMBER m_width NOTIFY widthChanged)
Q_PROPERTY(int height MEMBER m_height NOTIFY heightChanged) Q_PROPERTY(int height MEMBER m_height NOTIFY heightChanged)
@ -406,6 +407,7 @@ public:
void setRecursiveItemCount(int newRecursiveItemCount) { m_recursiveItemCount = newRecursiveItemCount; emit recursiveItemCountChanged(newRecursiveItemCount); } void setRecursiveItemCount(int newRecursiveItemCount) { m_recursiveItemCount = newRecursiveItemCount; emit recursiveItemCountChanged(newRecursiveItemCount); }
int childCount() const { return m_childCount.value_or(-1); } int childCount() const { return m_childCount.value_or(-1); }
void setChildCount(int newChildCount) { m_childCount = newChildCount; emit childCountChanged(newChildCount); } void setChildCount(int newChildCount) { m_childCount = newChildCount; emit childCountChanged(newChildCount); }
QString mediaType() const { return m_mediaType; }
//QQmlListProperty<MediaStream> mediaStreams() { return toReadOnlyQmlListProperty<MediaStream>(m_mediaStreams); } //QQmlListProperty<MediaStream> mediaStreams() { return toReadOnlyQmlListProperty<MediaStream>(m_mediaStreams); }
//QList<QObject *> mediaStreams() { return *reinterpret_cast<QList<QObject *> *>(&m_mediaStreams); } //QList<QObject *> mediaStreams() { return *reinterpret_cast<QList<QObject *> *>(&m_mediaStreams); }
@ -460,6 +462,7 @@ signals:
void imageTagsChanged(); void imageTagsChanged();
void backdropImageTagsChanged(); void backdropImageTagsChanged();
void imageBlurHashesChanged(); void imageBlurHashesChanged();
void mediaTypeChanged(const QString &newMediaType);
void widthChanged(int newWidth); void widthChanged(int newWidth);
void heightChanged(int newHeight); void heightChanged(int newHeight);
@ -520,6 +523,7 @@ protected:
QJsonObject m_imageTags; QJsonObject m_imageTags;
QStringList m_backdropImageTags; QStringList m_backdropImageTags;
QJsonObject m_imageBlurHashes; QJsonObject m_imageBlurHashes;
QString m_mediaType;
int m_width; int m_width;
int m_height; int m_height;

View file

@ -65,6 +65,7 @@ public:
Q_PROPERTY(int subtitleIndex MEMBER m_subtitleIndex NOTIFY subtitleIndexChanged) Q_PROPERTY(int subtitleIndex MEMBER m_subtitleIndex NOTIFY subtitleIndexChanged)
Q_PROPERTY(bool resumePlayback MEMBER m_resumePlayback NOTIFY resumePlaybackChanged) Q_PROPERTY(bool resumePlayback MEMBER m_resumePlayback NOTIFY resumePlaybackChanged)
Q_PROPERTY(QObject* mediaPlayer READ mediaPlayer WRITE setMediaPlayer NOTIFY mediaPlayerChanged) Q_PROPERTY(QObject* mediaPlayer READ mediaPlayer WRITE setMediaPlayer NOTIFY mediaPlayerChanged)
Q_PROPERTY(PlayMethod playMethod READ playMethod NOTIFY playMethodChanged)
Item *item() const { return m_item; } Item *item() const { return m_item; }
void setItem(Item *newItem); void setItem(Item *newItem);
@ -75,6 +76,7 @@ public:
void setMediaPlayer(QObject *qmlMediaPlayer); void setMediaPlayer(QObject *qmlMediaPlayer);
QString streamUrl() const { return m_streamUrl; } QString streamUrl() const { return m_streamUrl; }
PlayMethod playMethod() const { return m_playMethod; }
signals: signals:
void itemChanged(Item *newItemId); void itemChanged(Item *newItemId);
void streamUrlChanged(const QString &newStreamUrl); void streamUrlChanged(const QString &newStreamUrl);
@ -83,6 +85,7 @@ signals:
void subtitleIndexChanged(int subtitleIndex); void subtitleIndexChanged(int subtitleIndex);
void mediaPlayerChanged(QObject *newMediaPlayer); void mediaPlayerChanged(QObject *newMediaPlayer);
void resumePlaybackChanged(bool newResumePlayback); void resumePlaybackChanged(bool newResumePlayback);
void playMethodChanged(PlayMethod newPlayMethod);
public slots: public slots:
void updatePlaybackInfo(); void updatePlaybackInfo();
@ -103,7 +106,7 @@ private:
qint64 m_oldPosition = 0; qint64 m_oldPosition = 0;
qint64 m_stopPosition = 0; qint64 m_stopPosition = 0;
QMediaPlayer::State m_oldState = QMediaPlayer::StoppedState; QMediaPlayer::State m_oldState = QMediaPlayer::StoppedState;
PlayMethod m_playMethod; PlayMethod m_playMethod = Transcode;
QObject *m_qmlMediaPlayer = nullptr; QObject *m_qmlMediaPlayer = nullptr;
QMediaPlayer * m_mediaPlayer = nullptr; QMediaPlayer * m_mediaPlayer = nullptr;
bool m_resumePlayback = true; bool m_resumePlayback = true;

View file

@ -58,7 +58,12 @@ public:
KeepAlive, KeepAlive,
UserDataChanged UserDataChanged
}; };
Q_PROPERTY(QAbstractSocket::SocketState state READ state NOTIFY stateChanged)
Q_ENUM(MessageType) Q_ENUM(MessageType)
QAbstractSocket::SocketState state() const {
return m_webSocket.state();
}
public slots: public slots:
void open(); void open();
private slots: private slots:
@ -67,14 +72,18 @@ private slots:
void onDisconnected(); void onDisconnected();
void sendKeepAlive(); void sendKeepAlive();
void onWebsocketStateChanged(QAbstractSocket::SocketState newState) { emit stateChanged(newState); }
signals: signals:
void commandReceived(QString arts, QVariantMap args); void commandReceived(QString arts, QVariantMap args);
void stateChanged(QAbstractSocket::SocketState newState);
protected: protected:
ApiClient *m_apiClient; ApiClient *m_apiClient;
QWebSocket m_webSocket; QWebSocket m_webSocket;
QTimer m_keepAliveTimer; QTimer m_keepAliveTimer;
QTimer m_retryTimer;
int m_reconnectAttempt = 0;
void setupKeepAlive(int data); void setupKeepAlive(int data);

View file

@ -25,6 +25,8 @@ ApiClient::ApiClient(QObject *parent)
: QObject(parent), : QObject(parent),
m_webSocket(new WebSocket(this)) { m_webSocket(new WebSocket(this)) {
m_deviceName = QHostInfo::localHostName(); m_deviceName = QHostInfo::localHostName();
uint uuid1 = qHash(m_deviceName);
uint uuid2 = qHash(QSysInfo::productVersion());
m_deviceId = QUuid::createUuid().toString(); // TODO: make this not random? m_deviceId = QUuid::createUuid().toString(); // TODO: make this not random?
m_credManager = CredentialsManager::newInstance(this); m_credManager = CredentialsManager::newInstance(this);
@ -218,23 +220,6 @@ void ApiClient::deleteSession() {
}); });
} }
void ApiClient::fetchItem(const QString &id) {
QNetworkReply *rep = get("/Users/" + m_userId + "/Items/" + id);
connect(rep, &QNetworkReply::finished, this, [rep, id, this]() {
int status = statusCode(rep);
if (status >= 200 && status < 300) {
QJsonObject data = QJsonDocument::fromJson(rep->readAll()).object();
emit this->itemFetched(id, data);
}
rep->deleteLater();
});
connect(rep, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
this, [id, rep, this](QNetworkReply::NetworkError error) {
emit this->itemFetchFailed(id, error);
rep->deleteLater();
});
}
void ApiClient::postCapabilities() { void ApiClient::postCapabilities() {
QJsonObject capabilities; QJsonObject capabilities;
capabilities["SupportsPersistentIdentifier"] = false; // Technically untrue, but not implemented yet. capabilities["SupportsPersistentIdentifier"] = false; // Technically untrue, but not implemented yet.

View file

@ -46,6 +46,18 @@ QJsonObject DeviceProfile::generateProfile() {
using JsonPair = QPair<QString, QJsonValue>; using JsonPair = QPair<QString, QJsonValue>;
QJsonObject profile; QJsonObject profile;
QStringList audioCodes = {
"aac",
"flac",
"mp2",
"mp3",
"oga"
"ogg",
"wav",
"webma",
"wma"
};
QStringList videoAudioCodecs; QStringList videoAudioCodecs;
QStringList mp4VideoCodecs; QStringList mp4VideoCodecs;
QStringList hlsVideoCodecs; QStringList hlsVideoCodecs;
@ -171,6 +183,7 @@ QJsonObject DeviceProfile::generateProfile() {
})); }));
// Direct play profiles // Direct play profiles
// Video
QJsonArray directPlayProfiles; QJsonArray directPlayProfiles;
directPlayProfiles.append(QJsonObject { directPlayProfiles.append(QJsonObject {
JsonPair("Container", "mp4,m4v"), JsonPair("Container", "mp4,m4v"),
@ -178,6 +191,39 @@ QJsonObject DeviceProfile::generateProfile() {
JsonPair("VideoCodec", mp4VideoCodecs.join(',')), JsonPair("VideoCodec", mp4VideoCodecs.join(',')),
JsonPair("AudioCodec", videoAudioCodecs.join(',')) JsonPair("AudioCodec", videoAudioCodecs.join(','))
}); });
directPlayProfiles.append(QJsonObject {
JsonPair("Container", "mkv"),
JsonPair("Type", "Video"),
JsonPair("VideoCodec", mp4VideoCodecs.join(',')),
JsonPair("AudioCodec", videoAudioCodecs.join(','))
});
// Audio
for (auto it = audioCodes.begin(); it != audioCodes.end(); it++) {
if (*it == "mp2") {
directPlayProfiles.append(QJsonObject {
JsonPair("Container", "mp2,mp3"),
JsonPair("Type", "Audio"),
JsonPair("AudioCodec", "mp2")
});
} else if(*it == "mp3") {
directPlayProfiles.append(QJsonObject {
JsonPair("Container", "mp3"),
JsonPair("Type", "Audio"),
JsonPair("AudioCodec", "mp3")
});
} else if (*it == "webma") {
directPlayProfiles.append(QJsonObject {
JsonPair("Container", "webma,webm"),
JsonPair("Type", "Audio"),
});
} else {
directPlayProfiles.append(QJsonObject {
JsonPair("Container", *it),
JsonPair("Type", "Audio")
});
}
}
profile["CodecProfiles"] = codecProfiles; profile["CodecProfiles"] = codecProfiles;
profile["ContainerProfiles"] = QJsonArray(); profile["ContainerProfiles"] = QJsonArray();

View file

@ -29,7 +29,10 @@ PlaybackManager::PlaybackManager(QObject *parent)
} }
void PlaybackManager::fetchStreamUrl() { void PlaybackManager::fetchStreamUrl() {
if (m_item == nullptr || m_apiClient == nullptr) return; if (m_item == nullptr || m_apiClient == nullptr) {
qDebug() << "Item or apiClient not set";
return;
}
m_resumePosition = 0; m_resumePosition = 0;
if (m_resumePlayback && !m_item->property("userData").isNull()) { if (m_resumePlayback && !m_item->property("userData").isNull()) {
UserData* userData = qvariant_cast<UserData *>(m_item->property("userData")); UserData* userData = qvariant_cast<UserData *>(m_item->property("userData"));
@ -57,12 +60,36 @@ void PlaybackManager::fetchStreamUrl() {
if (this->m_autoOpen) { if (this->m_autoOpen) {
QJsonArray mediaSources = root["MediaSources"].toArray(); QJsonArray mediaSources = root["MediaSources"].toArray();
QJsonObject firstMediaSource = mediaSources[0].toObject();
//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!
QString streamUrl = this->m_apiClient->baseUrl() if (firstMediaSource.isEmpty()) {
+ mediaSources[0].toObject()["TranscodingUrl"].toString(); qWarning() << "No media source found";
} else if (firstMediaSource["SupportsDirectStream"].toBool()) {
QUrlQuery query;
query.addQueryItem("mediaSourceId", firstMediaSource["Id"].toString());
query.addQueryItem("deviceId", m_apiClient->m_deviceId);
query.addQueryItem("api_key", m_apiClient->token());
query.addQueryItem("Static", "True");
QString mediaType = "unknown";
if (m_item->mediaType() == "Audio") {
mediaType = "Audio";
} else if (m_item->mediaType() == "Video") {
mediaType = "Videos";
}
QString streamUrl = this->m_apiClient->baseUrl() + "/" + mediaType + "/" + m_item->jellyfinId() + "/stream."
+ firstMediaSource["Container"].toString() + "?" + query.toString(QUrl::EncodeReserved);
setStreamUrl(streamUrl);
this->m_playMethod = DirectPlay;
} else if (firstMediaSource["SupportsTranscoding"].toBool() && !firstMediaSource["TranscodingUrl"].isNull()) {
QString streamUrl = this->m_apiClient->baseUrl()
+ firstMediaSource["TranscodingUrl"].toString();
this->m_playMethod = Transcode; this->m_playMethod = Transcode;
setStreamUrl(streamUrl); setStreamUrl(streamUrl);
} else {
qDebug() << "No stream url found";
return;
}
qDebug() << "Found stream url: " << this->m_streamUrl; qDebug() << "Found stream url: " << this->m_streamUrl;
} }
@ -71,6 +98,7 @@ void PlaybackManager::fetchStreamUrl() {
} }
void PlaybackManager::setItem(Item *newItem) { void PlaybackManager::setItem(Item *newItem) {
if (m_mediaPlayer != nullptr) m_mediaPlayer->stop();
this->m_item = newItem; this->m_item = newItem;
// Don't try to start fetching when we're not completely parsed yet. // Don't try to start fetching when we're not completely parsed yet.
if (m_qmlIsParsingComponent) return; if (m_qmlIsParsingComponent) return;
@ -81,8 +109,14 @@ void PlaybackManager::setItem(Item *newItem) {
} }
// Deinitialize the streamUrl // Deinitialize the streamUrl
setStreamUrl(""); setStreamUrl("");
if (newItem != nullptr && !newItem->jellyfinId().isEmpty()) { if (newItem != nullptr) {
fetchStreamUrl(); if (m_item->status() == RemoteData::Ready) {
fetchStreamUrl();
} else {
connect(m_item, &RemoteData::ready, [this]() -> void {
fetchStreamUrl();
});
}
} }
} }
@ -161,6 +195,10 @@ void PlaybackManager::updatePlaybackInfo() {
void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) { void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
QJsonObject root; QJsonObject root;
if (m_item == nullptr) {
qWarning() << "Item is null. Not posting playback info";
return;
}
root["ItemId"] = m_item->jellyfinId(); root["ItemId"] = m_item->jellyfinId();
root["SessionId"] = m_playSessionId; root["SessionId"] = m_playSessionId;
@ -204,6 +242,7 @@ void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
void PlaybackManager::componentComplete() { void PlaybackManager::componentComplete() {
if (m_apiClient == nullptr) qWarning() << "No ApiClient set for PlaybackManager"; if (m_apiClient == nullptr) qWarning() << "No ApiClient set for PlaybackManager";
m_qmlIsParsingComponent = false;
if (m_item != nullptr) { if (m_item != nullptr) {
if (m_item->status() == RemoteData::Ready) { if (m_item->status() == RemoteData::Ready) {
fetchStreamUrl(); fetchStreamUrl();

View file

@ -28,6 +28,9 @@ WebSocket::WebSocket(ApiClient *client)
Q_UNUSED(error) Q_UNUSED(error)
qDebug() << "Connection error: " << m_webSocket.errorString(); qDebug() << "Connection error: " << m_webSocket.errorString();
}); });
connect(&m_webSocket, &QWebSocket::stateChanged, this, &WebSocket::onWebsocketStateChanged);
connect(&m_keepAliveTimer, &QTimer::timeout, this, &WebSocket::sendKeepAlive);
connect(&m_retryTimer, &QTimer::timeout, this, &WebSocket::open);
} }
void WebSocket::open() { void WebSocket::open() {
@ -39,16 +42,22 @@ void WebSocket::open() {
connectionUrl.setPath("/socket"); connectionUrl.setPath("/socket");
connectionUrl.setQuery(query); connectionUrl.setQuery(query);
m_webSocket.open(connectionUrl); m_webSocket.open(connectionUrl);
qDebug() << "Opening WebSocket connection to " << m_webSocket.requestUrl(); m_reconnectAttempt++;
qDebug() << "Opening WebSocket connection to " << m_webSocket.requestUrl() << ", connect attempt " << m_reconnectAttempt;
} }
void WebSocket::onConnected() { void WebSocket::onConnected() {
connect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebSocket::textMessageReceived); connect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebSocket::textMessageReceived);
m_reconnectAttempt = 0;
} }
void WebSocket::onDisconnected() { void WebSocket::onDisconnected() {
disconnect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebSocket::textMessageReceived); disconnect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebSocket::textMessageReceived);
m_keepAliveTimer.stop(); m_keepAliveTimer.stop();
if (m_reconnectAttempt <= 3) {
// 500, 2500, 12500
m_retryTimer.setInterval(100 * static_cast<int>(std::pow(5., m_reconnectAttempt)));
}
} }
void WebSocket::textMessageReceived(const QString &message) { void WebSocket::textMessageReceived(const QString &message) {
@ -112,7 +121,6 @@ void WebSocket::setupKeepAlive(int data) {
// Data is timeout in seconds, we want to send a keepalive at half the timeout // Data is timeout in seconds, we want to send a keepalive at half the timeout
m_keepAliveTimer.setInterval(data * 500); m_keepAliveTimer.setInterval(data * 500);
m_keepAliveTimer.setSingleShot(false); m_keepAliveTimer.setSingleShot(false);
connect(&m_keepAliveTimer, &QTimer::timeout, this, &WebSocket::sendKeepAlive);
m_keepAliveTimer.start(); m_keepAliveTimer.start();
sendKeepAlive(); sendKeepAlive();
} }

View file

@ -31,15 +31,15 @@ import "../"
SilicaItem { SilicaItem {
id: playerRoot id: playerRoot
property alias item : mediaSource.item property JellyfinItem item
property string title: item.name property string title: item.name
property alias resume: mediaSource.resumePlayback property bool resume
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 int audioTrack: 0
property alias subtitleTrack: mediaSource.subtitleIndex property int subtitleTrack: 0
// Blackground to prevent the ambience from leaking through // Blackground to prevent the ambience from leaking through
Rectangle { Rectangle {
@ -47,13 +47,6 @@ SilicaItem {
color: Theme.overlayBackgroundColor color: Theme.overlayBackgroundColor
} }
PlaybackManager {
id: mediaSource
apiClient: ApiClient
mediaPlayer: player
autoOpen: true
}
VideoOutput { VideoOutput {
id: videoOutput id: videoOutput
source: player source: player
@ -69,7 +62,8 @@ SilicaItem {
Label { Label {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.horizontalPageMargin anchors.margins: Theme.horizontalPageMargin
text: item.jellyfinId + "\n" + mediaSource.streamUrl + "\n" text: item.jellyfinId + "\n" + appWindow.playbackManager.streamUrl + "\n"
+ (appWindow.playbackManager.playMethod == PlaybackManager.DirectPlay ? "Direct Play" : "Transcoding") + "\n"
+ player.position + "\n" + player.position + "\n"
+ player.status + "\n" + player.status + "\n"
+ player.bufferProgress + "\n" + player.bufferProgress + "\n"
@ -78,7 +72,7 @@ SilicaItem {
+ player.errorString + "\n" + player.errorString + "\n"
font.pixelSize: Theme.fontSizeExtraSmall font.pixelSize: Theme.fontSizeExtraSmall
wrapMode: "WordWrap" wrapMode: "WordWrap"
visible: true visible: appWindow.showDebugInfo
} }
} }
@ -87,6 +81,13 @@ SilicaItem {
player: playerRoot.player player: playerRoot.player
} }
function start() {
appWindow.playbackManager.audioIndex = audioTrack
appWindow.playbackManager.subtitleIndex = subtitleTrack
appWindow.playbackManager.resumePlayback = resume
appWindow.playbackManager.item = item
}
function stop() { function stop() {
player.stop() player.stop()
} }

View file

@ -33,12 +33,16 @@ ApplicationWindow {
property bool _hasInitialized: false property bool _hasInitialized: false
// The global mediaPlayer instance // The global mediaPlayer instance
readonly property MediaPlayer mediaPlayer: _mediaPlayer readonly property MediaPlayer mediaPlayer: _mediaPlayer
readonly property PlaybackManager playbackManager: _playbackManager
// Data of the currently selected item. For use on the cover. // Data of the currently selected item. For use on the cover.
property JellyfinItem itemData property JellyfinItem itemData
// Id of the collection currently browsing. For use on the cover. // Id of the collection currently browsing. For use on the cover.
property string collectionId property string collectionId
// Bad way to implement settings, but it'll do for now.
property bool showDebugInfo: false
//FIXME: proper error handling //FIXME: proper error handling
Connections { Connections {
target: ApiClient target: ApiClient
@ -98,6 +102,14 @@ ApplicationWindow {
autoPlay: true autoPlay: true
} }
PlaybackManager {
id: _playbackManager
apiClient: ApiClient
mediaPlayer: _mediaPlayer
audioIndex: 0
autoOpen: true
}
// Keep the sytem alive while playing media // Keep the sytem alive while playing media
KeepAlive { KeepAlive {
enabled: _mediaPlayer.playbackState == MediaPlayer.PlayingState enabled: _mediaPlayer.playbackState == MediaPlayer.PlayingState

View file

@ -54,6 +54,7 @@ Page {
//appWindow.orientation = landscape ? Orientation.Landscape : Orientation.Portrait //appWindow.orientation = landscape ? Orientation.Landscape : Orientation.Portrait
videoPage.allowedOrientations = landscape ? Orientation.LandscapeMask : Orientation.PortraitMask videoPage.allowedOrientations = landscape ? Orientation.LandscapeMask : Orientation.PortraitMask
} }
} }
onStatusChanged: { onStatusChanged: {
@ -62,6 +63,7 @@ Page {
videoPlayer.stop() videoPlayer.stop()
break; break;
case PageStatus.Active: case PageStatus.Active:
videoPlayer.start()
appWindow.itemData = videoPage.itemData appWindow.itemData = videoPage.itemData
break; break;
} }

View file

@ -73,10 +73,13 @@ BaseDetailPage {
} }
} }
delegate: SongDelegate { delegate: SongDelegate {
id: songDelegate
name: model.name name: model.name
artists: model.artists artists: model.artists
duration: model.runTimeTicks duration: model.runTimeTicks
indexNumber: model.indexNumber indexNumber: model.indexNumber
onClicked: window.playbackManager.item = Qt.createQmlObject("import nl.netsoj.chris.Jellyfin 1.0;"
+ "JellyfinItem { jellyfinId: \"" + model.id + "\"; apiClient: ApiClient; }", songDelegate, "nonexistent.qml");
} }
VerticalScrollDecorator {} VerticalScrollDecorator {}

View file

@ -39,6 +39,55 @@ Page {
title: qsTr("Debug information") title: qsTr("Debug information")
} }
TextSwitch {
text: qsTr("Show debug information")
checked: appWindow.showDebugInfo
onCheckedChanged: appWindow.showDebugInfo = checked
}
SectionHeader {
text: qsTr("Websocket")
}
DetailItem {
label: qsTr("Connection state")
value: {
var stateText
switch( ApiClient.websocket.state) {
case 0:
//- Socket state
stateText = qsTr("Unconnected");
break;
case 1:
//- Socket state
stateText = "Looking up host";
break;
case 2:
//- Socket state
stateText = "Connecting";
break;
case 3:
//- Socket state
stateText = "Connected";
break;
case 4:
//- Socket state
stateText = "Bound";
break;
case 5:
//- Socket state
stateText = "Closing";
break;
case 6:
//- Socket state
stateText = "Listening";
break;
}
//- Socket state: "state no (state description)"
qsTr("%1 (%2)").arg(ApiClient.websocket.state).arg(stateText)
}
}
SectionHeader { SectionHeader {
text: qsTr("Device profile") text: qsTr("Device profile")
} }