using System.Diagnostics;
using Flaro.Core.Delivery;
using Flaro.Core.Library;
namespace Flaro.Media.Tests;
/// Dieselbe Zeile, diesmal wirklich ausgeführt.
///
///
///
/// Weil die reine Prüfung nur die Form zeigt. Sie sagt, dass
/// -hls_segment_type fmp4 in der Zeile steht — nicht, dass ffmpeg die
/// Kombination annimmt. Genau dort sitzen die teuren Fehler: eine Option, die
/// für sich richtig aussieht und in Verbindung mit einer anderen zum Abbruch
/// führt.
///
///
/// Übersprungen, wo kein ffmpeg liegt. Eine Prüfung, die auf einem
/// Rechner ohne Werkzeug rot wird, sagt nichts über den Code.
///
///
public class HlsLaufTests
{
private string _ordner = "";
[SetUp]
public void Anlegen()
{
if (!Testdateien.FfmpegVorhanden) Assert.Ignore("Ohne ffmpeg nicht prüfbar.");
_ordner = Directory.CreateDirectory(
Path.Combine(Path.GetTempPath(), "flaro-hls-" + Guid.CreateVersion7().ToString("N"))).FullName;
}
[TearDown]
public void Aufraeumen()
{
try { Directory.Delete(_ordner, recursive: true); } catch (IOException) { }
}
private static readonly DeviceProfile Browser = new(
Containers: new HashSet(StringComparer.OrdinalIgnoreCase) { "mp4" },
VideoCodecs: new HashSet(StringComparer.OrdinalIgnoreCase) { "h264", "avc1" },
AudioCodecs: new HashSet(StringComparer.OrdinalIgnoreCase) { "aac", "mp3" });
/// Führt die gebaute Zeile aus und gibt den Zielordner zurück.
private string Laufen(string quelle, int? hoehe = null, int? tonspur = null, double ab = 0)
{
var spuren = Spuren(quelle);
var ziel = Directory.CreateDirectory(Path.Combine(_ordner, "aus" + Guid.NewGuid().ToString("N")[..6])).FullName;
var plan = Stromentscheid.Fuer(Path.GetExtension(quelle).TrimStart('.'), spuren, Browser, hoehe);
var argumente = HlsArgumente.Fuer(new Stromanfrage(quelle, ziel, spuren, plan, tonspur, ab));
var start = new ProcessStartInfo(Testdateien.Suchen("ffmpeg")!)
{
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();
Assert.That(prozess.ExitCode, Is.Zero,
$"ffmpeg lehnte die gebaute Zeile ab:\n{string.Join(' ', argumente)}\n\n{fehler}");
return ziel;
}
/// Was wirklich in der Datei steht — nicht, was wir annehmen.
private static List Spuren(string datei)
{
var start = new ProcessStartInfo(Testdateien.Suchen("ffprobe")!)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
};
foreach (var a in new[]
{
"-v", "error", "-show_entries", "stream=index,codec_type,codec_name,height",
"-of", "csv=p=0", datei,
})
{
start.ArgumentList.Add(a);
}
using var prozess = Process.Start(start)!;
var ausgabe = prozess.StandardOutput.ReadToEnd();
prozess.WaitForExit();
var spuren = new List();
foreach (var zeile in ausgabe.Split('\n', StringSplitOptions.RemoveEmptyEntries))
{
var teile = zeile.Trim().Split(',');
if (teile.Length < 3) continue;
MediaStreamKind? art = teile[2] switch
{
"video" => MediaStreamKind.Video,
"audio" => MediaStreamKind.Audio,
"subtitle" => MediaStreamKind.Subtitle,
// Anhänge und Datenströme sind hier nichts, womit sich rechnen
// liesse — sie zu einer Art zu erklären wäre eine Erfindung.
_ => null,
};
if (art is null) continue;
int? hoehe = teile.Length > 3 && int.TryParse(teile[3], out var h) ? h : null;
spuren.Add(new ProbedStream(
int.Parse(teile[0]), art.Value, teile[1], null, null, false, false, null, null, hoehe));
}
return spuren;
}
// ------------------------------------------------------------------
/// Eine MKV wird umgepackt, und es entstehen abspielbare Segmente.
[Test]
public void Eine_mkv_wird_zu_Playlist_und_Segmenten()
{
var quelle = Path.Combine(_ordner, "film.mkv");
Testdateien.MitZweiTonspuren(quelle);
var ziel = Laufen(quelle);
var playlist = Path.Combine(ziel, "index.m3u8");
Assert.Multiple(() =>
{
Assert.That(File.Exists(playlist), "die Liste muss da sein");
Assert.That(File.Exists(Path.Combine(ziel, "init.mp4")),
"fMP4 braucht ein Anfangsstück, sonst kann kein Spieler einsteigen");
Assert.That(Directory.GetFiles(ziel, "*.m4s"), Is.Not.Empty);
});
var text = File.ReadAllText(playlist);
Assert.Multiple(() =>
{
Assert.That(text, Does.StartWith("#EXTM3U"));
Assert.That(text, Does.Contain("#EXT-X-MAP:URI=\"init.mp4\""));
});
}
/// Was herauskommt, ist H.264 und AAC — und nichts anderes.
///
///
/// Das ist der eigentliche Punkt. Die Quelle hat eine Tonspur, die
/// kein Browser kennt; wäre sie kopiert worden, bliebe der Film stumm —
/// und genau so war es vorher.
///
[Test]
public void Aus_untauglichem_Ton_wird_AAC()
{
var quelle = Path.Combine(_ordner, "dts.mkv");
Testdateien.MitUntauglichemTon(quelle);
var ziel = Laufen(quelle, tonspur: 2); // die DTS-Spur ausdrücklich wählen
var codecs = Spuren(Path.Combine(ziel, "index.m3u8")).Select(s => s.Codec).ToList();
Assert.Multiple(() =>
{
Assert.That(codecs, Does.Contain("h264"));
Assert.That(codecs, Does.Contain("aac"));
Assert.That(codecs, Does.Not.Contain("dts"));
});
}
/// Herunterskalieren ändert die Bildhöhe wirklich.
[Test]
public void Herunterskalieren_aendert_die_Hoehe()
{
var quelle = Path.Combine(_ordner, "gross.mkv");
Gross(quelle);
var ziel = Laufen(quelle, hoehe: 120);
var bild = Spuren(Path.Combine(ziel, "index.m3u8"))
.First(s => s.Kind == MediaStreamKind.Video);
Assert.That(bild.Height, Is.EqualTo(120));
}
/// Ein Sprung mitten hinein liefert ebenfalls Segmente.
[Test]
public void Ein_Sprungpunkt_liefert_den_Rest_des_Films()
{
var quelle = Path.Combine(_ordner, "lang.mkv");
Lang(quelle);
var ziel = Laufen(quelle, ab: 4);
Assert.That(Directory.GetFiles(ziel, "*.m4s"), Is.Not.Empty);
}
private static void Gross(string ziel) => Werkzeug(
[
"-y", "-loglevel", "error",
"-f", "lavfi", "-i", "testsrc=duration=1:size=640x480:rate=10",
"-f", "lavfi", "-i", "sine=frequency=440:duration=1",
"-c:v", "libx264", "-preset", "ultrafast", "-c:a", "aac",
ziel,
]);
private static void Lang(string ziel) => Werkzeug(
[
"-y", "-loglevel", "error",
"-f", "lavfi", "-i", "testsrc=duration=8:size=160x120:rate=10",
"-f", "lavfi", "-i", "sine=frequency=440:duration=8",
"-c:v", "libx264", "-preset", "ultrafast", "-c:a", "aac",
ziel,
]);
private static void Werkzeug(string[] argumente)
{
var start = new ProcessStartInfo(Testdateien.Suchen("ffmpeg")!)
{
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(fehler);
}
}