Untertitel kommen aus der Datei, und Fortsetzen geht wieder
Im Spurenblatt stand „CC" und sonst nichts — derselbe Fehler wie beim Ton: das ist AVFoundations Name für den einen Untertitel, den ein umgerechneter Stream mitbringt. Zur Auswahl steht jetzt, was in der Datei liegt, und die Nummer geht als `subtitleStreamIndex` mit in die Anfrage. `-1` heisst dabei ausdrücklich „keine". Das ist nicht dasselbe wie nichts zu schicken: dann sucht der Server sich einen aus, und es steht plötzlich ein Untertitel im Bild, den niemand bestellt hat. Und der Klick ins Bild: Anhalten ging, Fortsetzen nicht. Der Abdunkler hinter der Bedienung deckt das ganze Fenster ab und lag über dem Film — solange die Bedienung zu sehen war, landete jeder Klick auf ihm. Auffallen konnte das nur an einer Stelle: nach dem Pausieren bleibt die Bedienung stehen, also traf der Klick, der wieder starten sollte, nie das Bild. Der Abdunkler ist jetzt unantastbar; er dunkelt ab, mehr nicht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
60194c2d2a
commit
67c679006f
@@ -40,7 +40,8 @@ extension JellyfinBackend {
|
||||
itemID: intent.itemID,
|
||||
sourceID: sourceID,
|
||||
transcode: transcode,
|
||||
audioStreamIndex: intent.audioStreamIndex
|
||||
audioStreamIndex: intent.audioStreamIndex,
|
||||
subtitleStreamIndex: intent.subtitleStreamIndex
|
||||
)
|
||||
|
||||
let source = info.mediaSources?.first { $0.id == sourceID } ?? info.mediaSources?.first
|
||||
@@ -121,7 +122,8 @@ extension JellyfinBackend {
|
||||
itemID: String,
|
||||
sourceID: String,
|
||||
transcode: TranscodeSettings,
|
||||
audioStreamIndex: Int? = nil
|
||||
audioStreamIndex: Int? = nil,
|
||||
subtitleStreamIndex: Int? = nil
|
||||
) async throws(MediaBackendError) -> JellyfinPlaybackInfo {
|
||||
// Jellyfin rechnet in Bit, die App in Kilobit.
|
||||
let bitsPerSecond = (transcode.maximumBitrate ?? 8_000) * 1_000
|
||||
@@ -181,6 +183,8 @@ extension JellyfinBackend {
|
||||
URLQueryItem(name: "enableTranscoding", value: "true"),
|
||||
] + (audioStreamIndex.map {
|
||||
[URLQueryItem(name: "audioStreamIndex", value: String($0))]
|
||||
} ?? []) + (subtitleStreamIndex.map {
|
||||
[URLQueryItem(name: "subtitleStreamIndex", value: String($0))]
|
||||
} ?? []),
|
||||
body: body
|
||||
),
|
||||
|
||||
@@ -27,6 +27,16 @@ public struct PlaybackIntent: Sendable, Equatable {
|
||||
/// zweite ankommt. Genau daran scheiterte die Sprachwahl bisher: sie lief
|
||||
/// auf einer Liste mit einem Eintrag.
|
||||
public let audioStreamIndex: Int?
|
||||
/// Welcher Untertitel eingebrannt oder mitgeliefert werden soll.
|
||||
///
|
||||
/// Dieselbe Lage wie beim Ton: rechnet der Server um, liefert er genau
|
||||
/// den einen aus, für den er sich entschieden hat. Wer erst im Player
|
||||
/// wählen will, hat dann nichts mehr zur Auswahl.
|
||||
///
|
||||
/// **`-1` heisst ausdrücklich „keine".** Nicht dasselbe wie `nil`: das
|
||||
/// hiesse „such dir was aus", und der Server sucht sich dann durchaus
|
||||
/// etwas aus.
|
||||
public let subtitleStreamIndex: Int?
|
||||
|
||||
public init(
|
||||
itemID: String,
|
||||
@@ -35,7 +45,8 @@ public struct PlaybackIntent: Sendable, Equatable {
|
||||
transcode: TranscodeSettings? = nil,
|
||||
startTime: Duration = .zero,
|
||||
isLocalConnection: Bool = false,
|
||||
audioStreamIndex: Int? = nil
|
||||
audioStreamIndex: Int? = nil,
|
||||
subtitleStreamIndex: Int? = nil
|
||||
) {
|
||||
self.itemID = itemID
|
||||
self.versionIndex = versionIndex
|
||||
@@ -44,6 +55,7 @@ public struct PlaybackIntent: Sendable, Equatable {
|
||||
self.startTime = startTime
|
||||
self.isLocalConnection = isLocalConnection
|
||||
self.audioStreamIndex = audioStreamIndex
|
||||
self.subtitleStreamIndex = subtitleStreamIndex
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -285,3 +285,62 @@ struct AudioStreamIndexTests {
|
||||
#expect(!werte.contains { $0.name == "audioStreamIndex" })
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("Untertitel ausdrücklich bestellen")
|
||||
struct SubtitleStreamIndexTests {
|
||||
private func backend(_ transport: RecordingTransport) -> JellyfinBackend {
|
||||
JellyfinBackend(
|
||||
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
|
||||
),
|
||||
account: JellyfinAccount(
|
||||
userID: "u1", userName: "Test", token: "tok", isAdministrator: false
|
||||
),
|
||||
serverName: "Attrappe"
|
||||
)
|
||||
}
|
||||
|
||||
private func intent(subtitle: Int?) -> PlaybackIntent {
|
||||
PlaybackIntent(
|
||||
itemID: "abc",
|
||||
versionIndex: 0,
|
||||
file: MediaFile(id: "src", key: "src", itemID: "abc"),
|
||||
transcode: TranscodeSettings(maximumBitrate: 8_000, resolution: nil),
|
||||
subtitleStreamIndex: subtitle
|
||||
)
|
||||
}
|
||||
|
||||
private func query(_ transport: RecordingTransport) throws -> [URLQueryItem] {
|
||||
let asked = try #require(transport.lastRequest)
|
||||
return URLComponents(url: asked.url!, resolvingAgainstBaseURL: false)?.queryItems ?? []
|
||||
}
|
||||
|
||||
@Test("Die Nummer steht in der Anfrage")
|
||||
func indexIsSent() async throws {
|
||||
let transport = RecordingTransport()
|
||||
_ = try? await backend(transport).preparePlayback(intent(subtitle: 4))
|
||||
#expect(try query(transport).contains(URLQueryItem(name: "subtitleStreamIndex", value: "4")))
|
||||
}
|
||||
|
||||
/// **Der Unterschied, der zählt.** Ohne Angabe sucht der Server selbst
|
||||
/// einen aus; „keine" muss man ihm ausdrücklich sagen, sonst steht
|
||||
/// plötzlich ein Untertitel im Bild, den niemand bestellt hat.
|
||||
@Test("Minus eins heisst ausdrücklich keine")
|
||||
func minusOneMeansNone() async throws {
|
||||
let transport = RecordingTransport()
|
||||
_ = try? await backend(transport).preparePlayback(intent(subtitle: -1))
|
||||
#expect(try query(transport).contains(URLQueryItem(name: "subtitleStreamIndex", value: "-1")))
|
||||
}
|
||||
|
||||
@Test("Ohne Angabe steht sie nicht da")
|
||||
func indexIsOmitted() async throws {
|
||||
let transport = RecordingTransport()
|
||||
_ = try? await backend(transport).preparePlayback(intent(subtitle: nil))
|
||||
#expect(!(try query(transport)).contains { $0.name == "subtitleStreamIndex" })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user