mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2024-11-24 18:15: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
|
set(JellyfinQt_SOURCES
|
||||||
src/model/deviceprofile.cpp
|
src/model/deviceprofile.cpp
|
||||||
src/model/item.cpp
|
src/model/item.cpp
|
||||||
|
src/model/player.cpp
|
||||||
|
src/model/playbackmanager.cpp
|
||||||
|
src/model/playbackreporter.cpp
|
||||||
src/model/playlist.cpp
|
src/model/playlist.cpp
|
||||||
src/model/shuffle.cpp
|
src/model/shuffle.cpp
|
||||||
src/model/user.cpp
|
src/model/user.cpp
|
||||||
|
@ -48,6 +51,9 @@ list(APPEND JellyfinQt_SOURCES ${openapi_SOURCES})
|
||||||
set(JellyfinQt_HEADERS
|
set(JellyfinQt_HEADERS
|
||||||
include/JellyfinQt/model/deviceprofile.h
|
include/JellyfinQt/model/deviceprofile.h
|
||||||
include/JellyfinQt/model/item.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/playlist.h
|
||||||
include/JellyfinQt/model/shuffle.h
|
include/JellyfinQt/model/shuffle.h
|
||||||
include/JellyfinQt/model/user.h
|
include/JellyfinQt/model/user.h
|
||||||
|
|
|
@ -489,6 +489,10 @@ public:
|
||||||
this->endResetModel();
|
this->endResetModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QList<QSharedPointer<T>> &toList() {
|
||||||
|
return m_array;
|
||||||
|
}
|
||||||
|
|
||||||
// From AbstractListModel, gets implemented in ApiModel<T>
|
// From AbstractListModel, gets implemented in ApiModel<T>
|
||||||
//virtual QHash<int, QByteArray> roleNames() const override = 0;
|
//virtual QHash<int, QByteArray> roleNames() const override = 0;
|
||||||
/*virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) 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
|
* 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
|
* This library is free software; you can redistribute it and/or
|
||||||
* modify it under the terms of the GNU Lesser General Public
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
@ -96,9 +96,9 @@ public:
|
||||||
void clearList();
|
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
|
* @brief appendToList Appends a single item to the current list
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
#include <QtCore/QObject>
|
#include <QtCore/QObject>
|
||||||
#include <QtDBus/QtDBus>
|
#include <QtDBus/QtDBus>
|
||||||
#include <QMediaPlayer>
|
#include <QMediaPlayer>
|
||||||
|
#include "JellyfinQt/model/player.h"
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
QT_BEGIN_NAMESPACE
|
||||||
class QByteArray;
|
class QByteArray;
|
||||||
template<class T> class QList;
|
template<class T> class QList;
|
||||||
|
@ -190,9 +192,9 @@ private:
|
||||||
ViewModel::PlatformMediaControl *m_mediaControl;
|
ViewModel::PlatformMediaControl *m_mediaControl;
|
||||||
void notifyPropertiesChanged(QStringList properties);
|
void notifyPropertiesChanged(QStringList properties);
|
||||||
private slots:
|
private slots:
|
||||||
void onCurrentItemChanged(ViewModel::Item *newItem);
|
void onCurrentItemChanged();
|
||||||
void onPlaybackStateChanged(QMediaPlayer::State state);
|
void onPlaybackStateChanged(Jellyfin::Model::PlayerStateClass::Value state);
|
||||||
void onMediaStatusChanged(QMediaPlayer::MediaStatus status);
|
void onMediaStatusChanged(Jellyfin::Model::MediaStatusClass::Value status);
|
||||||
void onPositionChanged(qint64 position);
|
void onPositionChanged(qint64 position);
|
||||||
void onSeekableChanged(bool seekable);
|
void onSeekableChanged(bool seekable);
|
||||||
void onPlaybackManagerChanged(ViewModel::PlaybackManager *newPlaybackManager);
|
void onPlaybackManagerChanged(ViewModel::PlaybackManager *newPlaybackManager);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Sailfin: a Jellyfin client written using Qt
|
* 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
|
* This library is free software; you can redistribute it and/or
|
||||||
* modify it under the terms of the GNU Lesser General Public
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
@ -39,6 +39,7 @@
|
||||||
#include "../dto/playbackinforesponse.h"
|
#include "../dto/playbackinforesponse.h"
|
||||||
#include "../dto/playmethod.h"
|
#include "../dto/playmethod.h"
|
||||||
#include "../loader/requesttypes.h"
|
#include "../loader/requesttypes.h"
|
||||||
|
#include "../model/player.h"
|
||||||
#include "../model/playlist.h"
|
#include "../model/playlist.h"
|
||||||
#include "../support/jsonconv.h"
|
#include "../support/jsonconv.h"
|
||||||
#include "../viewmodel/item.h"
|
#include "../viewmodel/item.h"
|
||||||
|
@ -60,31 +61,34 @@ class PlaystateRequest;
|
||||||
namespace ViewModel {
|
namespace ViewModel {
|
||||||
Q_DECLARE_LOGGING_CATEGORY(playbackManager);
|
Q_DECLARE_LOGGING_CATEGORY(playbackManager);
|
||||||
|
|
||||||
// Later defined in this file
|
class PlaybackManagerPrivate;
|
||||||
class ItemUrlFetcherThread;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The PlaybackManager class manages the playback of Jellyfin items. It fetches streams based on Jellyfin items, posts
|
* @brief The PlaybackManager class manages the playback of Jellyfin items.
|
||||||
* the current playback state to the Jellyfin Server, contains the actual media player and so on.
|
|
||||||
*
|
*
|
||||||
* The PlaybackManager actually keeps two mediaPlayers, m_mediaPlayer1 and m_mediaPlayer2. When one is playing, the other is
|
* It is a small wrapper around an instance of Jellyfin::Model::PlaybackManager,
|
||||||
* preloading the next item in the queue. The current media player is pointed to by m_mediaPlayer.
|
* 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 {
|
class PlaybackManager : public QObject, public QQmlParserStatus {
|
||||||
friend class ItemUrlFetcherThread;
|
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
Q_DECLARE_PRIVATE(PlaybackManager);
|
||||||
Q_INTERFACES(QQmlParserStatus)
|
Q_INTERFACES(QQmlParserStatus)
|
||||||
public:
|
public:
|
||||||
using ItemUrlLoader = Support::Loader<DTO::PlaybackInfoResponse, Jellyfin::Loader::GetPostedPlaybackInfoParams>;
|
|
||||||
|
|
||||||
explicit PlaybackManager(QObject *parent = nullptr);
|
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(QString streamUrl READ streamUrl NOTIFY streamUrlChanged)
|
||||||
Q_PROPERTY(bool autoOpen MEMBER m_autoOpen NOTIFY autoOpenChanged)
|
//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)
|
* Whether the player should resume playback.
|
||||||
Q_PROPERTY(bool resumePlayback MEMBER m_resumePlayback NOTIFY resumePlaybackChanged)
|
*/
|
||||||
|
Q_PROPERTY(bool resumePlayback READ resumePlayback WRITE setResumePlayback NOTIFY resumePlaybackChanged)
|
||||||
Q_PROPERTY(Jellyfin::DTO::PlayMethodClass::Value playMethod READ playMethod NOTIFY playMethodChanged)
|
Q_PROPERTY(Jellyfin::DTO::PlayMethodClass::Value playMethod READ playMethod NOTIFY playMethodChanged)
|
||||||
|
|
||||||
// Current Item and queue informatoion
|
// Current Item and queue informatoion
|
||||||
|
@ -98,42 +102,50 @@ public:
|
||||||
Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged)
|
Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged)
|
||||||
Q_PROPERTY(bool hasVideo READ hasVideo NOTIFY hasVideoChanged)
|
Q_PROPERTY(bool hasVideo READ hasVideo NOTIFY hasVideoChanged)
|
||||||
Q_PROPERTY(bool seekable READ seekable NOTIFY seekableChanged)
|
Q_PROPERTY(bool seekable READ seekable NOTIFY seekableChanged)
|
||||||
Q_PROPERTY(QObject* mediaObject READ mediaObject NOTIFY mediaObjectChanged)
|
Q_PROPERTY(QObject* mediaObject READ mediaObject NOTIFY mediaObjectChanged);
|
||||||
Q_PROPERTY(QMediaPlayer::MediaStatus mediaStatus READ mediaStatus NOTIFY mediaStatusChanged)
|
Q_PROPERTY(Jellyfin::Model::MediaStatusClass::Value mediaStatus READ mediaStatus NOTIFY mediaStatusChanged)
|
||||||
Q_PROPERTY(QMediaPlayer::State playbackState READ playbackState NOTIFY playbackStateChanged)
|
Q_PROPERTY(Jellyfin::Model::PlayerStateClass::Value playbackState READ playbackState NOTIFY playbackStateChanged)
|
||||||
Q_PROPERTY(qint64 position READ position NOTIFY positionChanged)
|
Q_PROPERTY(qint64 position READ position NOTIFY positionChanged)
|
||||||
Q_PROPERTY(bool hasNext READ hasNext NOTIFY hasNextChanged)
|
Q_PROPERTY(bool hasNext READ hasNext NOTIFY hasNextChanged)
|
||||||
Q_PROPERTY(bool hasPrevious READ hasPrevious NOTIFY hasPreviousChanged)
|
Q_PROPERTY(bool hasPrevious READ hasPrevious NOTIFY hasPreviousChanged)
|
||||||
/// Whether playstate commands received over the websocket should be handled
|
/// Whether playstate commands received over the websocket should be handled
|
||||||
Q_PROPERTY(bool handlePlaystateCommands READ handlePlaystateCommands WRITE setHandlePlaystateCommands NOTIFY handlePlaystateCommandsChanged)
|
Q_PROPERTY(bool handlePlaystateCommands READ handlePlaystateCommands WRITE setHandlePlaystateCommands NOTIFY handlePlaystateCommandsChanged)
|
||||||
|
|
||||||
ViewModel::Item *item() const { return m_displayItem; }
|
// R/W props
|
||||||
QSharedPointer<Model::Item> dataItem() const { return m_item; }
|
ApiClient *apiClient() const;
|
||||||
ApiClient *apiClient() const { return m_apiClient; }
|
|
||||||
void setApiClient(ApiClient *apiClient);
|
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; }
|
ViewModel::Item *item() const;
|
||||||
PlayMethod playMethod() const { return m_playMethod; }
|
QSharedPointer<Model::Item> dataItem() const;
|
||||||
QObject *mediaObject() const { return m_mediaPlayer; }
|
|
||||||
qint64 position() const { return m_mediaPlayer->position(); }
|
QString streamUrl() const;
|
||||||
qint64 duration() const { return m_mediaPlayer->duration(); }
|
PlayMethod playMethod() const;
|
||||||
ViewModel::Playlist *queue() const { return m_displayQueue; }
|
qint64 position() const;
|
||||||
int queueIndex() const { return m_queueIndex; }
|
qint64 duration() const;
|
||||||
bool hasNext() const { return m_queue->hasNext(); }
|
ViewModel::Playlist *queue() const;
|
||||||
bool hasPrevious() const { return m_queue->hasPrevious(); }
|
int queueIndex() const;
|
||||||
|
bool hasNext() const;
|
||||||
|
bool hasPrevious() const;
|
||||||
|
|
||||||
// Current media player related property getters
|
// Current media player related property getters
|
||||||
QMediaPlayer::State playbackState() const { return m_playbackState; }
|
QObject* mediaObject() const;
|
||||||
QMediaPlayer::MediaStatus mediaStatus() const { return m_mediaPlayer->mediaStatus(); }
|
Model::PlayerState playbackState() const;
|
||||||
bool hasVideo() const { return m_mediaPlayer->isVideoAvailable(); }
|
Model::MediaStatus mediaStatus() const;
|
||||||
bool seekable() const { return m_mediaPlayer->isSeekable(); }
|
bool hasVideo() const;
|
||||||
|
bool seekable() const;
|
||||||
QMediaPlayer::Error error () const;
|
QMediaPlayer::Error error () const;
|
||||||
QString errorString() const;
|
QString errorString() const;
|
||||||
|
|
||||||
bool handlePlaystateCommands() const { return m_handlePlaystateCommands; }
|
bool handlePlaystateCommands() const;
|
||||||
void setHandlePlaystateCommands(bool newHandlePlaystateCommands) { m_handlePlaystateCommands = newHandlePlaystateCommands; emit handlePlaystateCommandsChanged(m_handlePlaystateCommands); }
|
void setHandlePlaystateCommands(bool newHandlePlaystateCommands);
|
||||||
signals:
|
signals:
|
||||||
void itemChanged(ViewModel::Item *newItemId);
|
void itemChanged();
|
||||||
void streamUrlChanged(const QString &newStreamUrl);
|
void streamUrlChanged(const QString &newStreamUrl);
|
||||||
void autoOpenChanged(bool autoOpen);
|
void autoOpenChanged(bool autoOpen);
|
||||||
void audioIndexChanged(int audioIndex);
|
void audioIndexChanged(int audioIndex);
|
||||||
|
@ -145,20 +157,21 @@ signals:
|
||||||
// Emitted when seek has been called.
|
// Emitted when seek has been called.
|
||||||
void seeked(qint64 newPosition);
|
void seeked(qint64 newPosition);
|
||||||
|
|
||||||
|
void hasNextChanged(bool newHasNext);
|
||||||
|
void hasPreviousChanged(bool newHasPrevious);
|
||||||
|
|
||||||
// Current media player related property signals
|
// Current media player related property signals
|
||||||
void mediaObjectChanged(QObject *newMediaObject);
|
void mediaObjectChanged(QObject *newPlayer);
|
||||||
void positionChanged(qint64 newPosition);
|
void positionChanged(qint64 newPosition);
|
||||||
void durationChanged(qint64 newDuration);
|
void durationChanged(qint64 newDuration);
|
||||||
void queueChanged(QAbstractItemModel *newQueue);
|
void queueChanged(QAbstractItemModel *newQueue);
|
||||||
void queueIndexChanged(int newIndex);
|
void queueIndexChanged(int newIndex);
|
||||||
void playbackStateChanged(QMediaPlayer::State newState);
|
void playbackStateChanged(Jellyfin::Model::PlayerStateClass::Value newState);
|
||||||
void mediaStatusChanged(QMediaPlayer::MediaStatus newMediaStatus);
|
void mediaStatusChanged(Jellyfin::Model::MediaStatusClass::Value newMediaStatus);
|
||||||
void hasVideoChanged(bool newHasVideo);
|
void hasVideoChanged(bool newHasVideo);
|
||||||
void seekableChanged(bool newSeekable);
|
void seekableChanged(bool newSeekable);
|
||||||
void errorChanged(QMediaPlayer::Error newError);
|
void errorChanged(QMediaPlayer::Error newError);
|
||||||
void errorStringChanged(const QString &newErrorString);
|
void errorStringChanged(const QString &newErrorString);
|
||||||
void hasNextChanged(bool newHasNext);
|
|
||||||
void hasPreviousChanged(bool newHasPrevious);
|
|
||||||
void handlePlaystateCommandsChanged(bool newHandlePlaystateCommands);
|
void handlePlaystateCommandsChanged(bool newHandlePlaystateCommands);
|
||||||
public slots:
|
public slots:
|
||||||
/**
|
/**
|
||||||
|
@ -187,7 +200,7 @@ public slots:
|
||||||
*/
|
*/
|
||||||
void skipToItemIndex(int index);
|
void skipToItemIndex(int index);
|
||||||
void play();
|
void play();
|
||||||
void pause() { m_mediaPlayer->pause(); setPlaybackState(QMediaPlayer::PausedState); }
|
void pause();
|
||||||
void seek(qint64 pos);
|
void seek(qint64 pos);
|
||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
|
@ -204,105 +217,21 @@ public slots:
|
||||||
void handlePlaystateRequest(const DTO::PlaystateRequest &request);
|
void handlePlaystateRequest(const DTO::PlaystateRequest &request);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void mediaPlayerStateChanged(QMediaPlayer::State newState);
|
void mediaPlayerItemChanged();
|
||||||
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);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/// Factor to multiply with when converting from milliseconds to ticks.
|
/// Factor to multiply with when converting from milliseconds to ticks.
|
||||||
const static int MS_TICK_FACTOR = 10000;
|
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;
|
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
|
// QQmlParserListener interface
|
||||||
void classBegin() override { m_qmlIsParsingComponent = true; }
|
void classBegin() override { m_qmlIsParsingComponent = true; }
|
||||||
void componentComplete() override;
|
void componentComplete() override;
|
||||||
bool m_qmlIsParsingComponent = false;
|
bool m_qmlIsParsingComponent = false;
|
||||||
|
|
||||||
/// Time in ms at what moment this playbackmanager should start loading the next item.
|
QScopedPointer<PlaybackManagerPrivate> d_ptr;
|
||||||
const qint64 PRELOAD_DURATION = 15 * 1000;
|
|
||||||
QTimer m_forceSeekTimer;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // NS ViewModel
|
} // NS ViewModel
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
#include "JellyfinQt/eventbus.h"
|
#include "JellyfinQt/eventbus.h"
|
||||||
#include "JellyfinQt/serverdiscoverymodel.h"
|
#include "JellyfinQt/serverdiscoverymodel.h"
|
||||||
#include "JellyfinQt/websocket.h"
|
#include "JellyfinQt/websocket.h"
|
||||||
|
#include "JellyfinQt/model/player.h"
|
||||||
#include "JellyfinQt/viewmodel/item.h"
|
#include "JellyfinQt/viewmodel/item.h"
|
||||||
#include "JellyfinQt/viewmodel/itemmodel.h"
|
#include "JellyfinQt/viewmodel/itemmodel.h"
|
||||||
#include "JellyfinQt/viewmodel/loader.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::ItemFieldsClass>(uri, 1, 0, "ItemFields", "Is an enum");
|
||||||
qmlRegisterUncreatableType<Jellyfin::DTO::ImageTypeClass>(uri, 1, 0, "ImageType", "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::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>();
|
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
|
* 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
|
* This library is free software; you can redistribute it and/or
|
||||||
* modify it under the terms of the GNU Lesser General Public
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
@ -132,13 +132,13 @@ QSharedPointer<Item> Playlist::nextItem() {
|
||||||
return m_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 start = m_list.size();
|
||||||
int count = model.size();
|
int count = items.size();
|
||||||
m_list.reserve(count);
|
m_list.reserve(count);
|
||||||
emit beforeItemsAddedToList(start, count);
|
emit beforeItemsAddedToList(start, count);
|
||||||
for (int i = 0; i < count; i++) {
|
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();
|
emit itemsAddedToList();
|
||||||
reshuffle();
|
reshuffle();
|
||||||
|
|
|
@ -165,12 +165,13 @@ QString PlayerAdaptor::playbackStatus() const
|
||||||
if (m_mediaControl == nullptr || m_mediaControl->playbackManager() == nullptr) {
|
if (m_mediaControl == nullptr || m_mediaControl->playbackManager() == nullptr) {
|
||||||
return "Stopped";
|
return "Stopped";
|
||||||
}
|
}
|
||||||
|
using PlayerState = Jellyfin::Model::PlayerState;
|
||||||
switch(m_mediaControl->playbackManager()->playbackState()) {
|
switch(m_mediaControl->playbackManager()->playbackState()) {
|
||||||
case QMediaPlayer::StoppedState:
|
case PlayerState::Stopped:
|
||||||
return "Stopped";
|
return "Stopped";
|
||||||
case QMediaPlayer::PlayingState:
|
case PlayerState::Playing:
|
||||||
return "Playing";
|
return "Playing";
|
||||||
case QMediaPlayer::PausedState:
|
case PlayerState::Paused:
|
||||||
return "Paused";
|
return "Paused";
|
||||||
default:
|
default:
|
||||||
return "Stopped";
|
return "Stopped";
|
||||||
|
@ -246,7 +247,8 @@ void PlayerAdaptor::Play()
|
||||||
void PlayerAdaptor::PlayPause()
|
void PlayerAdaptor::PlayPause()
|
||||||
{
|
{
|
||||||
// handle method call org.mpris.MediaPlayer2.Player.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();
|
m_mediaControl->playbackManager()->pause();
|
||||||
} else {
|
} else {
|
||||||
m_mediaControl->playbackManager()->play();
|
m_mediaControl->playbackManager()->play();
|
||||||
|
@ -290,14 +292,12 @@ void PlayerAdaptor::notifyPropertiesChanged(QStringList properties) {
|
||||||
QDBusConnection::sessionBus().send(signal);
|
QDBusConnection::sessionBus().send(signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayerAdaptor::onCurrentItemChanged(ViewModel::Item *item) {
|
void PlayerAdaptor::onCurrentItemChanged() {
|
||||||
Q_UNUSED(item)
|
|
||||||
|
|
||||||
QStringList properties;
|
QStringList properties;
|
||||||
properties << "Metadata" << "Position" << "CanPlay" << "CanPause" << "CanGoNext" << "CanGoPrevious";
|
properties << "Metadata" << "Position" << "CanPlay" << "CanPause" << "CanGoNext" << "CanGoPrevious";
|
||||||
notifyPropertiesChanged(properties);
|
notifyPropertiesChanged(properties);
|
||||||
}
|
}
|
||||||
void PlayerAdaptor::onPlaybackStateChanged(QMediaPlayer::State state) {
|
void PlayerAdaptor::onPlaybackStateChanged(Jellyfin::Model::PlayerStateClass::Value state) {
|
||||||
Q_UNUSED(state)
|
Q_UNUSED(state)
|
||||||
QStringList properties;
|
QStringList properties;
|
||||||
properties << "PlaybackStatus" << "Position";
|
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)
|
Q_UNUSED(status)
|
||||||
QStringList properties;
|
QStringList properties;
|
||||||
properties << "PlaybackStatus" << "Position";
|
properties << "PlaybackStatus" << "Position";
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Sailfin: a Jellyfin client written using Qt
|
* 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
|
* This library is free software; you can redistribute it and/or
|
||||||
* modify it under the terms of the GNU Lesser General Public
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
@ -20,13 +20,12 @@
|
||||||
#include "JellyfinQt/viewmodel/playbackmanager.h"
|
#include "JellyfinQt/viewmodel/playbackmanager.h"
|
||||||
|
|
||||||
#include "JellyfinQt/apimodel.h"
|
#include "JellyfinQt/apimodel.h"
|
||||||
#include "JellyfinQt/loader/http/mediainfo.h"
|
|
||||||
#include <JellyfinQt/dto/playstatecommand.h>
|
#include <JellyfinQt/dto/playstatecommand.h>
|
||||||
#include <JellyfinQt/dto/playstaterequest.h>
|
#include <JellyfinQt/dto/playstaterequest.h>
|
||||||
|
|
||||||
// #include "JellyfinQt/DTO/dto.h"
|
// #include "JellyfinQt/DTO/dto.h"
|
||||||
#include <JellyfinQt/loader/http/userlibrary.h>
|
|
||||||
#include <JellyfinQt/dto/useritemdatadto.h>
|
#include <JellyfinQt/dto/useritemdatadto.h>
|
||||||
|
#include <JellyfinQt/model/playbackmanager.h>
|
||||||
#include <JellyfinQt/viewmodel/settings.h>
|
#include <JellyfinQt/viewmodel/settings.h>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
@ -41,348 +40,288 @@ namespace ViewModel {
|
||||||
|
|
||||||
Q_LOGGING_CATEGORY(playbackManager, "jellyfin.viewmodel.playbackmanager")
|
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)
|
PlaybackManager::PlaybackManager(QObject *parent)
|
||||||
: QObject(parent),
|
: QObject(parent) {
|
||||||
m_item(nullptr),
|
QScopedPointer<PlaybackManagerPrivate> foo(new PlaybackManagerPrivate(this));
|
||||||
m_mediaPlayer(new QMediaPlayer(this)),
|
d_ptr.swap(foo);
|
||||||
m_queue(new Model::Playlist(this)) {
|
|
||||||
|
|
||||||
m_displayQueue = new ViewModel::Playlist(m_queue, this);
|
Q_D(PlaybackManager);
|
||||||
// Set up connections.
|
// Set up connections.
|
||||||
m_updateTimer.setInterval(10000); // 10 seconds
|
connect(d->m_impl, &Model::PlaybackManager::positionChanged, this, &PlaybackManager::positionChanged);
|
||||||
m_updateTimer.setSingleShot(false);
|
connect(d->m_impl, &Model::PlaybackManager::durationChanged, this, &PlaybackManager::durationChanged);
|
||||||
|
connect(d->m_impl, &Model::PlaybackManager::hasNextChanged, this, &PlaybackManager::hasNextChanged);
|
||||||
m_preloadTimer.setSingleShot(true);
|
connect(d->m_impl, &Model::PlaybackManager::hasPreviousChanged, this, &PlaybackManager::hasPreviousChanged);
|
||||||
|
connect(d->m_impl, &Model::PlaybackManager::seekableChanged, this, &PlaybackManager::seekableChanged);
|
||||||
connect(&m_updateTimer, &QTimer::timeout, this, &PlaybackManager::updatePlaybackInfo);
|
connect(d->m_impl, &Model::PlaybackManager::queueIndexChanged, this, &PlaybackManager::queueIndexChanged);
|
||||||
|
connect(d->m_impl, &Model::PlaybackManager::itemChanged, this, &PlaybackManager::mediaPlayerItemChanged);
|
||||||
connect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
|
connect(d->m_impl, &Model::PlaybackManager::playbackStateChanged, this, &PlaybackManager::playbackStateChanged);
|
||||||
connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged);
|
if (auto localImp = qobject_cast<Model::LocalPlaybackManager*>(d->m_impl)) {
|
||||||
connect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, &PlaybackManager::mediaPlayerDurationChanged);
|
connect(localImp, &Model::LocalPlaybackManager::streamUrlChanged, this, [this](const QUrl& newUrl){
|
||||||
connect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged);
|
this->streamUrlChanged(newUrl.toString());
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
connect(localImp, &Model::LocalPlaybackManager::playMethodChanged, this, &PlaybackManager::playMethodChanged);
|
||||||
|
}
|
||||||
|
connect(d->m_impl, &Model::PlaybackManager::mediaStatusChanged, this, &PlaybackManager::mediaStatusChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaybackManager::~PlaybackManager() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::setApiClient(ApiClient *apiClient) {
|
void PlaybackManager::setApiClient(ApiClient *apiClient) {
|
||||||
if (m_apiClient != nullptr) {
|
Q_D(PlaybackManager);
|
||||||
disconnect(m_apiClient->eventbus(), &EventBus::playstateCommandReceived, this, &PlaybackManager::handlePlaystateRequest);
|
if (d->m_apiClient != nullptr) {
|
||||||
|
disconnect(d->m_apiClient->eventbus(), &EventBus::playstateCommandReceived, this, &PlaybackManager::handlePlaystateRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_item.isNull()) {
|
if (!d->m_displayItem->data().isNull()) {
|
||||||
m_item->setApiClient(apiClient);
|
d->m_displayItem->data()->setApiClient(apiClient);
|
||||||
}
|
}
|
||||||
m_apiClient = apiClient;
|
d->m_apiClient = apiClient;
|
||||||
|
d->m_impl->setApiClient(apiClient);
|
||||||
|
|
||||||
if (m_apiClient != nullptr) {
|
if (d->m_apiClient != nullptr) {
|
||||||
connect(m_apiClient->eventbus(), &EventBus::playstateCommandReceived, this, &PlaybackManager::handlePlaystateRequest);
|
connect(d->m_apiClient->eventbus(), &EventBus::playstateCommandReceived, this, &PlaybackManager::handlePlaystateRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::setItem(QSharedPointer<Model::Item> newItem) {
|
bool PlaybackManager::resumePlayback() const {
|
||||||
if (m_mediaPlayer != nullptr) m_mediaPlayer->stop();
|
const Q_D(PlaybackManager);
|
||||||
bool shouldFetchStreamUrl = !newItem.isNull()
|
return d->m_impl->resumePlayback();
|
||||||
&& ((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;
|
void PlaybackManager::setResumePlayback(bool newResumePlayback) {
|
||||||
|
Q_D(PlaybackManager);
|
||||||
|
return d->m_impl->setResumePlayback(newResumePlayback);
|
||||||
|
}
|
||||||
|
|
||||||
if (newItem.isNull()) {
|
int PlaybackManager::audioIndex() const {
|
||||||
m_displayItem->setData(QSharedPointer<Model::Item>::create());
|
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 {
|
} else {
|
||||||
m_displayItem->setData(newItem);
|
return QStringLiteral("<not playing back locally>");
|
||||||
if (!newItem->userData().isNull()) {
|
|
||||||
this->m_resumePosition = newItem->userData()->playbackPositionTicks();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
emit itemChanged(m_displayItem);
|
|
||||||
|
|
||||||
emit hasNextChanged(m_queue->hasNext());
|
PlayMethod PlaybackManager::playMethod() const {
|
||||||
emit hasPreviousChanged(m_queue->hasPrevious());
|
const Q_D(PlaybackManager);
|
||||||
|
if (Model::LocalPlaybackManager *lpm = qobject_cast<Model::LocalPlaybackManager *>(d->m_impl)) {
|
||||||
this->m_seekToResumedPosition = m_resumePlayback;
|
return lpm->playMethod();
|
||||||
|
|
||||||
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);
|
|
||||||
} else {
|
} else {
|
||||||
setStreamUrl(m_nextStreamUrl);
|
return PlayMethod::EnumNotSet;
|
||||||
m_mediaPlayer->play();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
QMediaPlayer::Error PlaybackManager::error() const {
|
||||||
if (m_error != QMediaPlayer::NoError) {
|
return QMediaPlayer::NoError;
|
||||||
return m_error;
|
|
||||||
} else {
|
|
||||||
return m_mediaPlayer->error();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString PlaybackManager::errorString() const {
|
QString PlaybackManager::errorString() const {
|
||||||
if (!m_errorString.isEmpty()) {
|
const Q_D(PlaybackManager);
|
||||||
return m_errorString;
|
return d->m_impl->errorString();
|
||||||
} else {
|
|
||||||
return m_mediaPlayer->errorString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlaybackManager::mediaPlayerItemChanged() {
|
||||||
void PlaybackManager::setStreamUrl(const QUrl &streamUrl) {
|
Q_D(PlaybackManager);
|
||||||
m_streamUrl = streamUrl.toString();
|
d->m_displayItem->setData(d->m_impl->currentItem());
|
||||||
// Inspired by PHP naming schemes
|
emit itemChanged();
|
||||||
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::playItem(Item *item) {
|
void PlaybackManager::playItem(Item *item) {
|
||||||
this->playItem(item->data());
|
playItem(item->data());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void PlaybackManager::playItem(QSharedPointer<Model::Item> item) {
|
void PlaybackManager::playItem(QSharedPointer<Model::Item> item) {
|
||||||
m_queue->clearList();
|
Q_D(PlaybackManager);
|
||||||
m_queue->appendToList(item);
|
d->m_impl->playItem(item);
|
||||||
setItem(item);
|
|
||||||
emit hasNextChanged(m_queue->hasNext());
|
|
||||||
emit hasPreviousChanged(m_queue->hasPrevious());
|
|
||||||
setPlaybackState(QMediaPlayer::PlayingState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::playItemId(const QString &id) {
|
void PlaybackManager::playItemId(const QString &id) {
|
||||||
Jellyfin::Loader::HTTP::GetItemLoader *loader = new Jellyfin::Loader::HTTP::GetItemLoader(m_apiClient);
|
Q_D(PlaybackManager);
|
||||||
connect(loader, &Support::LoaderBase::error, this, [loader]() {
|
d->m_impl->playItemId(id);
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::playItemInList(ItemModel *playlist, int index) {
|
void PlaybackManager::playItemInList(ItemModel *playlist, int index) {
|
||||||
m_queue->clearList();
|
Q_D(PlaybackManager);
|
||||||
m_queue->appendToList(*playlist);
|
d->m_impl->playItemInList(playlist->toList(), index);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::skipToItemIndex(int index) {
|
void PlaybackManager::skipToItemIndex(int index) {
|
||||||
if (index < m_queue->queueSize()) {
|
Q_D(PlaybackManager);
|
||||||
// Skip until we hit the right number in the queue
|
d->m_impl->goTo(index);
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::play() {
|
void PlaybackManager::play() {
|
||||||
m_mediaPlayer->play();
|
Q_D(PlaybackManager);
|
||||||
if (m_queue->totalSize() != 0) {
|
d->m_impl->play();
|
||||||
setPlaybackState(QMediaPlayer::PlayingState);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::next() {
|
void PlaybackManager::pause() {
|
||||||
m_mediaPlayer->stop();
|
Q_D(PlaybackManager);
|
||||||
m_mediaPlayer->setMedia(QMediaContent());
|
return d->m_impl->pause();
|
||||||
|
|
||||||
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::seek(qint64 pos) {
|
void PlaybackManager::seek(qint64 pos) {
|
||||||
m_mediaPlayer->setPosition(pos);
|
Q_D(PlaybackManager);
|
||||||
postPlaybackInfo(Progress);
|
d->m_impl->seek(pos);
|
||||||
emit seeked(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) {
|
void PlaybackManager::handlePlaystateRequest(const DTO::PlaystateRequest &request) {
|
||||||
if (!m_handlePlaystateCommands) return;
|
//if (!m_handlePlaystateCommands) return;
|
||||||
switch(request.command()) {
|
switch(request.command()) {
|
||||||
case DTO::PlaystateCommand::Pause:
|
case DTO::PlaystateCommand::Pause:
|
||||||
pause();
|
pause();
|
||||||
break;
|
break;
|
||||||
case DTO::PlaystateCommand::PlayPause:
|
case DTO::PlaystateCommand::PlayPause:
|
||||||
if (playbackState() != QMediaPlayer::PlayingState) {
|
if (playbackState() != Model::PlayerState::Playing) {
|
||||||
play();
|
play();
|
||||||
} else {
|
} else {
|
||||||
pause();
|
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() {
|
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;
|
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 ViewModel
|
||||||
} // NS Jellyfin
|
} // NS Jellyfin
|
||||||
|
|
|
@ -110,7 +110,7 @@ ApplicationWindow {
|
||||||
enabled: playbackManager.hasPrevious
|
enabled: playbackManager.hasPrevious
|
||||||
}
|
}
|
||||||
Button {
|
Button {
|
||||||
readonly property bool _playing: playbackManager.playbackState === MediaPlayer.PlayingState;
|
readonly property bool _playing: playbackManager.playbackState === PlayerState.Playing
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: _playing ? "Pause" : "Play"
|
text: _playing ? "Pause" : "Play"
|
||||||
onClicked: _playing ? playbackManager.pause() : playbackManager.play()
|
onClicked: _playing ? playbackManager.pause() : playbackManager.play()
|
||||||
|
|
|
@ -48,7 +48,7 @@ PanelBackground {
|
||||||
property bool showQueue: false
|
property bool showQueue: false
|
||||||
|
|
||||||
property bool _pageWasShowingNavigationIndicator
|
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}
|
transform: Translate {id: playbackBarTranslate; y: 0}
|
||||||
|
@ -134,14 +134,24 @@ PanelBackground {
|
||||||
Label {
|
Label {
|
||||||
id: artists
|
id: artists
|
||||||
text: {
|
text: {
|
||||||
//return manager.item.mediaType;
|
|
||||||
if (manager.item === null) return qsTr("Play some media!")
|
if (manager.item === null) return qsTr("Play some media!")
|
||||||
switch(manager.item.mediaType) {
|
switch(manager.item.mediaType) {
|
||||||
case "Audio":
|
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")
|
return qsTr("No audio")
|
||||||
}
|
}
|
||||||
|
|
||||||
width: Math.min(contentWidth, parent.width)
|
width: Math.min(contentWidth, parent.width)
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
|
@ -151,7 +161,7 @@ PanelBackground {
|
||||||
onLinkActivated: {
|
onLinkActivated: {
|
||||||
appWindow.navigateToItem(link, "Audio", "MusicArtist", true)
|
appWindow.navigateToItem(link, "Audio", "MusicArtist", true)
|
||||||
}
|
}
|
||||||
textFormat: Text.RichText
|
textFormat: Text.StyledText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,7 +267,7 @@ PanelBackground {
|
||||||
states: [
|
states: [
|
||||||
State {
|
State {
|
||||||
name: ""
|
name: ""
|
||||||
when: manager.playbackState !== MediaPlayer.StoppedState && !isFullPage && !("__hidePlaybackBar" in pageStack.currentPage)
|
when: manager.playbackState !== J.PlayerState.Stopped && !isFullPage && !("__hidePlaybackBar" in pageStack.currentPage)
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "large"
|
name: "large"
|
||||||
|
@ -354,20 +364,6 @@ PanelBackground {
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: artists
|
target: artists
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
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 {
|
AnchorChanges {
|
||||||
|
@ -390,7 +386,7 @@ PanelBackground {
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: "hidden"
|
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 {
|
PropertyChanges {
|
||||||
target: playbackBarTranslate
|
target: playbackBarTranslate
|
||||||
// + small padding since the ProgressBar otherwise would stick out
|
// + small padding since the ProgressBar otherwise would stick out
|
||||||
|
|
|
@ -42,6 +42,7 @@ SilicaItem {
|
||||||
property int subtitleTrack: 0
|
property int subtitleTrack: 0
|
||||||
//FIXME: Once QTBUG-10822 is resolved, change to J.PlaybackManager
|
//FIXME: Once QTBUG-10822 is resolved, change to J.PlaybackManager
|
||||||
property var manager;
|
property var manager;
|
||||||
|
onManagerChanged: console.log(manager.player)
|
||||||
|
|
||||||
// Blackground to prevent the ambience from leaking through
|
// Blackground to prevent the ambience from leaking through
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
@ -53,6 +54,9 @@ SilicaItem {
|
||||||
id: videoOutput
|
id: videoOutput
|
||||||
source: manager
|
source: manager
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
Component.onCompleted: {
|
||||||
|
console.log(manager.player)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
VideoHud {
|
VideoHud {
|
||||||
|
@ -71,14 +75,16 @@ SilicaItem {
|
||||||
Label {
|
Label {
|
||||||
readonly property string _playbackMethod: {
|
readonly property string _playbackMethod: {
|
||||||
switch(manager.playMethod) {
|
switch(manager.playMethod) {
|
||||||
case J.PlaybackManager.DirectPlay:
|
case J.PlayMethod.EnumNotSet:
|
||||||
return"Direct Play"
|
return "Enum not set"
|
||||||
case J.PlaybackManager.Transcoding:
|
case J.PlayMethod.DirectPlay:
|
||||||
|
return "Direct Play"
|
||||||
|
case J.PlayMethod.Transcode:
|
||||||
return "Transcoding"
|
return "Transcoding"
|
||||||
case J.PlaybackManager.DirectStream:
|
case J.PlayMethod.DirectStream:
|
||||||
return "Direct Stream"
|
return "Direct Stream"
|
||||||
default:
|
default:
|
||||||
return "Unknown playback method"
|
return "Unknown playback method '%1'".arg(manager.playMethod)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
@ -86,6 +92,7 @@ SilicaItem {
|
||||||
text: item.jellyfinId + "\n" + appWindow.playbackManager.streamUrl + "\n"
|
text: item.jellyfinId + "\n" + appWindow.playbackManager.streamUrl + "\n"
|
||||||
+ "Playback method: " + _playbackMethod + "\n"
|
+ "Playback method: " + _playbackMethod + "\n"
|
||||||
+ "Media status: " + manager.mediaStatus + "\n"
|
+ "Media status: " + manager.mediaStatus + "\n"
|
||||||
|
+ "Playback state: " + manager.playbackState + "\n"
|
||||||
// + player.bufferProgress + "\n"
|
// + player.bufferProgress + "\n"
|
||||||
// + player.metaData.videoCodec + "@" + player.metaData.videoFrameRate + "(" + player.metaData.videoBitRate + ")" + "\n"
|
// + player.metaData.videoCodec + "@" + player.metaData.videoFrameRate + "(" + player.metaData.videoBitRate + ")" + "\n"
|
||||||
// + player.metaData.audioCodec + "(" + player.metaData.audioBitRate + ")" + "\n"
|
// + player.metaData.audioCodec + "(" + player.metaData.audioBitRate + ")" + "\n"
|
||||||
|
|
|
@ -79,16 +79,17 @@ Item {
|
||||||
id: busyIndicator
|
id: busyIndicator
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
size: BusyIndicatorSize.Medium
|
size: BusyIndicatorSize.Medium
|
||||||
running: [MediaPlayer.Loading, MediaPlayer.Stalled].indexOf(manager.mediaStatus) >= 0
|
running: [J.MediaStatus.Loading, J.MediaStatus.Stalled].indexOf(manager.mediaStatus) >= 0
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton {
|
IconButton {
|
||||||
id: playPause
|
id: playPause
|
||||||
enabled: !hidden
|
enabled: !hidden
|
||||||
anchors.centerIn: parent
|
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: {
|
onClicked: {
|
||||||
if (manager.playbackState === MediaPlayer.PlayingState) {
|
console.log(manager.playbackState)
|
||||||
|
if (manager.playbackState === J.PlayerState.Playing) {
|
||||||
manager.pause()
|
manager.pause()
|
||||||
} else {
|
} else {
|
||||||
manager.play()
|
manager.play()
|
||||||
|
@ -102,7 +103,7 @@ Item {
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: progress.height
|
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 {
|
gradient: Gradient {
|
||||||
GradientStop { position: 0.0; color: Theme.rgba(palette.overlayBackgroundColor, 0.15); }
|
GradientStop { position: 0.0; color: Theme.rgba(palette.overlayBackgroundColor, 0.15); }
|
||||||
|
@ -151,11 +152,11 @@ Item {
|
||||||
onMediaStatusChanged: {
|
onMediaStatusChanged: {
|
||||||
console.log("New mediaPlayer status: " + manager.mediaStatus)
|
console.log("New mediaPlayer status: " + manager.mediaStatus)
|
||||||
switch(manager.mediaStatus) {
|
switch(manager.mediaStatus) {
|
||||||
case MediaPlayer.Loaded:
|
case J.MediaStatus.Loaded:
|
||||||
case MediaPlayer.Buffering:
|
case J.MediaStatus.Buffering:
|
||||||
show(false)
|
show(false)
|
||||||
break;
|
break;
|
||||||
case MediaPlayer.Buffered:
|
case J.MediaStatus.Buffered:
|
||||||
hide(false)
|
hide(false)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Sailfin: a Jellyfin client written using Qt
|
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
|
This library is free software; you can redistribute it and/or
|
||||||
modify it under the terms of the GNU Lesser General Public
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
@ -114,7 +114,6 @@ ApplicationWindow {
|
||||||
id: _playbackManager
|
id: _playbackManager
|
||||||
apiClient: appWindow.apiClient
|
apiClient: appWindow.apiClient
|
||||||
audioIndex: 0
|
audioIndex: 0
|
||||||
autoOpen: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
|
|
Loading…
Reference in a new issue