„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>
202 lines
7.7 KiB
C#
202 lines
7.7 KiB
C#
using Flaro.Core.Delivery;
|
|
using Flaro.Core.Library;
|
|
|
|
namespace Flaro.Media.Tests;
|
|
|
|
/// <summary>Die ffmpeg-Zeile, betrachtet ohne sie auszuführen.</summary>
|
|
public class RemuxArgumentsTests
|
|
{
|
|
private static ProbedStream Bild() => new(0, MediaStreamKind.Video, "h264", null, null, true, false, null, 1920, 1080);
|
|
private static ProbedStream Ton(int i, string codec = "aac", int kanäle = 6) =>
|
|
new(i, MediaStreamKind.Audio, codec, "deu", null, false, false, kanäle, null, null);
|
|
private static ProbedStream Text(int i, string codec = "subrip") =>
|
|
new(i, MediaStreamKind.Subtitle, codec, "deu", null, false, false, null, null, null);
|
|
|
|
private static List<string> Bauen(IReadOnlyList<ProbedStream> spuren, string schachtel = "mkv")
|
|
{
|
|
var plan = DeliveryDecider.ForDownload(schachtel, spuren);
|
|
return RemuxArguments.For(new RemuxRequest("/medien/film.mkv", "/kratz/film.mp4", spuren, plan));
|
|
}
|
|
|
|
private static string AlsZeile(List<string> argumente) => string.Join(' ', argumente);
|
|
|
|
/// <summary>Jede Spur wird einzeln benannt — und zwar alle.</summary>
|
|
[Test]
|
|
public void Alle_Spuren_werden_abgebildet()
|
|
{
|
|
var zeile = AlsZeile(Bauen([Bild(), Ton(1), Ton(2), Text(3)]));
|
|
|
|
Assert.That(zeile, Does.Contain("-map 0:0"));
|
|
Assert.That(zeile, Does.Contain("-map 0:1"));
|
|
Assert.That(zeile, Does.Contain("-map 0:2"));
|
|
Assert.That(zeile, Does.Contain("-map 0:3"));
|
|
}
|
|
|
|
[Test]
|
|
public void Bild_und_Ton_werden_kopiert_nicht_gerechnet()
|
|
{
|
|
var zeile = AlsZeile(Bauen([Bild(), Ton(1), Ton(2)]));
|
|
|
|
Assert.That(zeile, Does.Contain("-c copy"));
|
|
Assert.That(zeile, Does.Not.Contain("-c:a:"), "Nichts davon muss neu gerechnet werden");
|
|
Assert.That(zeile, Does.Not.Contain("libx264"), "Das Bild wird nie angefasst");
|
|
}
|
|
|
|
[Test]
|
|
public void Textuntertitel_werden_nach_tx3g_umgeschrieben()
|
|
{
|
|
Assert.That(AlsZeile(Bauen([Bild(), Ton(1), Text(2)])), Does.Contain("-c:s mov_text"));
|
|
}
|
|
|
|
[Test]
|
|
public void Ohne_Untertitel_steht_auch_keine_Untertitel_Anweisung_da()
|
|
{
|
|
Assert.That(AlsZeile(Bauen([Bild(), Ton(1)])), Does.Not.Contain("-c:s"));
|
|
}
|
|
|
|
[Test]
|
|
public void Bilduntertitel_werden_gar_nicht_erst_abgebildet()
|
|
{
|
|
var zeile = AlsZeile(Bauen([Bild(), Ton(1), Text(2, "hdmv_pgs_subtitle"), Text(3, "subrip")]));
|
|
|
|
Assert.That(zeile, Does.Not.Contain("-map 0:2"), "PGS passt in keine MP4");
|
|
Assert.That(zeile, Does.Contain("-map 0:3"));
|
|
}
|
|
|
|
/// <summary>Die Verwechslung, vor der der Kommentar an MediaStream.Index warnt.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b><c>-c:a:1</c> meint die zweite Tonspur der Ausgabe, nicht die Spur
|
|
/// Nummer 1 der Datei.</b> Hier steht DTS an Dateinummer 2 und ist die
|
|
/// zweite Tonspur — also <c>-c:a:1</c>. Wer die Dateinummer einsetzte,
|
|
/// träfe eine andere Spur oder gar keine.
|
|
/// </remarks>
|
|
[Test]
|
|
public void Neu_gerechnet_wird_nach_der_Nummer_der_Ausgabe()
|
|
{
|
|
var zeile = AlsZeile(Bauen([Bild(), Ton(1, "aac"), Ton(2, "dts"), Ton(3, "aac")]));
|
|
|
|
Assert.That(zeile, Does.Contain("-c:a:1 aac"));
|
|
Assert.That(zeile, Does.Not.Contain("-c:a:0"), "Die erste Tonspur passt schon");
|
|
Assert.That(zeile, Does.Not.Contain("-c:a:2"), "Die dritte auch");
|
|
}
|
|
|
|
/// <summary>Fällt ein Untertitel weg, verschieben sich die Tonnummern nicht.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Tonspuren werden unter sich gezählt — ein weggelassener Untertitel
|
|
/// ändert daran nichts. Die Prüfung steht hier, weil der umgekehrte Fehler
|
|
/// naheliegt und erst an einer stummen Spur auffiele.
|
|
/// </remarks>
|
|
[Test]
|
|
public void Ein_weggelassener_Untertitel_verschiebt_die_Tonnummern_nicht()
|
|
{
|
|
var zeile = AlsZeile(Bauen([Bild(), Text(1, "hdmv_pgs_subtitle"), Ton(2, "aac"), Ton(3, "dts")]));
|
|
Assert.That(zeile, Does.Contain("-c:a:1 aac"));
|
|
}
|
|
|
|
[Test]
|
|
public void Mehr_Kanaele_bekommen_mehr_Bitrate()
|
|
{
|
|
Assert.That(AlsZeile(Bauen([Bild(), Ton(1, "dts", kanäle: 2)])), Does.Contain("-b:a:0 192000"));
|
|
Assert.That(AlsZeile(Bauen([Bild(), Ton(1, "dts", kanäle: 6)])), Does.Contain("-b:a:0 576000"));
|
|
Assert.That(AlsZeile(Bauen([Bild(), Ton(1, "dts", kanäle: 8)])), Does.Contain("-b:a:0 640000"),
|
|
"Gedeckelt — acht Kanäle bekämen sonst mehr, als irgendein Gerät braucht");
|
|
}
|
|
|
|
[Test]
|
|
public void Das_Inhaltsverzeichnis_wandert_nach_vorn()
|
|
{
|
|
Assert.That(AlsZeile(Bauen([Bild(), Ton(1)])), Does.Contain("-movflags +faststart"));
|
|
}
|
|
|
|
[Test]
|
|
public void Quelle_und_Ziel_stehen_an_der_richtigen_Stelle()
|
|
{
|
|
var argumente = Bauen([Bild(), Ton(1)]);
|
|
|
|
Assert.That(argumente[argumente.IndexOf("-i") + 1], Is.EqualTo("/medien/film.mkv"));
|
|
Assert.That(argumente[^1], Is.EqualTo("/kratz/film.mp4"), "Das Ziel steht bei ffmpeg immer zuletzt");
|
|
}
|
|
|
|
/// <summary>Genau eine Tonspur ist die Vorgabe — sonst laufen alle zugleich.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Gemeldet als „ich höre einfach alle Audiospuren gleichzeitig".</b>
|
|
/// MP4 ordnet Spuren derselben Art einer <c>alternate_group</c> 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, legt es sie in getrennte Gruppen, und beide laufen.
|
|
/// </remarks>
|
|
[Test]
|
|
public void Nur_eine_Tonspur_bleibt_Vorgabe()
|
|
{
|
|
var zwei = new List<ProbedStream>
|
|
{
|
|
Bild(),
|
|
Ton(1) with { IsDefault = true },
|
|
Ton(2) with { IsDefault = true },
|
|
};
|
|
|
|
var zeile = AlsZeile(Bauen(zwei));
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(zeile, Does.Contain("-disposition:a:0 default"));
|
|
Assert.That(zeile, Does.Contain("-disposition:a:1 0"),
|
|
"auch die Null gehört hingeschrieben: ffmpeg übernähme sonst die Markierung der Quelle");
|
|
});
|
|
}
|
|
|
|
/// <summary>Die Vorgabe der Quelle bleibt die Vorgabe.</summary>
|
|
[Test]
|
|
public void Die_schon_vorgegebene_Spur_bleibt_es()
|
|
{
|
|
var zeile = AlsZeile(Bauen([Bild(), Ton(1), Ton(2) with { IsDefault = true }]));
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(zeile, Does.Contain("-disposition:a:0 0"));
|
|
Assert.That(zeile, Does.Contain("-disposition:a:1 default"));
|
|
});
|
|
}
|
|
|
|
/// <summary>„Erzwungen" überlebt die Behandlung.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <c>-disposition</c> ersetzt die ganze Markierung, statt sie zu
|
|
/// ergänzen. Wer hier nur „default" schreibt, nimmt einer erzwungenen
|
|
/// Untertitelspur genau das, was sie ausmacht.
|
|
/// </remarks>
|
|
[Test]
|
|
public void Erzwungene_Untertitel_bleiben_erzwungen()
|
|
{
|
|
var zeile = AlsZeile(Bauen(
|
|
[Bild(), Ton(1), Text(2) with { IsForced = true }, Text(3)]));
|
|
|
|
Assert.That(zeile, Does.Contain("-disposition:s:0 forced"));
|
|
}
|
|
|
|
/// <summary>Ohne Vorgabe in der Quelle erscheint auch kein Untertitel von selbst.</summary>
|
|
[Test]
|
|
public void Untertitel_ohne_Vorgabe_bekommen_keine()
|
|
{
|
|
var zeile = AlsZeile(Bauen([Bild(), Ton(1), Text(2), Text(3)]));
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(zeile, Does.Contain("-disposition:s:0 0"));
|
|
Assert.That(zeile, Does.Not.Contain("-disposition:s:0 default"));
|
|
});
|
|
}
|
|
|
|
/// <summary>Bei einer einzigen Spur gibt es nichts zu entscheiden.</summary>
|
|
[Test]
|
|
public void Eine_einzige_Tonspur_wird_nicht_angefasst()
|
|
{
|
|
var zeile = AlsZeile(Bauen([Bild(), Ton(1)]));
|
|
|
|
Assert.That(zeile, Does.Not.Contain("-disposition:a"));
|
|
}
|
|
}
|