core: Split PlaybackManager up into smaller parts

The PlaybackManager was a giant class that handled UI bindings, fetching
stream URLS, playback logic.

It now has been split up into:

- ViewModel::PlaybackManager, which handles UI interfacing and allowing
  to swap out the Model::Playback implementation on the fly.
- Model::PlaybackManager, which is an interface for what a
  PlaybackManager must do, handling queues/playlists, and controlling a
  player.
- Model::LocalPlaybackManager, which is an Model::PlaybackManager
  implementation for playing back Jellyfin media within the application.
- Model::PlaybackReporter, which reports the current playback state to
  the Jellyfin server, for keeping track of played items.
- Model::Player, which handles playing back media from an URL and
  the usual play/pause et cetera.

In a future commit, this would allow for introducing a
Model::RemoteJellyfinPlaybackManager, to control other Jellyfin
instances.
This commit is contained in:
Chris Josten 2022-01-05 21:24:52 +01:00 committed by Henk Kalkwater
parent f91e9f88e7
commit c72c10bad4
No known key found for this signature in database
GPG Key ID: A69C050E9FD9FF6A
20 changed files with 1916 additions and 684 deletions

View File

@ -15,6 +15,9 @@ include(GeneratedSources.cmake)
set(JellyfinQt_SOURCES
src/model/deviceprofile.cpp
src/model/item.cpp
src/model/player.cpp
src/model/playbackmanager.cpp
src/model/playbackreporter.cpp
src/model/playlist.cpp
src/model/shuffle.cpp
src/model/user.cpp
@ -48,6 +51,9 @@ list(APPEND JellyfinQt_SOURCES ${openapi_SOURCES})
set(JellyfinQt_HEADERS
include/JellyfinQt/model/deviceprofile.h
include/JellyfinQt/model/item.h
include/JellyfinQt/model/player.h
include/JellyfinQt/model/playbackmanager.h
include/JellyfinQt/model/playbackreporter.h
include/JellyfinQt/model/playlist.h
include/JellyfinQt/model/shuffle.h
include/JellyfinQt/model/user.h

View File

@ -489,6 +489,10 @@ public:
this->endResetModel();
}
const QList<QSharedPointer<T>> &toList() {
return m_array;
}
// From AbstractListModel, gets implemented in ApiModel<T>
//virtual QHash<int, QByteArray> roleNames() const override = 0;
/*virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override = 0;*/

View File

@ -0,0 +1,218 @@
/*
* Sailfin: a Jellyfin client written using Qt
* Copyright (C) 2021-2022 Chris Josten and the Sailfin Contributors.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef JELLYFIN_MODEL_PLAYBACKMANAGER_H
#define JELLYFIN_MODEL_PLAYBACKMANAGER_H
#include <QLoggingCategory>
#include <QObject>
#include <QSharedPointer>
#include <JellyfinQt/dto/playmethod.h>
#include <JellyfinQt/model/player.h>
namespace Jellyfin {
namespace Model {
class Item;
class Playlist;
Q_DECLARE_LOGGING_CATEGORY(playbackManager);
class PlaybackManagerErrorClass {
Q_GADGET
public:
enum Value {
NoError,
PlaybackInfoError,
RemoteClientNotReachable,
PlayerGeneralError
};
Q_ENUM(Value);
};
using PlaybackManagerError = PlaybackManagerErrorClass::Value;
class PlaybackManagerPrivate;
/**
* @brief Base class for a playback manager.
*
* Besides some glue code for the properties,
* most of the actual playback logic is implemented in the two subclasses: {@link LocalPlaybackManager}
* and {@link RemotePlaybackManager}
*/
class PlaybackManager : public QObject {
Q_OBJECT
Q_DECLARE_PRIVATE(PlaybackManager);
Q_PROPERTY(bool resumePlayback READ resumePlayback WRITE setResumePlayback NOTIFY resumePlaybackChanged)
Q_PROPERTY(int audioIndex READ audioIndex WRITE setAudioIndex NOTIFY audioIndexChanged)
Q_PROPERTY(int subtitleIndex READ subtitleIndex WRITE setSubtitleIndex NOTIFY subtitleIndexChanged)
Q_PROPERTY(qint64 position READ position NOTIFY positionChanged)
Q_PROPERTY(qint64 duration READ duration NOTIFY durationChanged)
Q_PROPERTY(bool seekable READ seekable NOTIFY seekableChanged)
Q_PROPERTY(bool hasAudio READ hasAudio NOTIFY hasAudioChanged)
Q_PROPERTY(bool hasVideo READ hasVideo NOTIFY hasVideoChanged)
Q_PROPERTY(Jellyfin::Model::PlayerStateClass::Value playbackState READ playbackState NOTIFY playbackStateChanged)
Q_PROPERTY(Jellyfin::Model::MediaStatusClass::Value mediaStatus READ mediaStatus NOTIFY mediaStatusChanged)
Q_PROPERTY(Jellyfin::Model::Playlist *queue READ queue NOTIFY queueChanged)
Q_PROPERTY(int queueIndex READ queueIndex NOTIFY queueIndexChanged)
public:
explicit PlaybackManager(QObject *parent = nullptr);
virtual ~PlaybackManager();
virtual void swap(PlaybackManager& other) = 0;
ApiClient * apiClient() const;
void setApiClient(ApiClient *apiClient);
// Getters
QSharedPointer<Item> currentItem() const;
Playlist *queue() const;
int queueIndex() const;
bool resumePlayback() const;
void setResumePlayback(bool newResumePlayback);
int audioIndex() const;
void setAudioIndex(int newAudioIndex);
int subtitleIndex() const;
void setSubtitleIndex(int newSubtitleIndex);
virtual PlayerState playbackState() const = 0;
virtual MediaStatus mediaStatus() const = 0;
virtual bool hasNext() const = 0;
virtual bool hasPrevious() const = 0;
virtual PlaybackManagerError error() const = 0;
virtual const QString &errorString() const = 0;
virtual qint64 position() const = 0;
virtual qint64 duration() const = 0;
virtual bool seekable() const = 0;
virtual bool hasAudio() const = 0;
virtual bool hasVideo() const = 0;
virtual void playItem(QSharedPointer<Model::Item> item) = 0;
virtual void playItemInList(const QList<QSharedPointer<Model::Item>> &items, int index) = 0;
signals:
void playbackStateChanged(Jellyfin::Model::PlayerStateClass::Value newPlaybackState);
void mediaStatusChanged(Jellyfin::Model::MediaStatusClass::Value newMediaStatus);
void queueChanged(Jellyfin::Model::Playlist *newPlaylist);
void hasNextChanged(bool newHasNext);
void hasPreviousChanged(bool newHasPrevious);
void itemChanged();
void queueIndexChanged(int index);
void errorChanged(Jellyfin::Model::PlaybackManagerErrorClass::Value newError);
void errorStringChanged(const QString &newErrorString);
void positionChanged(qint64 newPosition);
void durationChanged(qint64 newDuration);
void seekableChanged(bool newSeekable);
void hasAudioChanged();
void hasVideoChanged();
void resumePlaybackChanged(bool newPlaybackChanged);
void audioIndexChanged(int newAudioIndex);
void subtitleIndexChanged(int newSubtitleIndex);
public slots:
virtual void pause() = 0;
virtual void play() = 0;
virtual void playItemId(const QString &id) = 0;
virtual void previous() = 0;
virtual void next() = 0;
/**
* @brief Play the item at the index in the current playlist
* @param index the item to go to;
*/
virtual void goTo(int index) = 0;
virtual void stop() = 0;
virtual void seek(qint64 pos) = 0;
protected:
explicit PlaybackManager(PlaybackManagerPrivate *d, QObject *parent = nullptr);
QScopedPointer<PlaybackManagerPrivate> d_ptr;
};
class LocalPlaybackManagerPrivate;
/**
* @brief Controls playback whithin this app.
*
* This class mostly consists of bookkeeping between the actual media player implementation (which is
* abstracted into yet another class) and the ViewModel.
*
* It does things like:
* * Fetching the actual media URL of an item and deciding which playback method to use
* * Managing the current playlist state and instructing the media player which item to play next
* * Keeping track of the playback state that the user would expect from a media player,
* instead of what the media player implementation reports.
*
*/
class LocalPlaybackManager : public PlaybackManager {
Q_DECLARE_PRIVATE(LocalPlaybackManager);
Q_OBJECT
Q_PROPERTY(Jellyfin::Model::Player *player READ player NOTIFY playerChanged)
Q_PROPERTY(Jellyfin::DTO::PlayMethodClass::Value playMethod READ playMethod NOTIFY playMethodChanged)
Q_PROPERTY(QUrl streamUrl READ streamUrl NOTIFY streamUrlChanged)
public:
explicit LocalPlaybackManager(QObject *parent = nullptr);
void swap(PlaybackManager& other) override;
Player *player() const;
QString sessionId() const;
DTO::PlayMethod playMethod() const;
const QUrl &streamUrl() const;
PlayerState playbackState() const override;
MediaStatus mediaStatus() const override;
PlaybackManagerError error() const override;
const QString& errorString() const override;
qint64 position() const override;
qint64 duration() const override;
bool seekable() const override;
bool hasAudio() const override;
bool hasVideo() const override;
bool hasNext() const override;
bool hasPrevious() const override;
void playItemInList(const QList<QSharedPointer<Model::Item>> &items, int index) override;
public slots:
void pause() override;
void play() override;
void playItem(QSharedPointer<Model::Item> item) override;
void playItemId(const QString &itemId) override;
void next() override;
void previous() override;
void stop() override;
void seek(qint64 pos) override;
void goTo(int index) override;
signals:
void playerChanged(Jellyfin::Model::Player *newPlayer);
void playMethodChanged(Jellyfin::DTO::PlayMethodClass::Value newPlayMethod);
void streamUrlChanged(const QUrl &newStreamUrl);
};
/**
* @brief Controls playback for remote devices, such as other Jellyfin clients, over the network.
*/
class RemoteJellyfinPlaybackManager {
};
} // NS Model
} // NS Jellyfin
#endif // JELLYFIN_MODEL_PLAYBACKMANAGER_H

View File

@ -0,0 +1,58 @@
/*
* Sailfin: a Jellyfin client written using Qt
* Copyright (C) 2021-2022 Chris Josten and the Sailfin Contributors.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef JELLYFIN_MODEL_PLAYBACKREPORTER_H
#define JELLYFIN_MODEL_PLAYBACKREPORTER_H
#include <QLoggingCategory>
#include <QObject>
#include <QScopedPointer>
namespace Jellyfin {
class ApiClient;
namespace Model {
Q_DECLARE_LOGGING_CATEGORY(playbackReporter);
class LocalPlaybackManager;
class PlaybackReporterPrivate;
/**
* @brief Reports the current playback state to the Jellyfin server
*
* Set a playbackManager using setPlaybackmanager() and this class
* will do its job.
*/
class PlaybackReporter : public QObject {
Q_OBJECT
Q_DECLARE_PRIVATE(PlaybackReporter);
public:
explicit PlaybackReporter(QObject *parent = nullptr);
void setPlaybackManager(LocalPlaybackManager *playbackManager);
private:
PlaybackReporterPrivate *d_ptr;
};
}
}
#endif //JELLYFIN_MODEL_PLAYBACKREPORTER_H

View File

@ -0,0 +1,152 @@
/*
* Sailfin: a Jellyfin client written using Qt
* Copyright (C) 2021-2022 Chris Josten and the Sailfin Contributors.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef JELLYFIN_MODEL_PLAYER_H
#define JELLYFIN_MODEL_PLAYER_H
#include <QLoggingCategory>
#include <QObject>
#include <QUrl>
namespace Jellyfin {
namespace Model {
Q_DECLARE_LOGGING_CATEGORY(player)
class PlayerStateClass {
Q_GADGET
public:
enum Value {
Stopped,
Playing,
Paused
};
Q_ENUM(Value);
private:
PlayerStateClass() {}
};
class MediaStatusClass {
Q_GADGET
public:
enum Value {
NoMedia,
Loading,
Loaded,
Stalled,
Buffering,
Buffered,
EndOfMedia,
Error
};
Q_ENUM(Value);
private:
MediaStatusClass() {}
};
using PlayerState = PlayerStateClass::Value;
using MediaStatus = MediaStatusClass::Value;
/**
* @brief Abstract class for a player
*/
class Player : public QObject {
Q_OBJECT
Q_PROPERTY(Jellyfin::Model::PlayerStateClass::Value state READ state NOTIFY stateChanged)
Q_PROPERTY(Jellyfin::Model::MediaStatusClass::Value mediaStatus READ mediaStatus NOTIFY mediaStatusChanged)
Q_PROPERTY(qint64 position READ position NOTIFY positionChanged)
Q_PROPERTY(qint64 duration READ duration NOTIFY durationChanged)
Q_PROPERTY(bool seekable READ seekable NOTIFY seekableChanged)
Q_PROPERTY(bool hasAudio READ hasAudio NOTIFY hasAudioChanged)
Q_PROPERTY(bool hasVideo READ hasVideo NOTIFY hasVideoChanged)
Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged)
// Used in QML by the VideoOutput
Q_PROPERTY(QObject* videoOutputSource READ videoOutputSource NOTIFY videoOutputSourceChanged);
public:
public:
~Player();
virtual PlayerState state() const = 0;
virtual MediaStatus mediaStatus() const = 0;
virtual qint64 position() const = 0;
virtual qint64 duration() const = 0;
virtual bool seekable() const = 0;
virtual bool hasVideo() const = 0;
virtual bool hasAudio() const = 0;
virtual QString errorString() const = 0;
virtual QObject *videoOutputSource() const = 0;
public slots:
virtual void pause() = 0;
virtual void play(qint64 startPos = 0) = 0;
virtual void stop() = 0;
virtual void seek(qint64 position) = 0;
virtual void setMedia(const QUrl &url, int audioIndex = -1, int subTitleIndex = -1) = 0;
signals:
void stateChanged(Jellyfin::Model::PlayerStateClass::Value newState);
void mediaStatusChanged(Jellyfin::Model::MediaStatusClass::Value newMediaStatus);
void positionChanged(qint64 newPosition);
void durationChanged(qint64 newDuration);
void errorStringChanged();
/**
* @brief Sent when the position changed due to calling the seek method.
*/
void seeked();
void seekableChanged(bool seekable);
void hasAudioChanged();
void hasVideoChanged();
void aboutToFinish();
void videoOutputSourceChanged();
};
#define USE_QTMULTIMEDIA_PLAYER
#ifdef USE_QTMULTIMEDIA_PLAYER
class QtMultimediaPlayerPrivate;
/**
* @brief Player implementation that uses QtMultimedia
*/
class QtMultimediaPlayer : public Player {
Q_OBJECT
Q_DECLARE_PRIVATE(QtMultimediaPlayer);
public:
explicit QtMultimediaPlayer(QObject *parent = nullptr);
virtual ~QtMultimediaPlayer();
PlayerState state() const override;
MediaStatus mediaStatus() const override;
qint64 position() const override;
qint64 duration() const override;
bool seekable() const override;
bool hasVideo() const override;
bool hasAudio() const override;
QString errorString() const override;
QObject *videoOutputSource() const override;
public slots:
void pause() override;
void play(qint64 startPos = 0) override;
void stop() override;
void seek(qint64 position) override;
void setMedia(const QUrl &url, int audioIndex, int subtitleIndex) override;
private:
QScopedPointer<QtMultimediaPlayerPrivate> d_ptr;
};
#endif // ifdef USE_QTMULTIMEDIA_PLAYER
} // NS Model
} // NS Jellyfin
#endif // JELLYFIN_MODEL_PLAYER_H

