mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2024-12-25 23:35:16 +00:00
Merge pull request #35 from heartfin/32-abstract-media-playback-implementation-from-viewmodel-playbackmanager
Abstract media playback implementation from ViewModel::PlaybackManager
This commit is contained in:
commit
b1bd15f2c1
|
@ -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
|
||||
|
|
|
@ -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;*/
|
||||
|
|
218
core/include/JellyfinQt/model/playbackmanager.h
Normal file
218
core/include/JellyfinQt/model/playbackmanager.h
Normal 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
|
58
core/include/JellyfinQt/model/playbackreporter.h
Normal file
58
core/include/JellyfinQt/model/playbackreporter.h
Normal 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
|
||||
|
152
core/include/JellyfinQt/model/player.h
Normal file
152
core/include/JellyfinQt/model/player.h
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>();
|
||||
}
|
||||
|
|
700
core/src/model/playbackmanager.cpp
Normal file
700
core/src/model/playbackmanager.cpp
Normal 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
|
200
core/src/model/playbackreporter.cpp
Normal file
200
core/src/model/playbackreporter.cpp
Normal 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
236
core/src/model/player.cpp
Normal 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
|
|
@ -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();
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue