From 2a3bd51def87e07896dab543d274bb299a2a8953 Mon Sep 17 00:00:00 2001 From: Henk Kalkwater Date: Fri, 10 Sep 2021 01:36:30 +0200 Subject: [PATCH] Add support for handling playstate commands --- core/include/JellyfinQt/eventbus.h | 3 + .../platform/freedesktop/mediaplayer2player.h | 1 + .../JellyfinQt/viewmodel/playbackmanager.h | 20 ++++- .../freedesktop/mediaplayer2player.cpp | 6 ++ core/src/viewmodel/playbackmanager.cpp | 89 ++++++++++++++++--- core/src/websocket.cpp | 8 ++ 6 files changed, 112 insertions(+), 15 deletions(-) diff --git a/core/include/JellyfinQt/eventbus.h b/core/include/JellyfinQt/eventbus.h index 1558d04..99a8888 100644 --- a/core/include/JellyfinQt/eventbus.h +++ b/core/include/JellyfinQt/eventbus.h @@ -25,6 +25,7 @@ namespace Jellyfin { namespace DTO { class UserItemDataDto; + class PlaystateRequest; } /** @@ -50,6 +51,8 @@ signals: * @param timeout Timeout in MS to show the message. -1: no timeout supplied. */ void displayMessage(const QString &header, const QString &message, int timeout = -1); + + void playstateCommandReceived(const Jellyfin::DTO::PlaystateRequest &request); }; } diff --git a/core/include/JellyfinQt/platform/freedesktop/mediaplayer2player.h b/core/include/JellyfinQt/platform/freedesktop/mediaplayer2player.h index 5e0f95c..7336404 100644 --- a/core/include/JellyfinQt/platform/freedesktop/mediaplayer2player.h +++ b/core/include/JellyfinQt/platform/freedesktop/mediaplayer2player.h @@ -196,6 +196,7 @@ private slots: void onPositionChanged(qint64 position); void onSeekableChanged(bool seekable); void onPlaybackManagerChanged(ViewModel::PlaybackManager *newPlaybackManager); + void onSeeked(qint64 newPosition); }; } // NS FreeDesktop diff --git a/core/include/JellyfinQt/viewmodel/playbackmanager.h b/core/include/JellyfinQt/viewmodel/playbackmanager.h index 4a79887..d38d4a4 100644 --- a/core/include/JellyfinQt/viewmodel/playbackmanager.h +++ b/core/include/JellyfinQt/viewmodel/playbackmanager.h @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -51,9 +52,13 @@ namespace Jellyfin { // Forward declaration of Jellyfin::ApiClient found in jellyfinapiclient.h class ApiClient; class ItemModel; -class RemoteItem; + +namespace DTO { +class PlaystateRequest; +} namespace ViewModel { +Q_DECLARE_LOGGING_CATEGORY(playbackManager); // Later defined in this file class ItemUrlFetcherThread; @@ -99,6 +104,8 @@ public: Q_PROPERTY(qint64 position READ position NOTIFY positionChanged) Q_PROPERTY(bool hasNext READ hasNext NOTIFY hasNextChanged) Q_PROPERTY(bool hasPrevious READ hasPrevious NOTIFY hasPreviousChanged) + /// Whether playstate commands received over the websocket should be handled + Q_PROPERTY(bool handlePlaystateCommands READ handlePlaystateCommands WRITE setHandlePlaystateCommands NOTIFY handlePlaystateCommandsChanged) ViewModel::Item *item() const { return m_displayItem; } QSharedPointer dataItem() const { return m_item; } @@ -122,6 +129,9 @@ public: bool seekable() const { return m_mediaPlayer->isSeekable(); } QMediaPlayer::Error error () const; QString errorString() const; + + bool handlePlaystateCommands() const { return m_handlePlaystateCommands; } + void setHandlePlaystateCommands(bool newHandlePlaystateCommands) { m_handlePlaystateCommands = newHandlePlaystateCommands; emit handlePlaystateCommandsChanged(m_handlePlaystateCommands); } signals: void itemChanged(ViewModel::Item *newItemId); void streamUrlChanged(const QString &newStreamUrl); @@ -132,6 +142,9 @@ signals: void resumePlaybackChanged(bool newResumePlayback); void playMethodChanged(PlayMethod newPlayMethod); + // Emitted when seek has been called. + void seeked(qint64 newPosition); + // Current media player related property signals void mediaObjectChanged(QObject *newMediaObject); void positionChanged(qint64 newPosition); @@ -146,6 +159,7 @@ signals: void errorStringChanged(const QString &newErrorString); void hasNextChanged(bool newHasNext); void hasPreviousChanged(bool newHasPrevious); + void handlePlaystateCommandsChanged(bool newHandlePlaystateCommands); public slots: /** * @brief playItem Replaces the current queue and plays the given item. @@ -187,6 +201,8 @@ public slots: */ void next(); + void handlePlaystateRequest(const DTO::PlaystateRequest &request); + private slots: void mediaPlayerStateChanged(QMediaPlayer::State newState); void mediaPlayerPositionChanged(qint64 position); @@ -260,6 +276,8 @@ private: int m_queueIndex = 0; bool m_resumePlayback = true; + bool m_handlePlaystateCommands = true; + // Helper methods void setItem(QSharedPointer newItem); diff --git a/core/src/platform/freedesktop/mediaplayer2player.cpp b/core/src/platform/freedesktop/mediaplayer2player.cpp index 907b67e..6ad854c 100644 --- a/core/src/platform/freedesktop/mediaplayer2player.cpp +++ b/core/src/platform/freedesktop/mediaplayer2player.cpp @@ -318,6 +318,11 @@ void PlayerAdaptor::onPositionChanged(qint64 position) { properties << "Position"; notifyPropertiesChanged(properties);*/ } + +void PlayerAdaptor::onSeeked(qint64 position) { + notifyPropertiesChanged(QStringList("Position")); +} + void PlayerAdaptor::onSeekableChanged(bool seekable) { QStringList properties; properties << "CanSeek"; @@ -330,6 +335,7 @@ void PlayerAdaptor::onPlaybackManagerChanged(ViewModel::PlaybackManager *newPlay connect(newPlaybackManager, &ViewModel::PlaybackManager::playbackStateChanged, this, &PlayerAdaptor::onPlaybackStateChanged); connect(newPlaybackManager, &ViewModel::PlaybackManager::mediaStatusChanged, this, &PlayerAdaptor::onMediaStatusChanged); connect(newPlaybackManager, &ViewModel::PlaybackManager::positionChanged, this, &PlayerAdaptor::onPositionChanged); + connect(newPlaybackManager, &ViewModel::PlaybackManager::seeked, this, &PlayerAdaptor::onSeeked); connect(newPlaybackManager, &ViewModel::PlaybackManager::seekableChanged, this, &PlayerAdaptor::onSeekableChanged); } } diff --git a/core/src/viewmodel/playbackmanager.cpp b/core/src/viewmodel/playbackmanager.cpp index 8332afb..a09f36c 100644 --- a/core/src/viewmodel/playbackmanager.cpp +++ b/core/src/viewmodel/playbackmanager.cpp @@ -21,6 +21,8 @@ #include "JellyfinQt/apimodel.h" #include "JellyfinQt/loader/http/mediainfo.h" +#include +#include // #include "JellyfinQt/DTO/dto.h" #include @@ -37,6 +39,8 @@ namespace DTO { namespace ViewModel { +Q_LOGGING_CATEGORY(playbackManager, "jellyfin.viewmodel.playbackmanager") + PlaybackManager::PlaybackManager(QObject *parent) : QObject(parent), m_item(nullptr), @@ -64,10 +68,18 @@ PlaybackManager::PlaybackManager(QObject *parent) } void PlaybackManager::setApiClient(ApiClient *apiClient) { + if (m_apiClient != nullptr) { + disconnect(m_apiClient->eventbus(), &EventBus::playstateCommandReceived, this, &PlaybackManager::handlePlaystateRequest); + } + if (!m_item.isNull()) { m_item->setApiClient(apiClient); } m_apiClient = apiClient; + + if (m_apiClient != nullptr) { + connect(m_apiClient->eventbus(), &EventBus::playstateCommandReceived, this, &PlaybackManager::handlePlaystateRequest); + } } void PlaybackManager::setItem(QSharedPointer newItem) { @@ -92,7 +104,7 @@ void PlaybackManager::setItem(QSharedPointer newItem) { if (m_apiClient == nullptr) { - qWarning() << "apiClient is not set on this MediaSource instance! Aborting."; + qCWarning(playbackManager) << "apiClient is not set on this MediaSource instance! Aborting."; return; } // Deinitialize the streamUrl @@ -171,7 +183,7 @@ void PlaybackManager::mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus ne if (newStatus == QMediaPlayer::LoadedMedia) { m_mediaPlayer->play(); if (m_resumePlayback) { - qDebug() << "Resuming playback by seeking to " << (m_resumePosition / MS_TICK_FACTOR); + qCDebug(playbackManager) << "Resuming playback by seeking to " << (m_resumePosition / MS_TICK_FACTOR); m_mediaPlayer->setPosition(m_resumePosition / MS_TICK_FACTOR); } } else if (newStatus == QMediaPlayer::EndOfMedia) { @@ -316,33 +328,82 @@ void PlaybackManager::stop() { void PlaybackManager::seek(qint64 pos) { m_mediaPlayer->setPosition(pos); postPlaybackInfo(Progress); + emit seeked(pos); +} + +void PlaybackManager::handlePlaystateRequest(const DTO::PlaystateRequest &request) { + if (!m_handlePlaystateCommands) return; + switch(request.command()) { + case DTO::PlaystateCommand::Pause: + pause(); + break; + case DTO::PlaystateCommand::PlayPause: + if (playbackState() != QMediaPlayer::PlayingState) { + play(); + } else { + pause(); + } + break; + case DTO::PlaystateCommand::Unpause: + play(); + break; + case DTO::PlaystateCommand::Stop: + stop(); + break; + case DTO::PlaystateCommand::NextTrack: + next(); + break; + case DTO::PlaystateCommand::PreviousTrack: + previous(); + break; + case DTO::PlaystateCommand::Seek: + if (request.seekPositionTicksNull()) { + qCWarning(playbackManager) << "Received seek command without position argument"; + } else { + seek(request.seekPositionTicks().value() / MS_TICK_FACTOR); + } + break; + default: + qCDebug(playbackManager) << "Unhandled PlaystateCommand: " << request.command(); + break; + } } void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) { - QJsonObject root; + DTO::PlaybackProgressInfo progress; if (m_item == nullptr) { qWarning() << "Item is null. Not posting playback info"; return; } - root["ItemId"] = Support::toString(m_item->jellyfinId()); - root["SessionId"] = m_playSessionId; + progress.setItemId(Support::toString(m_item->jellyfinId())); + progress.setSessionId(m_playSessionId); + progress.setRepeatMode(DTO::RepeatMode::RepeatNone); switch(type) { case Started: // FALLTHROUGH - case Progress: + case Progress: { + progress.setCanSeek(seekable()); + progress.setIsPaused(m_mediaPlayer->state() == QMediaPlayer::PausedState); + progress.setIsMuted(false); - root["IsPaused"] = m_mediaPlayer->state() != QMediaPlayer::PlayingState; - root["IsMuted"] = false; + progress.setAudioStreamIndex(m_audioIndex); + progress.setSubtitleStreamIndex(m_subtitleIndex); - root["AudioStreamIndex"] = m_audioIndex; - root["SubtitleStreamIndex"] = m_subtitleIndex; + progress.setPlayMethod(m_playMethod); + progress.setPositionTicks(m_mediaPlayer->position() * MS_TICK_FACTOR); - root["PlayMethod"] = QVariant::fromValue(m_playMethod).toString(); - root["PositionTicks"] = m_mediaPlayer->position() * MS_TICK_FACTOR; + QList queue; + for (int i = 0; i < m_queue->listSize(); i++) { + DTO::QueueItem queueItem; + queueItem.setJellyfinId(m_queue->listAt(i)->jellyfinId()); + queue.append(queueItem); + } + progress.setNowPlayingQueue(queue); break; + } case Stopped: - root["PositionTicks"] = m_stopPosition * MS_TICK_FACTOR; + progress.setPositionTicks(m_mediaPlayer->position() * MS_TICK_FACTOR); break; } @@ -359,7 +420,7 @@ void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) { break; } - QNetworkReply *rep = m_apiClient->post(path, QJsonDocument(root)); + QNetworkReply *rep = m_apiClient->post(path, QJsonDocument(progress.toJson())); connect(rep, &QNetworkReply::finished, this, [rep](){ rep->deleteLater(); }); diff --git a/core/src/websocket.cpp b/core/src/websocket.cpp index 769ff6a..8745e99 100644 --- a/core/src/websocket.cpp +++ b/core/src/websocket.cpp @@ -21,6 +21,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include #include +#include #include Q_LOGGING_CATEGORY(jellyfinWebSocket, "jellyfin.websocket"); @@ -114,6 +115,13 @@ void WebSocket::textMessageReceived(const QString &message) { } catch(QException &e) { qCWarning(jellyfinWebSocket()) << "Error while deserializing command: " << e.what(); } + } else if (messageType == QStringLiteral("Playstate")) { + try { + DTO::PlaystateRequest request = PlaystateRequest::fromJson(messageRoot["Data"].toObject()); + emit m_apiClient->eventbus()->playstateCommandReceived(request); + } catch (QException &e) { + qCWarning(jellyfinWebSocket()) << "Error while deserialzing PlaystateRequest " << e.what(); + } } else { qCDebug(jellyfinWebSocket) << messageType; }