View File

@ -1,6 +1,6 @@
/*
* Sailfin: a Jellyfin client written using Qt
* Copyright (C) 2021 Chris Josten and the Sailfin Contributors.
* Copyright (C) 2021-2022 Chris Josten and the Sailfin Contributors.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -96,9 +96,9 @@ public:
void clearList();
/**
* @brief Appends all items from the given itemModel to this list
* @brief Appends all items from the given item list to this list
*/
void appendToList(ViewModel::ItemModel &model);
void appendToList(const QList<QSharedPointer<Item>> &model);
/**
* @brief appendToList Appends a single item to the current list

View File

@ -16,6 +16,8 @@
#include <QtCore/QObject>
#include <QtDBus/QtDBus>
#include <QMediaPlayer>
#include "JellyfinQt/model/player.h"
QT_BEGIN_NAMESPACE
class QByteArray;
template<class T> class QList;
@ -190,9 +192,9 @@ private:
ViewModel::PlatformMediaControl *m_mediaControl;
void notifyPropertiesChanged(QStringList properties);
private slots:
void onCurrentItemChanged(ViewModel::Item *newItem);
void onPlaybackStateChanged(QMediaPlayer::State state);
void onMediaStatusChanged(QMediaPlayer::MediaStatus status);
void onCurrentItemChanged();
void onPlaybackStateChanged(Jellyfin::Model::PlayerStateClass::Value state);
void onMediaStatusChanged(Jellyfin::Model::MediaStatusClass::Value status);
void onPositionChanged(qint64 position);
void onSeekableChanged(bool seekable);
void onPlaybackManagerChanged(ViewModel::PlaybackManager *newPlaybackManager);

View File

@ -1,6 +1,6 @@
/*
* Sailfin: a Jellyfin client written using Qt
* Copyright (C) 2021 Chris Josten and the Sailfin Contributors.
* Copyright (C) 2021-2022 Chris Josten and the Sailfin Contributors.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -39,6 +39,7 @@
#include "../dto/playbackinforesponse.h"
#include "../dto/playmethod.h"
#include "../loader/requesttypes.h"
#include "../model/player.h"
#include "../model/playlist.h"
#include "../support/jsonconv.h"
#include "../viewmodel/item.h"
@ -60,31 +61,34 @@ class PlaystateRequest;
namespace ViewModel {
Q_DECLARE_LOGGING_CATEGORY(playbackManager);
// Later defined in this file
class ItemUrlFetcherThread;
class PlaybackManagerPrivate;
/**
* @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, contains the actual media player and so on.
* @brief The PlaybackManager class manages the playback of Jellyfin items.
*
* The PlaybackManager actually keeps two mediaPlayers, m_mediaPlayer1 and m_mediaPlayer2. When one is playing, the other is
* preloading the next item in the queue. The current media player is pointed to by m_mediaPlayer.
* It is a small wrapper around an instance of Jellyfin::Model::PlaybackManager,
* which do the actual work. The Jellyfin::Model::PlaybackManager can be switched
* on the fly, allowing this class to switch between controlling the playback locally
* or remote.
*/
class PlaybackManager : public QObject, public QQmlParserStatus {
friend class ItemUrlFetcherThread;
Q_OBJECT
Q_DECLARE_PRIVATE(PlaybackManager);
Q_INTERFACES(QQmlParserStatus)
public:
using ItemUrlLoader = Support::Loader<DTO::PlaybackInfoResponse, Jellyfin::Loader::GetPostedPlaybackInfoParams>;
explicit PlaybackManager(QObject *parent = nullptr);
virtual ~PlaybackManager();
Q_PROPERTY(ApiClient *apiClient MEMBER m_apiClient WRITE setApiClient)
Q_PROPERTY(ApiClient *apiClient READ apiClient WRITE setApiClient)
Q_PROPERTY(int audioIndex READ audioIndex WRITE setAudioIndex NOTIFY audioIndexChanged)
Q_PROPERTY(int subtitleIndex READ subtitleIndex WRITE setSubtitleIndex NOTIFY subtitleIndexChanged)
Q_PROPERTY(QString streamUrl READ streamUrl NOTIFY streamUrlChanged)
Q_PROPERTY(bool autoOpen MEMBER m_autoOpen NOTIFY autoOpenChanged)
Q_PROPERTY(int audioIndex MEMBER m_audioIndex NOTIFY audioIndexChanged)
Q_PROPERTY(int subtitleIndex MEMBER m_subtitleIndex NOTIFY subtitleIndexChanged)
Q_PROPERTY(bool resumePlayback MEMBER m_resumePlayback NOTIFY resumePlaybackChanged)
//Q_PROPERTY(bool autoOpen MEMBER m_autoOpen NOTIFY autoOpenChanged)
/**
* Whether the player should resume playback.
*/
Q_PROPERTY(bool resumePlayback READ resumePlayback WRITE setResumePlayback NOTIFY resumePlaybackChanged)
Q_PROPERTY(Jellyfin::DTO::PlayMethodClass::Value playMethod READ playMethod NOTIFY playMethodChanged)
// Current Item and queue informatoion
@ -98,42 +102,50 @@ public:
Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged)
Q_PROPERTY(bool hasVideo READ hasVideo NOTIFY hasVideoChanged)
Q_PROPERTY(bool seekable READ seekable NOTIFY seekableChanged)
Q_PROPERTY(QObject* mediaObject READ mediaObject NOTIFY mediaObjectChanged)
Q_PROPERTY(QMediaPlayer::MediaStatus mediaStatus READ mediaStatus NOTIFY mediaStatusChanged)
Q_PROPERTY(QMediaPlayer::State playbackState READ playbackState NOTIFY playbackStateChanged)
Q_PROPERTY(QObject* mediaObject READ mediaObject NOTIFY mediaObjectChanged);
Q_PROPERTY(Jellyfin::Model::MediaStatusClass::Value mediaStatus READ mediaStatus NOTIFY mediaStatusChanged)
Q_PROPERTY(Jellyfin::Model::PlayerStateClass::Value playbackState READ playbackState NOTIFY playbackStateChanged)
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<Model::Item> dataItem() const { return m_item; }
ApiClient *apiClient() const { return m_apiClient; }
// R/W props
ApiClient *apiClient() const;
void setApiClient(ApiClient *apiClient);
bool resumePlayback() const;
void setResumePlayback(bool newResumePlayback);
int audioIndex() const;
void setAudioIndex(int newAudioIndex);
int subtitleIndex() const;
void setSubtitleIndex(int newAudioIndex);
QString streamUrl() const { return m_streamUrl; }
PlayMethod playMethod() const { return m_playMethod; }
QObject *mediaObject() const { return m_mediaPlayer; }
qint64 position() const { return m_mediaPlayer->position(); }
qint64 duration() const { return m_mediaPlayer->duration(); }
ViewModel::Playlist *queue() const { return m_displayQueue; }
int queueIndex() const { return m_queueIndex; }
bool hasNext() const { return m_queue->hasNext(); }
bool hasPrevious() const { return m_queue->hasPrevious(); }
ViewModel::Item *item() const;
QSharedPointer<Model::Item> dataItem() const;
QString streamUrl() const;
PlayMethod playMethod() const;
qint64 position() const;
qint64 duration() const;
ViewModel::Playlist *queue() const;
int queueIndex() const;
bool hasNext() const;
bool hasPrevious() const;
// Current media player related property getters
QMediaPlayer::State playbackState() const { return m_playbackState; }
QMediaPlayer::MediaStatus mediaStatus() const { return m_mediaPlayer->mediaStatus(); }
bool hasVideo() const { return m_mediaPlayer->isVideoAvailable(); }
bool seekable() const { return m_mediaPlayer->isSeekable(); }
QObject* mediaObject() const;
Model::PlayerState playbackState() const;
Model::MediaStatus mediaStatus() const;
bool hasVideo() const;
bool seekable() const;
QMediaPlayer::Error error () const;
QString errorString() const;
bool handlePlaystateCommands() const { return m_handlePlaystateCommands; }
void setHandlePlaystateCommands(bool newHandlePlaystateCommands) { m_handlePlaystateCommands = newHandlePlaystateCommands; emit handlePlaystateCommandsChanged(m_handlePlaystateCommands); }
bool handlePlaystateCommands() const;
void setHandlePlaystateCommands(bool newHandlePlaystateCommands);
signals:
void itemChanged(ViewModel::Item *newItemId);
void itemChanged();
void streamUrlChanged(const QString &newStreamUrl);
void autoOpenChanged(bool autoOpen);
void audioIndexChanged(int audioIndex);
@ -145,20 +157,21 @@ signals:
// Emitted when seek has been called.
void seeked(qint64 newPosition);
void hasNextChanged(bool newHasNext);
void hasPreviousChanged(bool newHasPrevious);
// Current media player related property signals
void mediaObjectChanged(QObject *newMediaObject);
void mediaObjectChanged(QObject *newPlayer);
void positionChanged(qint64 newPosition);
void durationChanged(qint64 newDuration);
void queueChanged(QAbstractItemModel *newQueue);
void queueIndexChanged(int newIndex);
void playbackStateChanged(QMediaPlayer::State newState);
void mediaStatusChanged(QMediaPlayer::MediaStatus newMediaStatus);
void playbackStateChanged(Jellyfin::Model::PlayerStateClass::Value newState);
void mediaStatusChanged(Jellyfin::Model::MediaStatusClass::Value newMediaStatus);
void hasVideoChanged(bool newHasVideo);
void seekableChanged(bool newSeekable);
void errorChanged(QMediaPlayer::Error newError);
void errorStringChanged(const QString &newErrorString);
void hasNextChanged(bool newHasNext);
void hasPreviousChanged(bool newHasPrevious);
void handlePlaystateCommandsChanged(bool newHandlePlaystateCommands);
public slots:
/**
@ -187,7 +200,7 @@ public slots:
*/
void skipToItemIndex(int index);
void play();
void pause() { m_mediaPlayer->pause(); setPlaybackState(QMediaPlayer::PausedState); }
void pause();
void seek(qint64 pos);
void stop();
@ -204,105 +217,21 @@ public slots:
void handlePlaystateRequest(const DTO::PlaystateRequest &request);
private slots:
void mediaPlayerStateChanged(QMediaPlayer::State newState);
void mediaPlayerPositionChanged(qint64 position);
void mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus newStatus);
void mediaPlayerError(QMediaPlayer::Error error);
void mediaPlayerDurationChanged(qint64 newDuration);
void mediaPlayerSeekableChanged(bool seekable);
/**
* @brief updatePlaybackInfo Updates the Jellyfin server with the current playback progress etc.
*/
void updatePlaybackInfo();
/// Called when we have fetched the playback URL and playSession
void onItemUrlReceived(const QString &itemId, const QUrl &url, const QString &playSession,
// Fully specify class to please MOC
Jellyfin::DTO::PlayMethodClass::Value playMethod);
/// Called when we have encountered an error
void onItemErrorReceived(const QString &itemId, const QString &errorString);
void mediaPlayerItemChanged();
private:
/// Factor to multiply with when converting from milliseconds to ticks.
const static int MS_TICK_FACTOR = 10000;
enum PlaybackInfoType { Started, Stopped, Progress };
/// Timer used to update the play progress on the Jellyfin server
QTimer m_updateTimer;
/// Timer used to notify ourselves when we need to preload the next item
QTimer m_preloadTimer;
ApiClient *m_apiClient = nullptr;
/// The currently playing item
QSharedPointer<Model::Item> m_item;
/// The item that will be played next
QSharedPointer<Model::Item> m_nextItem;
/// The currently played item that will be shown in the GUI
ViewModel::Item *m_displayItem = new ViewModel::Item(this);
/// The currently played queue that will be shown in the GUI
ViewModel::Playlist *m_displayQueue = nullptr;
// Properties for making the streaming request.
QString m_streamUrl;
QString m_nextStreamUrl;
QString m_playSessionId;
QString m_nextPlaySessionId;
QString m_errorString;
QMediaPlayer::Error m_error = QMediaPlayer::NoError;
/// The index of the mediastreams of the to-be-played item containing the audio
int m_audioIndex = 0;
/// The index of the mediastreams of the to-be-played item containing subtitles
int m_subtitleIndex = -1;
/// The position in ticks to resume playback from
qint64 m_resumePosition = 0;
/// The position in ticks the playback was stopped
qint64 m_stopPosition = 0;
/// Keeps track of latest playback position
qint64 m_oldPosition = 0;
/**
* @brief Whether to automatically open the livestream of the item;
*/
bool m_autoOpen = false;
bool m_seekToResumedPosition = false;
QMediaPlayer::State m_oldState = QMediaPlayer::StoppedState;
/// State of the playbackManager. While the internal media player stops after a
/// song has ended, this will not do so.
QMediaPlayer::State m_playbackState = QMediaPlayer::StoppedState;
PlayMethod m_playMethod = PlayMethod::Transcode;
/// Pointer to the current media player.
QMediaPlayer *m_mediaPlayer = nullptr;
Model::Playlist *m_queue = nullptr;
int m_queueIndex = 0;
bool m_resumePlayback = false;
bool m_handlePlaystateCommands = true;
// Helper methods
void setItem(QSharedPointer<Model::Item> newItem);
void setStreamUrl(const QUrl &streamUrl);
void setPlaybackState(QMediaPlayer::State newState);
/**
* @brief Posts the playback information
*/
void postPlaybackInfo(PlaybackInfoType type);
void requestItemUrl(QSharedPointer<Model::Item> item);
void handlePlaybackInfoResponse(QString itemId, QString mediaType, DTO::PlaybackInfoResponse &response);
// QQmlParserListener interface
void classBegin() override { m_qmlIsParsingComponent = true; }
void componentComplete() override;
bool m_qmlIsParsingComponent = false;
/// Time in ms at what moment this playbackmanager should start loading the next item.
const qint64 PRELOAD_DURATION = 15 * 1000;
QTimer m_forceSeekTimer;
QScopedPointer<PlaybackManagerPrivate> d_ptr;
};
} // NS ViewModel

View File

@ -30,6 +30,7 @@
#include "JellyfinQt/eventbus.h"
#include "JellyfinQt/serverdiscoverymodel.h"
#include "JellyfinQt/websocket.h"
#include "JellyfinQt/model/player.h"
#include "JellyfinQt/viewmodel/item.h"
#include "JellyfinQt/viewmodel/itemmodel.h"
#include "JellyfinQt/viewmodel/loader.h"
@ -87,6 +88,8 @@ void JellyfinPlugin::registerTypes(const char *uri) {
qmlRegisterUncreatableType<Jellyfin::DTO::ItemFieldsClass>(uri, 1, 0, "ItemFields", "Is an enum");
qmlRegisterUncreatableType<Jellyfin::DTO::ImageTypeClass>(uri, 1, 0, "ImageType", "Is an enum");
qmlRegisterUncreatableType<Jellyfin::ViewModel::NowPlayingSection>(uri, 1, 0, "NowPlayingSection", "Is an enum");
qmlRegisterUncreatableType<Jellyfin::Model::PlayerStateClass>(uri, 1, 0, "PlayerState", "Is an enum");
qmlRegisterUncreatableType<Jellyfin::Model::MediaStatusClass>(uri, 1, 0, "MediaStatus", "Is an enum");
qRegisterMetaType<Jellyfin::DTO::PlayMethodClass::Value>();
}

View File

