1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2024-05-02 04:32:42 +00:00

Add user-configurable playback settings

* PlaybackManager has been updated to workaround limitiations in
  QtMultimedia
* PlaybackManager now sends the DeviceProfile to the server when
  determining the playback url. This makes the Jellyfin server send
  information back about transcoding.
* The DeviceProfile type has been changed from an QJsonObject into the
  DTO generated by the OpenAPI descripton.
* A settings page has been added on SailfishOS that allows the user to
  configure the PlaybackManager to their whishes.
* The DebugInfo page on SailfishOS now persists its settings (closes #8)
This commit is contained in:
Chris Josten 2021-09-08 21:44:42 +02:00
parent 64ad37707c
commit 6bfe783bec
13 changed files with 388 additions and 196 deletions

View file

@ -96,7 +96,7 @@ public:
Q_PROPERTY(QString baseUrl READ baseUrl WRITE setBaseUrl NOTIFY baseUrlChanged) Q_PROPERTY(QString baseUrl READ baseUrl WRITE setBaseUrl NOTIFY baseUrlChanged)
Q_PROPERTY(bool authenticated READ authenticated WRITE setAuthenticated NOTIFY authenticatedChanged) Q_PROPERTY(bool authenticated READ authenticated WRITE setAuthenticated NOTIFY authenticatedChanged)
Q_PROPERTY(QString userId READ userId NOTIFY userIdChanged) Q_PROPERTY(QString userId READ userId NOTIFY userIdChanged)
Q_PROPERTY(QJsonObject deviceProfile READ deviceProfile NOTIFY deviceProfileChanged) Q_PROPERTY(QJsonObject deviceProfile READ deviceProfileJson NOTIFY deviceProfileChanged)
Q_PROPERTY(QString version READ version) Q_PROPERTY(QString version READ version)
Q_PROPERTY(EventBus *eventbus READ eventbus FINAL) Q_PROPERTY(EventBus *eventbus READ eventbus FINAL)
Q_PROPERTY(Jellyfin::WebSocket *websocket READ websocket FINAL) Q_PROPERTY(Jellyfin::WebSocket *websocket READ websocket FINAL)
@ -139,8 +139,9 @@ public:
*/ */
QVariantList supportedCommands() const ; QVariantList supportedCommands() const ;
void setSupportedCommands(QVariantList newSupportedCommands); void setSupportedCommands(QVariantList newSupportedCommands);
const QJsonObject &deviceProfile() const; const QJsonObject deviceProfileJson() const;
const QJsonObject &playbackDeviceProfile() const; QSharedPointer<DTO::DeviceProfile> deviceProfile() const;
const QJsonObject clientCapabilities() const;
/** /**
* @brief Retrieves the authentication token. Null QString if not authenticated. * @brief Retrieves the authentication token. Null QString if not authenticated.
* @note This is not the full authentication header, just the token. * @note This is not the full authentication header, just the token.

View file

@ -30,10 +30,12 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#include <QtMultimedia/QMediaPlayer> #include <QtMultimedia/QMediaPlayer>
#include "../dto/deviceprofile.h"
namespace Jellyfin { namespace Jellyfin {
namespace Model { namespace Model {
namespace DeviceProfile { namespace DeviceProfile {
QJsonObject generateProfile(); DTO::DeviceProfile generateProfile();
// Transport // Transport
bool supportsHls(); bool supportsHls();

View file

@ -34,7 +34,7 @@ namespace ViewModel {
class Settings : public QObjectSettingsWrapper { class Settings : public QObjectSettingsWrapper {
Q_OBJECT Q_OBJECT
Q_PROPERTY(bool allowTranscoding READ allowTranscoding WRITE setAllowTranscoding NOTIFY allowTranscodingChanged) Q_PROPERTY(bool allowTranscoding READ allowTranscoding WRITE setAllowTranscoding NOTIFY allowTranscodingChanged)
Q_PROPERTY(int maxBitRate READ maxBitRate WRITE setMaxBitRate NOTIFY maxBitRateChanged) Q_PROPERTY(int maxStreamingBitRate READ maxStreamingBitRate WRITE setMaxStreamingBitRate NOTIFY maxStreamingBitRateChanged)
public: public:
explicit Settings(ApiClient *apiClient); explicit Settings(ApiClient *apiClient);
virtual ~Settings(); virtual ~Settings();
@ -42,14 +42,14 @@ public:
bool allowTranscoding() const; bool allowTranscoding() const;
void setAllowTranscoding(bool allowTranscoding); void setAllowTranscoding(bool allowTranscoding);
int maxBitRate() const; int maxStreamingBitRate() const;
void setMaxBitRate(int newMaxBitRate); void setMaxStreamingBitRate(int newMaxBitRate);
signals: signals:
void allowTranscodingChanged(bool newAllowTranscoding); void allowTranscodingChanged(bool newAllowTranscoding);
void maxBitRateChanged(int newMaxBitRate); void maxStreamingBitRateChanged(int newMaxBitRate);
private: private:
bool m_allowTranscoding = true; bool m_allowTranscoding = true;
int m_maxBitRate = 5000000; int m_maxStreamingBitRate = 5000000;
}; };

View file

@ -19,11 +19,15 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#include "JellyfinQt/apiclient.h" #include "JellyfinQt/apiclient.h"
#include <QSharedPointer>
#include "JellyfinQt/dto/clientcapabilitiesdto.h"
#include "JellyfinQt/support/jsonconv.h" #include "JellyfinQt/support/jsonconv.h"
#include "JellyfinQt/viewmodel/settings.h" #include "JellyfinQt/viewmodel/settings.h"
#include "JellyfinQt/websocket.h" #include "JellyfinQt/websocket.h"
namespace Jellyfin { namespace Jellyfin {
class ApiClientPrivate { class ApiClientPrivate {
@ -45,8 +49,8 @@ public:
QString userId; QString userId;
bool online = true; bool online = true;
QJsonObject deviceProfile; QSharedPointer<DTO::DeviceProfile> deviceProfile;
QJsonObject playbackDeviceProfile; QSharedPointer<DTO::ClientCapabilitiesDto> clientCapabilities;
QVariantList supportedCommands; QVariantList supportedCommands;
bool authenticated = false; bool authenticated = false;
@ -70,6 +74,9 @@ ApiClient::ApiClient(QObject *parent)
connect(d->credManager, &CredentialsManager::usersListed, this, &ApiClient::credManagerUsersListed); connect(d->credManager, &CredentialsManager::usersListed, this, &ApiClient::credManagerUsersListed);
connect(d->credManager, &CredentialsManager::tokenRetrieved, this, &ApiClient::credManagerTokenRetrieved); connect(d->credManager, &CredentialsManager::tokenRetrieved, this, &ApiClient::credManagerTokenRetrieved);
generateDeviceProfile(); generateDeviceProfile();
connect(d->settings, &ViewModel::Settings::maxStreamingBitRateChanged, this, [d](qint32 newBitrate){
d->deviceProfile->setMaxStreamingBitrate(newBitrate);
});
} }
ApiClient::~ApiClient() { ApiClient::~ApiClient() {
@ -149,13 +156,18 @@ void ApiClient::setSupportedCommands(QVariantList newSupportedCommands) {
d->supportedCommands = newSupportedCommands; d->supportedCommands = newSupportedCommands;
emit supportedCommandsChanged(); emit supportedCommandsChanged();
} }
const QJsonObject &ApiClient::deviceProfile() const { QSharedPointer<DTO::DeviceProfile> ApiClient::deviceProfile() const {
Q_D(const ApiClient); Q_D(const ApiClient);
return d->deviceProfile; return d->deviceProfile;
} }
const QJsonObject &ApiClient::playbackDeviceProfile() const {
const QJsonObject ApiClient::deviceProfileJson() const {
Q_D(const ApiClient); Q_D(const ApiClient);
return d->playbackDeviceProfile; return d->deviceProfile->toJson();
}
const QJsonObject ApiClient::clientCapabilities() const {
Q_D(const ApiClient);
return d->clientCapabilities->toJson();
} }
//////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////
// BASE HTTP METHODS // // BASE HTTP METHODS //
@ -367,26 +379,7 @@ void ApiClient::deleteSession() {
void ApiClient::postCapabilities() { void ApiClient::postCapabilities() {
Q_D(const ApiClient); Q_D(const ApiClient);
QJsonObject capabilities; QNetworkReply *rep = post("/Sessions/Capabilities/Full", QJsonDocument(d->clientCapabilities->toJson()));
QList<DTO::GeneralCommandType> supportedCommands;
supportedCommands.reserve(d->supportedCommands.size());
for (int i = 0; i < d->supportedCommands.size(); i++) {
if (d->supportedCommands[i].canConvert<DTO::GeneralCommandType>()) {
supportedCommands.append(d->supportedCommands[i].value<DTO::GeneralCommandType>());
}
}
QList<int> foo = {1, 2, 3};
qDebug() << Support::toJsonValue<int>(3713);
qDebug() << Support::toJsonValue<QList<int>>(foo);
capabilities["SupportedCommands"] = Support::toJsonValue<QList<DTO::GeneralCommandType>>(supportedCommands);
capabilities["SupportsPersistentIdentifier"] = true;
capabilities["SupportsMediaControl"] = false;
capabilities["SupportsSync"] = false;
capabilities["SupportsContentUploading"] = false;
capabilities["AppStoreUrl"] = "https://chris.netsoj.nl/projects/harbour-sailfin";
capabilities["IconUrl"] = "https://chris.netsoj.nl/static/img/logo.png";
capabilities["DeviceProfile"] = d->deviceProfile;
QNetworkReply *rep = post("/Sessions/Capabilities/Full", QJsonDocument(capabilities));
setDefaultErrorHandler(rep); setDefaultErrorHandler(rep);
} }
@ -397,18 +390,33 @@ QString ApiClient::downloadUrl(const QString &itemId) const {
void ApiClient::generateDeviceProfile() { void ApiClient::generateDeviceProfile() {
Q_D(ApiClient); Q_D(ApiClient);
QJsonObject root = Model::DeviceProfile::generateProfile(); QSharedPointer<DTO::DeviceProfile> deviceProfile = QSharedPointer<DTO::DeviceProfile>::create(Model::DeviceProfile::generateProfile());
d->playbackDeviceProfile = QJsonObject(root); deviceProfile->setName(d->deviceName);
root["Name"] = d->deviceName; deviceProfile->setJellyfinId(d->deviceId);
root["Id"] = d->deviceId; deviceProfile->setFriendlyName(QSysInfo::prettyProductName());
root["FriendlyName"] = QSysInfo::prettyProductName(); deviceProfile->setMaxStreamingBitrate(d->settings->maxStreamingBitRate());
QJsonArray playableMediaTypes; d->deviceProfile = deviceProfile;
playableMediaTypes.append("Audio");
playableMediaTypes.append("Video");
playableMediaTypes.append("Photo");
root["PlayableMediaTypes"] = playableMediaTypes;
d->deviceProfile = root; QList<DTO::GeneralCommandType> supportedCommands;
supportedCommands.reserve(d->supportedCommands.size());
for (int i = 0; i < d->supportedCommands.size(); i++) {
if (d->supportedCommands[i].canConvert<DTO::GeneralCommandType>()) {
supportedCommands.append(d->supportedCommands[i].value<DTO::GeneralCommandType>());
}
}
QSharedPointer<DTO::ClientCapabilitiesDto> clientCapabilities = QSharedPointer<DTO::ClientCapabilitiesDto>::create();
clientCapabilities->setPlayableMediaTypes({"Audio", "Video", "Photo"});
clientCapabilities->setDeviceProfile(deviceProfile);
clientCapabilities->setSupportedCommands(supportedCommands);
clientCapabilities->setAppStoreUrl("https://chris.netsoj.nl/projects/harbour-sailfin");
clientCapabilities->setIconUrl("https://chris.netsoj.nl/static/img/logo.png");
clientCapabilities->setSupportsPersistentIdentifier(true);
clientCapabilities->setSupportsSync(false);
clientCapabilities->setSupportsMediaControl(false);
clientCapabilities->setSupportsContentUploading(false);
d->clientCapabilities = clientCapabilities;
emit deviceProfileChanged(); emit deviceProfileChanged();
} }

View file

@ -22,6 +22,19 @@
namespace Jellyfin { namespace Jellyfin {
namespace Model { namespace Model {
DTO::ProfileCondition createCondition(DTO::ProfileConditionValue property,
DTO::ProfileConditionType condition,
const QString &value,
bool isRequired = true) {
DTO::ProfileCondition result;
result.setProperty(property);
result.setCondition(condition);
result.setValue(value);
result.setIsRequired(isRequired);
return result;
}
bool DeviceProfile::supportsHls() { bool DeviceProfile::supportsHls() {
return true; return true;
} }
@ -43,9 +56,9 @@ int DeviceProfile::maxStreamingBitrate() {
return 5000000; return 5000000;
} }
QJsonObject DeviceProfile::generateProfile() { DTO::DeviceProfile DeviceProfile::generateProfile() {
using JsonPair = QPair<QString, QJsonValue>; using JsonPair = QPair<QString, QJsonValue>;
QJsonObject profile; DTO::DeviceProfile profile;
QStringList audioCodes = { QStringList audioCodes = {
"aac", "aac",
@ -78,161 +91,155 @@ QJsonObject DeviceProfile::generateProfile() {
videoAudioCodecs.append("mp3"); videoAudioCodecs.append("mp3");
hlsVideoAudioCodecs.append("mp3"); hlsVideoAudioCodecs.append("mp3");
} }
videoAudioCodecs.append("aac");
hlsVideoAudioCodecs.append("aac");
QJsonArray codecProfiles = {}; using CondVal = DTO::ProfileConditionValue;
codecProfiles.append(QJsonObject { using Condition = DTO::ProfileConditionType;
JsonPair("Codec", "aac"),
JsonPair("Conditions", QJsonArray {
QJsonObject {
JsonPair("Property", "IsSecondaryAudio"),
JsonPair("Condition", "Equals"),
JsonPair("Value", "false"),
JsonPair("IsRequired", false)
}
}),
JsonPair("Type", "VideoAudio")
});
codecProfiles.append(QJsonObject {
JsonPair("Codec", "h264"),
JsonPair("Conditions", QJsonArray {
QJsonObject {
JsonPair("Property", "IsAnamorphic"),
JsonPair("Condition", "NotEquals"),
JsonPair("Value", "true"),
JsonPair("IsRequired", false)
},
QJsonObject {
JsonPair("Property", "VideoProfile"),
JsonPair("Condition", "EqualsAny"),
JsonPair("Value", "baseline|constrained baseline"), //"high|main|baseline|constrained baseline"),
JsonPair("IsRequired", false),
},
QJsonObject {
JsonPair("Property", "VideoLevel"),
JsonPair("Condition", "LessThanEqual"),
JsonPair("Value", "51"),
JsonPair("IsRequired", false)
},
QJsonObject {
JsonPair("Property", "IsInterlaced"),
JsonPair("Condition", "NotEquals"),
JsonPair("Value", "true"),
JsonPair("IsRequired", false)
}
}),
JsonPair("Type", "Video")
});
QJsonArray transcodingProfiles = {};
// AAC
DTO::CodecProfile codecProfile1;
codecProfile1.setCodec("aac");
QList<DTO::ProfileCondition> codecProfile1Conditions;
codecProfile1Conditions.append(createCondition(CondVal::IsSecondaryAudio,
Condition::Equals,
"false",
false));
codecProfile1.setConditions(codecProfile1Conditions);
codecProfile1.setType(DTO::CodecType::VideoAudio);
DTO::CodecProfile codecProfile2;
codecProfile2.setCodec("h264");
codecProfile2.setConditions({
createCondition(CondVal::IsAnamorphic,
Condition::NotEquals,
"true", false),
createCondition(CondVal::VideoProfile,
Condition::EqualsAny,
"baseline|constrained baseline", false), //"high|main|baseline|constrained baseline"
createCondition(CondVal::VideoLevel,
Condition::LessThanEqual,
"51", false),
createCondition(CondVal::IsInterlaced,
Condition::NotEquals,
"true", false)
});
codecProfile2.setType(DTO::CodecType::Video);
QList<DTO::CodecProfile> codecProfiles = {
codecProfile1,
codecProfile2
};
// Hard coded nr 1: // Hard coded nr 1:
QJsonObject transcoding1; DTO::TranscodingProfile transcoding1;
transcoding1["AudioCodec"] = "aac"; transcoding1.setAudioCodec("aac");
transcoding1["BreakOnNonKeyFrames"] =true; transcoding1.setBreakOnNonKeyFrames(true);
transcoding1["Container"] = "ts"; transcoding1.setContainer("ts");
transcoding1["Context"] = "Streaming"; transcoding1.setContext(DTO::EncodingContext::Streaming);
transcoding1["MaxAudioChannels"] = "2"; transcoding1.setMaxAudioChannels("2");
transcoding1["MinSegments"] = "1"; transcoding1.setMinSegments(1);
transcoding1["Protocol"] = "hls"; transcoding1.setProtocol("hls");
transcoding1["Type"] = "Audio"; transcoding1.setType(DTO::DlnaProfileType::Audio);
transcodingProfiles.append(transcoding1);
// Hard code nr 2 // Hard code nr 2
transcodingProfiles.append(QJsonObject({ DTO::TranscodingProfile transcoding2;
JsonPair("AudioCodec", "mp3,aac"), transcoding2.setAudioCodec("mp3,aac");
JsonPair("BreakOnNonKeyFrames", true), transcoding2.setBreakOnNonKeyFrames(true);
JsonPair("Container", "ts"), transcoding2.setContainer("ts");
JsonPair("Context", "Streaming"), transcoding2.setContext(DTO::EncodingContext::Streaming);
JsonPair("MaxAudioChannels", "2"), transcoding2.setMaxAudioChannels("2");
JsonPair("MinSegments", 1), transcoding2.setMinSegments(1);
JsonPair("Protocol", "hls"), transcoding2.setProtocol("hls");
JsonPair("Type", "Video"), transcoding2.setType(DTO::DlnaProfileType::Video);
JsonPair("VideoCodec", "h264") transcoding2.setVideoCodec("h264");
}));
// Fallback // Fallback
transcodingProfiles.append(QJsonObject { DTO::TranscodingProfile transcoding3;
JsonPair("Container", "mp4"), transcoding3.setContainer("mp4");
JsonPair("Type", "Video"), transcoding3.setType(DTO::DlnaProfileType::Video);
JsonPair("AudioCodec", videoAudioCodecs.join(',')), transcoding3.setAudioCodec(videoAudioCodecs.join(','));
JsonPair("VideoCodec", "h264"), transcoding3.setVideoCodec("h264");
JsonPair("Context", "Static"), transcoding3.setContext(DTO::EncodingContext::Static);
JsonPair("Protocol", "http") transcoding3.setProtocol("http");
});
QList<DTO::TranscodingProfile> transcodingProfiles = {
transcoding1, transcoding2, transcoding3
};
if (supportsHls() && !hlsVideoAudioCodecs.isEmpty()) { if (supportsHls() && !hlsVideoAudioCodecs.isEmpty()) {
transcodingProfiles.append(QJsonObject { DTO::TranscodingProfile transcoding4;
JsonPair("Container", "ts"), transcoding4.setContainer("ts");
JsonPair("Type", "Video"), transcoding4.setType(DTO::DlnaProfileType::Video);
JsonPair("AudioCodec", hlsVideoAudioCodecs.join(",")), transcoding4.setAudioCodec(hlsVideoAudioCodecs.join(','));
JsonPair("VideoCodec", hlsVideoCodecs.join(",")), transcoding4.setVideoCodec(hlsVideoCodecs.join(','));
JsonPair("Context", "Streaming"), transcoding4.setContext(DTO::EncodingContext::Streaming);
JsonPair("Protocol", "hls"), transcoding4.setProtocol("hls");
JsonPair("MaxAudioChannels", "2"), transcoding4.setMaxAudioChannels("2");
JsonPair("MinSegments", "1"), transcoding4.setMinSegments(1);
JsonPair("BreakOnNonKeyFrames", true) transcoding4.setBreakOnNonKeyFrames(true);
}); transcodingProfiles.append(transcoding4);
} }
// Response profiles (or whatever it actually does?) // Response profiles (or whatever it actually does?)
QJsonArray responseProfiles = {}; DTO::ResponseProfile responseProfile1;
responseProfiles.append(QJsonObject({ responseProfile1.setType(DTO::DlnaProfileType::Video);
JsonPair("Type", "Video"), responseProfile1.setContainer("m4v");
JsonPair("Container", "m4v"), responseProfile1.setMimeType("video/mp4");
JsonPair("MimeType", "video/mp4") QList<DTO::ResponseProfile> responseProfiles = {
})); responseProfile1
};
// Direct play profiles // Direct play profiles
// Video // Video
QJsonArray directPlayProfiles; DTO::DirectPlayProfile directPlayProfile1;
directPlayProfiles.append(QJsonObject { directPlayProfile1.setContainer("mp4,m4v");
JsonPair("Container", "mp4,m4v"), directPlayProfile1.setType(DTO::DlnaProfileType::Video);
JsonPair("Type", "Video"), directPlayProfile1.setVideoCodec(mp4VideoCodecs.join(','));
JsonPair("VideoCodec", mp4VideoCodecs.join(',')), directPlayProfile1.setAudioCodec(videoAudioCodecs.join(','));
JsonPair("AudioCodec", videoAudioCodecs.join(','))
});
directPlayProfiles.append(QJsonObject {
JsonPair("Container", "mkv"),
JsonPair("Type", "Video"),
JsonPair("VideoCodec", mp4VideoCodecs.join(',')),
JsonPair("AudioCodec", videoAudioCodecs.join(','))
});
DTO::DirectPlayProfile directPlayProfile2;
directPlayProfile2.setContainer("mkv");
directPlayProfile2.setType(DTO::DlnaProfileType::Video);
directPlayProfile2.setVideoCodec(mp4VideoCodecs.join(','));
directPlayProfile2.setAudioCodec(videoAudioCodecs.join(','));
QList<DTO::DirectPlayProfile> directPlayProfiles = {
directPlayProfile1, directPlayProfile2
};
// Audio // Audio
for (auto it = audioCodes.begin(); it != audioCodes.end(); it++) { for (auto it = audioCodes.begin(); it != audioCodes.end(); it++) {
if (*it == "mp2") { if (*it == "mp2") {
directPlayProfiles.append(QJsonObject { DTO::DirectPlayProfile profile;
JsonPair("Container", "mp2,mp3"), profile.setContainer("mp2,mp3");
JsonPair("Type", "Audio"), profile.setType(DTO::DlnaProfileType::Audio);
JsonPair("AudioCodec", "mp2") profile.setAudioCodec("mp2");
}); directPlayProfiles.append(profile);
} else if(*it == "mp3") { } else if(*it == "mp3") {
directPlayProfiles.append(QJsonObject { DTO::DirectPlayProfile profile;
JsonPair("Container", "mp3"), profile.setContainer("mp3");
JsonPair("Type", "Audio"), profile.setType(DTO::DlnaProfileType::Audio);
JsonPair("AudioCodec", "mp3") profile.setAudioCodec("mp3");
}); directPlayProfiles.append(profile);
} else if (*it == "webma") { } else if (*it == "webma") {
directPlayProfiles.append(QJsonObject { DTO::DirectPlayProfile profile;
JsonPair("Container", "webma,webm"), profile.setContainer("webma,webm");
JsonPair("Type", "Audio"), profile.setType(DTO::DlnaProfileType::Audio);
}); directPlayProfiles.append(profile);
} else { } else {
directPlayProfiles.append(QJsonObject { DTO::DirectPlayProfile profile;
JsonPair("Container", *it), profile.setContainer(*it);
JsonPair("Type", "Audio") profile.setType(DTO::DlnaProfileType::Audio);
}); directPlayProfiles.append(profile);
} }
} }
profile["CodecProfiles"] = codecProfiles; profile.setCodecProfiles(codecProfiles);
profile["ContainerProfiles"] = QJsonArray(); //profile["ContainerProfiles"] = QJsonArray();
profile["DirectPlayProfiles"] = directPlayProfiles; profile.setDirectPlayProfiles(directPlayProfiles);
profile["ResponseProfiles"] = responseProfiles; profile.setResponseProfiles(responseProfiles);
profile["SubtitleProfiles"] = QJsonArray(); //profile["SubtitleProfiles"] = QJsonArray();
profile["TranscodingProfiles"] = transcodingProfiles; profile.setTranscodingProfiles(transcodingProfiles);
profile["MaxStreamingBitrate"] = maxStreamingBitrate(); profile.setMaxStreamingBitrate(std::make_optional<qint32>(maxStreamingBitrate()));
return profile; return profile;
} }

View file

@ -24,6 +24,7 @@
// #include "JellyfinQt/DTO/dto.h" // #include "JellyfinQt/DTO/dto.h"
#include <JellyfinQt/dto/useritemdatadto.h> #include <JellyfinQt/dto/useritemdatadto.h>
#include <JellyfinQt/viewmodel/settings.h>
#include <utility> #include <utility>
namespace Jellyfin { namespace Jellyfin {
@ -89,6 +90,7 @@ void PlaybackManager::setItem(QSharedPointer<Model::Item> newItem) {
emit hasPreviousChanged(m_queue->hasPrevious()); emit hasPreviousChanged(m_queue->hasPrevious());
if (m_apiClient == nullptr) { if (m_apiClient == nullptr) {
qWarning() << "apiClient is not set on this MediaSource instance! Aborting."; qWarning() << "apiClient is not set on this MediaSource instance! Aborting.";
return; return;
} }
@ -177,6 +179,8 @@ void PlaybackManager::updatePlaybackInfo() {
void PlaybackManager::playItem(Item *item) { void PlaybackManager::playItem(Item *item) {
setItem(item->data()); setItem(item->data());
emit hasNextChanged(m_queue->hasNext());
emit hasPreviousChanged(m_queue->hasPrevious());
} }
void PlaybackManager::playItemInList(ItemModel *playlist, int index) { void PlaybackManager::playItemInList(ItemModel *playlist, int index) {
@ -186,6 +190,8 @@ void PlaybackManager::playItemInList(ItemModel *playlist, int index) {
m_queueIndex = index; m_queueIndex = index;
emit queueIndexChanged(m_queueIndex); emit queueIndexChanged(m_queueIndex);
setItem(playlist->itemAt(index)); setItem(playlist->itemAt(index));
emit hasNextChanged(m_queue->hasNext());
emit hasPreviousChanged(m_queue->hasPrevious());
} }
void PlaybackManager::skipToItemIndex(int index) { void PlaybackManager::skipToItemIndex(int index) {
@ -200,6 +206,8 @@ void PlaybackManager::skipToItemIndex(int index) {
m_queue->play(index); m_queue->play(index);
} }
setItem(m_queue->currentItem()); setItem(m_queue->currentItem());
emit hasNextChanged(m_queue->hasNext());
emit hasPreviousChanged(m_queue->hasPrevious());
} }
void PlaybackManager::next() { void PlaybackManager::next() {
@ -215,6 +223,8 @@ void PlaybackManager::next() {
setItem(m_nextItem); setItem(m_nextItem);
} }
m_mediaPlayer->play(); m_mediaPlayer->play();
emit hasNextChanged(m_queue->hasNext());
emit hasPreviousChanged(m_queue->hasPrevious());
} }
void PlaybackManager::previous() { void PlaybackManager::previous() {
@ -227,6 +237,8 @@ void PlaybackManager::previous() {
m_queue->previous(); m_queue->previous();
setItem(m_queue->currentItem()); setItem(m_queue->currentItem());
m_mediaPlayer->play(); m_mediaPlayer->play();
emit hasNextChanged(m_queue->hasNext());
emit hasPreviousChanged(m_queue->hasPrevious());
} }
void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) { void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
@ -285,21 +297,45 @@ void PlaybackManager::componentComplete() {
void PlaybackManager::requestItemUrl(QSharedPointer<Model::Item> item) { void PlaybackManager::requestItemUrl(QSharedPointer<Model::Item> item) {
ItemUrlLoader *loader = new Jellyfin::Loader::HTTP::GetPostedPlaybackInfoLoader(m_apiClient); ItemUrlLoader *loader = new Jellyfin::Loader::HTTP::GetPostedPlaybackInfoLoader(m_apiClient);
Jellyfin::Loader::GetPostedPlaybackInfoParams params; 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();
params.setItemId(item->jellyfinId()); params.setItemId(item->jellyfinId());
params.setUserId(m_apiClient->userId()); params.setUserId(m_apiClient->userId());
params.setEnableDirectPlay(true); playbackInfo->setEnableDirectPlay(true);
params.setEnableDirectStream(true); playbackInfo->setEnableDirectStream(!forceTranscoding);
params.setEnableTranscoding(true); playbackInfo->setEnableTranscoding(forceTranscoding || allowTranscoding);
params.setAudioStreamIndex(this->m_audioIndex); playbackInfo->setAudioStreamIndex(this->m_audioIndex);
params.setSubtitleStreamIndex(this->m_subtitleIndex); playbackInfo->setSubtitleStreamIndex(this->m_subtitleIndex);
playbackInfo->setDeviceProfile(m_apiClient->deviceProfile());
params.setBody(playbackInfo);
loader->setParameters(params); loader->setParameters(params);
connect(loader, &ItemUrlLoader::ready, [this, loader, item] { connect(loader, &ItemUrlLoader::ready, this, [this, loader, item] {
DTO::PlaybackInfoResponse result = loader->result(); DTO::PlaybackInfoResponse result = loader->result();
handlePlaybackInfoResponse(item->jellyfinId(), item->mediaType(), result); handlePlaybackInfoResponse(item->jellyfinId(), item->mediaType(), result);
loader->deleteLater(); loader->deleteLater();
}); });
connect(loader, &ItemUrlLoader::error, [this, loader, item](QString message) { connect(loader, &ItemUrlLoader::error, this, [this, loader, item](QString message) {
onItemErrorReceived(item->jellyfinId(), message); onItemErrorReceived(item->jellyfinId(), message);
loader->deleteLater(); loader->deleteLater();
}); });
@ -312,12 +348,43 @@ void PlaybackManager::handlePlaybackInfoResponse(QString itemId, QString mediaTy
QUrl resultingUrl; QUrl resultingUrl;
QString playSession = response.playSessionId(); QString playSession = response.playSessionId();
PlayMethod playMethod = PlayMethod::EnumNotSet; PlayMethod playMethod = PlayMethod::EnumNotSet;
bool transcodingAllowed = m_apiClient->settings()->allowTranscoding();
for (int i = 0; i < mediaSources.size(); i++) { for (int i = 0; i < mediaSources.size(); i++) {
const DTO::MediaSourceInfo &source = mediaSources.at(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;
}
}
qDebug() << "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())) { if (source.supportsDirectPlay() && QFile::exists(source.path())) {
resultingUrl = QUrl::fromLocalFile(source.path()); resultingUrl = QUrl::fromLocalFile(source.path());
playMethod = PlayMethod::DirectPlay; playMethod = PlayMethod::DirectPlay;
} else if (source.supportsDirectStream()) { } else if (source.supportsDirectStream() && !transcodePreferred) {
if (mediaType == "Video") { if (mediaType == "Video") {
mediaType.append('s'); mediaType.append('s');
} }
@ -329,7 +396,8 @@ void PlaybackManager::handlePlaybackInfoResponse(QString itemId, QString mediaTy
resultingUrl = QUrl(m_apiClient->baseUrl() + "/" + mediaType + "/" + itemId resultingUrl = QUrl(m_apiClient->baseUrl() + "/" + mediaType + "/" + itemId
+ "/stream." + source.container() + "?" + query.toString(QUrl::EncodeReserved)); + "/stream." + source.container() + "?" + query.toString(QUrl::EncodeReserved));
playMethod = PlayMethod::DirectStream; playMethod = PlayMethod::DirectStream;
} else if (source.supportsTranscoding()) { } else if (source.supportsTranscoding() && !source.transcodingUrlNull() && transcodingAllowed) {
qDebug() << "Transcoding url: " << source.transcodingUrl();
resultingUrl = QUrl(m_apiClient->baseUrl() + source.transcodingUrl()); resultingUrl = QUrl(m_apiClient->baseUrl() + source.transcodingUrl());
playMethod = PlayMethod::Transcode; playMethod = PlayMethod::Transcode;
} else { } else {
@ -341,6 +409,7 @@ void PlaybackManager::handlePlaybackInfoResponse(QString itemId, QString mediaTy
qWarning() << "Could not find suitable media source for item " << itemId; qWarning() << "Could not find suitable media source for item " << itemId;
onItemErrorReceived(itemId, tr("Cannot fetch stream URL")); onItemErrorReceived(itemId, tr("Cannot fetch stream URL"));
} else { } else {
emit playMethodChanged(playMethod);
onItemUrlReceived(itemId, resultingUrl, playSession, playMethod); onItemUrlReceived(itemId, resultingUrl, playSession, playMethod);
} }
} }

View file

@ -43,13 +43,13 @@ void Settings::setAllowTranscoding(bool allowTranscoding) {
emit allowTranscodingChanged(allowTranscoding); emit allowTranscodingChanged(allowTranscoding);
} }
int Settings::maxBitRate() const { int Settings::maxStreamingBitRate() const {
return m_maxBitRate; return m_maxStreamingBitRate;
} }
void Settings::setMaxBitRate(int newMaxBitRate) { void Settings::setMaxStreamingBitRate(int newMaxBitRate) {
m_maxBitRate = newMaxBitRate; m_maxStreamingBitRate = newMaxBitRate;
emit maxBitRateChanged(newMaxBitRate); emit maxStreamingBitRateChanged(newMaxBitRate);
} }
} // NS ViewModel } // NS ViewModel

View file

@ -34,6 +34,11 @@ Page {
Column { Column {
id: content id: content
width: parent.width width: parent.width
CheckBox {
checked: ApiClient.settings.allowTranscoding
text: "allow transcoding"
onCheckedChanged: ApiClient.settings.allowTranscoding = checked
}
Repeater { Repeater {
model: mediaLibraryModel model: mediaLibraryModel
Column { Column {

View file

@ -69,12 +69,23 @@ SilicaItem {
} }
Label { Label {
readonly property string _playbackMethod: {
switch(manager.playMethod) {
case J.PlaybackManager.DirectPlay:
return"Direct Play"
case J.PlaybackManager.Transcoding:
return "Transcoding"
case J.PlaybackManager.DirectStream:
return "Direct Stream"
default:
return "Unknown playback method"
}
}
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.horizontalPageMargin anchors.margins: Theme.horizontalPageMargin
text: item.jellyfinId + "\n" + appWindow.playbackManager.streamUrl + "\n" text: item.jellyfinId + "\n" + appWindow.playbackManager.streamUrl + "\n"
+ (manager.playMethod === J.PlaybackManager.DirectPlay ? "Direct Play" : "Transcoding") + "\n" + "Playback method: " + _playbackMethod + "\n"
+ manager.position + "\n" + "Media status: " + manager.mediaStatus + "\n"
+ manager.mediaStatus + "\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"

View file

@ -22,6 +22,7 @@ import Sailfish.Silica 1.0
import QtMultimedia 5.6 import QtMultimedia 5.6
import nl.netsoj.chris.Jellyfin 1.0 import nl.netsoj.chris.Jellyfin 1.0
import Nemo.Configuration 1.0
import Nemo.Notifications 1.0 import Nemo.Notifications 1.0
import Nemo.KeepAlive 1.2 import Nemo.KeepAlive 1.2
@ -41,9 +42,10 @@ ApplicationWindow {
property var itemData: pageStack.currentPage.itemData property var itemData: pageStack.currentPage.itemData
// Bad way to implement settings, but it'll do for now. // Bad way to implement settings, but it'll do for now.
property bool showDebugInfo: true property alias showDebugInfo: config.showDebugInfo
property bool _hidePlaybackBar: false property bool _hidePlaybackBar: false
bottomMargin: playbackBar.visibleSize bottomMargin: playbackBar.visibleSize
ApiClient { ApiClient {
id: _apiClient id: _apiClient
@ -132,6 +134,12 @@ ApplicationWindow {
Component.onCompleted: playbackBar.parent = __silica_applicationwindow_instance._rotatingItem Component.onCompleted: playbackBar.parent = __silica_applicationwindow_instance._rotatingItem
} }
ConfigurationGroup {
id: config
path: "/nl/netsoj/chris/Sailfin"
property bool showDebugInfo: false
}
//FIXME: proper error handling //FIXME: proper error handling
Connections { Connections {
target: apiClient target: apiClient

View file

@ -117,6 +117,13 @@ Page {
text: qsTr("Other") text: qsTr("Other")
} }
IconListItem {
//: Settings list item for settings related to streaming
text: qsTr("Streaming settings")
iconSource: "image://theme/icon-m-cloud-download"
onClicked: pageStack.push(Qt.resolvedUrl("settings/StreamingPage.qml"))
}
IconListItem { IconListItem {
//: Debug information settings menu itemy //: Debug information settings menu itemy
text: qsTr("Debug information") text: qsTr("Debug information")

View file

@ -18,6 +18,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/ */
import QtQuick 2.6 import QtQuick 2.6
import Sailfish.Silica 1.0 import Sailfish.Silica 1.0
import Nemo.Configuration 1.0
import nl.netsoj.chris.Jellyfin 1.0 as J import nl.netsoj.chris.Jellyfin 1.0 as J
@ -30,6 +31,12 @@ Page {
// The effective value will be restricted by ApplicationWindow.allowedOrientations // The effective value will be restricted by ApplicationWindow.allowedOrientations
allowedOrientations: Orientation.All allowedOrientations: Orientation.All
ConfigurationGroup {
id: config
path: "/nl/netsoj/chris/Sailfin"
property bool showDebugInfo: false
}
SilicaFlickable { SilicaFlickable {
anchors.fill: parent anchors.fill: parent
contentHeight: content.height contentHeight: content.height
@ -42,8 +49,8 @@ Page {
TextSwitch { TextSwitch {
text: qsTr("Show debug information") text: qsTr("Show debug information")
checked: appWindow.showDebugInfo checked: config.showDebugInfo
onCheckedChanged: appWindow.showDebugInfo = checked onCheckedChanged: config.showDebugInfo = checked
} }
SectionHeader { SectionHeader {

View file

@ -0,0 +1,67 @@
/*
Sailfin: a Jellyfin client written using Qt
Copyright (C) 2021 Chris Josten
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
*/
import QtQuick 2.6
import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0 as J
import "../../components"
import "../.."
Page {
id: page
// The effective value will be restricted by ApplicationWindow.allowedOrientations
allowedOrientations: Orientation.All
SilicaFlickable {
anchors.fill: parent
contentHeight: content.height
Column {
id: content
width: parent.width
PageHeader {
title: qsTr("Streaming settings")
}
TextSwitch {
text: qsTr("Allow transcoding")
description: qsTr("If enabled, Sailfin may request the Jellyfin server " +
"to transcode media to a more suitable media format for this device. " +
"It is recommended to leave this enabled unless your server is weak.")
checked: appWindow.apiClient.settings.allowTranscoding
onCheckedChanged: appWindow.apiClient.settings.allowTranscoding = checked
}
Slider {
minimumValue: 0
maximumValue: 64 * 1024 * 1024
stepSize: 1024 * 128
valueText: qsTr("%1 mbps").arg((value / 1024 / 1024).toPrecision(4))
value: appWindow.apiClient.settings.maxStreamingBitRate
onDownChanged: if (!down) appWindow.apiClient.settings.maxStreamingBitRate = value
label: qsTr("Maximum streaming bitrate")
width: parent.width
}
}
}
}