Beim Umpacken fällt die Spurwahl an — also wird sie dort getroffen

Dein Befund an „Notruf Hafenkante": keine Spuren, nichts auswählbar. Die
technischen Angaben derselben Folge sagen, warum:

    Container    MKV
    Tonspur      AAC - Stereo - Standard
    Tonspur      Deutsch - German - AAC - Stereo - Standard
    Untertitel   German - Standard - SUBRIP

Die Quelle ist Matroska. AVFoundation öffnet das nicht, also lief der
Download bisher über die zweite Engine — und die meldete die Spurliste leer.
Offline wählen ging damit nie.

**Umpacken allein hätte es nicht gelöst.** Jellyfin bildet in die neue Hülle
genau **eine** Tonspur ab und übernimmt den Untertitel nur, wenn man ihn
nennt. Ohne Angabe nimmt der Server seine eigene Vorgabe — und das ist bei
deutschen Aufnahmen regelmäßig die Spur mit Audiodeskription.

Also fällt die Wahl jetzt beim Herunterladen, mit derselben Abwägung wie beim
Abspielen (`AudioTrackChooser`, der für genau diesen Fall gebaut wurde: die
als deutsch ausgezeichnete Spur ist die mit Audiodeskription). Der Untertitel
geht als `subtitleMethod=Embed` mit — als **Spur**, nicht ins Bild gebrannt,
sonst liesse er sich nicht abschalten.

Die Mediathek bleibt bei der Abwägung aussen vor: sie nachzuschlagen kostet
eine Anfrage je Folge, und Serie und allgemeine Einstellung tragen den Wunsch
ohnehin.

Der Offline-Abzug behauptet nicht mehr, alle Sprachen seien dabei — beim
Umpacken bleibt je eine übrig, und alle zu notieren wäre ein Versprechen, das
die Datei nicht hält.

**Nebenbei richtiggestellt:** die technischen Angaben rechneten mit
`.directPlayOnly` und zeigten deshalb „Direkt (Systemplayer)" direkt über
„iOS öffnet den Container MKV nicht" — zwei Angaben, die sich widersprechen.
Das war früher richtig (ohne Serverhilfe übernahm die zweite Engine und hiess
auch so); jetzt wird mit Serverhilfe gerechnet.

NOCH NICHT AN EINEM ECHTEN SERVER GEPRÜFT.

iOS und tvOS bauen, 494 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-03 08:32:41 +02:00
co-authored by Claude Opus 5
parent 6b927c21b0
commit a432592271
6 changed files with 111 additions and 20 deletions
@@ -394,17 +394,36 @@ public func isInLocalNetwork() async -> Bool {
/// NOCH NICHT AN EINEM ECHTEN SERVER GEPRÜFT: ob Jellyfin das Kopieren
/// hier tatsächlich wählt, steht erst nach dem ersten Download fest — die
/// Dateigröße verrät es.
public nonisolated func remuxDownloadURL(itemID: String, versionIndex: Int) -> URL? {
client.url(
public nonisolated func remuxDownloadURL(
itemID: String,
versionIndex: Int,
audioStreamIndex: Int?,
subtitleStreamIndex: Int?
) -> URL? {
var query = [
URLQueryItem(name: "videoCodec", value: "h264,hevc"),
URLQueryItem(name: "audioCodec", value: "aac,ac3,eac3"),
URLQueryItem(name: "allowVideoStreamCopy", value: "true"),
URLQueryItem(name: "allowAudioStreamCopy", value: "true"),
URLQueryItem(name: "TranscodingContainer", value: "mp4"),
URLQueryItem(name: "TranscodingMaxAudioChannels", value: "6"),
]
// **Eine Tonspur, nicht alle.** Jellyfin bildet beim Umpacken genau
// eine ab; welche, muss hier stehen. Ohne diese Zeile nimmt der Server
// seine eigene Vorgabe — und das ist bei deutschen Aufnahmen
// regelmäßig die mit Audiodeskription.
if let audioStreamIndex {
query.append(URLQueryItem(name: "audioStreamIndex", value: String(audioStreamIndex)))
}
// `Embed` legt den Untertitel als **Spur** in die Datei, statt ihn ins
// Bild zu brennen. Nur so bleibt er abschaltbar.
if let subtitleStreamIndex {
query.append(URLQueryItem(name: "subtitleStreamIndex", value: String(subtitleStreamIndex)))
query.append(URLQueryItem(name: "subtitleMethod", value: "Embed"))
}
return client.url(
path: "/Videos/\(itemID)/stream.mp4",
query: [
URLQueryItem(name: "videoCodec", value: "h264,hevc"),
URLQueryItem(name: "audioCodec", value: "aac,ac3,eac3"),
URLQueryItem(name: "allowVideoStreamCopy", value: "true"),
URLQueryItem(name: "allowAudioStreamCopy", value: "true"),
URLQueryItem(name: "TranscodingContainer", value: "mp4"),
URLQueryItem(name: "TranscodingMaxAudioChannels", value: "6"),
],
query: query,
includingToken: account.token
)
}
@@ -98,7 +98,17 @@ public protocol MediaBackend: Sendable {
///
/// `nil`, wenn der Server das nicht anbietet. Dann bleibt das Original,
/// und der Titel ist offline nur so gut wie der Systemplayer.
nonisolated func remuxDownloadURL(itemID: String, versionIndex: Int) -> URL?
///
/// **Mit Spurnummern, weil beim Umpacken eine Wahl ansteht.** Der Server
/// legt in die neue Hülle genau eine Tonspur und höchstens einen
/// Untertitel. Welche, entscheidet der Aufrufer — offline lässt sich das
/// nicht nachholen.
nonisolated func remuxDownloadURL(
itemID: String,
versionIndex: Int,
audioStreamIndex: Int?,
subtitleStreamIndex: Int?
) -> URL?
// MARK: - Abspielen
@@ -193,7 +203,17 @@ public extension MediaBackend {
///
/// So muss nicht jeder Server eine Adresse erfinden, die es bei ihm nicht
/// gibt — und wer es anbietet, sagt es ausdrücklich.
nonisolated func remuxDownloadURL(itemID: String, versionIndex: Int) -> URL? { nil }
///
/// **Mit Spurnummern, weil beim Umpacken eine Wahl ansteht.** Der Server
/// legt in die neue Hülle genau eine Tonspur und höchstens einen
/// Untertitel. Welche, entscheidet der Aufrufer — offline lässt sich das
/// nicht nachholen.
nonisolated func remuxDownloadURL(
itemID: String,
versionIndex: Int,
audioStreamIndex: Int?,
subtitleStreamIndex: Int?
) -> URL? { nil }
/// Der allgemeine Weg: Staffeln, dann je Staffel die Folgen.
///