@ -0,0 +1,700 @@
/*
* Sailfin: a Jellyfin client written using Qt
* Copyright (C) 2021-2022 Chris Josten and the Sailfin Contributors.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <JellyfinQt/model/playbackmanager.h>
#include <QTimer>
#include <JellyfinQt/dto/playbackinforesponse.h>
#include <JellyfinQt/loader/http/userlibrary.h>
#include <JellyfinQt/loader/http/mediainfo.h>
#include <JellyfinQt/model/playbackreporter.h>
#include <JellyfinQt/model/playlist.h>
#include <JellyfinQt/viewmodel/settings.h>
namespace Jellyfin {
namespace Model {
Q_LOGGING_CATEGORY(playbackManager, "jellyfinqt.model.playbackmanager");
class PlaybackManagerPrivate {
Q_DECLARE_PUBLIC(PlaybackManager);
public:
PlaybackManagerPrivate(PlaybackManager *q);
ApiClient *m_apiClient = nullptr;
/// Timer used to notify ourselves when we need to preload the next item
QTimer m_preloadTimer;
PlaybackManagerError m_error;
QString m_errorString;
QString m_playSessionId;
QString m_nextPlaySessionId;
/// The index of the mediastreams of the to-be-played item containing the audio
int m_audioIndex = 0;
/// The index of the mediastreams of the to-be-played item containing subtitles
int m_subtitleIndex = -1;
/// The currently playing item
QSharedPointer<Model::Item> m_item;
/// The item that will be played next
QSharedPointer<Model::Item> m_nextItem;
PlayerState m_state;
Model::Playlist *m_queue = nullptr;
int m_queueIndex = 0;
bool m_resumePlayback = false;
/// The position in ticks to resume playback from
qint64 m_resumePosition = 0;
bool m_handlePlaystateCommands = true;
PlaybackManager *q_ptr;
virtual void setItem(QSharedPointer<Model::Item> newItem);
void skipToItemIndex(int index);
void setState(PlayerState newState);
};
PlaybackManagerPrivate::PlaybackManagerPrivate(PlaybackManager *q)
: q_ptr(q) {
}
void PlaybackManagerPrivate::setItem(QSharedPointer<Model::Item> newItem) {
Q_Q(PlaybackManager);
this->m_item = newItem;
emit q->itemChanged();
}
void PlaybackManagerPrivate::skipToItemIndex(int index) {
Q_Q(PlaybackManager);
if (index < m_queue->queueSize()) {
// Skip until we hit the right number in the queue
index++;
while(index != 0) {
m_queue->next();
index--;
}
} else {
m_queue->play(index);
}
setItem(m_queue->currentItem());
emit q->hasNextChanged(m_queue->hasNext());
emit q->hasPreviousChanged(m_queue->hasPrevious());
}
void PlaybackManagerPrivate::setState(PlayerState newState) {
Q_Q(PlaybackManager);
m_state = newState;
emit q->playbackStateChanged(newState);
}
/*****************************************************************************
* PlaybackManager *
*****************************************************************************/
PlaybackManager::PlaybackManager(QObject *parent)
: PlaybackManager(new PlaybackManagerPrivate(this), parent) {
Q_D(PlaybackManager);
}
PlaybackManager::PlaybackManager(PlaybackManagerPrivate *d, QObject *parent)
: QObject(parent) {
QScopedPointer<PlaybackManagerPrivate> foo(d);
d_ptr.swap(foo);
d_ptr->m_queue = new Playlist(this);
}
PlaybackManager::~PlaybackManager() {}
ApiClient *PlaybackManager::apiClient() const {
const Q_D(PlaybackManager);
return d->m_apiClient;
}
void PlaybackManager::setApiClient(ApiClient *apiClient) {
Q_D(PlaybackManager);
d->m_apiClient = apiClient;
}
QSharedPointer<Item> PlaybackManager::currentItem() const {
const Q_D(PlaybackManager);
return d->m_item;
}
Playlist *PlaybackManager::queue() const {
const Q_D(PlaybackManager);
return d->m_queue;
}
int PlaybackManager::queueIndex() const {
const Q_D(PlaybackManager);
return d->m_queueIndex;
}
void PlaybackManager::playItemId(const QString &id) {}
bool PlaybackManager::resumePlayback() const {
const Q_D(PlaybackManager);
return d->m_resumePlayback;
}
void PlaybackManager::setResumePlayback(bool newResumePlayback) {
Q_D(PlaybackManager);
d->m_resumePlayback = newResumePlayback;
emit resumePlaybackChanged(newResumePlayback);
}
int PlaybackManager::audioIndex() const {
const Q_D(PlaybackManager);
return d->m_audioIndex;
}
void PlaybackManager::setAudioIndex(int newAudioIndex) {
Q_D(PlaybackManager);
d->m_audioIndex = newAudioIndex;
emit audioIndexChanged(newAudioIndex);
}
int PlaybackManager::subtitleIndex() const {
const Q_D(PlaybackManager);
return d->m_subtitleIndex;
}
void PlaybackManager::setSubtitleIndex(int newSubtitleIndex) {
Q_D(PlaybackManager);
d->m_subtitleIndex = newSubtitleIndex;
emit subtitleIndexChanged(newSubtitleIndex);
}
/*****************************************************************************
* LocalPlaybackManagerPrivate *
*****************************************************************************/
class LocalPlaybackManagerPrivate : public PlaybackManagerPrivate {
Q_DECLARE_PUBLIC(LocalPlaybackManager);
public:
explicit LocalPlaybackManagerPrivate(LocalPlaybackManager *q);
Player *m_mediaPlayer;
// Properties for making the streaming request.
QUrl m_streamUrl;
QUrl m_nextStreamUrl;
DTO::PlayMethod m_playMethod = DTO::PlayMethod::Transcode;
void setItem(QSharedPointer<Model::Item> newItem) override;
void setStreamUrl(const QUrl &streamUrl);
void requestItemUrl(QSharedPointer<Model::Item> item);
// slots
void handlePlaybackInfoResponse(QString itemId, QString mediaType, DTO::PlaybackInfoResponse &response);
/// Called when we have fetched the playback URL and playSession
void onItemUrlReceived(const QString &itemId, const QUrl &url, const QString &playSession,
// Fully specify class to please MOC
Jellyfin::DTO::PlayMethodClass::Value playMethod);
/// Called when we have encountered an error
void onItemErrorReceived(const QString &itemId, const QString &errorString);
/**
* @brief Whether to automatically open the livestream of the item;
*/
bool m_autoOpen = false;
PlaybackReporter *m_reporter = nullptr;
public slots:
void onPlayerError();
void onMediaStatusChanged(Jellyfin::Model::MediaStatusClass::Value newMediaStatus);
};
LocalPlaybackManagerPrivate::LocalPlaybackManagerPrivate(LocalPlaybackManager *q)
: PlaybackManagerPrivate(q),
m_reporter(new PlaybackReporter()){
}
void LocalPlaybackManagerPrivate::setStreamUrl(const QUrl &streamUrl) {
Q_Q(LocalPlaybackManager);
m_streamUrl = streamUrl;
Q_ASSERT_X(streamUrl.isValid() || streamUrl.isEmpty(), "setStreamUrl", "StreamURL Jellyfin returned is not valid");
emit q->streamUrlChanged(m_streamUrl);
}
void LocalPlaybackManagerPrivate::requestItemUrl(QSharedPointer<Model::Item> item) {
Q_Q(LocalPlaybackManager);
using ItemUrlLoader = Support::Loader<DTO::PlaybackInfoResponse, Jellyfin::Loader::GetPostedPlaybackInfoParams>;
ItemUrlLoader *loader = new Jellyfin::Loader::HTTP::GetPostedPlaybackInfoLoader(m_apiClient);
Jellyfin::Loader::GetPostedPlaybackInfoParams params;
// Check if we'd prefer to transcode if the video file contains multiple audio tracks
// or if a subtitle track was selected.
// This has to be done due to the lack of support of selecting audio tracks within QtMultimedia
bool allowTranscoding = m_apiClient->settings()->allowTranscoding();
bool transcodePreferred = m_subtitleIndex > 0;
int audioTracks = 0;
const QList<DTO::MediaStream> &streams = item->mediaStreams();
for(int i = 0; i < streams.size(); i++) {
const DTO::MediaStream &stream = streams[i];
if (stream.type() == MediaStreamType::Audio) {
audioTracks++;
}
}
if (audioTracks > 1) {
transcodePreferred = true;
}
bool forceTranscoding = allowTranscoding && transcodePreferred;
QSharedPointer<DTO::PlaybackInfoDto> playbackInfo = QSharedPointer<DTO::PlaybackInfoDto>::create(m_apiClient->deviceProfile());
params.setItemId(item->jellyfinId());
params.setUserId(m_apiClient->userId());
playbackInfo->setEnableDirectPlay(true);
playbackInfo->setEnableDirectStream(!forceTranscoding);
playbackInfo->setEnableTranscoding(forceTranscoding || allowTranscoding);
playbackInfo->setAudioStreamIndex(this->m_audioIndex);
playbackInfo->setSubtitleStreamIndex(this->m_subtitleIndex);
params.setBody(playbackInfo);
loader->setParameters(params);
q->connect(loader, &ItemUrlLoader::ready, q, [this, loader, item] {
DTO::PlaybackInfoResponse result = loader->result();
handlePlaybackInfoResponse(item->jellyfinId(), item->mediaType(), result);
loader->deleteLater();
});
q->connect(loader, &ItemUrlLoader::error, q, [this, loader, item](QString message) {
onItemErrorReceived(item->jellyfinId(), message);
loader->deleteLater();
});
loader->load();
}
void LocalPlaybackManagerPrivate::setItem(QSharedPointer<Model::Item> newItem) {
Q_Q(LocalPlaybackManager);
if (m_mediaPlayer != nullptr) m_mediaPlayer->stop();
bool shouldFetchStreamUrl = !newItem.isNull()
&& ((m_streamUrl.isEmpty() || (!m_item.isNull()
&& m_item->jellyfinId() != newItem->jellyfinId()))
|| (m_nextStreamUrl.isEmpty() || (!m_nextItem.isNull()
&& m_nextItem->jellyfinId() != newItem->jellyfinId())));
this->m_item = newItem;
if (!newItem.isNull()) {
if (!newItem->userData().isNull()) {
m_resumePosition = newItem->userData()->playbackPositionTicks();
}
}
emit q->itemChanged();
emit q->hasNextChanged(m_queue->hasNext());
emit q->hasPreviousChanged(m_queue->hasPrevious());
if (m_apiClient == nullptr) {
qCWarning(playbackManager) << "apiClient is not set on this playbackmanager instance! Aborting.";
return;
}
// Deinitialize the streamUrl
if (shouldFetchStreamUrl) {
qCDebug(playbackManager) << "Fetching streamUrl before playing";
setStreamUrl(QUrl());
requestItemUrl(m_item);
} else {
qCDebug(playbackManager) << "StreamUrl already fetched, playing!";
setStreamUrl(m_nextStreamUrl);
if (m_mediaPlayer != nullptr) m_mediaPlayer->play();
}
}
void LocalPlaybackManagerPrivate::handlePlaybackInfoResponse(QString itemId, QString mediaType, DTO::PlaybackInfoResponse &response) {
Q_Q(LocalPlaybackManager);
//TODO: move the item URL fetching logic out of this function, into MediaSourceInfo?
QList<DTO::MediaSourceInfo> mediaSources = response.mediaSources();
QUrl resultingUrl;
QString playSession = response.playSessionId();
PlayMethod playMethod = PlayMethod::EnumNotSet;
bool transcodingAllowed = m_apiClient->settings()->allowTranscoding();
for (int i = 0; i < mediaSources.size(); i++) {
const DTO::MediaSourceInfo &source = mediaSources.at(i);
// Check if we'd prefer to transcode if the video file contains multiple audio tracks
// or if a subtitle track was selected.
// This has to be done due to the lack of support of selecting audio tracks within QtMultimedia
bool transcodePreferred = false;
if (transcodingAllowed) {
transcodePreferred = m_subtitleIndex > 0;
int audioTracks = 0;
const QList<DTO::MediaStream> &streams = source.mediaStreams();
for (int i = 0; i < streams.size(); i++) {
DTO::MediaStream stream = streams[i];
if (stream.type() == MediaStreamType::Audio) {
audioTracks++;
}
}
if (audioTracks > 1) {
transcodePreferred = false;
}
}
qCDebug(playbackManager()) << "Media source: " << source.name() << "\n"
<< "Prefer transcoding: " << transcodePreferred << "\n"
<< "DirectPlay supported: " << source.supportsDirectPlay() << "\n"
<< "DirectStream supported: " << source.supportsDirectStream() << "\n"
<< "Transcode supported: " << source.supportsTranscoding() << source.transcodingUrl();
if (source.supportsDirectPlay() && QFile::exists(source.path())) {
resultingUrl = QUrl::fromLocalFile(source.path());
playMethod = PlayMethod::DirectPlay;
} else if (source.supportsDirectStream() && !transcodePreferred) {
if (mediaType == "Video") {
mediaType.append('s');
}
QUrlQuery query;
query.addQueryItem("mediaSourceId", source.jellyfinId());
query.addQueryItem("deviceId", m_apiClient->deviceId());
query.addQueryItem("api_key", m_apiClient->token());
query.addQueryItem("Static", "True");
resultingUrl = QUrl(m_apiClient->baseUrl() + "/" + mediaType + "/" + itemId
+ "/stream." + source.container() + "?" + query.toString(QUrl::EncodeReserved));
playMethod = PlayMethod::DirectStream;
} else if (source.supportsTranscoding() && !source.transcodingUrlNull() && transcodingAllowed) {
qCDebug(playbackManager) << "Transcoding url: " << source.transcodingUrl();
resultingUrl = QUrl(m_apiClient->baseUrl() + source.transcodingUrl());
playMethod = PlayMethod::Transcode;
} else {
qCDebug(playbackManager) << "No suitable sources for item " << itemId;
}
if (!resultingUrl.isEmpty()) break;
}
if (resultingUrl.isEmpty()) {
qCWarning(playbackManager) << "Could not find suitable media source for item " << itemId;
onItemErrorReceived(itemId, q->tr("Could not find a suitable media source."));
} else {
emit q->playMethodChanged(playMethod);
onItemUrlReceived(itemId, resultingUrl, playSession, playMethod);
}
}
void LocalPlaybackManagerPrivate::onItemUrlReceived(const QString &itemId, const QUrl &url, const QString &playSession, Jellyfin::DTO::PlayMethodClass::Value playMethod) {
Q_Q(LocalPlaybackManager);
Q_UNUSED(playSession)
qCDebug(playbackManager) << "Item URL received for item" << itemId;
if (!m_item.isNull() && m_item->jellyfinId() == itemId) {
// We want to play the item probably right now
m_playSessionId = playSession;
m_playMethod = playMethod;
m_resumePosition = m_item->userData()->playbackPositionTicks();
setStreamUrl(url);
qCDebug(playbackManager) << "Starting playback!";
emit q->playMethodChanged(m_playMethod);
// Clear the error string if it is currently set
if (!m_errorString.isEmpty()) {
m_errorString.clear();
emit q->errorStringChanged(m_errorString);
}
if (m_error != PlaybackManagerError::NoError) {
m_error = PlaybackManagerError::NoError;
emit q->errorChanged(m_error);
}
m_mediaPlayer->setMedia(url, m_audioIndex, m_subtitleIndex);
m_mediaPlayer->play(m_resumePosition);
m_resumePosition = 0;
} else {
qDebug() << "Late reply for " << itemId << " received, ignoring";
}
}
void LocalPlaybackManagerPrivate::onItemErrorReceived(const QString &itemId, const QString &errorString) {
Q_Q(LocalPlaybackManager);
qWarning() << "Error while fetching streaming url for " << itemId << ": " << errorString;
if (!m_item.isNull() && m_item->jellyfinId() == itemId) {
setStreamUrl(QUrl());
m_error = PlaybackManagerError::PlaybackInfoError;
emit q->errorChanged(PlaybackManagerError::PlaybackInfoError);
m_errorString = errorString;
emit q->errorStringChanged(errorString);
}
}
void LocalPlaybackManagerPrivate::onPlayerError() {
Q_Q(LocalPlaybackManager);
m_error = PlaybackManagerError::PlayerGeneralError;
m_errorString = m_mediaPlayer->errorString();
emit q->errorChanged(m_error);
emit q->errorStringChanged(m_errorString);
qWarning() << "Player error: " << m_errorString;
}
void LocalPlaybackManagerPrivate::onMediaStatusChanged(MediaStatus newStatus) {
Q_Q(LocalPlaybackManager);
if (m_state == PlayerState::Stopped) return;
if (newStatus == MediaStatus::Loaded) {
m_mediaPlayer->play();
} else if (newStatus == MediaStatus::EndOfMedia) {
if (m_queue->hasNext() && m_queue->totalSize() > 1) {
q->next();
} else {
// End of the playlist
setState(PlayerState::Stopped);
}
}
}
/*****************************************************************************
* LocalPlaybackManager *
*****************************************************************************/
LocalPlaybackManager::LocalPlaybackManager(QObject *parent)
: PlaybackManager(new LocalPlaybackManagerPrivate(this), parent) {
Q_D(LocalPlaybackManager);
d->m_mediaPlayer = new QtMultimediaPlayer(this);
d->m_reporter->setPlaybackManager(this);
connect(d->m_mediaPlayer, &Player::positionChanged, this, &LocalPlaybackManager::positionChanged);
connect(d->m_mediaPlayer, &Player::durationChanged, this, &LocalPlaybackManager::durationChanged);
connect(d->m_mediaPlayer, &Player::stateChanged, this, &LocalPlaybackManager::playbackStateChanged);
connect(d->m_mediaPlayer, &Player::seekableChanged, this, &LocalPlaybackManager::seekableChanged);
connect(d->m_mediaPlayer, &Player::mediaStatusChanged, this, [this, d](MediaStatus newStatus) -> void {
d->onMediaStatusChanged(newStatus);
emit mediaStatusChanged(d->m_mediaPlayer->mediaStatus());
});
connect(d->m_mediaPlayer, &Player::hasAudioChanged, this, &LocalPlaybackManager::hasAudioChanged);
connect(d->m_mediaPlayer, &Player::hasVideoChanged, this, &LocalPlaybackManager::hasVideoChanged);
connect(d->m_mediaPlayer, &Player::errorStringChanged, this, [d]() {
d->onPlayerError();
});
}
void LocalPlaybackManager::swap(PlaybackManager &other) {
Q_UNIMPLEMENTED();
}
Player* LocalPlaybackManager::player() const {
const Q_D(LocalPlaybackManager);
return d->m_mediaPlayer;
}
QString LocalPlaybackManager::sessionId() const {
const Q_D(LocalPlaybackManager);
return d->m_playSessionId;
}
DTO::PlayMethod LocalPlaybackManager::playMethod() const {
const Q_D(LocalPlaybackManager);
return d->m_playMethod;
}
const QUrl& LocalPlaybackManager::streamUrl() const {
const Q_D(LocalPlaybackManager);
return d->m_streamUrl;
}
PlayerState LocalPlaybackManager::playbackState() const {
const Q_D(LocalPlaybackManager);
return d->m_mediaPlayer->state();
}
MediaStatus LocalPlaybackManager::mediaStatus() const {
const Q_D(LocalPlaybackManager);
return d->m_mediaPlayer->mediaStatus();
}
PlaybackManagerError LocalPlaybackManager::error() const {
const Q_D(LocalPlaybackManager);
return d->m_error;
}
const QString &LocalPlaybackManager::errorString() const {
const Q_D(LocalPlaybackManager);
return d->m_errorString;
}
qint64 LocalPlaybackManager::position() const {
const Q_D(LocalPlaybackManager);
return d->m_mediaPlayer->position();
}
qint64 LocalPlaybackManager::duration() const {
const Q_D(LocalPlaybackManager);
return d->m_mediaPlayer->duration();
}
bool LocalPlaybackManager::seekable() const {
const Q_D(LocalPlaybackManager);
return d->m_mediaPlayer->seekable();
}
void LocalPlaybackManager::pause() {
Q_D(LocalPlaybackManager);
d->m_mediaPlayer->pause();
}
void LocalPlaybackManager::play() {
Q_D(LocalPlaybackManager);
if (d->m_queue->totalSize() > 0) {
d->m_mediaPlayer->play();
d->setState(PlayerState::Playing);
}
}
void LocalPlaybackManager::playItem(QSharedPointer<Model::Item> item) {
Q_D(LocalPlaybackManager);
d->m_queue->clearList();
d->m_queue->appendToList(item);
d->m_queue->play();
d->m_queueIndex = 0;
d->setItem(item);
emit hasNextChanged(d->m_queue->hasNext());
emit hasPreviousChanged(d->m_queue->hasPrevious());
d->setState(PlayerState::Playing);
}
void LocalPlaybackManager::playItemId(const QString &itemId) {
Q_D(PlaybackManager);
Jellyfin::Loader::HTTP::GetItemLoader *loader = new Jellyfin::Loader::HTTP::GetItemLoader(d->m_apiClient);
connect(loader, &Support::LoaderBase::error, this, [loader]() {
// TODO: error handling
loader->deleteLater();
});
connect(loader, &Support::LoaderBase::ready, this, [this, loader](){
this->playItem(QSharedPointer<Model::Item>::create(loader->result()));
loader->deleteLater();
});
Jellyfin::Loader::GetItemParams params;
params.setUserId(d->m_apiClient->userId());
params.setItemId(itemId);
loader->setParameters(params);
loader->load();
}
void LocalPlaybackManager::playItemInList(const QList<QSharedPointer<Model::Item>> &items, int index) {
Q_D(LocalPlaybackManager);
d->m_queue->clearList();
d->m_queue->appendToList(items);
d->m_queue->play(index);
d->m_queueIndex = index;
emit queueIndexChanged(d->m_queueIndex);
d->setItem(items.at(index));
emit hasNextChanged(d->m_queue->hasNext());
emit hasPreviousChanged(d->m_queue->hasPrevious());
d->setState(PlayerState::Playing);
}
void LocalPlaybackManager::goTo(int index) {
Q_D(LocalPlaybackManager);
d->m_queue->play(index);
d->m_queueIndex = index;
emit queueIndexChanged(index);
d->setItem(d->m_queue->currentItem());
emit hasNextChanged(d->m_queue->hasNext());
emit hasPreviousChanged(d->m_queue->hasPrevious());
d->setState(PlayerState::Playing);
}
bool LocalPlaybackManager::hasNext() const {
const Q_D(LocalPlaybackManager);
return d->m_queue->hasNext();
}
bool LocalPlaybackManager::hasPrevious() const {
const Q_D(LocalPlaybackManager);
return d->m_queue->hasPrevious();
}
void LocalPlaybackManager::next() {
Q_D(LocalPlaybackManager);
d->m_mediaPlayer->stop();
d->m_mediaPlayer->setMedia(QUrl());
if (d->m_nextItem.isNull() || !d->m_queue->nextItem()->sameAs(*d->m_nextItem)) {
d->setItem(d->m_queue->nextItem());
d->m_nextStreamUrl = QString();
d->m_queue->next();
d->m_nextItem.clear();
} else {
d->m_item = d->m_nextItem;
d->m_streamUrl = d->m_nextStreamUrl;
d->m_nextItem.clear();
d->m_nextStreamUrl = QString();
d->m_queue->next();
d->setItem(d->m_nextItem);
}
emit hasNextChanged(d->m_queue->hasNext());
emit hasPreviousChanged(d->m_queue->hasPrevious());
}
void LocalPlaybackManager::previous() {
Q_D(LocalPlaybackManager);
d->m_mediaPlayer->stop();
d->m_mediaPlayer->seek(0);
d->m_item.clear();
d->m_streamUrl = QString();
d->m_nextStreamUrl = d->m_streamUrl;
d->m_nextItem = d->m_queue->nextItem();
d->m_queue->previous();
d->setItem(d->m_queue->currentItem());
emit hasNextChanged(d->m_queue->hasNext());
emit hasPreviousChanged(d->m_queue->hasPrevious());
}
void LocalPlaybackManager::stop() {
Q_D(LocalPlaybackManager);
d->m_queue->clearList();
d->m_mediaPlayer->stop();
d->setState(PlayerState::Stopped);
}
void LocalPlaybackManager::seek(qint64 newPosition) {
Q_D(LocalPlaybackManager);
d->m_mediaPlayer->seek(newPosition);
}
bool LocalPlaybackManager::hasAudio() const {
const Q_D(LocalPlaybackManager);
return d->m_mediaPlayer->hasAudio();
}
bool LocalPlaybackManager::hasVideo() const {
const Q_D(LocalPlaybackManager);
return d->m_mediaPlayer->hasVideo();
}
} // NS Model
} // NS Jellyfin

