diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 8f7a688..a45f0d1 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -17,6 +17,7 @@ set(JellyfinQt_SOURCES src/viewmodel/item.cpp src/viewmodel/itemmodel.cpp src/viewmodel/loader.cpp + src/viewmodel/mediastream.cpp src/viewmodel/modelstatus.cpp src/viewmodel/playbackmanager.cpp src/viewmodel/playlist.cpp @@ -47,6 +48,7 @@ set(JellyfinQt_HEADERS include/JellyfinQt/viewmodel/item.h include/JellyfinQt/viewmodel/itemmodel.h include/JellyfinQt/viewmodel/loader.h + include/JellyfinQt/viewmodel/mediastream.h include/JellyfinQt/viewmodel/modelstatus.h include/JellyfinQt/viewmodel/propertyhelper.h include/JellyfinQt/viewmodel/playbackmanager.h diff --git a/core/include/JellyfinQt/viewmodel/item.h b/core/include/JellyfinQt/viewmodel/item.h index 09ca93a..5f3bfb5 100644 --- a/core/include/JellyfinQt/viewmodel/item.h +++ b/core/include/JellyfinQt/viewmodel/item.h @@ -43,6 +43,7 @@ #include "../loader/http/getitem.h" #include "../loader/requesttypes.h" #include "../model/item.h" +#include "mediastream.h" #include "loader.h" namespace Jellyfin { @@ -109,8 +110,11 @@ public: Q_PROPERTY(QString seriesId READ seriesId NOTIFY seriesIdChanged) Q_PROPERTY(QString seasonId READ seasonId NOTIFY seasonIdChanged) Q_PROPERTY(QString seasonName READ seasonName NOTIFY seasonNameChanged) - /*Q_PROPERTY(QList __list__mediaStreams MEMBER __list__m_mediaStreams NOTIFY mediaStreamsChanged) - Q_PROPERTY(QVariantList mediaStreams READ mediaStreams NOTIFY mediaStreamsChanged STORED false)*/ + /*Q_PROPERTY(QList __list__mediaStreams MEMBER __list__m_mediaStreams NOTIFY mediaStreamsChanged)*/ + Q_PROPERTY(QList mediaStreams READ mediaStreams NOTIFY mediaStreamsChanged) + Q_PROPERTY(QList audioStreams READ audioStreams NOTIFY audioStreamsChanged) + Q_PROPERTY(QList videoStreams READ videoStreams NOTIFY videoStreamsChanged) + Q_PROPERTY(QList subtitleStreams READ subtitleStreams NOTIFY subtitleStreamsChanged) Q_PROPERTY(QStringList artists READ artists NOTIFY artistsChanged) // Why is this a QJsonObject? Well, because I couldn't be bothered to implement the deserialisations of // a QHash at the moment. @@ -152,6 +156,10 @@ public: QString seasonId() const { return m_data->seasonId(); } QString seasonName() const { return m_data->seasonName(); } + QObjectList mediaStreams() const { return m_allMediaStreams; } + QObjectList audioStreams() const { return m_audioStreams; } + QObjectList videoStreams() const { return m_videoStreams; } + QObjectList subtitleStreams() const { return m_subtitleStreams; } QStringList artists() const { return m_data->artists(); } QJsonObject imageTags() const { return m_data->imageTags(); } QStringList backdropImageTags() const { return m_data->backdropImageTags(); } @@ -208,7 +216,10 @@ signals: void seriesIdChanged(const QString &newSeriesId); void seasonIdChanged(const QString &newSeasonId); void seasonNameChanged(const QString &newSeasonName); - void mediaStreamsChanged(/*const QList &newMediaStreams*/); + void mediaStreamsChanged(QVariantList &newMediaStreams); + void audioStreamsChanged(QVariantList &newAudioStreams); + void videoStreamsChanged(QVariantList &newVideoStreams); + void subtitleStreamsChanged(QVariantList &newSubtitleStreams); void artistsChanged(const QStringList &newArtists); void imageTagsChanged(); void backdropImageTagsChanged(); @@ -219,9 +230,14 @@ signals: protected: void setUserData(DTO::UserItemDataDto &newData); void setUserData(QSharedPointer newData); + void updateMediaStreams(); QSharedPointer m_data; UserData *m_userData = nullptr; + QObjectList m_allMediaStreams; + QObjectList m_audioStreams; + QObjectList m_videoStreams; + QObjectList m_subtitleStreams; private slots: void onUserDataChanged(const DTO::UserItemDataDto &userData); }; diff --git a/core/include/JellyfinQt/viewmodel/itemmodel.h b/core/include/JellyfinQt/viewmodel/itemmodel.h index f9885d4..5680fad 100644 --- a/core/include/JellyfinQt/viewmodel/itemmodel.h +++ b/core/include/JellyfinQt/viewmodel/itemmodel.h @@ -34,7 +34,17 @@ #include "propertyhelper.h" // Jellyfin Forward Read/Write Property -#define FWDPROP(type, propName, propSetName) JF_FWD_RW_PROP(type, propName, propSetName, this->m_parameters) +#define FWDPROP(type, propName, propSetName) \ + public: \ + Q_PROPERTY(type propName READ propName WRITE set##propSetName NOTIFY propName##Changed) \ + type propName() const { return this->m_parameters.propName(); } \ + void set##propSetName(type newValue) { \ + this->m_parameters.set##propSetName( newValue ); \ + emit propName##Changed(); \ + autoReloadIfNeeded(); \ + } \ + Q_SIGNALS: \ + void propName##Changed(); namespace Jellyfin { @@ -161,6 +171,7 @@ public: FWDPROP(bool, isPlaceHolder, IsPlaceHolder) FWDPROP(bool, isPlayed, IsPlayed) FWDPROP(bool, isUnaired, IsUnaired) + FWDPROP(int, limit, Limit) FWDPROP(QList, locationTypes, LocationTypes) FWDPROP(qint32, maxHeight, MaxHeight) FWDPROP(QString, maxOfficialRating, MaxOfficialRating) diff --git a/core/include/JellyfinQt/viewmodel/loader.h b/core/include/JellyfinQt/viewmodel/loader.h index 651601a..13dc5d2 100644 --- a/core/include/JellyfinQt/viewmodel/loader.h +++ b/core/include/JellyfinQt/viewmodel/loader.h @@ -185,17 +185,22 @@ private: * @brief Updates the data when finished. */ void onLoaderReady() { - R newData = m_loader->result(); - if (m_dataViewModel->data()->sameAs(newData)) { - // Replace the data the model holds - m_dataViewModel->data()->replaceData(newData); - } else { - // Replace the model - using PointerType = typename decltype(m_dataViewModel->data())::Type; - m_dataViewModel = new T(this, QSharedPointer::create(newData, m_apiClient)); + try { + R newData = m_loader->result(); + if (m_dataViewModel->data()->sameAs(newData)) { + // Replace the data the model holds + m_dataViewModel->data()->replaceData(newData); + } else { + // Replace the model + using PointerType = typename decltype(m_dataViewModel->data())::Type; + m_dataViewModel = new T(this, QSharedPointer::create(newData, m_apiClient)); + } + setStatus(Ready); + emitDataChanged(); + } catch(QException &e) { + setErrorString(e.what()); + setStatus(Error); } - setStatus(Ready); - emitDataChanged(); } void onLoaderError(QString message) { diff --git a/core/include/JellyfinQt/viewmodel/mediastream.h b/core/include/JellyfinQt/viewmodel/mediastream.h new file mode 100644 index 0000000..0728fdf --- /dev/null +++ b/core/include/JellyfinQt/viewmodel/mediastream.h @@ -0,0 +1,153 @@ +/* * Sailfin: a Jellyfin client written using Qt + * Copyright (C) 2021 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_VIEWMODEL_MEDIASTREAM_H +#define JELLYFIN_VIEWMODEL_MEDIASTREAM_H + +#include +#include + +#include "../dto/mediastream.h" + +namespace Jellyfin { +namespace ViewModel { + +class MediaStream : public QObject { + Q_OBJECT +public: + explicit MediaStream(QSharedPointer data, QObject *parent = nullptr); + + Q_PROPERTY(QString codec READ codec NOTIFY codecChanged) + Q_PROPERTY(QString codecTag READ codecTag NOTIFY codecTagChanged) + Q_PROPERTY(QString language READ language NOTIFY languageChanged) + Q_PROPERTY(QString colorRange READ colorRange NOTIFY colorRangeChanged) + Q_PROPERTY(QString colorSpace READ colorSpace NOTIFY colorSpaceChanged) + Q_PROPERTY(QString colorTransfer READ colorTransfer NOTIFY colorTransferChanged) + Q_PROPERTY(QString colorPrimaries READ colorPrimaries NOTIFY colorPrimariesChanged) + Q_PROPERTY(QString comment READ comment NOTIFY commentChanged); + Q_PROPERTY(QString timeBase READ timeBase NOTIFY timeBaseChanged); + Q_PROPERTY(QString title READ title NOTIFY titleChanged); + Q_PROPERTY(QString videoRange READ videoRange NOTIFY videoRangeChanged); + Q_PROPERTY(QString localizedUndefined READ localizedUndefined NOTIFY localizedUndefinedChanged); + Q_PROPERTY(QString localizedDefault READ localizedDefault NOTIFY localizedDefaultChanged); + Q_PROPERTY(QString localizedForced READ localizedForced NOTIFY localizedForcedChanged); + Q_PROPERTY(QString displayTitle READ displayTitle NOTIFY displayTitleChanged); + Q_PROPERTY(QString nalLengthSize READ nalLengthSize NOTIFY nalLengthSizeChanged); + Q_PROPERTY(bool interlaced READ interlaced NOTIFY interlacedChanged) + Q_PROPERTY(bool avc READ avc NOTIFY avcChanged) + Q_PROPERTY(QString channelLayout READ channelLayout NOTIFY channelLayoutChanged) + Q_PROPERTY(qint32 bitRate READ bitRate NOTIFY bitRateChanged) + Q_PROPERTY(qint32 bitDepth READ bitDepth NOTIFY bitDepthChanged) + Q_PROPERTY(qint32 refFrames READ refFrames NOTIFY refFramesChanged) + Q_PROPERTY(qint32 packetLength READ packetLength NOTIFY packetLengthChanged) + Q_PROPERTY(qint32 channels READ channels NOTIFY channelsChanged) + Q_PROPERTY(qint32 sampleRate READ sampleRate NOTIFY sampleRateChanged) + Q_PROPERTY(bool isDefault READ isDefault NOTIFY isDefaultChanged) + Q_PROPERTY(bool forced READ forced NOTIFY forcedChanged) + Q_PROPERTY(qint32 width READ width NOTIFY widthChanged) + Q_PROPERTY(qint32 height READ height NOTIFY heightChanged) + Q_PROPERTY(float averageFrameRate READ averageFrameRate NOTIFY averageFrameRateChanged) + Q_PROPERTY(float realFrameRate READ realFrameRate NOTIFY realFrameRateChanged) + Q_PROPERTY(QString profile READ profile NOTIFY profileChanged) + Q_PROPERTY(Jellyfin::DTO::MediaStreamTypeClass::Value type READ type NOTIFY typeChanged) + Q_PROPERTY(QString aspectRatio READ aspectRatio NOTIFY aspectRatioChanged) + Q_PROPERTY(qint32 index READ index NOTIFY indexChanged) + + + QString codec() const { return m_data->codec(); } + QString codecTag() const { return m_data->codecTag(); } + QString language() const { return m_data->language(); } + QString colorRange() const { return m_data->colorRange(); } + QString colorSpace() const { return m_data->colorSpace(); } + QString colorTransfer() const { return m_data->colorTransfer(); } + QString colorPrimaries() const { return m_data->colorPrimaries(); } + QString comment() const { return m_data->comment(); } + QString timeBase() const { return m_data->timeBase(); } + QString title() const { return m_data->title(); } + QString videoRange() const { return m_data->videoRange(); } + QString localizedUndefined() const { return m_data->localizedUndefined(); } + QString localizedDefault() const { return m_data->localizedDefault(); } + QString localizedForced() const { return m_data->localizedForced(); } + QString displayTitle() const { return m_data->displayTitle(); } + QString nalLengthSize() const { return m_data->nalLengthSize(); } + bool interlaced() const { return m_data->isInterlaced(); } + bool avc() const { return m_data->isAVC().value_or(false); } + QString channelLayout() const { return m_data->channelLayout(); } + qint32 bitRate() const { return m_data->bitRate().value_or(-1); } + qint32 bitDepth() const { return m_data->bitDepth().value_or(-1); } + qint32 refFrames() const { return m_data->refFrames().value_or(-1); } + qint32 packetLength() const { return m_data->packetLength().value_or(-1); } + qint32 channels() const { return m_data->channels().value_or(-1); } + qint32 sampleRate() const { return m_data->sampleRate().value_or(-1); } + bool isDefault() const { return m_data->isDefault(); } + bool forced() const { return m_data->isForced(); } + qint32 width() const { return m_data->width().value_or(-1); } + qint32 height() const { return m_data->height().value_or(-1); } + float averageFrameRate() const { return m_data->averageFrameRate().value_or(-1.0); } + float realFrameRate() const { return m_data->realFrameRate().value_or(-1.0); } + QString profile() const { return m_data->profile(); } + DTO::MediaStreamType type() const { return m_data->type(); } + QString aspectRatio() const { return m_data->aspectRatio(); } + qint32 index() const { return m_data->index(); } + + +signals: + void codecChanged(const QString &newCodec); + void codecTagChanged(const QString &newCodecTag); + void languageChanged(const QString &newLanguage); + void colorRangeChanged(const QString &newColorRange); + void colorSpaceChanged(const QString &newColorSpace); + void colorTransferChanged(const QString &newColorTransfer); + void colorPrimariesChanged(const QString &newColorPrimaries); + void commentChanged(const QString &newComment); + void timeBaseChanged(const QString &newTimeBase); + void titleChanged(const QString &newTitle); + void videoRangeChanged(const QString &newVideoRanged); + void localizedUndefinedChanged(const QString &newLocalizedUndefined); + void localizedDefaultChanged(const QString &newLocalizedDefault); + void localizedForcedChanged(const QString &newLocalizedForced); + void displayTitleChanged(const QString &newDisplayTitle); + void nalLengthSizeChanged(const QString &newNalLengthSize); + void interlacedChanged(bool newInterlaced); + void avcChanged(bool newAVC); + void channelLayoutChanged(const QString &newChannelLayout); + void bitRateChanged(qint32 newBitRate); + void bitDepthChanged(qint32 newBitDepth); + void refFramesChanged(qint32 newRefFrames); + void packetLengthChanged(qint32 newPacketLength); + void channelsChanged(qint32 newChannels); + void sampleRateChanged(qint32 newSampleRate); + void isDefaultChanged(bool newIsDefault); + void forcedChanged(bool newForced); + void heightChanged(qint32 newHeight); + void widthChanged(qint32 newWidth); + void averageFrameRateChanged(float newAverageFrameRate); + void realFrameRateChanged(float newRealFrameRate); + void profileChanged(const QString &newProfile); + void typeChanged(const Jellyfin::DTO::MediaStreamTypeClass::Value newType); + void aspectRatioChanged(const QString &newAspectRatio); + void indexChanged(qint32 newIndex); + +private: + QSharedPointer m_data; +}; + + +} // ViewModel +} // Jellyfin + +#endif // JELLYFIN_VIEWMODEL_MEDIASTREAM_H diff --git a/core/src/viewmodel/item.cpp b/core/src/viewmodel/item.cpp index cd8b140..9ae87c0 100644 --- a/core/src/viewmodel/item.cpp +++ b/core/src/viewmodel/item.cpp @@ -28,6 +28,7 @@ Item::Item(QObject *parent, QSharedPointer data) m_userData(new UserData(this)){ connect(m_data.data(), &Model::Item::userDataChanged, this, &Item::onUserDataChanged); m_userData->setData(data->userData()); + updateMediaStreams(); } void Item::setData(QSharedPointer newData) { @@ -41,9 +42,39 @@ void Item::setData(QSharedPointer newData) { connect(m_data.data(), &Model::Item::userDataChanged, this, &Item::onUserDataChanged); setUserData(m_data->userData()); } + emit userDataChanged(m_userData); } +void Item::updateMediaStreams() { + m_allMediaStreams.clear(); + m_audioStreams.clear(); + m_videoStreams.clear(); + m_subtitleStreams.clear(); + const QList streams = m_data->mediaStreams(); + for (auto it = streams.cbegin(); it != streams.cend(); it++) { + MediaStream *stream = new MediaStream(QSharedPointer::create(*it), this); + + m_allMediaStreams.append(stream); + switch(stream->type()) { + case DTO::MediaStreamType::Audio: + m_audioStreams.append(stream); + break; + case DTO::MediaStreamType::Video: + m_videoStreams.append(stream); + break; + case DTO::MediaStreamType::Subtitle: + m_subtitleStreams.append(stream); + break; + default: + break; + } + } + qDebug() << m_audioStreams.size() << " audio streams, " << m_videoStreams.size() << " video streams, " + << m_subtitleStreams.size() << " subtitle streams, " << m_allMediaStreams.size() << " streams total"; + +} + void Item::setUserData(DTO::UserItemDataDto &newData) { setUserData(QSharedPointer::create(newData)); } diff --git a/core/src/viewmodel/mediastream.cpp b/core/src/viewmodel/mediastream.cpp new file mode 100644 index 0000000..007f585 --- /dev/null +++ b/core/src/viewmodel/mediastream.cpp @@ -0,0 +1,30 @@ +/* + * Sailfin: a Jellyfin client written using Qt + * Copyright (C) 2021 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/viewmodel/mediastream.h" + +namespace Jellyfin { +namespace ViewModel { + +MediaStream::MediaStream(QSharedPointer data, QObject *parent) + : QObject(parent), + m_data(data) {} + +} +} diff --git a/core/src/viewmodel/playbackmanager.cpp b/core/src/viewmodel/playbackmanager.cpp index dfec717..59fc5a0 100644 --- a/core/src/viewmodel/playbackmanager.cpp +++ b/core/src/viewmodel/playbackmanager.cpp @@ -286,6 +286,8 @@ void PlaybackManager::requestItemUrl(QSharedPointer item) { params.setEnableDirectPlay(true); params.setEnableDirectStream(true); params.setEnableTranscoding(true); + params.setAudioStreamIndex(this->m_audioIndex); + params.setSubtitleStreamIndex(this->m_subtitleIndex); loader->setParameters(params); connect(loader, &ItemUrlLoader::ready, [this, loader, item] { diff --git a/sailfish/qml/components/VideoTrackSelector.qml b/sailfish/qml/components/VideoTrackSelector.qml index 519f8a2..0083306 100644 --- a/sailfish/qml/components/VideoTrackSelector.qml +++ b/sailfish/qml/components/VideoTrackSelector.qml @@ -19,13 +19,18 @@ 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 +import nl.netsoj.chris.Jellyfin 1.0 as J Column { - property var tracks + property var audioTracks + property var videoTracks + property var subtitleTracks + + readonly property int videoTrack: videoSelector.currentItem ? videoSelector.currentItem._index : 0 readonly property int audioTrack: audioSelector.currentItem ? audioSelector.currentItem._index : 0 readonly property int subtitleTrack: subitleSelector.currentItem._index + ListModel { id: videoModel } @@ -40,13 +45,13 @@ Column { ComboBox { id: videoSelector label: qsTr("Video track") - enabled: videoModel.count > 1 + enabled: videoTracks.length > 1 menu: ContextMenu { Repeater { - model: videoModel + model: videoTracks MenuItem { - readonly property int _index: model.index - text: model.displayTitle + readonly property int _index: modelData.index + text: modelData.displayTitle } } } @@ -55,13 +60,13 @@ Column { ComboBox { id: audioSelector label: qsTr("Audio track") - enabled: audioModel.count > 1 + enabled: audioTracks.length > 1 menu: ContextMenu { Repeater { - model: audioModel + model: audioTracks MenuItem { - readonly property int _index: model.index - text: model.displayTitle + readonly property int _index: modelData.index + text: modelData.displayTitle } } } @@ -70,7 +75,7 @@ Column { ComboBox { id: subitleSelector label: qsTr("Subtitle track") - enabled: subtitleModel.count > 0 + enabled: subtitleTracks.length> 0 menu: ContextMenu { MenuItem { readonly property int _index: -1 @@ -78,39 +83,12 @@ Column { text: qsTr("Off") } Repeater { - model: subtitleModel + model: subtitleTracks MenuItem { - readonly property int _index: model.index - text: model.displayTitle + readonly property int _index: modelData.index + text: modelData.displayTitle } } } } - - onTracksChanged: { - audioModel.clear() - subtitleModel.clear() - if (typeof tracks === "undefined") { - console.log("tracks undefined") - return - } - console.log(tracks) - for(var i = 0; i < tracks.length; i++) { - var track = tracks[i]; - switch(track.type) { - case MediaStream.Video: - videoModel.append(track) - break; - case MediaStream.Audio: - audioModel.append(track) - break; - case MediaStream.Subtitle: - subtitleModel.append(track) - break; - default: - console.log("Ignored " + track.displayTitle + "(" + track.type + ")") - break; - } - } - } } diff --git a/sailfish/qml/components/videoplayer/VideoError.qml b/sailfish/qml/components/videoplayer/VideoError.qml index 4d7aa23..0e15a4c 100644 --- a/sailfish/qml/components/videoplayer/VideoError.qml +++ b/sailfish/qml/components/videoplayer/VideoError.qml @@ -26,8 +26,9 @@ Rectangle { id: videoError //FIXME: Once QTBUG-10822 is resolved, change to J.PlaybackManager property var player + property bool showError: false color: pal.palette.overlayBackgroundColor - opacity: player.error === MediaPlayer.NoError ? 0.0 : 1.0 + opacity: showError ? 1.0 : 0.0 Behavior on opacity { FadeAnimator {} } SilicaItem { @@ -88,6 +89,20 @@ Rectangle { text: qsTr("Retry") onClicked: player.play() } + + Button { + text: qsTr("Hide") + onClicked: showError = false + } + } + } + + Connections { + target: player + onErrorChanged: { + if (player.error !== MediaPlayer.NoError) { + showError = true + } } } diff --git a/sailfish/qml/pages/itemdetails/VideoPage.qml b/sailfish/qml/pages/itemdetails/VideoPage.qml index a45faab..fe80dec 100644 --- a/sailfish/qml/pages/itemdetails/VideoPage.qml +++ b/sailfish/qml/pages/itemdetails/VideoPage.qml @@ -76,7 +76,13 @@ BaseDetailPage { VideoTrackSelector { id: trackSelector width: parent.width - tracks: itemData.mediaStreams + audioTracks: itemData.audioStreams + videoTracks: itemData.videoStreams + subtitleTracks: itemData.subtitleStreams + } + + Label { + text: "Video %1, audio %2, subtitle %3".arg(trackSelector.videoTrack).arg(trackSelector.audioTrack).arg(trackSelector.subtitleTrack) } } }