1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2024-05-12 17:12:42 +00:00
harbour-sailfin/core/src/playbackmanager.cpp

371 lines
14 KiB
C++

/*
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
*/
#include "JellyfinQt/playbackmanager.h"
#include "JellyfinQt/apimodel.h"
#include "JellyfinQt/DTO/dto.h"
#include "JellyfinQt/DTO/userdata.h"
namespace Jellyfin {
class ItemModel;
PlaybackManager::PlaybackManager(QObject *parent)
: QObject(parent), m_mediaPlayer1(new QMediaPlayer(this)), m_mediaPlayer2(new QMediaPlayer(this)) {
// Set up connections.
swapMediaPlayer();
m_updateTimer.setInterval(10000); // 10 seconds
m_updateTimer.setSingleShot(false);
connect(&m_updateTimer, &QTimer::timeout, this, &PlaybackManager::updatePlaybackInfo);
}
void PlaybackManager::fetchStreamUrl(const Item *item, bool autoOpen, const FetchCallback &callback) {
if (item == nullptr || m_apiClient == nullptr) {
qDebug() << "Item or apiClient not set";
}
m_resumePosition = 0;
if (m_resumePlayback && !item->property("userData").isNull()) {
UserData* userData = qvariant_cast<UserData *>(m_item->property("userData"));
if (userData != nullptr) {
m_resumePosition = userData->playbackPositionTicks();
}
}
QUrlQuery params;
params.addQueryItem("UserId", m_apiClient->userId());
params.addQueryItem("StartTimeTicks", QString::number(m_resumePosition));
params.addQueryItem("IsPlayback", "true");
params.addQueryItem("AutoOpenLiveStream", autoOpen? "true" : "false");
params.addQueryItem("MediaSourceId", item->jellyfinId());
params.addQueryItem("SubtitleStreamIndex", QString::number(m_subtitleIndex));
params.addQueryItem("AudioStreamIndex", QString::number(m_audioIndex));
QJsonObject root;
root["DeviceProfile"] = m_apiClient->playbackDeviceProfile();
QNetworkReply *rep = m_apiClient->post("/Items/" + item->jellyfinId() + "/PlaybackInfo", QJsonDocument(root), params);
connect(rep, &QNetworkReply::finished, this, [this, rep, callback]() {
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();
QJsonObject firstMediaSource = mediaSources[0].toObject();
//FIXME: relies on the fact that the returned transcode url always has a query!
if (firstMediaSource.isEmpty()) {
qWarning() << "No media source found";
} else if (firstMediaSource["SupportsDirectStream"].toBool()) {
QUrlQuery query;
query.addQueryItem("mediaSourceId", firstMediaSource["Id"].toString());
query.addQueryItem("deviceId", m_apiClient->m_deviceId);
query.addQueryItem("api_key", m_apiClient->token());
query.addQueryItem("Static", "True");
QString mediaType = "unknown";
if (m_item->mediaType() == "Audio") {
mediaType = "Audio";
} else if (m_item->mediaType() == "Video") {
mediaType = "Videos";
}
QString streamUrl = this->m_apiClient->baseUrl() + "/" + mediaType + "/" + m_item->jellyfinId() + "/stream."
+ firstMediaSource["Container"].toString() + "?" + query.toString(QUrl::EncodeReserved);
callback(QUrl(streamUrl), DirectPlay);
} else if (firstMediaSource["SupportsTranscoding"].toBool() && !firstMediaSource["TranscodingUrl"].isNull()) {
QString streamUrl = this->m_apiClient->baseUrl()
+ firstMediaSource["TranscodingUrl"].toString();
this->m_playMethod = Transcode;
callback(QUrl(streamUrl), Transcode);
} else {
qDebug() << "No stream url found";
return;
}
}
rep->deleteLater();
});
}
void PlaybackManager::fetchAndSetStreamUrl(const Item *item) {
fetchStreamUrl(item, m_autoOpen, [this, item](QUrl &&url, PlayMethod playbackMethod) {
if (m_item == item) {
setStreamUrl(url.toString());
m_playMethod = playbackMethod;
emit playMethodChanged(m_playMethod);
m_mediaPlayer->setMedia(QMediaContent(url));
m_mediaPlayer->play();
}
});
}
void PlaybackManager::setItem(Item *newItem) {
if (m_mediaPlayer != nullptr) m_mediaPlayer->stop();
// If we own the item, delete it.
if (m_item != nullptr && m_item->parent() == this) {
m_item->deleteLater();
}
this->m_item = newItem;
emit itemChanged(newItem);
if (m_apiClient == nullptr) {
qWarning() << "apiClient is not set on this MediaSource instance! Aborting.";
return;
}
// Deinitialize the streamUrl
setStreamUrl("");
if (newItem != nullptr) {
if (newItem->parent() != this) {
// The new item may outlive the lifetime of the element it was created on. In the Sailfish
// application for example, the player is given an Jellyfin::Item that sits on a Page on a PageStack.
// As soon as the user pops the Page from the PageStack, newItem would be destroyed. Therefore, we
// take ownership of the given newItem, as this object will usually exist throughout the lifetime of
// the application. A better solution would be to create a copy of the newItem, but no way I'm going
// to create an handwritten copy of that.
QQmlEngine::setObjectOwnership(newItem, QQmlEngine::ObjectOwnership::CppOwnership);
newItem->setParent(this);
}
if (m_item->status() == RemoteData::Ready) {
fetchAndSetStreamUrl(m_item);
} else {
connect(m_item, &RemoteData::ready, [this]() -> void {
fetchAndSetStreamUrl(m_item);
});
}
}
}
void PlaybackManager::setStreamUrl(const QString &streamUrl) {
this->m_streamUrl = streamUrl;
// Inspired by PHP naming schemes
QUrl realStreamUrl(streamUrl);
Q_ASSERT_X(realStreamUrl.isValid(), "setStreamUrl", "StreamURL Jellyfin returned is not valid");
emit streamUrlChanged(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 (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) {
// We've stopped playing the media. Post a stop signal.
m_updateTimer.stop();
postPlaybackInfo(Stopped);
setPlaybackState(QMediaPlayer::StoppedState);
} else {
postPlaybackInfo(Progress);
}
m_oldState = newState;
}
void PlaybackManager::mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus newStatus) {
emit mediaStatusChanged(newStatus);
if (newStatus == QMediaPlayer::LoadedMedia) {
m_mediaPlayer->play();
setPlaybackState(playbackState());
if (m_resumePlayback) {
qDebug() << "Resuming playback by seeking to " << (m_resumePosition / MS_TICK_FACTOR);
m_mediaPlayer->setPosition(m_resumePosition / MS_TICK_FACTOR);
}
}
}
void PlaybackManager::mediaPlayerError(QMediaPlayer::Error error) {
emit errorChanged(error);
emit errorStringChanged(m_mediaPlayer->errorString());
}
void PlaybackManager::updatePlaybackInfo() {
postPlaybackInfo(Progress);
}
void PlaybackManager::playItem(const QString &itemId) {
Item *newItem = new Item(itemId, m_apiClient, this);
QString parentId = newItem->parentId();
setItem(newItem);
ItemModel *queue = new UserItemModel(this);
setQueue(queue);
connect(newItem, &Item::ready, this, [this, queue, parentId](){
queue->setParentId(parentId);
queue->setLimit(10000);
queue->setApiClient(m_apiClient);
queue->reload();
});
connect(queue, &BaseApiModel::ready, this, [this, queue, newItem]() {
for (int i = 0; i < queue->size(); i++) {
if (queue->at(i)->jellyfinId() == newItem->jellyfinId()) {
m_queueIndex = i;
emit queueIndexChanged(m_queueIndex);
break;
}
}
});
setPlaybackState(QMediaPlayer::PlayingState);
}
void PlaybackManager::playItemInList(ItemModel *playlist, int itemIdx) {
playlist->setParent(this);
setQueue(playlist);
m_queueIndex = itemIdx;
emit queueIndexChanged(m_queueIndex);
setItem(playlist->at(itemIdx));
}
void PlaybackManager::next() {
Q_UNIMPLEMENTED();
}
void PlaybackManager::previous() {
Q_UNIMPLEMENTED();
}
void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
QJsonObject root;
if (m_item == nullptr) {
qWarning() << "Item is null. Not posting playback info";
return;
}
root["ItemId"] = m_item->jellyfinId();
root["SessionId"] = m_playSessionId;
switch(type) {
case Started: // FALLTHROUGH
case Progress:
root["IsPaused"] = m_mediaPlayer->state() != QMediaPlayer::PlayingState;
root["IsMuted"] = false;
root["AudioStreamIndex"] = m_audioIndex;
root["SubtitleStreamIndex"] = m_subtitleIndex;
root["PlayMethod"] = QVariant::fromValue(m_playMethod).toString();
root["PositionTicks"] = m_mediaPlayer->position() * MS_TICK_FACTOR;
break;
case Stopped:
root["PositionTicks"] = 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(root));
connect(rep, &QNetworkReply::finished, this, [rep](){
rep->deleteLater();
});
m_apiClient->setDefaultErrorHandler(rep);
}
void PlaybackManager::swapMediaPlayer() {
if (m_mediaPlayer != nullptr) {
disconnect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
disconnect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged);
disconnect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, &PlaybackManager::durationChanged);
disconnect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged);
disconnect(m_mediaPlayer, &QMediaPlayer::videoAvailableChanged, this, &PlaybackManager::hasVideoChanged);
// I do not like the complicated overload cast
disconnect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::error)), this, SLOT(mediaPlayerError(QmediaPlayer::error)));
}
if (m_mediaPlayer == m_mediaPlayer1) {
m_mediaPlayer = m_mediaPlayer2;
emit mediaPlayerChanged(m_mediaPlayer);
} else {
m_mediaPlayer = m_mediaPlayer1;
emit mediaPlayerChanged(m_mediaPlayer);
}
connect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged);
connect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, &PlaybackManager::durationChanged);
connect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged);
connect(m_mediaPlayer, &QMediaPlayer::videoAvailableChanged, this, &PlaybackManager::hasVideoChanged);
// I do not like the complicated overload cast
connect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::error)), this, SLOT(mediaPlayerError(QmediaPlayer::error)));
}
Item *PlaybackManager::nextItem() {
if (m_queue == nullptr) return nullptr;
// TODO: shuffle etc.
if (m_queueIndex < m_queue->size()) {
return m_queue->at(m_queueIndex + 1);
}
return nullptr;
}
void PlaybackManager::setQueue(ItemModel *model) {
if (m_queue != nullptr) {
if (QQmlEngine::objectOwnership(m_queue) == QQmlEngine::CppOwnership) {
m_queue->deleteLater();
} else {
m_queue->setParent(nullptr);
}
}
m_queue = model;
emit queueChanged(m_queue);
}
void PlaybackManager::componentComplete() {
if (m_apiClient == nullptr) qWarning() << "No ApiClient set for PlaybackManager";
m_qmlIsParsingComponent = false;
if (m_item != nullptr) {
if (m_item->status() == RemoteData::Ready) {
fetchAndSetStreamUrl(m_item);
} else {
connect(m_item, &RemoteData::ready, [this]() -> void {
fetchAndSetStreamUrl(m_item);
});
}
}
}
}