View File

@ -0,0 +1,200 @@
/*
* Sailfin: a Jellyfin client written using Qt
* Copyright (C) 2021-2022 Chris Josten and the Sailfin Contributors.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <JellyfinQt/model/playbackreporter.h>
#include <optional>
#include <QDebug>
#include <JellyfinQt/apiclient.h>
#include <JellyfinQt/dto/playbackprogressinfo.h>
#include <JellyfinQt/model/item.h>
#include <JellyfinQt/model/playlist.h>
#include <JellyfinQt/model/playbackmanager.h>
#include <JellyfinQt/model/player.h>
namespace Jellyfin {
namespace Model {
Q_LOGGING_CATEGORY(playbackReporter, "jellyfin.model.playbackReporter");
class PlaybackReporterPrivate : public QObject {
Q_DECLARE_PUBLIC(PlaybackReporter);
public:
explicit PlaybackReporterPrivate(PlaybackReporter *parent)
: QObject(parent) {
q_ptr = parent;
}
PlaybackReporter *q_ptr;
LocalPlaybackManager *m_playbackManager = nullptr;
/// Timer used to update the play progress on the Jellyfin server
QTimer m_updateTimer;
enum PlaybackInfoType { Started, Stopped, Progress };
qint64 m_oldPosition = 0;
quint64 m_stopPosition = 0;
PlayerState m_oldState;
static const int MS_TICK_FACTOR = 10000;
public slots:
void positionChanged(qint64 position);
void playerStateChanged(Jellyfin::Model::PlayerStateClass::Value newState);
/**
* @brief updatePlaybackInfo Updates the Jellyfin server with the current playback progress etc.
*/
void updatePlaybackInfo();
void postPlaybackInfo(Jellyfin::Model::PlaybackReporterPrivate::PlaybackInfoType type);
};
// PlaybackReporter
PlaybackReporter::PlaybackReporter(QObject *parent)
: QObject(parent),
d_ptr(new PlaybackReporterPrivate(this)){
Q_D(PlaybackReporter);
d->m_updateTimer.setInterval(10000); // 10 seconds
d->m_updateTimer.setSingleShot(false);
connect(&d->m_updateTimer, &QTimer::timeout, d, &PlaybackReporterPrivate::updatePlaybackInfo);
}
void PlaybackReporter::setPlaybackManager(LocalPlaybackManager *playbackManager) {
Q_D(PlaybackReporter);
if (d->m_playbackManager != nullptr) {
// Disconnect
disconnect(d->m_playbackManager, &PlaybackManager::playbackStateChanged, d, &PlaybackReporterPrivate::playerStateChanged);
disconnect(d->m_playbackManager->player(), &Player::seeked, d, &PlaybackReporterPrivate::updatePlaybackInfo);
disconnect(d->m_playbackManager->player(), &Player::positionChanged, d, &PlaybackReporterPrivate::positionChanged);
}
d->m_playbackManager = playbackManager;
if (d->m_playbackManager != nullptr) {
this->setParent(d->m_playbackManager);
connect(d->m_playbackManager, &PlaybackManager::playbackStateChanged, d, &PlaybackReporterPrivate::playerStateChanged);
connect(d->m_playbackManager->player(), &Player::seeked, d, &PlaybackReporterPrivate::updatePlaybackInfo);
connect(d->m_playbackManager->player(), &Player::positionChanged, d, &PlaybackReporterPrivate::positionChanged);
} else {
this->setParent(nullptr);
}
}
// Private
void PlaybackReporterPrivate::positionChanged(qint64 newPosition) {
if (newPosition == 0 && m_oldPosition != 0) {
// 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
// want the old position.
m_stopPosition = m_oldPosition;
}
m_oldPosition = newPosition;
}
void PlaybackReporterPrivate::playerStateChanged(PlayerState newState) {
if (m_oldState == newState) return;
if (m_oldState == PlayerState::Stopped) {
// We're transitioning from stopped to either playing or paused.
// Set up the recurring timer
m_updateTimer.start();
postPlaybackInfo(Started);
} else if (newState == PlayerState::Stopped) {
// We've stopped playing the media. Post a stop signal.
m_updateTimer.stop();
postPlaybackInfo(Stopped);
} else {
postPlaybackInfo(Progress);
}
m_oldState = newState;
}
void PlaybackReporterPrivate::updatePlaybackInfo() {
postPlaybackInfo(Progress);
}
void PlaybackReporterPrivate::postPlaybackInfo(PlaybackInfoType type) {
if (m_playbackManager == nullptr) {
qCWarning(playbackReporter) << "PlaybackManager not set. Not posting playback info";
return;
} else if (m_playbackManager->apiClient() == nullptr) {
qCWarning(playbackReporter) << "Set PlaybackManager does not have a apiClient set. Not posting playback info";
return;
} else if (m_playbackManager->currentItem().isNull()) {
qCWarning(playbackReporter) << "Item is null. Not posting playback info";
return;
}
DTO::PlaybackProgressInfo progress(
m_playbackManager->player()->seekable(),
m_playbackManager->currentItem(),
m_playbackManager->currentItem()->jellyfinId(),
m_playbackManager->player()->state() == PlayerState::Paused,
false, // is muted?
m_playbackManager->playMethod(),
DTO::RepeatMode::RepeatNone);
progress.setSessionId(m_playbackManager->sessionId());
switch(type) {
case Started: // FALLTHROUGH
case Progress: {
progress.setAudioStreamIndex(m_playbackManager->audioIndex());
progress.setSubtitleStreamIndex(m_playbackManager->subtitleIndex());
progress.setPositionTicks(m_playbackManager->player()->position() * MS_TICK_FACTOR);
Playlist *playlist = m_playbackManager->queue();
QList<DTO::QueueItem> queue;
for (int i = 0; i < playlist->listSize(); i++) {
DTO::QueueItem queueItem(playlist->listAt(i)->jellyfinId());
queue.append(queueItem);
}
progress.setNowPlayingQueue(queue);
break;
}
case Stopped:
progress.setPositionTicks(m_stopPosition * MS_TICK_FACTOR);
break;
}
QString path;
switch (type) {
case Started:
path = "/Sessions/Playing";
break;
case Progress:
path = "/Sessions/Playing/Progress";
break;
case Stopped:
path = "/Sessions/Playing/Stopped";
break;
}
// client is never null, checked at the start of this function.
ApiClient *client = m_playbackManager->apiClient();
QNetworkReply *rep = client->post(path, QJsonDocument(progress.toJson()));
connect(rep, &QNetworkReply::finished, this, [rep](){
rep->deleteLater();
});
client->setDefaultErrorHandler(rep);
}
} // NS Model
} // NS Jellyfin

236
core/src/model/player.cpp Normal file
View File

@ -0,0 +1,236 @@
/*
* Sailfin: a Jellyfin client written using Qt
* Copyright (C) 2021-2022 Chris Josten and the Sailfin Contributors.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <JellyfinQt/model/player.h>
#ifdef USE_QTMULTIMEDIA_PLAYER
#include <QtMultimedia>
#include <QtMultimedia/QMediaStreamsControl>
#include <QTimer>
#endif // USE_QTMULTIMEDIA_PLAYER
namespace Jellyfin {
namespace Model {
Q_LOGGING_CATEGORY(player, "jellyfin.model.player");
Player::~Player() {}
#ifdef USE_QTMULTIMEDIA_PLAYER
class QtMultimediaPlayerPrivate {
Q_DECLARE_PUBLIC(QtMultimediaPlayer);
public:
explicit QtMultimediaPlayerPrivate(QtMultimediaPlayer *q);
QMediaPlayer *m_mediaPlayer;
QMediaStreamsControl *m_mediaStreamsControl;
QTimer m_forceSeekTimer;
bool m_seekToResumePosition = false;
qint64 m_resumePosition;
int m_audioIndex = -1;
int m_subtitleIndex = -1;
static const qint64 MS_TICK_FACTOR = 10000;
protected:
QtMultimediaPlayer *q_ptr;
};
QtMultimediaPlayerPrivate::QtMultimediaPlayerPrivate(QtMultimediaPlayer *q)
: m_mediaPlayer(new QMediaPlayer(q, QMediaPlayer::VideoSurface)),
q_ptr(q) {
m_mediaStreamsControl = m_mediaPlayer->service()->requestControl<QMediaStreamsControl *>();
// Yes, this is a very ugly way of forcing the video player to seek to the resume position
q->connect(&m_forceSeekTimer, &QTimer::timeout, q, [this]() {
if (m_seekToResumePosition && m_mediaPlayer->isSeekable()) {
qCDebug(player) << "Trying to seek to the resume position" << (m_resumePosition / MS_TICK_FACTOR);
if (m_mediaPlayer->position() > m_resumePosition / MS_TICK_FACTOR - 500) {
m_seekToResumePosition = false;
m_forceSeekTimer.stop();
} else {
m_mediaPlayer->setPosition(m_resumePosition / MS_TICK_FACTOR);
}
}
});
q->connect(m_mediaPlayer, &QMediaPlayer::seekableChanged, q, [this](bool newSeekable) {
if (newSeekable && m_seekToResumePosition) {
m_forceSeekTimer.start();
}
});
m_forceSeekTimer.setInterval(500);
m_forceSeekTimer.setSingleShot(false);
// Connect other properties
q->connect(m_mediaPlayer, &QMediaPlayer::stateChanged, q, [q](QMediaPlayer::State /*newState*/){
emit q->stateChanged(q->state());
});
q->connect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, q, [q, this](QMediaPlayer::MediaStatus newMediaStatus) {
emit q->mediaStatusChanged(q->mediaStatus());
});
q->connect(m_mediaPlayer, &QMediaPlayer::positionChanged, q, &QtMultimediaPlayer::positionChanged);
q->connect(m_mediaPlayer, &QMediaPlayer::durationChanged, q, &QtMultimediaPlayer::durationChanged);
q->connect(m_mediaPlayer, &QMediaPlayer::seekableChanged, q, &QtMultimediaPlayer::seekableChanged);
q->connect(m_mediaPlayer, &QMediaPlayer::audioAvailableChanged, q, &QtMultimediaPlayer::hasAudioChanged);
q->connect(m_mediaPlayer, &QMediaPlayer::videoAvailableChanged, q, &QtMultimediaPlayer::hasVideoChanged);
q->connect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::Error)), q, SLOT(errorStringChanged));
if (m_mediaStreamsControl != nullptr) {
q->connect(m_mediaStreamsControl, &QMediaStreamsControl::streamsChanged, q, [this](){
qCDebug(player) << m_mediaStreamsControl->streamCount() << " streams in the medi source";
if (m_audioIndex >= 0) {
m_mediaStreamsControl->setActive(m_audioIndex, true);
}
if (m_subtitleIndex >= 0) {
m_mediaStreamsControl->setActive(m_subtitleIndex, true);
}
});
}
}
QtMultimediaPlayer::QtMultimediaPlayer(QObject *parent)
: d_ptr(new QtMultimediaPlayerPrivate(this)){
}
QtMultimediaPlayer::~QtMultimediaPlayer() {}
PlayerState QtMultimediaPlayer::state() const {
const Q_D(QtMultimediaPlayer);
switch(d->m_mediaPlayer->state()) {
case QMediaPlayer::StoppedState:
return PlayerState::Stopped;
case QMediaPlayer::PlayingState:
return PlayerState::Playing;
case QMediaPlayer::PausedState:
return PlayerState::Paused;
default:
Q_ASSERT_X(false, "QtMultimediaPlayer::state()", "Invalid switch case");
return PlayerState::Stopped;
}
}
MediaStatus QtMultimediaPlayer::mediaStatus() const {
const Q_D(QtMultimediaPlayer);
switch(d->m_mediaPlayer->mediaStatus()) {
case QMediaPlayer::UnknownMediaStatus:
return MediaStatus::Error;
case QMediaPlayer::NoMedia:
return MediaStatus::NoMedia;
case QMediaPlayer::LoadingMedia:
return MediaStatus::Loading;
case QMediaPlayer::LoadedMedia:
return MediaStatus::Loaded;
case QMediaPlayer::StalledMedia:
return MediaStatus::Stalled;
case QMediaPlayer::BufferingMedia:
return MediaStatus::Buffering;
case QMediaPlayer::BufferedMedia:
return MediaStatus::Buffered;
case QMediaPlayer::EndOfMedia:
return MediaStatus::EndOfMedia;
case QMediaPlayer::InvalidMedia:
default:
return MediaStatus::Error;
}
}
qint64 QtMultimediaPlayer::position() const {
const Q_D(QtMultimediaPlayer);
return d->m_mediaPlayer->position();
}
qint64 QtMultimediaPlayer::duration() const {
const Q_D(QtMultimediaPlayer);
return d->m_mediaPlayer->duration();
}
bool QtMultimediaPlayer::seekable() const {
const Q_D(QtMultimediaPlayer);
return d->m_mediaPlayer->isSeekable();
}
bool QtMultimediaPlayer::hasAudio() const {
const Q_D(QtMultimediaPlayer);
return d->m_mediaPlayer->isAudioAvailable();
}
bool QtMultimediaPlayer::hasVideo() const {
const Q_D(QtMultimediaPlayer);
return d->m_mediaPlayer->isVideoAvailable();
}
QString QtMultimediaPlayer::errorString() const {
const Q_D(QtMultimediaPlayer);
return d->m_mediaPlayer->errorString();
}
void QtMultimediaPlayer::pause() {
Q_D(QtMultimediaPlayer);
d->m_mediaPlayer->pause();
}
void QtMultimediaPlayer::play(qint64 startPosition) {
Q_D(QtMultimediaPlayer);
qCDebug(player) << "Play from position " << startPosition;
d->m_mediaPlayer->play();
d->m_resumePosition = startPosition;
if (startPosition > 0) {
d->m_seekToResumePosition = true;
}
}
void QtMultimediaPlayer::stop() {
Q_D(QtMultimediaPlayer);
d->m_mediaPlayer->stop();
}
void QtMultimediaPlayer::seek(qint64 pos) {
Q_D(QtMultimediaPlayer);
d->m_mediaPlayer->setPosition(pos);
}
void QtMultimediaPlayer::setMedia(const QUrl &url, int audioIndex, int subtitleIndex) {
Q_D(QtMultimediaPlayer);
qCDebug(player) << "Media set to " << url;
if (url.isEmpty()) {
d->m_mediaPlayer->setMedia(QMediaContent());
} else {
d->m_mediaPlayer->setMedia(QMediaContent(url));
}
d->m_audioIndex = audioIndex;
d->m_subtitleIndex = subtitleIndex;
if (d->m_mediaStreamsControl != nullptr) {
qCDebug(player) << "Total stream count: " << d->m_mediaStreamsControl->streamCount();
if (audioIndex >= 0) {
d->m_mediaStreamsControl->setActive(audioIndex, true);
}
if (subtitleIndex >= 0) {
d->m_mediaStreamsControl->setActive(subtitleIndex, true);
}
}
}
QObject *QtMultimediaPlayer::videoOutputSource() const {
const Q_D(QtMultimediaPlayer);
return d->m_mediaPlayer;
}
#endif // USE_QTMULTIMEDIA_PLAYER
} // NS Model
} // NS Jellfyin

