diff --git a/Packages/FlixyKit/Sources/JellyfinKit/JellyfinBackend+Playback.swift b/Packages/FlixyKit/Sources/JellyfinKit/JellyfinBackend+Playback.swift index 35ebb60..b996b60 100644 --- a/Packages/FlixyKit/Sources/JellyfinKit/JellyfinBackend+Playback.swift +++ b/Packages/FlixyKit/Sources/JellyfinKit/JellyfinBackend+Playback.swift @@ -39,7 +39,8 @@ extension JellyfinBackend { let info = try await playbackInfo( itemID: intent.itemID, sourceID: sourceID, - transcode: transcode + transcode: transcode, + audioStreamIndex: intent.audioStreamIndex ) let source = info.mediaSources?.first { $0.id == sourceID } ?? info.mediaSources?.first @@ -119,7 +120,8 @@ extension JellyfinBackend { func playbackInfo( itemID: String, sourceID: String, - transcode: TranscodeSettings + transcode: TranscodeSettings, + audioStreamIndex: Int? = nil ) async throws(MediaBackendError) -> JellyfinPlaybackInfo { // Jellyfin rechnet in Bit, die App in Kilobit. let bitsPerSecond = (transcode.maximumBitrate ?? 8_000) * 1_000 @@ -169,10 +171,17 @@ extension JellyfinBackend { // Minuten Fortschritt landete man bei zweiundvierzig. // Gesprungen wird an genau einer Stelle, und das ist der // Player. + // **Die Tonspur ausdrücklich nennen.** Ohne sie wählt + // der Server, und bei einem umgerechneten Stream kommt + // danach nur noch diese eine an — dann ist im Player + // nichts mehr umzustellen. Die Sprachwahl muss hier + // stattfinden oder gar nicht. URLQueryItem(name: "enableDirectPlay", value: "false"), URLQueryItem(name: "enableDirectStream", value: "false"), URLQueryItem(name: "enableTranscoding", value: "true"), - ], + ] + (audioStreamIndex.map { + [URLQueryItem(name: "audioStreamIndex", value: String($0))] + } ?? []), body: body ), as: JellyfinPlaybackInfo.self diff --git a/Packages/FlixyKit/Sources/MediaKit/MediaPlayback.swift b/Packages/FlixyKit/Sources/MediaKit/MediaPlayback.swift index 0d9e657..44dce61 100644 --- a/Packages/FlixyKit/Sources/MediaKit/MediaPlayback.swift +++ b/Packages/FlixyKit/Sources/MediaKit/MediaPlayback.swift @@ -19,6 +19,14 @@ public struct PlaybackIntent: Sendable, Equatable { public let startTime: Duration /// Ob der Server im selben Netz steht. Plex rechnet dann anders. public let isLocalConnection: Bool + /// Welche Tonspur gespielt werden soll. + /// + /// **Ohne diese Angabe wählt der Server.** Bei einem umgerechneten Stream + /// gibt es danach nur noch *eine* Tonspur — was hier nicht gesagt wird, + /// lässt sich später nicht mehr umstellen, weil im Player schlicht keine + /// zweite ankommt. Genau daran scheiterte die Sprachwahl bisher: sie lief + /// auf einer Liste mit einem Eintrag. + public let audioStreamIndex: Int? public init( itemID: String, @@ -26,7 +34,8 @@ public struct PlaybackIntent: Sendable, Equatable { file: MediaFile, transcode: TranscodeSettings? = nil, startTime: Duration = .zero, - isLocalConnection: Bool = false + isLocalConnection: Bool = false, + audioStreamIndex: Int? = nil ) { self.itemID = itemID self.versionIndex = versionIndex @@ -34,6 +43,7 @@ public struct PlaybackIntent: Sendable, Equatable { self.transcode = transcode self.startTime = startTime self.isLocalConnection = isLocalConnection + self.audioStreamIndex = audioStreamIndex } } diff --git a/Packages/FlixyKit/Tests/JellyfinKitTests/PlaybackTests.swift b/Packages/FlixyKit/Tests/JellyfinKitTests/PlaybackTests.swift index aad13bd..5fe2643 100644 --- a/Packages/FlixyKit/Tests/JellyfinKitTests/PlaybackTests.swift +++ b/Packages/FlixyKit/Tests/JellyfinKitTests/PlaybackTests.swift @@ -227,3 +227,61 @@ private final class RecordingTransport: JellyfinTransport, @unchecked Sendable { ) } } + +@Suite("Tonspur ausdrücklich bestellen") +struct AudioStreamIndexTests { + private func backend(_ transport: RecordingTransport) -> JellyfinBackend { + let client = JellyfinClient( + baseURL: URL(string: "http://server:8096")!, + identity: JellyfinIdentity( + client: "Flixy", device: "Testgerät", deviceID: "gerät-1", version: "1" + ), + token: "tok", + transport: transport + ) + return JellyfinBackend( + client: client, + account: JellyfinAccount( + userID: "u1", userName: "Test", token: "tok", isAdministrator: false + ), + serverName: "Attrappe" + ) + } + + private func intent(audio: Int?) -> PlaybackIntent { + PlaybackIntent( + itemID: "abc", + versionIndex: 0, + file: MediaFile(id: "src", key: "src", itemID: "abc"), + transcode: TranscodeSettings(maximumBitrate: 8_000, resolution: nil), + audioStreamIndex: audio + ) + } + + private func query(_ transport: RecordingTransport) throws -> [URLQueryItem] { + let asked = try #require(transport.lastRequest) + return URLComponents(url: asked.url!, resolvingAgainstBaseURL: false)?.queryItems ?? [] + } + + /// Ohne diese Angabe wählt der Server — und bei einem umgerechneten + /// Stream kommt danach nur noch **eine** Tonspur an. Dann ist im Player + /// nichts mehr umzustellen: die Wahl muss in der Aushandlung stattfinden + /// oder gar nicht. + @Test("Die Spurnummer steht in der Anfrage") + func indexIsSent() async throws { + let transport = RecordingTransport() + _ = try? await backend(transport).preparePlayback(intent(audio: 3)) + + let werte = try query(transport) + #expect(werte.contains(URLQueryItem(name: "audioStreamIndex", value: "3"))) + } + + @Test("Ohne Wunsch steht sie nicht da") + func indexIsOmitted() async throws { + let transport = RecordingTransport() + _ = try? await backend(transport).preparePlayback(intent(audio: nil)) + + let werte = try query(transport) + #expect(!werte.contains { $0.name == "audioStreamIndex" }) + } +}