Files
Flaro/server/tests/Flaro.Media.Tests/Testdateien.cs
T
ScarriffleandClaude Opus 5 40f41f0cee 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>
2026-10-08 06:32:25 +02:00

120 lines
5.1 KiB
C#

using System.Diagnostics;
namespace Flaro.Media.Tests;
/// <summary>Baut echte Mediendateien mit ffmpeg.</summary>
///
/// <remarks>
/// <b>Echte Dateien und keine aufgezeichneten Antworten.</b> Bei TMDB ist das
/// umgekehrt richtig — dort steht ein fremder Dienst dahinter. Hier geht es
/// um das Werkzeug, das Flaro im Container ohnehin mitbringt, und die Frage
/// „liest er die zweite Tonspur mitsamt Sprache?" lässt sich nur an einer
/// Datei beantworten, die wirklich zwei hat.
/// </remarks>
public static class Testdateien
{
public static bool FfmpegVorhanden => Suchen("ffmpeg") is not null && Suchen("ffprobe") is not null;
public static string? Suchen(string name)
{
foreach (var ordner in (Environment.GetEnvironmentVariable("PATH") ?? "").Split(Path.PathSeparator))
{
if (ordner.Length == 0) continue;
var pfad = Path.Combine(ordner, name);
if (File.Exists(pfad)) return pfad;
}
return null;
}
/// <summary>Zwei Tonspuren mit Sprache und Namen, dazu ein Untertitel.</summary>
public static void MitZweiTonspuren(string ziel)
{
var untertitel = Path.ChangeExtension(ziel, ".srt");
File.WriteAllText(untertitel,
"1\n00:00:00,000 --> 00:00:01,000\nEin Untertitel\n\n");
Ausführen("ffmpeg",
[
"-y", "-loglevel", "error",
"-f", "lavfi", "-i", "testsrc=duration=1:size=160x120:rate=10",
"-f", "lavfi", "-i", "sine=frequency=440:duration=1",
"-f", "lavfi", "-i", "sine=frequency=880:duration=1",
"-i", untertitel,
"-map", "0:v", "-map", "1:a", "-map", "2:a", "-map", "3:s",
"-metadata:s:a:0", "language=deu", "-metadata:s:a:0", "title=Deutsch",
"-metadata:s:a:1", "language=deu", "-metadata:s:a:1", "title=Audiodeskription",
"-metadata:s:s:0", "language=deu",
"-c:v", "libx264", "-preset", "ultrafast", "-c:a", "aac", "-c:s", "srt",
ziel,
]);
File.Delete(untertitel);
}
/// <summary>Zwei Tonspuren, und beide sind als Vorgabe markiert.</summary>
///
/// <remarks>
/// <b>Keine Schikane, sondern der Normalfall.</b> Wer eine Datei aus
/// mehreren Quellen zusammenmuxt, lässt die Markierung gern auf jeder
/// stehen. In Matroska stört das niemanden; in MP4 heisst es, dass ein
/// Abspieler beide Spuren für etwas Eigenes hält und gleichzeitig
/// abspielt.
/// </remarks>
public static void MitZweiVorgabetonspuren(string ziel) => Ausführen("ffmpeg",
[
"-y", "-loglevel", "error",
"-f", "lavfi", "-i", "testsrc=duration=1:size=160x120:rate=10",
"-f", "lavfi", "-i", "sine=frequency=440:duration=1",
"-f", "lavfi", "-i", "sine=frequency=880:duration=1",
"-map", "0:v", "-map", "1:a", "-map", "2:a",
"-metadata:s:a:0", "language=deu", "-metadata:s:a:1", "language=eng",
"-disposition:a:0", "default", "-disposition:a:1", "default",
"-c:v", "libx264", "-preset", "ultrafast", "-c:a", "aac",
ziel,
]);
/// <summary>Eine schlichte MP4 mit einer Tonspur.</summary>
public static void Einfach(string ziel) => Ausführen("ffmpeg",
[
"-y", "-loglevel", "error",
"-f", "lavfi", "-i", "testsrc=duration=1:size=160x120:rate=10",
"-f", "lavfi", "-i", "sine=frequency=440:duration=1",
"-c:v", "libx264", "-preset", "ultrafast", "-c:a", "aac",
ziel,
]);
/// <summary>Eine MKV mit einer Tonspur in einem Codec, den Apple nicht kennt.</summary>
///
/// <remarks>
/// <b>DTS, also der echte Fall.</b> So liegen Blu-ray-Rips vor, und genau
/// daran entscheidet sich, ob eine sonst tadellose Datei stumm bliebe.
/// Der Encoder gilt bei ffmpeg als erprobungsweise und braucht deshalb
/// <c>-strict -2</c> — das betrifft nur das Erzeugen dieser Testvorlage,
/// nicht Flaro.
/// </remarks>
public static void MitUntauglichemTon(string ziel) => Ausführen("ffmpeg",
[
"-y", "-loglevel", "error",
"-f", "lavfi", "-i", "testsrc=duration=1:size=160x120:rate=10",
"-f", "lavfi", "-i", "sine=frequency=440:duration=1",
"-f", "lavfi", "-i", "sine=frequency=880:duration=1",
"-map", "0:v", "-map", "1:a", "-map", "2:a",
"-metadata:s:a:0", "language=deu", "-metadata:s:a:0", "title=Deutsch",
"-metadata:s:a:1", "language=eng", "-metadata:s:a:1", "title=English",
"-c:v", "libx264", "-preset", "ultrafast",
"-c:a:0", "aac", "-c:a:1", "dca", "-strict", "-2",
ziel,
]);
private static void Ausführen(string werkzeug, string[] argumente)
{
var start = new ProcessStartInfo(Suchen(werkzeug)!) { RedirectStandardError = true, UseShellExecute = false };
foreach (var a in argumente) start.ArgumentList.Add(a);
using var prozess = Process.Start(start)!;
var fehler = prozess.StandardError.ReadToEnd();
prozess.WaitForExit();
if (prozess.ExitCode != 0)
throw new InvalidOperationException($"{werkzeug} scheiterte: {fehler}");
}
}