Die Tonspur wird jetzt bestellt, nicht abgewartet

Der Befund, der die ganze Umsetzung bestimmt: `AudioStreamIndex` kam in
der PlaybackInfo-Anfrage **nicht vor**. Der Server wählte, wir nahmen was
kam — und bei einem umgerechneten Stream kommt danach nur noch diese eine
Tonspur an. `selectAudioTrack` schob seither ins Leere, weil im Player
eine Liste mit einem Eintrag stand.

Damit ist auch erklärt, warum die Spracheinstellung beim ZDF-Fall nichts
ausrichten konnte: sie lief auf einer Auswahl, die es gar nicht gab.

`PlaybackIntent` trägt die Spurnummer jetzt mit, und die Aushandlung
nennt sie ausdrücklich. Zwei Tests halten fest, dass sie ankommt — und
dass sie ohne Wunsch weggelassen wird, statt eine Null zu schicken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-27 19:16:51 +02:00
co-authored by Claude Opus 5
parent afb4a63655
commit e61cb0403d
3 changed files with 81 additions and 4 deletions
@@ -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
@@ -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
}
}
@@ -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" })
}
}