mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2024-11-22 09:15:18 +00:00
Add support for handling playstate commands
This commit is contained in:
parent
7c21eb425d
commit
2a3bd51def
|
@ -25,6 +25,7 @@
|
||||||
namespace Jellyfin {
|
namespace Jellyfin {
|
||||||
namespace DTO {
|
namespace DTO {
|
||||||
class UserItemDataDto;
|
class UserItemDataDto;
|
||||||
|
class PlaystateRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,6 +51,8 @@ signals:
|
||||||
* @param timeout Timeout in MS to show the message. -1: no timeout supplied.
|
* @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 displayMessage(const QString &header, const QString &message, int timeout = -1);
|
||||||
|
|
||||||
|
void playstateCommandReceived(const Jellyfin::DTO::PlaystateRequest &request);
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -196,6 +196,7 @@ private slots:
|
||||||
void onPositionChanged(qint64 position);
|
void onPositionChanged(qint64 position);
|
||||||
void onSeekableChanged(bool seekable);
|
void onSeekableChanged(bool seekable);
|
||||||
void onPlaybackManagerChanged(ViewModel::PlaybackManager *newPlaybackManager);
|
void onPlaybackManagerChanged(ViewModel::PlaybackManager *newPlaybackManager);
|
||||||
|
void onSeeked(qint64 newPosition);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // NS FreeDesktop
|
} // NS FreeDesktop
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
#include <QAbstractItemModel>
|
#include <QAbstractItemModel>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QLoggingCategory>
|
||||||
#include <QFuture>
|
#include <QFuture>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
@ -51,9 +52,13 @@ namespace Jellyfin {
|
||||||
// Forward declaration of Jellyfin::ApiClient found in jellyfinapiclient.h
|
// Forward declaration of Jellyfin::ApiClient found in jellyfinapiclient.h
|
||||||
class ApiClient;
|
class ApiClient;
|
||||||
class ItemModel;
|
class ItemModel;
|
||||||
class RemoteItem;
|
|
||||||
|
namespace DTO {
|
||||||
|
class PlaystateRequest;
|
||||||
|
}
|
||||||
|
|
||||||
namespace ViewModel {
|
namespace ViewModel {
|
||||||
|
Q_DECLARE_LOGGING_CATEGORY(playbackManager);
|
||||||
|
|
||||||
// Later defined in this file
|
// Later defined in this file
|
||||||
class ItemUrlFetcherThread;
|
class ItemUrlFetcherThread;
|
||||||
|
@ -99,6 +104,8 @@ public:
|
||||||
Q_PROPERTY(qint64 position READ position NOTIFY positionChanged)
|
Q_PROPERTY(qint64 position READ position NOTIFY positionChanged)
|
||||||
Q_PROPERTY(bool hasNext READ hasNext NOTIFY hasNextChanged)
|
Q_PROPERTY(bool hasNext READ hasNext NOTIFY hasNextChanged)
|
||||||
Q_PROPERTY(bool hasPrevious READ hasPrevious NOTIFY hasPreviousChanged)
|
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; }
|
ViewModel::Item *item() const { return m_displayItem; }
|
||||||
QSharedPointer<Model::Item> dataItem() const { return m_item; }
|
QSharedPointer<Model::Item> dataItem() const { return m_item; }
|
||||||
|
@ -122,6 +129,9 @@ public:
|
||||||
bool seekable() const { return m_mediaPlayer->isSeekable(); }
|
bool seekable() const { return m_mediaPlayer->isSeekable(); }
|
||||||
QMediaPlayer::Error error () const;
|
QMediaPlayer::Error error () const;
|
||||||
QString errorString() const;
|
QString errorString() const;
|
||||||
|
|
||||||
|
bool handlePlaystateCommands() const { return m_handlePlaystateCommands; }
|
||||||
|
void setHandlePlaystateCommands(bool newHandlePlaystateCommands) { m_handlePlaystateCommands = newHandlePlaystateCommands; emit handlePlaystateCommandsChanged(m_handlePlaystateCommands); }
|
||||||
signals:
|
signals:
|
||||||
void itemChanged(ViewModel::Item *newItemId);
|
void itemChanged(ViewModel::Item *newItemId);
|
||||||
void streamUrlChanged(const QString &newStreamUrl);
|
void streamUrlChanged(const QString &newStreamUrl);
|
||||||
|
@ -132,6 +142,9 @@ signals:
|
||||||
void resumePlaybackChanged(bool newResumePlayback);
|
void resumePlaybackChanged(bool newResumePlayback);
|
||||||
void playMethodChanged(PlayMethod newPlayMethod);
|
void playMethodChanged(PlayMethod newPlayMethod);
|
||||||
|
|
||||||
|
// Emitted when seek has been called.
|
||||||
|
void seeked(qint64 newPosition);
|
||||||
|
|
||||||
// Current media player related property signals
|
// Current media player related property signals
|
||||||
void mediaObjectChanged(QObject *newMediaObject);
|
void mediaObjectChanged(QObject *newMediaObject);
|
||||||
void positionChanged(qint64 newPosition);
|
void positionChanged(qint64 newPosition);
|
||||||
|
@ -146,6 +159,7 @@ signals:
|
||||||
void errorStringChanged(const QString &newErrorString);
|
void errorStringChanged(const QString &newErrorString);
|
||||||
void hasNextChanged(bool newHasNext);
|
void hasNextChanged(bool newHasNext);
|
||||||
void hasPreviousChanged(bool newHasPrevious);
|
void hasPreviousChanged(bool newHasPrevious);
|
||||||
|
void handlePlaystateCommandsChanged(bool newHandlePlaystateCommands);
|
||||||
public slots:
|
public slots:
|
||||||
/**
|
/**
|
||||||
* @brief playItem Replaces the current queue and plays the given item.
|
* @brief playItem Replaces the current queue and plays the given item.
|
||||||
|
@ -187,6 +201,8 @@ public slots:
|
||||||
*/
|
*/
|
||||||
void next();
|
void next();
|
||||||
|
|
||||||
|
void handlePlaystateRequest(const DTO::PlaystateRequest &request);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void mediaPlayerStateChanged(QMediaPlayer::State newState);
|
void mediaPlayerStateChanged(QMediaPlayer::State newState);
|
||||||
void mediaPlayerPositionChanged(qint64 position);
|
void mediaPlayerPositionChanged(qint64 position);
|
||||||
|
@ -260,6 +276,8 @@ private:
|
||||||
int m_queueIndex = 0;
|
int m_queueIndex = 0;
|
||||||
bool m_resumePlayback = true;
|
bool m_resumePlayback = true;
|
||||||
|
|
||||||
|
bool m_handlePlaystateCommands = true;
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
void setItem(QSharedPointer<Model::Item> newItem);
|
void setItem(QSharedPointer<Model::Item> newItem);
|
||||||
|
|
||||||
|
|
|
@ -318,6 +318,11 @@ void PlayerAdaptor::onPositionChanged(qint64 position) {
|
||||||
properties << "Position";
|
properties << "Position";
|
||||||
notifyPropertiesChanged(properties);*/
|
notifyPropertiesChanged(properties);*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlayerAdaptor::onSeeked(qint64 position) {
|
||||||
|
notifyPropertiesChanged(QStringList("Position"));
|
||||||
|
}
|
||||||
|
|
||||||
void PlayerAdaptor::onSeekableChanged(bool seekable) {
|
void PlayerAdaptor::onSeekableChanged(bool seekable) {
|
||||||
QStringList properties;
|
QStringList properties;
|
||||||
properties << "CanSeek";
|
properties << "CanSeek";
|
||||||
|
@ -330,6 +335,7 @@ void PlayerAdaptor::onPlaybackManagerChanged(ViewModel::PlaybackManager *newPlay
|
||||||
connect(newPlaybackManager, &ViewModel::PlaybackManager::playbackStateChanged, this, &PlayerAdaptor::onPlaybackStateChanged);
|
connect(newPlaybackManager, &ViewModel::PlaybackManager::playbackStateChanged, this, &PlayerAdaptor::onPlaybackStateChanged);
|
||||||
connect(newPlaybackManager, &ViewModel::PlaybackManager::mediaStatusChanged, this, &PlayerAdaptor::onMediaStatusChanged);
|
connect(newPlaybackManager, &ViewModel::PlaybackManager::mediaStatusChanged, this, &PlayerAdaptor::onMediaStatusChanged);
|
||||||
connect(newPlaybackManager, &ViewModel::PlaybackManager::positionChanged, this, &PlayerAdaptor::onPositionChanged);
|
connect(newPlaybackManager, &ViewModel::PlaybackManager::positionChanged, this, &PlayerAdaptor::onPositionChanged);
|
||||||
|
connect(newPlaybackManager, &ViewModel::PlaybackManager::seeked, this, &PlayerAdaptor::onSeeked);
|
||||||
connect(newPlaybackManager, &ViewModel::PlaybackManager::seekableChanged, this, &PlayerAdaptor::onSeekableChanged);
|
connect(newPlaybackManager, &ViewModel::PlaybackManager::seekableChanged, this, &PlayerAdaptor::onSeekableChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,8 @@
|
||||||
|
|
||||||
#include "JellyfinQt/apimodel.h"
|
#include "JellyfinQt/apimodel.h"
|
||||||
#include "JellyfinQt/loader/http/mediainfo.h"
|
#include "JellyfinQt/loader/http/mediainfo.h"
|
||||||
|
#include <JellyfinQt/dto/playstatecommand.h>
|
||||||
|
#include <JellyfinQt/dto/playstaterequest.h>
|
||||||
|
|
||||||
// #include "JellyfinQt/DTO/dto.h"
|
// #include "JellyfinQt/DTO/dto.h"
|
||||||
#include <JellyfinQt/loader/http/userlibrary.h>
|
#include <JellyfinQt/loader/http/userlibrary.h>
|
||||||
|
@ -37,6 +39,8 @@ namespace DTO {
|
||||||
|
|
||||||
namespace ViewModel {
|
namespace ViewModel {
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(playbackManager, "jellyfin.viewmodel.playbackmanager")
|
||||||
|
|
||||||
PlaybackManager::PlaybackManager(QObject *parent)
|
PlaybackManager::PlaybackManager(QObject *parent)
|
||||||
: QObject(parent),
|
: QObject(parent),
|
||||||
m_item(nullptr),
|
m_item(nullptr),
|
||||||
|
@ -64,10 +68,18 @@ PlaybackManager::PlaybackManager(QObject *parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::setApiClient(ApiClient *apiClient) {
|
void PlaybackManager::setApiClient(ApiClient *apiClient) {
|
||||||
|
if (m_apiClient != nullptr) {
|
||||||
|
disconnect(m_apiClient->eventbus(), &EventBus::playstateCommandReceived, this, &PlaybackManager::handlePlaystateRequest);
|
||||||
|
}
|
||||||
|
|
||||||
if (!m_item.isNull()) {
|
if (!m_item.isNull()) {
|
||||||
m_item->setApiClient(apiClient);
|
m_item->setApiClient(apiClient);
|
||||||
}
|
}
|
||||||
m_apiClient = apiClient;
|
m_apiClient = apiClient;
|
||||||
|
|
||||||
|
if (m_apiClient != nullptr) {
|
||||||
|
connect(m_apiClient->eventbus(), &EventBus::playstateCommandReceived, this, &PlaybackManager::handlePlaystateRequest);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::setItem(QSharedPointer<Model::Item> newItem) {
|
void PlaybackManager::setItem(QSharedPointer<Model::Item> newItem) {
|
||||||
|
@ -92,7 +104,7 @@ void PlaybackManager::setItem(QSharedPointer<Model::Item> newItem) {
|
||||||
|
|
||||||
if (m_apiClient == nullptr) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
// Deinitialize the streamUrl
|
// Deinitialize the streamUrl
|
||||||
|
@ -171,7 +183,7 @@ void PlaybackManager::mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus ne
|
||||||
if (newStatus == QMediaPlayer::LoadedMedia) {
|
if (newStatus == QMediaPlayer::LoadedMedia) {
|
||||||
m_mediaPlayer->play();
|
m_mediaPlayer->play();
|
||||||
if (m_resumePlayback) {
|
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);
|
m_mediaPlayer->setPosition(m_resumePosition / MS_TICK_FACTOR);
|
||||||
}
|
}
|
||||||
} else if (newStatus == QMediaPlayer::EndOfMedia) {
|
} else if (newStatus == QMediaPlayer::EndOfMedia) {
|
||||||
|
@ -316,33 +328,82 @@ void PlaybackManager::stop() {
|
||||||
void PlaybackManager::seek(qint64 pos) {
|
void PlaybackManager::seek(qint64 pos) {
|
||||||
m_mediaPlayer->setPosition(pos);
|
m_mediaPlayer->setPosition(pos);
|
||||||
postPlaybackInfo(Progress);
|
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) {
|
void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
|
||||||
QJsonObject root;
|
DTO::PlaybackProgressInfo progress;
|
||||||
|
|
||||||
if (m_item == nullptr) {
|
if (m_item == nullptr) {
|
||||||
qWarning() << "Item is null. Not posting playback info";
|
qWarning() << "Item is null. Not posting playback info";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
root["ItemId"] = Support::toString(m_item->jellyfinId());
|
progress.setItemId(Support::toString(m_item->jellyfinId()));
|
||||||
root["SessionId"] = m_playSessionId;
|
progress.setSessionId(m_playSessionId);
|
||||||
|
progress.setRepeatMode(DTO::RepeatMode::RepeatNone);
|
||||||
|
|
||||||
switch(type) {
|
switch(type) {
|
||||||
case Started: // FALLTHROUGH
|
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;
|
progress.setAudioStreamIndex(m_audioIndex);
|
||||||
root["IsMuted"] = false;
|
progress.setSubtitleStreamIndex(m_subtitleIndex);
|
||||||
|
|
||||||
root["AudioStreamIndex"] = m_audioIndex;
|
progress.setPlayMethod(m_playMethod);
|
||||||
root["SubtitleStreamIndex"] = m_subtitleIndex;
|
progress.setPositionTicks(m_mediaPlayer->position() * MS_TICK_FACTOR);
|
||||||
|
|
||||||
root["PlayMethod"] = QVariant::fromValue(m_playMethod).toString();
|
QList<DTO::QueueItem> queue;
|
||||||
root["PositionTicks"] = m_mediaPlayer->position() * MS_TICK_FACTOR;
|
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;
|
break;
|
||||||
|
}
|
||||||
case Stopped:
|
case Stopped:
|
||||||
root["PositionTicks"] = m_stopPosition * MS_TICK_FACTOR;
|
progress.setPositionTicks(m_mediaPlayer->position() * MS_TICK_FACTOR);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -359,7 +420,7 @@ void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkReply *rep = m_apiClient->post(path, QJsonDocument(root));
|
QNetworkReply *rep = m_apiClient->post(path, QJsonDocument(progress.toJson()));
|
||||||
connect(rep, &QNetworkReply::finished, this, [rep](){
|
connect(rep, &QNetworkReply::finished, this, [rep](){
|
||||||
rep->deleteLater();
|
rep->deleteLater();
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,6 +21,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
#include <JellyfinQt/dto/generalcommand.h>
|
#include <JellyfinQt/dto/generalcommand.h>
|
||||||
#include <JellyfinQt/dto/generalcommandtype.h>
|
#include <JellyfinQt/dto/generalcommandtype.h>
|
||||||
|
#include <JellyfinQt/dto/playstaterequest.h>
|
||||||
#include <JellyfinQt/dto/useritemdatadto.h>
|
#include <JellyfinQt/dto/useritemdatadto.h>
|
||||||
|
|
||||||
Q_LOGGING_CATEGORY(jellyfinWebSocket, "jellyfin.websocket");
|
Q_LOGGING_CATEGORY(jellyfinWebSocket, "jellyfin.websocket");
|
||||||
|
@ -114,6 +115,13 @@ void WebSocket::textMessageReceived(const QString &message) {
|
||||||
} catch(QException &e) {
|
} catch(QException &e) {
|
||||||
qCWarning(jellyfinWebSocket()) << "Error while deserializing command: " << e.what();
|
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 {
|
} else {
|
||||||
qCDebug(jellyfinWebSocket) << messageType;
|
qCDebug(jellyfinWebSocket) << messageType;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue