From 1c9c1e36d5bd1f3b45cb866b474bd9efdbabc7a1 Mon Sep 17 00:00:00 2001 From: Aiden Schembri Date: Wed, 15 Apr 2026 19:00:21 +0200 Subject: [PATCH] fix: playback and reporting for merged/multi-version media --- lib/models/information_model.dart | 24 ++++++++++++------- .../playback/direct_playback_model.dart | 6 ++--- lib/models/playback/playback_model.dart | 20 +++++++++++----- .../playback/transcode_playback_model.dart | 6 ++--- lib/models/playback/tv_playback_model.dart | 6 +++-- lib/providers/items/information_provider.dart | 5 +++- 6 files changed, 43 insertions(+), 24 deletions(-) diff --git a/lib/models/information_model.dart b/lib/models/information_model.dart index 738c3030e..fbd8ef55e 100644 --- a/lib/models/information_model.dart +++ b/lib/models/information_model.dart @@ -1,5 +1,4 @@ -// ignore_for_file: constant_identifier_names - +import 'package:collection/collection.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/util/bitrate_formatting.dart'; import 'package:fladder/util/size_formatting.dart'; @@ -16,17 +15,24 @@ class InformationModel { required this.subStreams, }); - static InformationModel? fromResponse(BaseItemDto? item) { + static InformationModel? fromResponse(BaseItemDto? item, {String? selectedMediaSourceId}) { if (item == null) return null; - var videoStreams = item.mediaStreams?.where((element) => element.type == MediaStreamType.video).toList() ?? []; - var audioStreams = item.mediaStreams?.where((element) => element.type == MediaStreamType.audio).toList() ?? []; - var subStreams = item.mediaStreams?.where((element) => element.type == MediaStreamType.subtitle).toList() ?? []; + + final source = (selectedMediaSourceId != null) + ? (item.mediaSources?.firstWhereOrNull((e) => e.id == selectedMediaSourceId) ?? item.mediaSources?.firstOrNull) + : item.mediaSources?.firstOrNull; + + final streams = source?.mediaStreams ?? item.mediaStreams ?? []; + + var videoStreams = streams.where((element) => element.type == MediaStreamType.video).toList(); + var audioStreams = streams.where((element) => element.type == MediaStreamType.audio).toList(); + var subStreams = streams.where((element) => element.type == MediaStreamType.subtitle).toList(); return InformationModel( baseInformation: { "Title": item.name, - "Container": item.container, - "Path": item.path, - "Size": item.mediaSources?.firstOrNull?.size.byteFormat, + "Container": source?.container ?? item.container, + "Path": source?.path ?? item.path, + "Size": source?.size.byteFormat, }, videoStreams: videoStreams .map( diff --git a/lib/models/playback/direct_playback_model.dart b/lib/models/playback/direct_playback_model.dart index 19edfbfea..54722c2f1 100644 --- a/lib/models/playback/direct_playback_model.dart +++ b/lib/models/playback/direct_playback_model.dart @@ -62,7 +62,7 @@ class DirectPlaybackModel extends PlaybackModel { body: PlaybackStartInfo( canSeek: true, itemId: item.id, - mediaSourceId: item.id, + mediaSourceId: mediaStreams?.currentVersionStream?.id ?? item.id, playSessionId: playbackInfo?.playSessionId, subtitleStreamIndex: item.streamModel?.defaultSubStreamIndex, audioStreamIndex: item.streamModel?.defaultAudioStreamIndex, @@ -84,7 +84,7 @@ class DirectPlaybackModel extends PlaybackModel { await ref.read(jellyApiProvider).sessionsPlayingStoppedPost( body: PlaybackStopInfo( itemId: item.id, - mediaSourceId: item.id, + mediaSourceId: mediaStreams?.currentVersionStream?.id ?? item.id, playSessionId: playbackInfo?.playSessionId, positionTicks: position.toRuntimeTicks, ), @@ -100,7 +100,7 @@ class DirectPlaybackModel extends PlaybackModel { body: PlaybackProgressInfo( canSeek: true, itemId: item.id, - mediaSourceId: item.id, + mediaSourceId: mediaStreams?.currentVersionStream?.id ?? item.id, playSessionId: playbackInfo?.playSessionId, subtitleStreamIndex: item.streamModel?.defaultSubStreamIndex, audioStreamIndex: item.streamModel?.defaultAudioStreamIndex, diff --git a/lib/models/playback/playback_model.dart b/lib/models/playback/playback_model.dart index 32f77e593..9178b44e8 100644 --- a/lib/models/playback/playback_model.dart +++ b/lib/models/playback/playback_model.dart @@ -13,6 +13,7 @@ import 'package:fladder/models/items/channel_model.dart'; import 'package:fladder/models/items/chapters_model.dart'; import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/item_shared_models.dart'; +import 'package:fladder/models/items/item_stream_model.dart'; import 'package:fladder/models/items/media_segments_model.dart'; import 'package:fladder/models/items/media_streams_model.dart'; import 'package:fladder/models/items/season_model.dart'; @@ -344,6 +345,8 @@ class PlaybackModelHelper { oldModel?.mediaStreams?.currentSubStream, newStreamModel?.subStreams, newStreamModel?.defaultSubStreamIndex); + final actualItem = + (item is ItemStreamModel && newStreamModel != null) ? item.copyWith(mediaStreams: newStreamModel) : item; //Native player does not allow for loading external subtitles with transcoding final isNativePlayer = @@ -351,7 +354,7 @@ class PlaybackModelHelper { final isExternalSub = newStreamModel?.currentSubStream?.isExternal == true; final Response response = await api.itemsItemIdPlaybackInfoPost( - itemId: item.id, + itemId: actualItem.id, body: PlaybackInfoDto( startTimeTicks: startPosition?.toRuntimeTicks, audioStreamIndex: audioStreamIndex, @@ -374,7 +377,11 @@ class PlaybackModelHelper { return null; } - final mediaSource = playbackInfo.mediaSources?[newStreamModel?.versionStreamIndex ?? 0]; + final requestedMediaSourceId = newStreamModel?.currentVersionStream?.id; + final mediaSource = (requestedMediaSourceId != null) + ? (playbackInfo.mediaSources?.firstWhereOrNull((element) => element.id == requestedMediaSourceId) ?? + playbackInfo.mediaSources?.firstOrNull) + : playbackInfo.mediaSources?[newStreamModel?.versionStreamIndex ?? 0]; if (mediaSource == null) { return null; @@ -396,12 +403,13 @@ class PlaybackModelHelper { if (type == PlaybackType.tv && mediaPath != null) { final tvModel = TvPlaybackModel( - channel: item as ChannelModel, + channel: actualItem as ChannelModel, isNativePlayerBackend: isNativePlayer, - item: item, + item: actualItem, queue: libraryQueue, playbackInfo: playbackInfo, media: Media(url: mediaPath), + mediaStreams: mediaStreamsWithUrls, ); tvModel.startTracking(ref); return tvModel; @@ -429,7 +437,7 @@ class PlaybackModelHelper { ); return DirectPlaybackModel( - item: item, + item: actualItem, queue: libraryQueue, mediaSegments: mediaSegments?.body, chapters: chapters, @@ -441,7 +449,7 @@ class PlaybackModelHelper { ); } else if ((mediaSource.supportsTranscoding ?? false) && mediaSource.transcodingUrl != null) { return TranscodePlaybackModel( - item: item, + item: actualItem, queue: libraryQueue, mediaSegments: mediaSegments?.body, chapters: chapters, diff --git a/lib/models/playback/transcode_playback_model.dart b/lib/models/playback/transcode_playback_model.dart index 4b0110542..b720d14c8 100644 --- a/lib/models/playback/transcode_playback_model.dart +++ b/lib/models/playback/transcode_playback_model.dart @@ -60,7 +60,7 @@ class TranscodePlaybackModel extends PlaybackModel { body: PlaybackStartInfo( canSeek: true, itemId: item.id, - mediaSourceId: item.id, + mediaSourceId: mediaStreams?.currentVersionStream?.id ?? item.id, playSessionId: playbackInfo?.playSessionId, sessionId: playbackInfo?.playSessionId, subtitleStreamIndex: item.streamModel?.defaultSubStreamIndex, @@ -83,7 +83,7 @@ class TranscodePlaybackModel extends PlaybackModel { await ref.read(jellyApiProvider).sessionsPlayingStoppedPost( body: PlaybackStopInfo( itemId: item.id, - mediaSourceId: item.id, + mediaSourceId: mediaStreams?.currentVersionStream?.id ?? item.id, playSessionId: playbackInfo?.playSessionId, positionTicks: position.toRuntimeTicks, ), @@ -99,7 +99,7 @@ class TranscodePlaybackModel extends PlaybackModel { body: PlaybackProgressInfo( canSeek: true, itemId: item.id, - mediaSourceId: item.id, + mediaSourceId: mediaStreams?.currentVersionStream?.id ?? item.id, playSessionId: playbackInfo?.playSessionId, sessionId: playbackInfo?.playSessionId, subtitleStreamIndex: item.streamModel?.defaultSubStreamIndex, diff --git a/lib/models/playback/tv_playback_model.dart b/lib/models/playback/tv_playback_model.dart index 610c74bfa..8587e9439 100644 --- a/lib/models/playback/tv_playback_model.dart +++ b/lib/models/playback/tv_playback_model.dart @@ -51,6 +51,7 @@ class TvPlaybackModel extends PlaybackModel { this.isNativePlayerBackend = false, super.media, super.queue, + super.mediaStreams, }); void startTracking(Ref ref) { @@ -261,7 +262,7 @@ class TvPlaybackModel extends PlaybackModel { body: PlaybackStartInfo( canSeek: true, itemId: item.id, - mediaSourceId: item.id, + mediaSourceId: mediaStreams?.currentVersionStream?.id ?? item.id, playSessionId: playbackInfo?.playSessionId, subtitleStreamIndex: item.streamModel?.defaultSubStreamIndex, audioStreamIndex: item.streamModel?.defaultAudioStreamIndex, @@ -283,7 +284,7 @@ class TvPlaybackModel extends PlaybackModel { await ref.read(jellyApiProvider).sessionsPlayingStoppedPost( body: PlaybackStopInfo( itemId: item.id, - mediaSourceId: item.id, + mediaSourceId: mediaStreams?.currentVersionStream?.id ?? item.id, playSessionId: playbackInfo?.playSessionId, ), ); @@ -334,5 +335,6 @@ class TvPlaybackModel extends PlaybackModel { duration: duration ?? this.duration, media: media ?? this.media, queue: queue ?? this.queue, + mediaStreams: mediaStreams, ); } diff --git a/lib/providers/items/information_provider.dart b/lib/providers/items/information_provider.dart index 94828e109..5d3d213e8 100644 --- a/lib/providers/items/information_provider.dart +++ b/lib/providers/items/information_provider.dart @@ -40,8 +40,11 @@ class InformationNotifier extends StateNotifier { Future getItemInformation(ItemBaseModel item) async { state = state.copyWith(loading: true); final response = await api.usersUserIdItemsItemIdGetBaseItem(itemId: item.id); + final selectedMediaSourceId = item.streamModel?.currentVersionStream?.id; await Future.delayed(const Duration(milliseconds: 250)); - state = state.copyWith(loading: false, model: InformationModel.fromResponse(response.body)); + state = state.copyWith( + loading: false, + model: InformationModel.fromResponse(response.body, selectedMediaSourceId: selectedMediaSourceId)); return response; } }