1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2025-09-05 10:12:46 +00:00

Added videoplayer and many unrelated things

This commit is contained in:
Chris Josten 2020-09-25 14:46:39 +02:00
parent 53b3eac213
commit 92a18c4fa5
28 changed files with 889 additions and 51 deletions

View file

@ -11,17 +11,20 @@
#include "jellyfinapiclient.h"
#include "jellyfinapimodel.h"
#include "jellyfinmediasource.h"
#include "serverdiscoverymodel.h"
void registerQml() {
const char* QML_NAMESPACE = "nl.netsoj.chris.Jellyfin";
qmlRegisterSingletonType<JellyfinApiClient>(QML_NAMESPACE, 1, 0, "ApiClient", [](QQmlEngine *eng, QJSEngine *js) {
// Singletons are perhaps bad, but they are convenient :)
qmlRegisterSingletonType<Jellyfin::ApiClient>(QML_NAMESPACE, 1, 0, "ApiClient", [](QQmlEngine *eng, QJSEngine *js) {
Q_UNUSED(eng)
Q_UNUSED(js)
return dynamic_cast<QObject*>(new JellyfinApiClient());
return dynamic_cast<QObject*>(new Jellyfin::ApiClient());
});
qmlRegisterType<ServerDiscoveryModel>(QML_NAMESPACE, 1, 0, "ServerDiscoveryModel");
qmlRegisterType<Jellyfin::ServerDiscoveryModel>(QML_NAMESPACE, 1, 0, "ServerDiscoveryModel");
qmlRegisterType<Jellyfin::MediaSource>(QML_NAMESPACE, 1, 0, "MediaSource");
// API models
Jellyfin::registerModels(QML_NAMESPACE);

View file

@ -3,11 +3,14 @@
#define STR2(x) #x
#define STR(x) STR2(x)
JellyfinApiClient::JellyfinApiClient(QObject *parent)
namespace Jellyfin {
ApiClient::ApiClient(QObject *parent)
: QObject(parent) {
m_deviceName = QHostInfo::localHostName();
m_deviceId = QUuid::createUuid().toString();
m_deviceId = QUuid::createUuid().toString(); // TODO: make this not random?
m_credManager = CredentialsManager::getInstance(this);
generateDeviceProfile();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
@ -15,8 +18,17 @@ JellyfinApiClient::JellyfinApiClient(QObject *parent)
////////////////////////////////////////////////////////////////////////////////////////////////////
void JellyfinApiClient::addBaseRequestHeaders(QNetworkRequest &request, const QString &path, const QUrlQuery &params) {
QString authentication = "MediaBrowser ";
void ApiClient::addBaseRequestHeaders(QNetworkRequest &request, const QString &path, const QUrlQuery &params) {
addTokenHeader(request);
request.setRawHeader("Accept", "application/json;"); // profile=\"CamelCase\"");
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Sailfin/%1").arg(STR(SAILFIN_VERSION)));
QString url = this->m_baseUrl + path;
if (!params.isEmpty()) url += "?" + params.toString();
request.setUrl(url);
}
void ApiClient::addTokenHeader(QNetworkRequest &request) {
QString authentication = "MediaBrowser ";
authentication += "Client=\"Sailfin\"";
authentication += ", Device=\"" + m_deviceName + "\"";
authentication += ", DeviceId=\"" + m_deviceId + "\"";
@ -25,22 +37,20 @@ void JellyfinApiClient::addBaseRequestHeaders(QNetworkRequest &request, const QS
authentication += ", token=\"" + m_token + "\"";
}
request.setRawHeader("X-Emby-Authorization", authentication.toUtf8());
request.setRawHeader("Accept", "application/json");
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Sailfin/%1").arg(STR(SAILFIN_VERSION)));
request.setUrl(this->m_baseUrl + path + "?" + params.toString());
qDebug() << "REQUEST TO: " << request.url();
}
QNetworkReply *JellyfinApiClient::get(const QString &path, const QUrlQuery &params) {
QNetworkReply *ApiClient::get(const QString &path, const QUrlQuery &params) {
QNetworkRequest req;
addBaseRequestHeaders(req, path, params);
qDebug() << "GET " << req.url();
return m_naManager.get(req);
}
QNetworkReply *JellyfinApiClient::post(const QString &path, const QJsonDocument &data) {
QNetworkReply *ApiClient::post(const QString &path, const QJsonDocument &data, const QUrlQuery &params) {
QNetworkRequest req;
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
addBaseRequestHeaders(req, path);
addBaseRequestHeaders(req, path, params);
qDebug() << "POST " << req.url();
if (data.isEmpty())
return m_naManager.post(req, QByteArray());
else {
@ -52,7 +62,7 @@ QNetworkReply *JellyfinApiClient::post(const QString &path, const QJsonDocument
// Nice to have methods //
////////////////////////////////////////////////////////////////////////////////////////////////////
void JellyfinApiClient::initialize(){
void ApiClient::restoreSavedSession(){
QObject *ctx1 = new QObject(this);
connect(m_credManager, &CredentialsManager::serversListed, ctx1, [this, ctx1](const QStringList &servers) {
qDebug() << "Servers listed: " << servers;
@ -82,6 +92,7 @@ void JellyfinApiClient::initialize(){
this->m_token = token;
this->setUserId(user);
this->setAuthenticated(true);
this->postCapabilities();
disconnect(ctx3);
}, Qt::UniqueConnection);
m_credManager->get(server, user);
@ -95,7 +106,7 @@ void JellyfinApiClient::initialize(){
m_credManager->listServers();
}
void JellyfinApiClient::setupConnection() {
void ApiClient::setupConnection() {
// First detect redirects:
// Note that this is done without calling JellyfinApiClient::get since that automatically includes the base_url,
// which is something we want to avoid here.
@ -132,7 +143,7 @@ void JellyfinApiClient::setupConnection() {
});
}
void JellyfinApiClient::getBrandingConfiguration() {
void ApiClient::getBrandingConfiguration() {
QNetworkReply *rep = get("/Branding/Configuration");
connect(rep, &QNetworkReply::finished, this, [rep, this]() {
qDebug() << "RESPONSE: " << statusCode(rep);
@ -161,7 +172,7 @@ void JellyfinApiClient::getBrandingConfiguration() {
});
}
void JellyfinApiClient::authenticate(QString username, QString password, bool storeCredentials) {
void ApiClient::authenticate(QString username, QString password, bool storeCredentials) {
QJsonObject requestData;
requestData["Username"] = username;
@ -173,9 +184,13 @@ void JellyfinApiClient::authenticate(QString username, QString password, bool st
if (status >= 200 && status < 300) {
QJsonObject authInfo = QJsonDocument::fromJson(rep->readAll()).object();
this->m_token = authInfo["AccessToken"].toString();
this->setAuthenticated(true);
// Fool this class's addRequestheaders to add the token, without
// notifying QML that we're authenticated, to prevent other requests going first.
this->m_authenticated = true;
this->setUserId(authInfo["User"].toObject()["Id"].toString());
this->postCapabilities();
this->setAuthenticated(true);
if (storeCredentials) {
m_credManager->store(this->m_baseUrl, this->m_userId, this->m_token);
@ -184,10 +199,10 @@ void JellyfinApiClient::authenticate(QString username, QString password, bool st
rep->deleteLater();
});
connect(rep, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
this, &JellyfinApiClient::defaultNetworkErrorHandler);
this, &ApiClient::defaultNetworkErrorHandler);
}
void JellyfinApiClient::fetchItem(const QString &id) {
void ApiClient::fetchItem(const QString &id) {
QNetworkReply *rep = get("/Users/" + m_userId + "/Items/" + id);
connect(rep, &QNetworkReply::finished, this, [rep, id, this]() {
int status = statusCode(rep);
@ -199,7 +214,36 @@ void JellyfinApiClient::fetchItem(const QString &id) {
});
}
void JellyfinApiClient::defaultNetworkErrorHandler(QNetworkReply::NetworkError error) {
void ApiClient::postCapabilities() {
QJsonObject capabilities;
capabilities["SupportsPersistentIdentifier"] = false; // Technically untrue, but not implemented yet.
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"] = m_deviceProfile;
QNetworkReply *rep = post("/Sessions/Capabilities/Full", QJsonDocument(capabilities));
connect(rep, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
this, &ApiClient::defaultNetworkErrorHandler);
}
void ApiClient::generateDeviceProfile() {
QJsonObject root = DeviceProfile::generateProfile();
m_playbackDeviceProfile = QJsonObject(root);
root["Name"] = m_deviceName;
root["Id"] = m_deviceId;
root["FriendlyName"] = QSysInfo::prettyProductName();
QJsonArray playableMediaTypes;
playableMediaTypes.append("Audio");
playableMediaTypes.append("Video");
playableMediaTypes.append("Photo");
root["PlayableMediaTypes"] = playableMediaTypes;
m_deviceProfile = root;
}
void ApiClient::defaultNetworkErrorHandler(QNetworkReply::NetworkError error) {
QObject *signalSender = sender();
QNetworkReply *rep = dynamic_cast<QNetworkReply *>(signalSender);
if (rep != nullptr && statusCode(rep) == 401) {
@ -209,6 +253,7 @@ void JellyfinApiClient::defaultNetworkErrorHandler(QNetworkReply::NetworkError e
}
rep->deleteLater();
}
}
#undef STR
#undef STR2

View file

@ -9,6 +9,7 @@
#include <QObject>
#include <QString>
#include <QSysInfo>
#include <QtQml>
#include <QUuid>
@ -16,12 +17,41 @@
#include <QUrlQuery>
#include "credentialmanager.h"
#include "jellyfindeviceprofile.h"
class JellyfinApiClient : public QObject {
namespace Jellyfin {
class MediaSource;
/**
* @brief An Api client for Jellyfin. Handles requests and authentication.
*
* This class should also be given to certain models and other sources, so they are able to make
* requests to the correct server.
*
* General usage is as follows:
* 1. (Optional) Call restoreSavedSession(). This will try to load previously saved credentials and connect to the server.
* If all succeeds, the property authenticated should be set to true and its signal should be emitted. All is done.
* If it fails, setupRequired will be emitted. Continue following these steps.
* 2. If opting in to manually manage the session or restoreSavedSession() failed, you'll need to set the property
* baseUrl to the root of the Jellyfin server, e.g. "https://jellyfin.example.com:8098", so not the url to the
* web interface! Nearby servers can be discovered using Jellyfin::ServerDiscoveryModel.
* 3. Call ::setupConnection(). First of all, the client will try to resolve any redirects and will update
* the baseUrl property if following redirects. Then it will emit connectionSuccess(QString). The QString from
* the signal contains a user-oriented login message configured by the user that should be displayed in the URL
* somewhere.
* 4. After ::connected is emitted, call ::authenticate(QString, QString, bool). with the username and password.
* The last boolean argument is used if you want to have the ApiClient store your credentials, so that they
* later can be used with restoreSavedSession().
* 5. If the authenticated property is set to true, you are now authenticated! If loginError() is emitted, you aren't and
* you should go back to step 4.
*
* These steps might change. I'm considering decoupling CredentialsManager from this class to clean some code up.
*/
class ApiClient : public QObject {
friend class MediaSource;
Q_OBJECT
public:
explicit JellyfinApiClient(QObject *parent = nullptr);
Q_PROPERTY(QString baseUrl MEMBER m_baseUrl NOTIFY baseUrlChanged)
explicit ApiClient(QObject *parent = nullptr);
Q_PROPERTY(QString baseUrl MEMBER m_baseUrl READ baseUrl NOTIFY baseUrlChanged)
Q_PROPERTY(bool authenticated READ authenticated WRITE setAuthenticated NOTIFY authenticatedChanged)
Q_PROPERTY(QString userId READ userId NOTIFY userIdChanged)
@ -38,7 +68,7 @@ public:
}
QNetworkReply *get(const QString &path, const QUrlQuery &params = QUrlQuery());
QNetworkReply *post(const QString &path, const QJsonDocument &data = QJsonDocument());
QNetworkReply *post(const QString &path, const QJsonDocument &data = QJsonDocument(), const QUrlQuery &params = QUrlQuery());
void getPublicUsers();
enum ApiError {
@ -48,7 +78,10 @@ public:
INVALID_PASSWORD
};
QString &baseUrl() { return this->m_baseUrl; }
QString &userId() { return m_userId; }
QJsonObject &deviceProfile() { return m_deviceProfile; }
QJsonObject &playbackDeviceProfile() { return m_playbackDeviceProfile; }
signals:
/*
* Emitted when the server requires authentication. Please authenticate your user via authenticate.
@ -79,7 +112,7 @@ public slots:
* @brief Tries to access credentials and connect to a server. If nothing has been configured yet,
* emits setupRequired();
*/
void initialize();
void restoreSavedSession();
/*
* Try to connect with the server. Tries to resolve redirects and retrieves information
* about the login procedure. Emits connectionSuccess on success, networkError or ConnectionFailed
@ -89,6 +122,11 @@ public slots:
void authenticate(QString username, QString password, bool storeCredentials = false);
void fetchItem(const QString &id);
/**
* @brief Shares the capabilities of this device to the server.
*/
void postCapabilities();
protected slots:
void defaultNetworkErrorHandler(QNetworkReply::NetworkError error);
@ -100,19 +138,49 @@ protected:
*/
void addBaseRequestHeaders(QNetworkRequest &request, const QString &path, const QUrlQuery &params = QUrlQuery());
/**
* @brief Adds the authorization to the header
* @param The request to add the header to
*/
void addTokenHeader(QNetworkRequest &request);
/**
* @brief getBrandingConfiguration Gets the login message and custom CSS (which we ignore)
*/
void getBrandingConfiguration();
/**
* @brief Generates a profile, containing the name of the application, manufacturer and most importantly,
* which media types this device supports.
*
* The actual detection of supported media types is done within jellyfindeviceprofile.cpp, since the code
* is a big mess and should be safely contained in it's own file.
*/
void generateDeviceProfile();
QString &token() { return m_token; }
private:
QNetworkAccessManager m_naManager;
/*
* State information
*/
CredentialsManager * m_credManager;
QString m_token;
QString m_deviceName;
QString m_deviceId;
QString m_userId = "";
QJsonObject m_deviceProfile;
QJsonObject m_playbackDeviceProfile;
bool m_authenticated = false;
/**
* @brief The base url of the request.
*/
QString m_baseUrl;
/*
* Setters
*/
void setAuthenticated(bool authenticated) {
this->m_authenticated = authenticated;
@ -123,14 +191,21 @@ private:
emit userIdChanged(userId);
}
bool m_authenticated = false;
QString m_baseUrl;
/*
* Utilities
*/
QNetworkAccessManager m_naManager;
/**
* @brief Returns the statusCode of a QNetworkReply
* @param The reply to obtain the statusCode of
* @return The statuscode of the reply
*
* Seriously, Qt, why is your method to obtain the status code of a request so horrendous?
*/
static inline int statusCode(QNetworkReply *rep) {
return rep->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
}
};
} // NS Jellyfin
#endif // JELLYFIN_API_CLIENT

View file

@ -82,7 +82,7 @@ void ApiModel::generateFields() {
QByteArray keyArr = keyName.toUtf8();
if (!m_roles.values().contains(keyArr)) {
m_roles.insert(i++, keyArr);
qDebug() << m_path << " adding " << keyName << " as " << ( i - 1);
//qDebug() << m_path << " adding " << keyName << " as " << ( i - 1);
}
}
this->endResetModel();

View file

@ -100,7 +100,7 @@ public:
* Subfield should be set to "data" in this example.
*/
explicit ApiModel(QString path, QString subfield, QObject *parent = nullptr);
Q_PROPERTY(JellyfinApiClient *apiClient MEMBER m_apiClient)
Q_PROPERTY(ApiClient *apiClient MEMBER m_apiClient)
Q_PROPERTY(ModelStatus status READ status NOTIFY statusChanged)
Q_PROPERTY(int limit MEMBER m_limit NOTIFY limitChanged)
Q_PROPERTY(QString parentId MEMBER m_parentId NOTIFY parentIdChanged)
@ -141,7 +141,7 @@ public slots:
*/
void reload();
protected:
JellyfinApiClient *m_apiClient = nullptr;
ApiClient *m_apiClient = nullptr;
ModelStatus m_status = Uninitialised;
QString m_path;

View file

@ -0,0 +1,172 @@
#include "jellyfindeviceprofile.h"
namespace Jellyfin {
bool DeviceProfile::supportsHls() {
return true;
}
bool DeviceProfile::canPlayH264() {
return true;
}
bool DeviceProfile::canPlayAc3() {
return true;
}
bool DeviceProfile::supportsMp3VideoAudio() {
qDebug() << "Mp3VideoAudio: " << QMediaPlayer::hasSupport("video/mp4", {"avc1.640029", "mp3"}, QMediaPlayer::StreamPlayback);
return true;
}
int DeviceProfile::maxStreamingBitrate() {
return 5000000;
}
QJsonObject DeviceProfile::generateProfile() {
using JsonPair = QPair<QString, QJsonValue>;
QJsonObject profile;
QStringList videoAudioCodecs;
QStringList mp4VideoCodecs;
QStringList hlsVideoCodecs;
QStringList hlsVideoAudioCodecs;
if (canPlayH264()) {
mp4VideoCodecs.append("h264");
hlsVideoCodecs.append("h264");
}
if (canPlayAc3()) {
videoAudioCodecs.append("ac3");
hlsVideoAudioCodecs.append("ac3");
}
if (supportsMp3VideoAudio()) {
videoAudioCodecs.append("mp3");
hlsVideoAudioCodecs.append("mp3");
}
QJsonArray codecProfiles = {};
codecProfiles.append(QJsonObject {
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("Coded", "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 = {};
// Hard coded nr 1:
QJsonObject transcoding1;
transcoding1["AudioCodec"] = "aac";
transcoding1["BreakOnNonKeyFrames"] =true;
transcoding1["Container"] = "ts";
transcoding1["Context"] = "Streaming";
transcoding1["MaxAudioChannels"] = 2;
transcoding1["MinSegments"] = 1;
transcoding1["Protocol"] = "hls";
transcoding1["Type"] = "Audio";
transcodingProfiles.append(transcoding1);
// Hard code nr 2
transcodingProfiles.append(QJsonObject({
JsonPair("AudioCodec", "mp3,aac"),
JsonPair("BreakOnNonKeyFrames", true),
JsonPair("Container", "ts"),
JsonPair("Context", "Streaming"),
JsonPair("MaxAudioChannels", 2),
JsonPair("MinSegments", 1),
JsonPair("Protocol", "hls"),
JsonPair("Type", "Video"),
JsonPair("VideoCodec", "h264")
}));
// Fallback
transcodingProfiles.append(QJsonObject {
JsonPair("Container", "mp4"),
JsonPair("Type", "Video"),
JsonPair("AudioCodec", videoAudioCodecs.join(',')),
JsonPair("VideoCodec", "h264"),
JsonPair("Context", "Static"),
JsonPair("Protocol", "http")
});
if (supportsHls() && !hlsVideoAudioCodecs.isEmpty()) {
transcodingProfiles.append(QJsonObject {
JsonPair("Container", "ts"),
JsonPair("Type", "Video"),
JsonPair("AudioCodec", hlsVideoAudioCodecs.join(",")),
JsonPair("VideoCodec", hlsVideoCodecs.join(",")),
JsonPair("Context", "Streaming"),
JsonPair("Protocol", "hls"),
JsonPair("MaxAudioChannels", 2),
JsonPair("MinSegments", 1),
JsonPair("BreakOnNonKeyFrames", true)
});
}
// Response profiles (or whatever it actually does?)
QJsonArray responseProfiles = {};
responseProfiles.append(QJsonObject({
JsonPair("Type", "Video"),
JsonPair("Container", "m4v"),
JsonPair("MimeType", "video/mp4")
}));
// Direct play profiles
QJsonArray directPlayProfiles;
directPlayProfiles.append(QJsonObject {
JsonPair("Container", "mp4,m4v"),
JsonPair("Type", "Video"),
JsonPair("VideoCodec", mp4VideoCodecs.join(',')),
JsonPair("AudioCodec", videoAudioCodecs.join(','))
});
profile["CodecProfiles"] = codecProfiles;
profile["ContainerProfiles"] = QJsonArray();
profile["DirectPlayProfiles"] = directPlayProfiles;
profile["ResponseProfiles"] = responseProfiles;
profile["SubtitleProfiles"] = QJsonArray();
profile["TranscodingProfiles"] = transcodingProfiles;
profile["MaxStreamingBitrate"] = maxStreamingBitrate();
return profile;
}
}

View file

@ -0,0 +1,33 @@
#ifndef JELLYFIN_DEVICE_PROFILE_H
#define JELLYFIN_DEVICE_PROFILE_H
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>
#include <QList>
#include <QMap>
#include <QString>
#include <QSysInfo>
#include <QtMultimedia/QMediaPlayer>
namespace Jellyfin {
namespace DeviceProfile {
QJsonObject generateProfile();
// Transport
bool supportsHls();
// Bitrate
int maxStreamingBitrate();
// Video codecs
bool canPlayH264();
bool canPlayH265();
// Audio codecs
bool canPlayAc3();
bool supportsMp3VideoAudio();
}
}
#endif // JELLYFIN_DEVICE_PROFILE_H

View file

@ -0,0 +1,84 @@
#include "jellyfinmediasource.h"
namespace Jellyfin {
MediaSource::MediaSource(QObject *parent)
: QObject(parent),
m_mediaPlayer(new QMediaPlayer(this)){
}
void MediaSource::fetchStreamUrl() {
QUrlQuery params;
params.addQueryItem("UserId", m_apiClient->userId());
params.addQueryItem("StartTimeTicks", "0");
params.addQueryItem("IsPlayback", "true");
params.addQueryItem("AutoOpenLiveStream", this->m_autoOpen ? "true" : "false");
params.addQueryItem("MediaSourceId", this->m_itemId);
params.addQueryItem("SubtitleStreamIndex", "-1");
params.addQueryItem("AudioStreamIndex", "0");
QJsonObject root;
root["DeviceProfile"] = m_apiClient->playbackDeviceProfile();
QNetworkReply *rep = m_apiClient->post("/Items/" + this->m_itemId + "/PlaybackInfo", QJsonDocument(root), params);
connect(rep, &QNetworkReply::finished, this, [this, rep]() {
QJsonObject root = QJsonDocument::fromJson(rep->readAll()).object();
this->m_playSessionId = root["PlaySessionId"].toString();
qDebug() << "Session id: " << this->m_playSessionId;
if (this->m_autoOpen) {
QJsonArray mediaSources = root["MediaSources"].toArray();
//FIXME: relies on the fact that the returned transcode url always has a query!
this->m_streamUrl = this->m_apiClient->baseUrl()
+ mediaSources[0].toObject()["TranscodingUrl"].toString();
emit this->streamUrlChanged(this->m_streamUrl);
qDebug() << "Found stream url: " << this->m_streamUrl;
/*QNetworkRequest req;
req.setUrl(this->m_streamUrl);
m_apiClient->addTokenHeader(req);
m_mediaPlayer->setMedia(QMediaContent(req));
if (m_autoPlay) m_mediaPlayer->play();*/
}
rep->deleteLater();
});
}
void MediaSource::setItemId(const QString &newItemId) {
if (m_apiClient == nullptr) {
qWarning() << "apiClient is not set on this MediaSource instance! Aborting.";
return;
}
if (m_mediaPlayer == nullptr) {
qWarning() << "mediaPlayer is not set on this MediaSource instance! Aborting.";
return;
}
this->m_itemId = newItemId;
// Deinitialize the streamUrl
setStreamUrl("");
if (!newItemId.isEmpty()) {
fetchStreamUrl();
}
}
void MediaSource::setStreamUrl(const QString &streamUrl) {
this->m_streamUrl = streamUrl;
emit streamUrlChanged(streamUrl);
}
void MediaSource::play() {
this->m_mediaPlayer->play();
}
void MediaSource::pause() {
this->m_mediaPlayer->pause();
}
void MediaSource::stop() {
this->m_mediaPlayer->stop();
}
}

62
src/jellyfinmediasource.h Normal file
View file

@ -0,0 +1,62 @@
#ifndef JELLYFIN_MEDIA_SOURCE_H
#define JELLYFIN_MEDIA_SOURCE_H
#include <QJsonArray>
#include <QJsonObject>
#include <QObject>
#include <QUrlQuery>
#include <QtMultimedia/QMediaPlayer>
#include "jellyfinapiclient.h"
namespace Jellyfin {
class MediaSource : public QObject {
Q_OBJECT
public:
explicit MediaSource(QObject *parent = nullptr);
Q_PROPERTY(ApiClient *apiClient MEMBER m_apiClient)
Q_PROPERTY(QString itemId READ itemId WRITE setItemId NOTIFY itemIdChanged)
Q_PROPERTY(QString streamUrl READ streamUrl NOTIFY streamUrlChanged)
Q_PROPERTY(bool autoOpen MEMBER m_autoOpen NOTIFY autoOpenChanged)
Q_PROPERTY(QMediaPlayer *mediaPlayer READ mediaPlayer)
Q_PROPERTY(bool autoPlay MEMBER m_autoPlay)
QString itemId() const { return m_itemId; }
void setItemId(const QString &newItemId);
QString streamUrl() const { return m_streamUrl; }
QMediaPlayer *mediaPlayer() { return m_mediaPlayer; }
signals:
void itemIdChanged(const QString &newItemId);
void streamUrlChanged(const QString &newStreamUrl);
void autoOpenChanged(bool autoOpen);
public slots:
void play();
void pause();
void stop();
private:
ApiClient *m_apiClient = nullptr;
QMediaPlayer *m_mediaPlayer = nullptr;
QString m_itemId;
QString m_streamUrl;
QString m_playSessionId;
/**
* @brief Whether to automatically open the livestream of the item;
*/
bool m_autoOpen = false;
bool m_autoPlay = false;
void fetchStreamUrl();
void setStreamUrl(const QString &streamUrl);
};
}
#endif // JELLYFIN_MEDIA_SOURCE_H

View file

@ -1,5 +1,6 @@
#include "serverdiscoverymodel.h"
namespace Jellyfin {
ServerDiscoveryModel::ServerDiscoveryModel(QObject *parent)
: QAbstractListModel (parent) {
connect(&m_socket, &QUdpSocket::readyRead, this, &ServerDiscoveryModel::on_datagramsAvailable);
@ -71,3 +72,4 @@ void ServerDiscoveryModel::on_datagramsAvailable() {
m_discoveredServers.insert(m_discoveredServers.end(), discoveredServers.begin(), discoveredServers.end());
endInsertRows();
};
}

View file

@ -12,6 +12,7 @@
#include <QHostAddress>
#include <QUdpSocket>
namespace Jellyfin {
struct ServerDiscovery {
QString name;
QString address;
@ -59,5 +60,5 @@ private:
QUdpSocket m_socket;
std::vector<ServerDiscovery> m_discoveredServers;
};
}
#endif //SERVER_DISCOVERY_MODEL_H