Zwei Tonspuren sind eine Wahl, kein Chor
„Auf der App höre ich einfach alle Audiospuren gleichzeitig."
MP4 ordnet Spuren derselben Art einer `alternate_group` zu, und ein
Abspieler nimmt daraus genau eine. ffmpeg leitet diese Gruppe aus der
Markierung der Quelle ab — sind dort zwei Tonspuren als Vorgabe markiert,
hält es sie für Verschiedenes und legt sie in getrennte Gruppen. Dann ist
jede für sich eingeschaltet, und man hört beide übereinander. Bei Rips aus
freier Wildbahn ist die doppelte Markierung eher Regel als Ausnahme; in
Matroska stört sie niemanden, in MP4 ist sie der Unterschied zwischen Wahl
und Chor.
Gemessen an einer Quelle mit zwei markierten Tonspuren, am Spurkopf der
fertigen MP4:
vorher Spur 2: an, Gruppe 1
Spur 3: an, Gruppe 2 <- beide laufen
nachher Spur 2: an, Gruppe 1
Spur 3: aus, Gruppe 1 <- eine zur Wahl
Das Umpacken schreibt jetzt genau eine Vorgabe je Spurart: die, die es in
der Quelle schon war, sonst die erste. Auch die Nullen gehören
hingeschrieben — ffmpeg übernähme sonst die Markierung der Quelle für alle
übrigen, und genau die ist ja das Problem. „Erzwungen" wird dabei
ausdrücklich erhalten, weil `-disposition` die ganze Markierung ersetzt
statt sie zu ergänzen.
Untertitel bekommen dieselbe Behandlung. War in der Quelle keiner
vorgesehen, bleibt es dabei — sonst erschienen auf einmal zwei übereinander.
Der Name im Zwischenspeicher trägt jetzt die Bauart mit. Ohne das bliebe
auf einem NAS, das schon umgepackt hat, die fehlerhafte Datei liegen und
würde weiter ausgeliefert, obwohl der Server es längst besser kann.
Geprüft an der echten Datei, am `tkhd` und nicht an der Disposition: was
ein Abspieler tut, hängt an „enabled" und `alternate_group`, und ffprobe
zeigt davon nur einen Teil. Ohne die Korrektur fällt die Prüfung um
(Gruppe 1 gegen 2).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1531163bc1
commit
40f41f0cee
@@ -59,8 +59,24 @@ public sealed class PackageCoordinator(
|
||||
/// heisst das Ergebnis anders — und niemand bekommt die alte Fassung
|
||||
/// ausgeliefert, bloss weil sie zufällig noch herumliegt.
|
||||
/// </remarks>
|
||||
/// <summary>Wie das Umpacken gebaut wurde — Teil des Namens.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Sonst bleibt ein Fehler liegen, der behoben ist.</b> Der Name trug
|
||||
/// bisher nur Kennung und Änderungszeitpunkt der Quelle. Ändert sich das
|
||||
/// Umpacken selbst, passt beides weiterhin — und eine fehlerhaft
|
||||
/// gebaute Datei wird weiter ausgeliefert, obwohl der Server es längst
|
||||
/// besser könnte.
|
||||
///
|
||||
/// <b>2:</b> genau eine Vorgabe je Spurart. Davor landeten zwei als
|
||||
/// Vorgabe markierte Tonspuren in getrennten Alternativgruppen und
|
||||
/// liefen gleichzeitig.
|
||||
/// </remarks>
|
||||
private const int Bauart = 2;
|
||||
|
||||
public string PathFor(Guid fileId, DateTimeOffset geändert) =>
|
||||
Path.Combine(pfade.Transcode, "packages", $"{fileId:N}-{geändert.ToUnixTimeMilliseconds()}.mp4");
|
||||
Path.Combine(pfade.Transcode, "packages",
|
||||
$"{fileId:N}-{geändert.ToUnixTimeMilliseconds()}-{Bauart}.mp4");
|
||||
|
||||
public bool IsReady(Guid fileId, DateTimeOffset geändert) => File.Exists(PathFor(fileId, geändert));
|
||||
|
||||
|
||||
@@ -87,6 +87,30 @@ public static class RemuxArguments
|
||||
argumente.Add(Bitrate(spur.Channels).ToString());
|
||||
}
|
||||
|
||||
// **Genau eine Vorgabe je Art — sonst laufen alle Spuren zugleich.**
|
||||
//
|
||||
// Gemeldet als „auf der App höre ich einfach alle Audiospuren
|
||||
// gleichzeitig", und so entsteht es: MP4 ordnet Spuren derselben Art
|
||||
// einer `alternate_group` zu, und ein Abspieler nimmt daraus genau
|
||||
// eine. ffmpeg leitet diese Gruppe aus der Markierung der Quelle ab.
|
||||
// Sind dort zwei Tonspuren als Vorgabe markiert — bei Rips aus freier
|
||||
// Wildbahn eher Regel als Ausnahme —, hält ffmpeg sie für
|
||||
// Verschiedenes und legt sie in getrennte Gruppen.
|
||||
//
|
||||
// Gemessen an einer Quelle mit zwei markierten Tonspuren:
|
||||
//
|
||||
// ohne diese Zeilen Spur 2: an, Gruppe 1
|
||||
// Spur 3: an, Gruppe 2 <- beide laufen
|
||||
// mit diesen Zeilen Spur 2: an, Gruppe 1
|
||||
// Spur 3: aus, Gruppe 1 <- eine zur Wahl
|
||||
//
|
||||
// Welche die Vorgabe wird: die, die es in der Quelle schon war. Gab
|
||||
// es keine, die erste. Das ist keine Auswahl für den Zuschauer — er
|
||||
// schaltet im Spieler um —, sondern nur die Zusage, dass es eine
|
||||
// gibt und nicht zwei.
|
||||
Vorgabe(argumente, "a", tonspuren);
|
||||
Vorgabe(argumente, "s", genommen.Where(s => s.Kind == MediaStreamKind.Subtitle).ToList());
|
||||
|
||||
// Das Inhaltsverzeichnis nach vorn. Ohne das muss ein Abspieler erst
|
||||
// ans Dateiende springen, bevor er irgendetwas zeigen kann — bei einer
|
||||
// heruntergeladenen Datei ist das der Unterschied zwischen „läuft
|
||||
@@ -98,6 +122,43 @@ public static class RemuxArguments
|
||||
return argumente;
|
||||
}
|
||||
|
||||
/// <summary>Markiert genau eine Spur einer Art als Vorgabe, alle anderen nicht.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Auch die Null gehört hingeschrieben.</b> Es genügt nicht, die eine
|
||||
/// zu markieren: ffmpeg übernimmt sonst die Markierung der Quelle für
|
||||
/// alle übrigen, und genau die ist ja das Problem.
|
||||
///
|
||||
/// <b>Bei Untertiteln kann die Vorgabe fehlen</b>, und das ist richtig
|
||||
/// so — war in der Quelle keiner vorgesehen, soll auch keiner von selbst
|
||||
/// erscheinen. Bei Ton gibt es dagegen immer eine; eine Datei ganz ohne
|
||||
/// eingeschaltete Tonspur wäre stumm.
|
||||
///
|
||||
/// <b><c>-disposition</c> ersetzt die ganze Markierung, sie ergänzt sie
|
||||
/// nicht.</b> „Erzwungen" muss deshalb ausdrücklich mitgeschrieben
|
||||
/// werden, sonst verlöre eine erzwungene Untertitelspur genau das, was
|
||||
/// sie ausmacht. Was <see cref="ProbedStream"/> nicht kennt — etwa
|
||||
/// „für Hörgeschädigte" oder „Kommentar" —, geht dabei verloren; das ist
|
||||
/// der Preis dafür, dass überhaupt etwas abspielbar herauskommt.
|
||||
/// </remarks>
|
||||
private static void Vorgabe(List<string> argumente, string art, IReadOnlyList<ProbedStream> spuren)
|
||||
{
|
||||
if (spuren.Count < 2) return;
|
||||
|
||||
var ausQuelle = spuren.ToList().FindIndex(s => s.IsDefault);
|
||||
var gewaehlt = ausQuelle >= 0 ? ausQuelle : art == "a" ? 0 : -1;
|
||||
|
||||
for (var stelle = 0; stelle < spuren.Count; stelle++)
|
||||
{
|
||||
var teile = new List<string>();
|
||||
if (stelle == gewaehlt) teile.Add("default");
|
||||
if (spuren[stelle].IsForced) teile.Add("forced");
|
||||
|
||||
argumente.Add($"-disposition:{art}:{stelle}");
|
||||
argumente.Add(teile.Count > 0 ? string.Join('+', teile) : "0");
|
||||
}
|
||||
}
|
||||
|
||||
private static int Bitrate(int? kanäle) =>
|
||||
Math.Min(HöchsteBitrate, Math.Max(1, kanäle ?? 2) * BitrateJeKanal);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user