View File

@ -1,6 +1,6 @@
/*
* Sailfin: a Jellyfin client written using Qt
* Copyright (C) 2021 Chris Josten and the Sailfin Contributors.
* Copyright (C) 2021-2022 Chris Josten and the Sailfin Contributors.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -132,13 +132,13 @@ QSharedPointer<Item> Playlist::nextItem() {
return m_nextItem;
}
void Playlist::appendToList(ViewModel::ItemModel &model) {
void Playlist::appendToList(const QList<QSharedPointer<Model::Item>> &items) {
int start = m_list.size();
int count = model.size();
int count = items.size();
m_list.reserve(count);
emit beforeItemsAddedToList(start, count);
for (int i = 0; i < count; i++) {
m_list.append(QSharedPointer<Model::Item>(model.at(i)));
m_list.append(QSharedPointer<Model::Item>(items.at(i)));
}
emit itemsAddedToList();
reshuffle();

View File

@ -165,12 +165,13 @@ QString PlayerAdaptor::playbackStatus() const
if (m_mediaControl == nullptr || m_mediaControl->playbackManager() == nullptr) {
return "Stopped";
}
using PlayerState = Jellyfin::Model::PlayerState;
switch(m_mediaControl->playbackManager()->playbackState()) {
case QMediaPlayer::StoppedState:
case PlayerState::Stopped:
return "Stopped";
case QMediaPlayer::PlayingState:
case PlayerState::Playing:
return "Playing";
case QMediaPlayer::PausedState:
case PlayerState::Paused:
return "Paused";
default:
return "Stopped";
@ -246,7 +247,8 @@ void PlayerAdaptor::Play()
void PlayerAdaptor::PlayPause()
{
// handle method call org.mpris.MediaPlayer2.Player.PlayPause
if (m_mediaControl->playbackManager()->playbackState() == QMediaPlayer::PlayingState) {
using PlayerState = Jellyfin::Model::PlayerState;
if (m_mediaControl->playbackManager()->playbackState() == PlayerState::Playing) {
m_mediaControl->playbackManager()->pause();
} else {
m_mediaControl->playbackManager()->play();
@ -290,14 +292,12 @@ void PlayerAdaptor::notifyPropertiesChanged(QStringList properties) {
QDBusConnection::sessionBus().send(signal);
}
void PlayerAdaptor::onCurrentItemChanged(ViewModel::Item *item) {
Q_UNUSED(item)
void PlayerAdaptor::onCurrentItemChanged() {
QStringList properties;
properties << "Metadata" << "Position" << "CanPlay" << "CanPause" << "CanGoNext" << "CanGoPrevious";
notifyPropertiesChanged(properties);
}
void PlayerAdaptor::onPlaybackStateChanged(QMediaPlayer::State state) {
void PlayerAdaptor::onPlaybackStateChanged(Jellyfin::Model::PlayerStateClass::Value state) {
Q_UNUSED(state)
QStringList properties;
properties << "PlaybackStatus" << "Position";
@ -305,7 +305,7 @@ void PlayerAdaptor::onPlaybackStateChanged(QMediaPlayer::State state) {
}
void PlayerAdaptor::onMediaStatusChanged(QMediaPlayer::MediaStatus status) {
void PlayerAdaptor::onMediaStatusChanged(Jellyfin::Model::MediaStatusClass::Value status) {
Q_UNUSED(status)
QStringList properties;
properties << "PlaybackStatus" << "Position";

View File

@ -1,6 +1,6 @@
/*
* Sailfin: a Jellyfin client written using Qt
* Copyright (C) 2021 Chris Josten and the Sailfin Contributors.
* Copyright (C) 2021-2022 Chris Josten and the Sailfin Contributors.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -20,13 +20,12 @@
#include "JellyfinQt/viewmodel/playbackmanager.h"
#include "JellyfinQt/apimodel.h"
#include "JellyfinQt/loader/http/mediainfo.h"
#include <JellyfinQt/dto/playstatecommand.h>
#include <JellyfinQt/dto/playstaterequest.h>
// #include "JellyfinQt/DTO/dto.h"
#include <JellyfinQt/loader/http/userlibrary.h>
#include <JellyfinQt/dto/useritemdatadto.h>
#include <JellyfinQt/model/playbackmanager.h>
#include <JellyfinQt/viewmodel/settings.h>
#include <utility>
@ -41,348 +40,288 @@ namespace ViewModel {
Q_LOGGING_CATEGORY(playbackManager, "jellyfin.viewmodel.playbackmanager")
class PlaybackManagerPrivate {
Q_DECLARE_PUBLIC(PlaybackManager);
public:
explicit PlaybackManagerPrivate(PlaybackManager *q);
PlaybackManager *q_ptr = nullptr;
ApiClient *m_apiClient = nullptr;
Model::PlaybackManager *m_impl = nullptr;
/// The currently played item that will be shown in the GUI
ViewModel::Item *m_displayItem = nullptr;
/// The currently played queue that will be shown in the GUI
ViewModel::Playlist *m_displayQueue = nullptr;
bool m_handlePlaystateCommands;
};
PlaybackManagerPrivate::PlaybackManagerPrivate(PlaybackManager *q)
: q_ptr(q),
m_impl(new Model::LocalPlaybackManager(q)),
m_displayItem(new ViewModel::Item(q)),
m_displayQueue(new ViewModel::Playlist(m_impl->queue())) {
}
// PlaybackManager
PlaybackManager::PlaybackManager(QObject *parent)
: QObject(parent),
m_item(nullptr),
m_mediaPlayer(new QMediaPlayer(this)),
m_queue(new Model::Playlist(this)) {
: QObject(parent) {
QScopedPointer<PlaybackManagerPrivate> foo(new PlaybackManagerPrivate(this));
d_ptr.swap(foo);
m_displayQueue = new ViewModel::Playlist(m_queue, this);
Q_D(PlaybackManager);
// Set up connections.
m_updateTimer.setInterval(10000); // 10 seconds
m_updateTimer.setSingleShot(false);
connect(d->m_impl, &Model::PlaybackManager::positionChanged, this, &PlaybackManager::positionChanged);
connect(d->m_impl, &Model::PlaybackManager::durationChanged, this, &PlaybackManager::durationChanged);
connect(d->m_impl, &Model::PlaybackManager::hasNextChanged, this, &PlaybackManager::hasNextChanged);
connect(d->m_impl, &Model::PlaybackManager::hasPreviousChanged, this, &PlaybackManager::hasPreviousChanged);
connect(d->m_impl, &Model::PlaybackManager::seekableChanged, this, &PlaybackManager::seekableChanged);
connect(d->m_impl, &Model::PlaybackManager::queueIndexChanged, this, &PlaybackManager::queueIndexChanged);
connect(d->m_impl, &Model::PlaybackManager::itemChanged, this, &PlaybackManager::mediaPlayerItemChanged);
connect(d->m_impl, &Model::PlaybackManager::playbackStateChanged, this, &PlaybackManager::playbackStateChanged);
if (auto localImp = qobject_cast<Model::LocalPlaybackManager*>(d->m_impl)) {
connect(localImp, &Model::LocalPlaybackManager::streamUrlChanged, this, [this](const QUrl& newUrl){
this->streamUrlChanged(newUrl.toString());
});
connect(localImp, &Model::LocalPlaybackManager::playMethodChanged, this, &PlaybackManager::playMethodChanged);
}
connect(d->m_impl, &Model::PlaybackManager::mediaStatusChanged, this, &PlaybackManager::mediaStatusChanged);
}
m_preloadTimer.setSingleShot(true);
connect(&m_updateTimer, &QTimer::timeout, this, &PlaybackManager::updatePlaybackInfo);
connect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged);
connect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, &PlaybackManager::mediaPlayerDurationChanged);
connect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged);
connect(m_mediaPlayer, &QMediaPlayer::videoAvailableChanged, this, &PlaybackManager::hasVideoChanged);
connect(m_mediaPlayer, &QMediaPlayer::seekableChanged, this, &PlaybackManager::mediaPlayerSeekableChanged);
// I do not like the complicated overload cast
connect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::Error)), this, SLOT(mediaPlayerError(QMediaPlayer::Error)));
m_forceSeekTimer.setInterval(500);
m_forceSeekTimer.setSingleShot(false);
// Yes, this is a very ugly way of forcing the video player to seek to the resume position
connect(&m_forceSeekTimer, &QTimer::timeout, this, [this]() {
if (m_seekToResumedPosition && m_mediaPlayer->isSeekable()) {
qCDebug(playbackManager) << "Trying to seek to the resume position" << (m_resumePosition / MS_TICK_FACTOR);
if (m_mediaPlayer->position() > m_resumePosition / MS_TICK_FACTOR - 500) {
m_seekToResumedPosition = false;
m_forceSeekTimer.stop();
} else {
m_mediaPlayer->setPosition(m_resumePosition / MS_TICK_FACTOR);
}
}
});
PlaybackManager::~PlaybackManager() {
}
void PlaybackManager::setApiClient(ApiClient *apiClient) {
if (m_apiClient != nullptr) {
disconnect(m_apiClient->eventbus(), &EventBus::playstateCommandReceived, this, &PlaybackManager::handlePlaystateRequest);
Q_D(PlaybackManager);
if (d->m_apiClient != nullptr) {
disconnect(d->m_apiClient->eventbus(), &EventBus::playstateCommandReceived, this, &PlaybackManager::handlePlaystateRequest);
}
if (!m_item.isNull()) {
m_item->setApiClient(apiClient);
if (!d->m_displayItem->data().isNull()) {
d->m_displayItem->data()->setApiClient(apiClient);
}
m_apiClient = apiClient;
d->m_apiClient = apiClient;
d->m_impl->setApiClient(apiClient);
if (m_apiClient != nullptr) {
connect(m_apiClient->eventbus(), &EventBus::playstateCommandReceived, this, &PlaybackManager::handlePlaystateRequest);
if (d->m_apiClient != nullptr) {
connect(d->m_apiClient->eventbus(), &EventBus::playstateCommandReceived, this, &PlaybackManager::handlePlaystateRequest);
}
}
void PlaybackManager::setItem(QSharedPointer<Model::Item> newItem) {
if (m_mediaPlayer != nullptr) m_mediaPlayer->stop();
bool shouldFetchStreamUrl = !newItem.isNull()
&& ((m_streamUrl.isEmpty() || (!m_item.isNull()
&& m_item->jellyfinId() != newItem->jellyfinId()))
|| (m_nextStreamUrl.isEmpty() || (!m_nextItem.isNull()
&& m_nextItem->jellyfinId() != newItem->jellyfinId())));
bool PlaybackManager::resumePlayback() const {
const Q_D(PlaybackManager);
return d->m_impl->resumePlayback();
}
this->m_item = newItem;
void PlaybackManager::setResumePlayback(bool newResumePlayback) {
Q_D(PlaybackManager);
return d->m_impl->setResumePlayback(newResumePlayback);
}
if (newItem.isNull()) {
m_displayItem->setData(QSharedPointer<Model::Item>::create());
int PlaybackManager::audioIndex() const {
const Q_D(PlaybackManager);
return d->m_impl->audioIndex();
}
void PlaybackManager::setAudioIndex(int newAudioIndex){
Q_D(PlaybackManager);
d->m_impl->setAudioIndex(newAudioIndex);
}
int PlaybackManager::subtitleIndex() const {
const Q_D(PlaybackManager);
return d->m_impl->subtitleIndex();
}
void PlaybackManager::setSubtitleIndex(int newSubtitleIndex){
Q_D(PlaybackManager);
d->m_impl->setSubtitleIndex(newSubtitleIndex);
}
ViewModel::Item *PlaybackManager::item() const {
const Q_D(PlaybackManager);
return d->m_displayItem;
}
QSharedPointer<Model::Item> PlaybackManager::dataItem() const {
const Q_D(PlaybackManager);
return d->m_displayItem->data();
}
ApiClient * PlaybackManager::apiClient() const {
const Q_D(PlaybackManager);
return d->m_apiClient;
}
QString PlaybackManager::streamUrl() const {
const Q_D(PlaybackManager);
if (Model::LocalPlaybackManager *lpm = qobject_cast<Model::LocalPlaybackManager *>(d->m_impl)) {
return lpm->streamUrl().toString();
} else {
m_displayItem->setData(newItem);
if (!newItem->userData().isNull()) {
this->m_resumePosition = newItem->userData()->playbackPositionTicks();
}
return QStringLiteral("<not playing back locally>");
}
emit itemChanged(m_displayItem);
}
emit hasNextChanged(m_queue->hasNext());
emit hasPreviousChanged(m_queue->hasPrevious());
this->m_seekToResumedPosition = m_resumePlayback;
if (m_apiClient == nullptr) {
qCWarning(playbackManager) << "apiClient is not set on this MediaSource instance! Aborting.";
return;
}
// Deinitialize the streamUrl
if (shouldFetchStreamUrl) {
setStreamUrl(QUrl());
requestItemUrl(m_item);
PlayMethod PlaybackManager::playMethod() const {
const Q_D(PlaybackManager);
if (Model::LocalPlaybackManager *lpm = qobject_cast<Model::LocalPlaybackManager *>(d->m_impl)) {
return lpm->playMethod();
} else {
setStreamUrl(m_nextStreamUrl);
m_mediaPlayer->play();
return PlayMethod::EnumNotSet;
}
}
Model::MediaStatus PlaybackManager::mediaStatus() const {
const Q_D(PlaybackManager);
return d->m_impl->mediaStatus();
}
qint64 PlaybackManager::position() const {
const Q_D(PlaybackManager);
return d->m_impl->position();
}
qint64 PlaybackManager::duration() const {
const Q_D(PlaybackManager);
return d->m_impl->duration();
}
ViewModel::Playlist *PlaybackManager::queue() const {
const Q_D(PlaybackManager);
return d->m_displayQueue;
}
int PlaybackManager::queueIndex() const {
const Q_D(PlaybackManager);
return d->m_impl->queueIndex();
}
bool PlaybackManager::hasNext() const {
const Q_D(PlaybackManager);
return d->m_impl->hasNext();
}
bool PlaybackManager::hasPrevious() const {
const Q_D(PlaybackManager);
return d->m_impl->hasPrevious();
}
QObject* PlaybackManager::mediaObject() const {
const Q_D(PlaybackManager);
if (auto localPb = qobject_cast<Model::LocalPlaybackManager*>(d->m_impl)) {
return localPb->player()->videoOutputSource();
} else {
return nullptr;
}
}
Model::PlayerState PlaybackManager::playbackState() const {
const Q_D(PlaybackManager);
return d->m_impl->playbackState();
}
bool PlaybackManager::hasVideo() const {
const Q_D(PlaybackManager);
return d->m_impl->hasVideo();
}
bool PlaybackManager::seekable() const {
const Q_D(PlaybackManager);
return d->m_impl->seekable();
}
bool PlaybackManager::handlePlaystateCommands() const {
const Q_D(PlaybackManager);
return d->m_handlePlaystateCommands;
}
void PlaybackManager::setHandlePlaystateCommands(bool newHandlePlaystateCommands) {
Q_D(PlaybackManager);
d->m_handlePlaystateCommands = newHandlePlaystateCommands;
emit handlePlaystateCommandsChanged(newHandlePlaystateCommands);
}
QMediaPlayer::Error PlaybackManager::error() const {
if (m_error != QMediaPlayer::NoError) {
return m_error;
} else {
return m_mediaPlayer->error();
}
return QMediaPlayer::NoError;
}
QString PlaybackManager::errorString() const {
if (!m_errorString.isEmpty()) {
return m_errorString;
} else {
return m_mediaPlayer->errorString();
}
const Q_D(PlaybackManager);
return d->m_impl->errorString();
}
void PlaybackManager::setStreamUrl(const QUrl &streamUrl) {
m_streamUrl = streamUrl.toString();
// Inspired by PHP naming schemes
Q_ASSERT_X(streamUrl.isValid() || streamUrl.isEmpty(), "setStreamUrl", "StreamURL Jellyfin returned is not valid");
emit streamUrlChanged(m_streamUrl);
}
void PlaybackManager::setPlaybackState(QMediaPlayer::State newState) {
if (newState != m_playbackState) {
m_playbackState = newState;
emit playbackStateChanged(newState);
}
}
void PlaybackManager::mediaPlayerPositionChanged(qint64 position) {
emit positionChanged(position);
if (position == 0 && m_oldPosition != 0) {
// 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
// want the old position.
m_stopPosition = m_oldPosition;
}
m_oldPosition = position;
}
void PlaybackManager::mediaPlayerStateChanged(QMediaPlayer::State newState) {
if (m_oldState == newState) return;
if (newState == QMediaPlayer::PlayingState) {
if (m_seekToResumedPosition) {
if (m_mediaPlayer->isSeekable()) {
qCDebug(playbackManager) << "Resuming playback by seeking to " << (m_resumePosition / MS_TICK_FACTOR);
m_mediaPlayer->setPosition(m_resumePosition / MS_TICK_FACTOR);
m_seekToResumedPosition = false;
}
}
}
if (m_oldState == QMediaPlayer::StoppedState) {
// We're transitioning from stopped to either playing or paused.
// Set up the recurring timer
m_updateTimer.start();
postPlaybackInfo(Started);
} else if (newState == QMediaPlayer::StoppedState && m_playbackState == QMediaPlayer::StoppedState) {
// We've stopped playing the media. Post a stop signal.
m_updateTimer.stop();
postPlaybackInfo(Stopped);
} else {
postPlaybackInfo(Progress);
}
m_oldState = newState;
emit playbackStateChanged(newState);
}
void PlaybackManager::mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus newStatus) {
emit mediaStatusChanged(newStatus);
if (m_playbackState == QMediaPlayer::StoppedState) return;
if (newStatus == QMediaPlayer::LoadedMedia) {
m_mediaPlayer->play();
} else if (newStatus == QMediaPlayer::EndOfMedia) {
if (m_queue->hasNext() && m_queue->totalSize() > 1) {
next();
} else {
// End of the playlist
setPlaybackState(QMediaPlayer::StoppedState);
}
}
}
void PlaybackManager::mediaPlayerDurationChanged(qint64 newDuration) {
emit durationChanged(newDuration);
if (newDuration > 0 && !m_nextItem.isNull()) {
m_preloadTimer.stop();
m_preloadTimer.start(std::max(static_cast<int>(newDuration - PRELOAD_DURATION), 0));
}
}
void PlaybackManager::mediaPlayerSeekableChanged(bool newSeekable) {
emit seekableChanged(newSeekable);
if (m_seekToResumedPosition) {
m_forceSeekTimer.start();
}
/*if (m_seekToResumedPosition && newSeekable) {
qCDebug(playbackManager) << "Trying to seek to the resume position";
m_mediaPlayer->setPosition(m_resumePosition / MS_TICK_FACTOR);
if (m_mediaPlayer->position() > 1000) {
m_seekToResumedPosition = false;
}
}*/
}
void PlaybackManager::mediaPlayerError(QMediaPlayer::Error error) {
emit errorChanged(error);
emit errorStringChanged(m_mediaPlayer->errorString());
}
void PlaybackManager::updatePlaybackInfo() {
postPlaybackInfo(Progress);
void PlaybackManager::mediaPlayerItemChanged() {
Q_D(PlaybackManager);
d->m_displayItem->setData(d->m_impl->currentItem());
emit itemChanged();
}
void PlaybackManager::playItem(Item *item) {
this->playItem(item->data());
playItem(item->data());
}
void PlaybackManager::playItem(QSharedPointer<Model::Item> item) {
m_queue->clearList();
m_queue->appendToList(item);
setItem(item);
emit hasNextChanged(m_queue->hasNext());
emit hasPreviousChanged(m_queue->hasPrevious());
setPlaybackState(QMediaPlayer::PlayingState);
Q_D(PlaybackManager);
d->m_impl->playItem(item);
}
void PlaybackManager::playItemId(const QString &id) {
Jellyfin::Loader::HTTP::GetItemLoader *loader = new Jellyfin::Loader::HTTP::GetItemLoader(m_apiClient);
connect(loader, &Support::LoaderBase::error, this, [loader]() {
// TODO: error handling
loader->deleteLater();
});
connect(loader, &Support::LoaderBase::ready, this, [this, loader](){
this->playItem(QSharedPointer<Model::Item>::create(loader->result()));
loader->deleteLater();
});
Jellyfin::Loader::GetItemParams params;
params.setUserId(m_apiClient->userId());
params.setItemId(id);
loader->setParameters(params);
loader->load();
Q_D(PlaybackManager);
d->m_impl->playItemId(id);
}
void PlaybackManager::playItemInList(ItemModel *playlist, int index) {
m_queue->clearList();
m_queue->appendToList(*playlist);
m_queue->play(index);
m_queueIndex = index;
emit queueIndexChanged(m_queueIndex);
setItem(playlist->itemAt(index));
emit hasNextChanged(m_queue->hasNext());
emit hasPreviousChanged(m_queue->hasPrevious());
setPlaybackState(QMediaPlayer::PlayingState);
Q_D(PlaybackManager);
d->m_impl->playItemInList(playlist->toList(), index);
}
void PlaybackManager::skipToItemIndex(int index) {
if (index < m_queue->queueSize()) {
// Skip until we hit the right number in the queue
index++;
while(index != 0) {
m_queue->next();
index--;
}
} else {
m_queue->play(index);
}
setItem(m_queue->currentItem());
emit hasNextChanged(m_queue->hasNext());
emit hasPreviousChanged(m_queue->hasPrevious());
Q_D(PlaybackManager);
d->m_impl->goTo(index);
}
void PlaybackManager::play() {
m_mediaPlayer->play();
if (m_queue->totalSize() != 0) {
setPlaybackState(QMediaPlayer::PlayingState);
}
Q_D(PlaybackManager);
d->m_impl->play();
}
void PlaybackManager::next() {
m_mediaPlayer->stop();
m_mediaPlayer->setMedia(QMediaContent());
if (m_nextItem.isNull() || !m_queue->nextItem()->sameAs(*m_nextItem)) {
setItem(m_queue->nextItem());
m_nextStreamUrl = QString();
m_queue->next();
m_nextItem.clear();
} else {
m_item = m_nextItem;
m_streamUrl = m_nextStreamUrl;
m_nextItem.clear();
m_nextStreamUrl = QString();
m_queue->next();
setItem(m_nextItem);
}
emit hasNextChanged(m_queue->hasNext());
emit hasPreviousChanged(m_queue->hasPrevious());
}
void PlaybackManager::previous() {
m_mediaPlayer->stop();
m_mediaPlayer->setPosition(0);
m_item.clear();
m_streamUrl = QString();
m_nextStreamUrl = m_streamUrl;
m_nextItem = m_queue->nextItem();
m_queue->previous();
setItem(m_queue->currentItem());
emit hasNextChanged(m_queue->hasNext());
emit hasPreviousChanged(m_queue->hasPrevious());
}
void PlaybackManager::stop() {
setPlaybackState(QMediaPlayer::StoppedState);
m_queue->clearList();
m_mediaPlayer->stop();
void PlaybackManager::pause() {
Q_D(PlaybackManager);
return d->m_impl->pause();
}
void PlaybackManager::seek(qint64 pos) {
m_mediaPlayer->setPosition(pos);
postPlaybackInfo(Progress);
Q_D(PlaybackManager);
d->m_impl->seek(pos);
emit seeked(pos);
}
void PlaybackManager::stop() {
Q_D(PlaybackManager);
d->m_impl->stop();
}
void PlaybackManager::next() {
Q_D(PlaybackManager);
d->m_impl->next();
}
void PlaybackManager::previous() {
Q_D(PlaybackManager);
d->m_impl->previous();
}
void PlaybackManager::handlePlaystateRequest(const DTO::PlaystateRequest &request) {
if (!m_handlePlaystateCommands) return;
//if (!m_handlePlaystateCommands) return;
switch(request.command()) {
case DTO::PlaystateCommand::Pause:
pause();
break;
case DTO::PlaystateCommand::PlayPause:
if (playbackState() != QMediaPlayer::PlayingState) {
if (playbackState() != Model::PlayerState::Playing) {
play();
} else {
pause();
@ -413,229 +352,11 @@ void PlaybackManager::handlePlaystateRequest(const DTO::PlaystateRequest &reques
}
}
void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
if (m_item == nullptr) {
qCWarning(playbackManager) << "Item is null. Not posting playback info";
return;
}
DTO::PlaybackProgressInfo progress(
seekable(),
m_item,
m_item->jellyfinId(),
playbackState() == QMediaPlayer::PausedState,
false, // is muted?
m_playMethod,
DTO::RepeatMode::RepeatNone);
progress.setSessionId(m_playSessionId);
switch(type) {
case Started: // FALLTHROUGH
case Progress: {
progress.setAudioStreamIndex(m_audioIndex);
progress.setSubtitleStreamIndex(m_subtitleIndex);
progress.setPositionTicks(m_mediaPlayer->position() * MS_TICK_FACTOR);
QList<DTO::QueueItem> queue;
for (int i = 0; i < m_queue->listSize(); i++) {
DTO::QueueItem queueItem(m_queue->listAt(i)->jellyfinId());
queue.append(queueItem);
}
progress.setNowPlayingQueue(queue);
break;
}
case Stopped:
progress.setPositionTicks(m_stopPosition * MS_TICK_FACTOR);
break;
}
QString path;
switch (type) {
case Started:
path = "/Sessions/Playing";
break;
case Progress:
path = "/Sessions/Playing/Progress";
break;
case Stopped:
path = "/Sessions/Playing/Stopped";
break;
}
QNetworkReply *rep = m_apiClient->post(path, QJsonDocument(progress.toJson()));
connect(rep, &QNetworkReply::finished, this, [rep](){
rep->deleteLater();
});
m_apiClient->setDefaultErrorHandler(rep);
}
void PlaybackManager::componentComplete() {
if (m_apiClient == nullptr) qCWarning(playbackManager) << "No ApiClient set for PlaybackManager";
Q_D(PlaybackManager);
if (d->m_apiClient == nullptr) qCWarning(playbackManager) << "No ApiClient set for PlaybackManager";
m_qmlIsParsingComponent = false;
}
void PlaybackManager::requestItemUrl(QSharedPointer<Model::Item> item) {
ItemUrlLoader *loader = new Jellyfin::Loader::HTTP::GetPostedPlaybackInfoLoader(m_apiClient);
Jellyfin::Loader::GetPostedPlaybackInfoParams params;
// Check if we'd prefer to transcode if the video file contains multiple audio tracks
// or if a subtitle track was selected.
// This has to be done due to the lack of support of selecting audio tracks within QtMultimedia
bool allowTranscoding = m_apiClient->settings()->allowTranscoding();
bool transcodePreferred = m_subtitleIndex > 0;
int audioTracks = 0;
const QList<DTO::MediaStream> &streams = item->mediaStreams();
for(int i = 0; i < streams.size(); i++) {
const DTO::MediaStream &stream = streams[i];
if (stream.type() == MediaStreamType::Audio) {
audioTracks++;
}
}
if (audioTracks > 1) {
transcodePreferred = true;
}
bool forceTranscoding = allowTranscoding && transcodePreferred;
QSharedPointer<DTO::PlaybackInfoDto> playbackInfo = QSharedPointer<DTO::PlaybackInfoDto>::create(m_apiClient->deviceProfile());
params.setItemId(item->jellyfinId());
params.setUserId(m_apiClient->userId());
playbackInfo->setEnableDirectPlay(true);
playbackInfo->setEnableDirectStream(!forceTranscoding);
playbackInfo->setEnableTranscoding(forceTranscoding || allowTranscoding);
playbackInfo->setAudioStreamIndex(this->m_audioIndex);
playbackInfo->setSubtitleStreamIndex(this->m_subtitleIndex);
params.setBody(playbackInfo);
loader->setParameters(params);
connect(loader, &ItemUrlLoader::ready, this, [this, loader, item] {
DTO::PlaybackInfoResponse result = loader->result();
handlePlaybackInfoResponse(item->jellyfinId(), item->mediaType(), result);
loader->deleteLater();
});
connect(loader, &ItemUrlLoader::error, this, [this, loader, item](QString message) {
onItemErrorReceived(item->jellyfinId(), message);
loader->deleteLater();
});
loader->load();
}
void PlaybackManager::handlePlaybackInfoResponse(QString itemId, QString mediaType, DTO::PlaybackInfoResponse &response) {
//TODO: move the item URL fetching logic out of this function, into MediaSourceInfo?
QList<DTO::MediaSourceInfo> mediaSources = response.mediaSources();
QUrl resultingUrl;
QString playSession = response.playSessionId();
PlayMethod playMethod = PlayMethod::EnumNotSet;
bool transcodingAllowed = m_apiClient->settings()->allowTranscoding();
for (int i = 0; i < mediaSources.size(); i++) {
const DTO::MediaSourceInfo &source = mediaSources.at(i);
// Check if we'd prefer to transcode if the video file contains multiple audio tracks
// or if a subtitle track was selected.
// This has to be done due to the lack of support of selecting audio tracks within QtMultimedia
bool transcodePreferred = false;
if (transcodingAllowed) {
transcodePreferred = m_subtitleIndex > 0;
int audioTracks = 0;
const QList<DTO::MediaStream> &streams = source.mediaStreams();
for (int i = 0; i < streams.size(); i++) {
DTO::MediaStream stream = streams[i];
if (stream.type() == MediaStreamType::Audio) {
audioTracks++;
}
}
if (audioTracks > 1) {
transcodePreferred = true;
}
}
qCDebug(playbackManager()) << "Media source: " << source.name() << "\n"
<< "Prefer transcoding: " << transcodePreferred << "\n"
<< "DirectPlay supported: " << source.supportsDirectPlay() << "\n"
<< "DirectStream supported: " << source.supportsDirectStream() << "\n"
<< "Transcode supported: " << source.supportsTranscoding();
if (source.supportsDirectPlay() && QFile::exists(source.path())) {
resultingUrl = QUrl::fromLocalFile(source.path());
playMethod = PlayMethod::DirectPlay;
} else if (source.supportsDirectStream() && !transcodePreferred) {
if (mediaType == "Video") {
mediaType.append('s');
}
QUrlQuery query;
query.addQueryItem("mediaSourceId", source.jellyfinId());
query.addQueryItem("deviceId", m_apiClient->deviceId());
query.addQueryItem("api_key", m_apiClient->token());
query.addQueryItem("Static", "True");
resultingUrl = QUrl(m_apiClient->baseUrl() + "/" + mediaType + "/" + itemId
+ "/stream." + source.container() + "?" + query.toString(QUrl::EncodeReserved));
playMethod = PlayMethod::DirectStream;
} else if (source.supportsTranscoding() && !source.transcodingUrlNull() && transcodingAllowed) {
qCDebug(playbackManager) << "Transcoding url: " << source.transcodingUrl();
resultingUrl = QUrl(m_apiClient->baseUrl() + source.transcodingUrl());
playMethod = PlayMethod::Transcode;
} else {
qCDebug(playbackManager) << "No suitable sources for item " << itemId;
}
if (!resultingUrl.isEmpty()) break;
}
if (resultingUrl.isEmpty()) {
qCWarning(playbackManager) << "Could not find suitable media source for item " << itemId;
onItemErrorReceived(itemId, tr("Could not find a suitable media source."));
} else {
emit playMethodChanged(playMethod);
onItemUrlReceived(itemId, resultingUrl, playSession, playMethod);
}
}
void PlaybackManager::onItemUrlReceived(const QString &itemId, const QUrl &url,
const QString &playSession, PlayMethod playMethod) {
Q_UNUSED(url)
Q_UNUSED(playSession)
if (!m_item.isNull() && m_item->jellyfinId() == itemId) {
// We want to play the item probably right now
m_playSessionId = playSession;
m_playMethod = playMethod;
setStreamUrl(url);
emit playMethodChanged(m_playMethod);
// Clear the error string if it is currently set
if (!m_errorString.isEmpty()) {
m_errorString.clear();
emit errorStringChanged(m_errorString);
}
if (m_error != QMediaPlayer::NoError) {
m_error = QMediaPlayer::NoError;
emit errorChanged(error());
}
m_mediaPlayer->setMedia(QMediaContent(url));
m_mediaPlayer->play();
} else {
qDebug() << "Late reply for " << itemId << " received, ignoring";
}
}
/// Called when the fetcherThread encountered an error
void PlaybackManager::onItemErrorReceived(const QString &itemId, const QString &errorString) {
Q_UNUSED(itemId)
Q_UNUSED(errorString)
qWarning() << "Error while fetching streaming url for " << itemId << ": " << errorString;
if (!m_item.isNull() && m_item->jellyfinId() == itemId) {
setStreamUrl(QUrl());
m_error = QMediaPlayer::ResourceError;
emit errorChanged(error());
m_errorString = errorString;
emit errorStringChanged(errorString);
}
}
} // NS ViewModel
} // NS Jellyfin

View File

@ -110,7 +110,7 @@ ApplicationWindow {
enabled: playbackManager.hasPrevious
}
Button {
readonly property bool _playing: playbackManager.playbackState === MediaPlayer.PlayingState;
readonly property bool _playing: playbackManager.playbackState === PlayerState.Playing
anchors.verticalCenter: parent.verticalCenter
text: _playing ? "Pause" : "Play"
onClicked: _playing ? playbackManager.pause() : playbackManager.play()

View File

@ -48,7 +48,7 @@ PanelBackground {
property bool showQueue: false
property bool _pageWasShowingNavigationIndicator
readonly property bool mediaLoading: [MediaPlayer.Loading, MediaPlayer.Buffering].indexOf(manager.mediaStatus) >= 0
readonly property bool mediaLoading: [J.MediaStatus.Loading, J.MediaStatus.Buffering].indexOf(manager.mediaStatus) >= 0
transform: Translate {id: playbackBarTranslate; y: 0}
@ -134,14 +134,24 @@ PanelBackground {
Label {
id: artists
text: {
//return manager.item.mediaType;
if (manager.item === null) return qsTr("Play some media!")
switch(manager.item.mediaType) {
case "Audio":
return manager.item.artists.join(", ")
var links = [];
var items = manager.item.artistItems;
console.log(items)
for (var i = 0; i < items.length; i++) {
links.push("<a href=\"%1\" style=\"text-decoration:none;color:%3\">%2</a>"
.arg(items[i].jellyfinId)
.arg(items[i].name)
.arg(Theme.secondaryColor)
)
}
return links.join(", ")
}
return qsTr("No audio")
}
width: Math.min(contentWidth, parent.width)
font.pixelSize: Theme.fontSizeSmall
maximumLineCount: 1
@ -151,7 +161,7 @@ PanelBackground {
onLinkActivated: {
appWindow.navigateToItem(link, "Audio", "MusicArtist", true)
}
textFormat: Text.RichText
textFormat: Text.StyledText
}
}
@ -257,7 +267,7 @@ PanelBackground {
states: [
State {
name: ""
when: manager.playbackState !== MediaPlayer.StoppedState && !isFullPage && !("__hidePlaybackBar" in pageStack.currentPage)
when: manager.playbackState !== J.PlayerState.Stopped && !isFullPage && !("__hidePlaybackBar" in pageStack.currentPage)
},
State {
name: "large"
@ -354,20 +364,6 @@ PanelBackground {
PropertyChanges {
target: artists
font.pixelSize: Theme.fontSizeMedium
text: {
var links = [];
var items = manager.item.artistItems;
console.log(items)
for (var i = 0; i < items.length; i++) {
links.push("<a href=\"%1\" style=\"text-decoration:none;color:%3\">%2</a>"
.arg(items[i].jellyfinId)
.arg(items[i].name)
.arg(Theme.secondaryColor)
)
}
return links.join(", ")
}
}
AnchorChanges {
@ -390,7 +386,7 @@ PanelBackground {
},
State {
name: "hidden"
when: ((manager.playbackState === MediaPlayer.StoppedState && !mediaLoading) || "__hidePlaybackBar" in pageStack.currentPage) && !isFullPage
when: ((manager.playbackState === J.PlayerState.Stopped && !mediaLoading) || "__hidePlaybackBar" in pageStack.currentPage) && !isFullPage
PropertyChanges {
target: playbackBarTranslate
// + small padding since the ProgressBar otherwise would stick out

View File

@ -42,6 +42,7 @@ SilicaItem {
property int subtitleTrack: 0
//FIXME: Once QTBUG-10822 is resolved, change to J.PlaybackManager
property var manager;
onManagerChanged: console.log(manager.player)
// Blackground to prevent the ambience from leaking through
Rectangle {
@ -53,6 +54,9 @@ SilicaItem {
id: videoOutput
source: manager
anchors.fill: parent
Component.onCompleted: {
console.log(manager.player)
}
}
VideoHud {
@ -71,14 +75,16 @@ SilicaItem {
Label {
readonly property string _playbackMethod: {
switch(manager.playMethod) {
case J.PlaybackManager.DirectPlay:
return"Direct Play"
case J.PlaybackManager.Transcoding:
case J.PlayMethod.EnumNotSet:
return "Enum not set"
case J.PlayMethod.DirectPlay:
return "Direct Play"
case J.PlayMethod.Transcode:
return "Transcoding"
case J.PlaybackManager.DirectStream:
case J.PlayMethod.DirectStream:
return "Direct Stream"
default:
return "Unknown playback method"
return "Unknown playback method '%1'".arg(manager.playMethod)
}
}
anchors.fill: parent
@ -86,6 +92,7 @@ SilicaItem {
text: item.jellyfinId + "\n" + appWindow.playbackManager.streamUrl + "\n"
+ "Playback method: " + _playbackMethod + "\n"
+ "Media status: " + manager.mediaStatus + "\n"
+ "Playback state: " + manager.playbackState + "\n"
// + player.bufferProgress + "\n"
// + player.metaData.videoCodec + "@" + player.metaData.videoFrameRate + "(" + player.metaData.videoBitRate + ")" + "\n"
// + player.metaData.audioCodec + "(" + player.metaData.audioBitRate + ")" + "\n"

View File

@ -79,16 +79,17 @@ Item {
id: busyIndicator
anchors.centerIn: parent
size: BusyIndicatorSize.Medium
running: [MediaPlayer.Loading, MediaPlayer.Stalled].indexOf(manager.mediaStatus) >= 0
running: [J.MediaStatus.Loading, J.MediaStatus.Stalled].indexOf(manager.mediaStatus) >= 0
}
IconButton {
id: playPause
enabled: !hidden
anchors.centerIn: parent
icon.source: manager.playbackState === MediaPlayer.PausedState ? "image://theme/icon-l-play" : "image://theme/icon-l-pause"
icon.source: manager.playbackState === J.PlayerState.Paused ? "image://theme/icon-l-play" : "image://theme/icon-l-pause"
onClicked: {
if (manager.playbackState === MediaPlayer.PlayingState) {
console.log(manager.playbackState)
if (manager.playbackState === J.PlayerState.Playing) {
manager.pause()
} else {
manager.play()
@ -102,7 +103,7 @@ Item {
anchors.bottom: parent.bottom
width: parent.width
height: progress.height
visible: [MediaPlayer.Unavailable, MediaPlayer.Loading, MediaPlayer.NoMedia].indexOf(manager.mediaStatus) == -1
visible: [J.MediaStatus.Unavailable, J.MediaStatus.Loading, J.MediaStatus.NoMedia].indexOf(manager.mediaStatus) == -1
gradient: Gradient {
GradientStop { position: 0.0; color: Theme.rgba(palette.overlayBackgroundColor, 0.15); }
@ -151,11 +152,11 @@ Item {
onMediaStatusChanged: {
console.log("New mediaPlayer status: " + manager.mediaStatus)
switch(manager.mediaStatus) {
case MediaPlayer.Loaded:
case MediaPlayer.Buffering:
case J.MediaStatus.Loaded:
case J.MediaStatus.Buffering:
show(false)
break;
case MediaPlayer.Buffered:
case J.MediaStatus.Buffered:
hide(false)
break;
}

View File

@ -1,6 +1,6 @@
/*
Sailfin: a Jellyfin client written using Qt
Copyright (C) 2020 Chris Josten
Copyright (C) 2020-2022 Chris Josten
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
@ -114,7 +114,6 @@ ApplicationWindow {
id: _playbackManager
apiClient: appWindow.apiClient
audioIndex: 0
autoOpen: true
}
Connections {