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:
co-authored by
Claude Opus 5
parent
afb4a63655
commit
e61cb0403d
@@ -39,7 +39,8 @@ extension JellyfinBackend {
|
|||||||
let info = try await playbackInfo(
|
let info = try await playbackInfo(
|
||||||
itemID: intent.itemID,
|
itemID: intent.itemID,
|
||||||
sourceID: sourceID,
|
sourceID: sourceID,
|
||||||
transcode: transcode
|
transcode: transcode,
|
||||||
|
audioStreamIndex: intent.audioStreamIndex
|
||||||
)
|
)
|
||||||
|
|
||||||
let source = info.mediaSources?.first { $0.id == sourceID } ?? info.mediaSources?.first
|
let source = info.mediaSources?.first { $0.id == sourceID } ?? info.mediaSources?.first
|
||||||
@@ -119,7 +120,8 @@ extension JellyfinBackend {
|
|||||||
func playbackInfo(
|
func playbackInfo(
|
||||||
itemID: String,
|
itemID: String,
|
||||||
sourceID: String,
|
sourceID: String,
|
||||||
transcode: TranscodeSettings
|
transcode: TranscodeSettings,
|
||||||
|
audioStreamIndex: Int? = nil
|
||||||
) async throws(MediaBackendError) -> JellyfinPlaybackInfo {
|
) async throws(MediaBackendError) -> JellyfinPlaybackInfo {
|
||||||
// Jellyfin rechnet in Bit, die App in Kilobit.
|
// Jellyfin rechnet in Bit, die App in Kilobit.
|
||||||
let bitsPerSecond = (transcode.maximumBitrate ?? 8_000) * 1_000
|
let bitsPerSecond = (transcode.maximumBitrate ?? 8_000) * 1_000
|
||||||
@@ -169,10 +171,17 @@ extension JellyfinBackend {
|
|||||||
// Minuten Fortschritt landete man bei zweiundvierzig.
|
// Minuten Fortschritt landete man bei zweiundvierzig.
|
||||||
// Gesprungen wird an genau einer Stelle, und das ist der
|
// Gesprungen wird an genau einer Stelle, und das ist der
|
||||||
// Player.
|
// 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: "enableDirectPlay", value: "false"),
|
||||||
URLQueryItem(name: "enableDirectStream", value: "false"),
|
URLQueryItem(name: "enableDirectStream", value: "false"),
|
||||||
URLQueryItem(name: "enableTranscoding", value: "true"),
|
URLQueryItem(name: "enableTranscoding", value: "true"),
|
||||||
],
|
] + (audioStreamIndex.map {
|
||||||
|
[URLQueryItem(name: "audioStreamIndex", value: String($0))]
|
||||||
|
} ?? []),
|
||||||
body: body
|
body: body
|
||||||
),
|
),
|
||||||
as: JellyfinPlaybackInfo.self
|
as: JellyfinPlaybackInfo.self
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ public struct PlaybackIntent: Sendable, Equatable {
|
|||||||
public let startTime: Duration
|
public let startTime: Duration
|
||||||
/// Ob der Server im selben Netz steht. Plex rechnet dann anders.
|
/// Ob der Server im selben Netz steht. Plex rechnet dann anders.
|
||||||
public let isLocalConnection: Bool
|
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(
|
public init(
|
||||||
itemID: String,
|
itemID: String,
|
||||||
@@ -26,7 +34,8 @@ public struct PlaybackIntent: Sendable, Equatable {
|
|||||||
file: MediaFile,
|
file: MediaFile,
|
||||||
transcode: TranscodeSettings? = nil,
|
transcode: TranscodeSettings? = nil,
|
||||||
startTime: Duration = .zero,
|
startTime: Duration = .zero,
|
||||||
isLocalConnection: Bool = false
|
isLocalConnection: Bool = false,
|
||||||
|
audioStreamIndex: Int? = nil
|
||||||
) {
|
) {
|
||||||
self.itemID = itemID
|
self.itemID = itemID
|
||||||
self.versionIndex = versionIndex
|
self.versionIndex = versionIndex
|
||||||
@@ -34,6 +43,7 @@ public struct PlaybackIntent: Sendable, Equatable {
|
|||||||
self.transcode = transcode
|
self.transcode = transcode
|
||||||
self.startTime = startTime
|
self.startTime = startTime
|
||||||
self.isLocalConnection = isLocalConnection
|
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" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user