mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2025-09-05 18:22:46 +00:00
Added Direct Play and websocket improvements
* [backend]: Websocket now automatically tries to reconnect if connection was lost, up to 3 times. * [backend]: Move more playback and resume logic to the backend, to avoid having it in multiple places within the QML. Regression: pausing playback sometimes halts the video player for an unknown reason. * [playback]: Sailfin will try to play without the server transcoding, if possible. * [ui]: added a debug page in the settings
This commit is contained in:
parent
a244c27b1a
commit
7e77abc173
13 changed files with 204 additions and 45 deletions
|
@ -25,6 +25,8 @@ ApiClient::ApiClient(QObject *parent)
|
|||
: QObject(parent),
|
||||
m_webSocket(new WebSocket(this)) {
|
||||
m_deviceName = QHostInfo::localHostName();
|
||||
uint uuid1 = qHash(m_deviceName);
|
||||
uint uuid2 = qHash(QSysInfo::productVersion());
|
||||
m_deviceId = QUuid::createUuid().toString(); // TODO: make this not random?
|
||||
m_credManager = CredentialsManager::newInstance(this);
|
||||
|
||||
|
@ -218,23 +220,6 @@ void ApiClient::deleteSession() {
|
|||
});
|
||||
}
|
||||
|
||||
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);
|
||||
if (status >= 200 && status < 300) {
|
||||
QJsonObject data = QJsonDocument::fromJson(rep->readAll()).object();
|
||||
emit this->itemFetched(id, data);
|
||||
}
|
||||
rep->deleteLater();
|
||||
});
|
||||
connect(rep, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
|
||||
this, [id, rep, this](QNetworkReply::NetworkError error) {
|
||||
emit this->itemFetchFailed(id, error);
|
||||
rep->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
void ApiClient::postCapabilities() {
|
||||
QJsonObject capabilities;
|
||||
capabilities["SupportsPersistentIdentifier"] = false; // Technically untrue, but not implemented yet.
|
||||
|
|
|
@ -46,6 +46,18 @@ QJsonObject DeviceProfile::generateProfile() {
|
|||
using JsonPair = QPair<QString, QJsonValue>;
|
||||
QJsonObject profile;
|
||||
|
||||
QStringList audioCodes = {
|
||||
"aac",
|
||||
"flac",
|
||||
"mp2",
|
||||
"mp3",
|
||||
"oga"
|
||||
"ogg",
|
||||
"wav",
|
||||
"webma",
|
||||
"wma"
|
||||
};
|
||||
|
||||
QStringList videoAudioCodecs;
|
||||
QStringList mp4VideoCodecs;
|
||||
QStringList hlsVideoCodecs;
|
||||
|
@ -171,6 +183,7 @@ QJsonObject DeviceProfile::generateProfile() {
|
|||
}));
|
||||
|
||||
// Direct play profiles
|
||||
// Video
|
||||
QJsonArray directPlayProfiles;
|
||||
directPlayProfiles.append(QJsonObject {
|
||||
JsonPair("Container", "mp4,m4v"),
|
||||
|
@ -178,6 +191,39 @@ QJsonObject DeviceProfile::generateProfile() {
|
|||
JsonPair("VideoCodec", mp4VideoCodecs.join(',')),
|
||||
JsonPair("AudioCodec", videoAudioCodecs.join(','))
|
||||
});
|
||||
directPlayProfiles.append(QJsonObject {
|
||||
JsonPair("Container", "mkv"),
|
||||
JsonPair("Type", "Video"),
|
||||
JsonPair("VideoCodec", mp4VideoCodecs.join(',')),
|
||||
JsonPair("AudioCodec", videoAudioCodecs.join(','))
|
||||
});
|
||||
|
||||
// Audio
|
||||
for (auto it = audioCodes.begin(); it != audioCodes.end(); it++) {
|
||||
if (*it == "mp2") {
|
||||
directPlayProfiles.append(QJsonObject {
|
||||
JsonPair("Container", "mp2,mp3"),
|
||||
JsonPair("Type", "Audio"),
|
||||
JsonPair("AudioCodec", "mp2")
|
||||
});
|
||||
} else if(*it == "mp3") {
|
||||
directPlayProfiles.append(QJsonObject {
|
||||
JsonPair("Container", "mp3"),
|
||||
JsonPair("Type", "Audio"),
|
||||
JsonPair("AudioCodec", "mp3")
|
||||
});
|
||||
} else if (*it == "webma") {
|
||||
directPlayProfiles.append(QJsonObject {
|
||||
JsonPair("Container", "webma,webm"),
|
||||
JsonPair("Type", "Audio"),
|
||||
});
|
||||
} else {
|
||||
directPlayProfiles.append(QJsonObject {
|
||||
JsonPair("Container", *it),
|
||||
JsonPair("Type", "Audio")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
profile["CodecProfiles"] = codecProfiles;
|
||||
profile["ContainerProfiles"] = QJsonArray();
|
||||
|
|
|
@ -29,7 +29,10 @@ PlaybackManager::PlaybackManager(QObject *parent)
|
|||
}
|
||||
|
||||
void PlaybackManager::fetchStreamUrl() {
|
||||
if (m_item == nullptr || m_apiClient == nullptr) return;
|
||||
if (m_item == nullptr || m_apiClient == nullptr) {
|
||||
qDebug() << "Item or apiClient not set";
|
||||
return;
|
||||
}
|
||||
m_resumePosition = 0;
|
||||
if (m_resumePlayback && !m_item->property("userData").isNull()) {
|
||||
UserData* userData = qvariant_cast<UserData *>(m_item->property("userData"));
|
||||
|
@ -57,12 +60,36 @@ void PlaybackManager::fetchStreamUrl() {
|
|||
|
||||
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!
|
||||
QString streamUrl = this->m_apiClient->baseUrl()
|
||||
+ mediaSources[0].toObject()["TranscodingUrl"].toString();
|
||||
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);
|
||||
setStreamUrl(streamUrl);
|
||||
this->m_playMethod = DirectPlay;
|
||||
} else if (firstMediaSource["SupportsTranscoding"].toBool() && !firstMediaSource["TranscodingUrl"].isNull()) {
|
||||
QString streamUrl = this->m_apiClient->baseUrl()
|
||||
+ firstMediaSource["TranscodingUrl"].toString();
|
||||
|
||||
this->m_playMethod = Transcode;
|
||||
setStreamUrl(streamUrl);
|
||||
this->m_playMethod = Transcode;
|
||||
setStreamUrl(streamUrl);
|
||||
} else {
|
||||
qDebug() << "No stream url found";
|
||||
return;
|
||||
}
|
||||
qDebug() << "Found stream url: " << this->m_streamUrl;
|
||||
}
|
||||
|
||||
|
@ -71,6 +98,7 @@ void PlaybackManager::fetchStreamUrl() {
|
|||
}
|
||||
|
||||
void PlaybackManager::setItem(Item *newItem) {
|
||||
if (m_mediaPlayer != nullptr) m_mediaPlayer->stop();
|
||||
this->m_item = newItem;
|
||||
// Don't try to start fetching when we're not completely parsed yet.
|
||||
if (m_qmlIsParsingComponent) return;
|
||||
|
@ -81,8 +109,14 @@ void PlaybackManager::setItem(Item *newItem) {
|
|||
}
|
||||
// Deinitialize the streamUrl
|
||||
setStreamUrl("");
|
||||
if (newItem != nullptr && !newItem->jellyfinId().isEmpty()) {
|
||||
fetchStreamUrl();
|
||||
if (newItem != nullptr) {
|
||||
if (m_item->status() == RemoteData::Ready) {
|
||||
fetchStreamUrl();
|
||||
} else {
|
||||
connect(m_item, &RemoteData::ready, [this]() -> void {
|
||||
fetchStreamUrl();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,6 +195,10 @@ void PlaybackManager::updatePlaybackInfo() {
|
|||
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;
|
||||
|
||||
|
@ -204,6 +242,7 @@ void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
|
|||
|
||||
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) {
|
||||
fetchStreamUrl();
|
||||
|
|
|
@ -28,6 +28,9 @@ WebSocket::WebSocket(ApiClient *client)
|
|||
Q_UNUSED(error)
|
||||
qDebug() << "Connection error: " << m_webSocket.errorString();
|
||||
});
|
||||
connect(&m_webSocket, &QWebSocket::stateChanged, this, &WebSocket::onWebsocketStateChanged);
|
||||
connect(&m_keepAliveTimer, &QTimer::timeout, this, &WebSocket::sendKeepAlive);
|
||||
connect(&m_retryTimer, &QTimer::timeout, this, &WebSocket::open);
|
||||
}
|
||||
|
||||
void WebSocket::open() {
|
||||
|
@ -39,16 +42,22 @@ void WebSocket::open() {
|
|||
connectionUrl.setPath("/socket");
|
||||
connectionUrl.setQuery(query);
|
||||
m_webSocket.open(connectionUrl);
|
||||
qDebug() << "Opening WebSocket connection to " << m_webSocket.requestUrl();
|
||||
m_reconnectAttempt++;
|
||||
qDebug() << "Opening WebSocket connection to " << m_webSocket.requestUrl() << ", connect attempt " << m_reconnectAttempt;
|
||||
}
|
||||
|
||||
void WebSocket::onConnected() {
|
||||
connect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebSocket::textMessageReceived);
|
||||
m_reconnectAttempt = 0;
|
||||
}
|
||||
|
||||
void WebSocket::onDisconnected() {
|
||||
disconnect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebSocket::textMessageReceived);
|
||||
m_keepAliveTimer.stop();
|
||||
if (m_reconnectAttempt <= 3) {
|
||||
// 500, 2500, 12500
|
||||
m_retryTimer.setInterval(100 * static_cast<int>(std::pow(5., m_reconnectAttempt)));
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocket::textMessageReceived(const QString &message) {
|
||||
|
@ -112,7 +121,6 @@ void WebSocket::setupKeepAlive(int data) {
|
|||
// Data is timeout in seconds, we want to send a keepalive at half the timeout
|
||||
m_keepAliveTimer.setInterval(data * 500);
|
||||
m_keepAliveTimer.setSingleShot(false);
|
||||
connect(&m_keepAliveTimer, &QTimer::timeout, this, &WebSocket::sendKeepAlive);
|
||||
m_keepAliveTimer.start();
|
||||
sendKeepAlive();
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue