[iOS] Fix External Subtitle Selection (#1445)

* | Type          | Internal Subtitles | Internal Audio | External Subtitles | External Audio |
|---------------|-----------------|--------------------|----------------|----------------|
| Transcode     |               |                  |              |              |
| DirectPlay     |               |                  |              | *             |

* WIP - GetMasterHlsVideoPlaylistParameters

* WIP

* Cleanup unused code.

* Cleanup comments.

* Remove changes to GetMasterHlsVideoPlaylistParameters

* Change to use Max instead of a magic number.

* Update MediaStream.swift

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>

* Update MediaStream.swift

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>

* Update MediaStream.swift

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>

* Update MediaStream.swift

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>

* New and Improved.

* Ensure we are using the right audio track.

---------

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
Joe Kribs 2025-03-14 11:40:18 -06:00 committed by GitHub
parent 1eef7c9ff5
commit e901317317
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 63 additions and 33 deletions

View File

@ -125,11 +125,12 @@ extension BaseItemDto {
let userSession = Container.shared.currentUserSession()! let userSession = Container.shared.currentUserSession()!
let testStartTime = Date() 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 testDuration = Date().timeIntervalSince(testStartTime)
let testSizeBits = Double(testSize * 8) let testSizeBits = Double(testSize * 8)
let testBitrate = testSizeBits / testDuration let testBitrate = testSizeBits / testDuration
return Int(testBitrate) /// Exceeding 500 mbps will produce an invalid URL
return min(Int(testBitrate), PlaybackBitrate.max.rawValue)
} }
} }

View File

@ -216,37 +216,59 @@ extension MediaStream {
extension [MediaStream] { extension [MediaStream] {
// TODO: explain why adjustment is necessary /// Adjusts track indexes for a full set of media streams.
func adjustExternalSubtitleIndexes(audioStreamCount: Int) -> [MediaStream] { /// For non-transcode stream types:
guard allSatisfy({ $0.type == .subtitle }) else { return self } /// Internal tracks (non-external) are ordered as: Video, Audio, Subtitles, then any others.
let embeddedSubtitleCount = filter { !($0.isExternal ?? false) }.count /// 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() { let subtitleInternal = internalTracks.filter { $0.type == .subtitle }
guard mediaStream.isExternal ?? false else { continue }
var copy = mediaStream
copy.index = (copy.index ?? 0) + 1 + embeddedSubtitleCount + audioStreamCount
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 var newInternalTracks: [MediaStream] = []
} for (index, var track) in orderedInternal.enumerated() {
track.index = index
// TODO: explain why adjustment is necessary newInternalTracks.append(track)
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
} }
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 { var has4KVideo: Bool {

View File

@ -34,7 +34,7 @@ extension SubtitleProfile {
@ArrayBuilder<SubtitleFormat> containers: () -> [SubtitleFormat] @ArrayBuilder<SubtitleFormat> containers: () -> [SubtitleFormat]
) -> [SubtitleProfile] { ) -> [SubtitleProfile] {
containers().map { containers().map {
SubtitleProfile(container: $0.rawValue, method: method) SubtitleProfile(container: nil, format: $0.rawValue, method: method)
} }
} }
} }

View File

@ -29,7 +29,6 @@ class VideoPlayerViewModel: ViewModel {
let streamType: StreamType let streamType: StreamType
var hlsPlaybackURL: URL { var hlsPlaybackURL: URL {
let parameters = Paths.GetMasterHlsVideoPlaylistParameters( let parameters = Paths.GetMasterHlsVideoPlaylistParameters(
isStatic: true, isStatic: true,
tag: mediaSource.eTag, tag: mediaSource.eTag,
@ -90,9 +89,11 @@ class VideoPlayerViewModel: ViewModel {
item: BaseItemDto, item: BaseItemDto,
mediaSource: MediaSourceInfo, mediaSource: MediaSourceInfo,
playSessionID: String, playSessionID: String,
// TODO: Remove?
videoStreams: [MediaStream], videoStreams: [MediaStream],
audioStreams: [MediaStream], audioStreams: [MediaStream],
subtitleStreams: [MediaStream], subtitleStreams: [MediaStream],
// <- End of Potential Remove?
selectedAudioStreamIndex: Int, selectedAudioStreamIndex: Int,
selectedSubtitleStreamIndex: Int, selectedSubtitleStreamIndex: Int,
chapters: [ChapterInfo.FullInfo], chapters: [ChapterInfo.FullInfo],
@ -102,11 +103,17 @@ class VideoPlayerViewModel: ViewModel {
self.mediaSource = mediaSource self.mediaSource = mediaSource
self.playSessionID = playSessionID self.playSessionID = playSessionID
self.playbackURL = playbackURL self.playbackURL = playbackURL
self.videoStreams = videoStreams
self.audioStreams = audioStreams guard let mediaStreams = mediaSource.mediaStreams else {
.adjustAudioForExternalSubtitles(externalMediaStreamCount: subtitleStreams.filter { $0.isExternal ?? false }.count) fatalError("Media source does not have any streams")
self.subtitleStreams = subtitleStreams }
.adjustExternalSubtitleIndexes(audioStreamCount: audioStreams.count)
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.selectedAudioStreamIndex = selectedAudioStreamIndex
self.selectedSubtitleStreamIndex = selectedSubtitleStreamIndex self.selectedSubtitleStreamIndex = selectedSubtitleStreamIndex
self.chapters = chapters self.chapters = chapters