diff --git a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+VideoPlayerViewModel.swift b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+VideoPlayerViewModel.swift index 30294d02..3c3db2e9 100644 --- a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+VideoPlayerViewModel.swift +++ b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+VideoPlayerViewModel.swift @@ -125,11 +125,12 @@ extension BaseItemDto { let userSession = Container.shared.currentUserSession()! let testStartTime = Date() - try await userSession.client.send(Paths.getBitrateTestBytes(size: testSize)) + _ = try await userSession.client.send(Paths.getBitrateTestBytes(size: testSize)) let testDuration = Date().timeIntervalSince(testStartTime) let testSizeBits = Double(testSize * 8) let testBitrate = testSizeBits / testDuration - return Int(testBitrate) + /// Exceeding 500 mbps will produce an invalid URL + return min(Int(testBitrate), PlaybackBitrate.max.rawValue) } } diff --git a/Shared/Extensions/JellyfinAPI/MediaStream.swift b/Shared/Extensions/JellyfinAPI/MediaStream.swift index 117f53f4..94dcc5b2 100644 --- a/Shared/Extensions/JellyfinAPI/MediaStream.swift +++ b/Shared/Extensions/JellyfinAPI/MediaStream.swift @@ -216,37 +216,59 @@ extension MediaStream { extension [MediaStream] { - // TODO: explain why adjustment is necessary - func adjustExternalSubtitleIndexes(audioStreamCount: Int) -> [MediaStream] { - guard allSatisfy({ $0.type == .subtitle }) else { return self } - let embeddedSubtitleCount = filter { !($0.isExternal ?? false) }.count + /// Adjusts track indexes for a full set of media streams. + /// For non-transcode stream types: + /// Internal tracks (non-external) are ordered as: Video, Audio, Subtitles, then any others. + /// Their relative order within each group is preserved and indexes start at 0. + /// For transcode stream type: + /// Only the first internal video track and the first internal audio track are included, in that order. + /// In both cases, external tracks are appended in their original order with indexes continuing after internal tracks. + func adjustedTrackIndexes(for streamType: StreamType, selectedAudioStreamIndex: Int) -> [MediaStream] { + let internalTracks = self.filter { !($0.isExternal ?? false) } + let externalTracks = self.filter { $0.isExternal ?? false } - var mediaStreams = self + var orderedInternal: [MediaStream] = [] - for (i, mediaStream) in mediaStreams.enumerated() { - guard mediaStream.isExternal ?? false else { continue } - var copy = mediaStream - copy.index = (copy.index ?? 0) + 1 + embeddedSubtitleCount + audioStreamCount + let subtitleInternal = internalTracks.filter { $0.type == .subtitle } - mediaStreams[i] = copy + // TODO: Do we need this for other media types? I think movies/shows we only care about video, audio, and subtitles. + let otherInternal = internalTracks.filter { $0.type != .video && $0.type != .audio && $0.type != .subtitle } + + if streamType == .transcode { + // Only include the first video and first audio track for transcode. + let videoInternal = internalTracks.filter { $0.type == .video } + let audioInternal = internalTracks.filter { $0.type == .audio } + + if let firstVideo = videoInternal.first { + orderedInternal.append(firstVideo) + } + if let selectedAudio = audioInternal.first(where: { $0.index == selectedAudioStreamIndex }) { + orderedInternal.append(selectedAudio) + } + + orderedInternal += subtitleInternal + orderedInternal += otherInternal + } else { + let videoInternal = internalTracks.filter { $0.type == .video } + let audioInternal = internalTracks.filter { $0.type == .audio } + + orderedInternal = videoInternal + audioInternal + subtitleInternal + otherInternal } - return mediaStreams - } - - // TODO: explain why adjustment is necessary - func adjustAudioForExternalSubtitles(externalMediaStreamCount: Int) -> [MediaStream] { - guard allSatisfy({ $0.type == .audio }) else { return self } - - var mediaStreams = self - - for (i, mediaStream) in mediaStreams.enumerated() { - var copy = mediaStream - copy.index = (copy.index ?? 0) - externalMediaStreamCount - mediaStreams[i] = copy + var newInternalTracks: [MediaStream] = [] + for (index, var track) in orderedInternal.enumerated() { + track.index = index + newInternalTracks.append(track) } - return mediaStreams + var newExternalTracks: [MediaStream] = [] + let startingIndexForExternal = newInternalTracks.count + for (offset, var track) in externalTracks.enumerated() { + track.index = startingIndexForExternal + offset + newExternalTracks.append(track) + } + + return newInternalTracks + newExternalTracks } var has4KVideo: Bool { diff --git a/Shared/Extensions/JellyfinAPI/SubtitleProfile.swift b/Shared/Extensions/JellyfinAPI/SubtitleProfile.swift index fd8ec3bc..059e663c 100644 --- a/Shared/Extensions/JellyfinAPI/SubtitleProfile.swift +++ b/Shared/Extensions/JellyfinAPI/SubtitleProfile.swift @@ -34,7 +34,7 @@ extension SubtitleProfile { @ArrayBuilder containers: () -> [SubtitleFormat] ) -> [SubtitleProfile] { containers().map { - SubtitleProfile(container: $0.rawValue, method: method) + SubtitleProfile(container: nil, format: $0.rawValue, method: method) } } } diff --git a/Shared/ViewModels/VideoPlayerViewModel.swift b/Shared/ViewModels/VideoPlayerViewModel.swift index af9957b9..de45be5a 100644 --- a/Shared/ViewModels/VideoPlayerViewModel.swift +++ b/Shared/ViewModels/VideoPlayerViewModel.swift @@ -29,7 +29,6 @@ class VideoPlayerViewModel: ViewModel { let streamType: StreamType var hlsPlaybackURL: URL { - let parameters = Paths.GetMasterHlsVideoPlaylistParameters( isStatic: true, tag: mediaSource.eTag, @@ -90,9 +89,11 @@ class VideoPlayerViewModel: ViewModel { item: BaseItemDto, mediaSource: MediaSourceInfo, playSessionID: String, + // TODO: Remove? videoStreams: [MediaStream], audioStreams: [MediaStream], subtitleStreams: [MediaStream], + // <- End of Potential Remove? selectedAudioStreamIndex: Int, selectedSubtitleStreamIndex: Int, chapters: [ChapterInfo.FullInfo], @@ -102,11 +103,17 @@ class VideoPlayerViewModel: ViewModel { self.mediaSource = mediaSource self.playSessionID = playSessionID self.playbackURL = playbackURL - self.videoStreams = videoStreams - self.audioStreams = audioStreams - .adjustAudioForExternalSubtitles(externalMediaStreamCount: subtitleStreams.filter { $0.isExternal ?? false }.count) - self.subtitleStreams = subtitleStreams - .adjustExternalSubtitleIndexes(audioStreamCount: audioStreams.count) + + guard let mediaStreams = mediaSource.mediaStreams else { + fatalError("Media source does not have any streams") + } + + let adjustedStreams = mediaStreams.adjustedTrackIndexes(for: streamType, selectedAudioStreamIndex: selectedAudioStreamIndex) + + self.videoStreams = adjustedStreams.filter { $0.type == .video } + self.audioStreams = adjustedStreams.filter { $0.type == .audio } + self.subtitleStreams = adjustedStreams.filter { $0.type == .subtitle } + self.selectedAudioStreamIndex = selectedAudioStreamIndex self.selectedSubtitleStreamIndex = selectedSubtitleStreamIndex self.chapters = chapters