using Halo.Core.Delivery; using Halo.Core.Library; using Microsoft.Extensions.Logging.Abstractions; namespace Halo.Media.Tests; /// Umpacken, von der echten Datei bis zum Ergebnis. public class RemuxTests { private string _ordner = ""; private FfprobeRunner _ffprobe = null!; private FfmpegRemuxer _umpacker = null!; [SetUp] public void Anlegen() { if (!Testdateien.FfmpegVorhanden) Assert.Ignore("ffmpeg ist auf diesem Rechner nicht installiert."); _ordner = Directory.CreateDirectory( Path.Combine(Path.GetTempPath(), "halo-umpacken-" + Guid.CreateVersion7().ToString("N"))).FullName; var optionen = new FfmpegOptions(Testdateien.Suchen("ffmpeg")!, Testdateien.Suchen("ffprobe")!); _ffprobe = new FfprobeRunner(optionen, NullLogger.Instance); _umpacker = new FfmpegRemuxer(optionen, NullLogger.Instance); } [TearDown] public void Aufraeumen() { try { if (_ordner.Length > 0) Directory.Delete(_ordner, recursive: true); } catch (IOException) { } } private async Task<(RemuxOutcome Ergebnis, ProbeResult? Danach)> Umpacken( string quelle, IProgress? fortschritt = null) { var davor = await _ffprobe.ProbeAsync(quelle); Assert.That(davor, Is.Not.Null, "Die Quelldatei liess sich nicht lesen"); var plan = DeliveryDecider.ForDownload(davor!.Container, davor.Streams); var ziel = Path.Combine(_ordner, Path.GetFileNameWithoutExtension(quelle) + ".mp4"); var ergebnis = await _umpacker.RunAsync( new RemuxRequest(quelle, ziel, davor.Streams, plan), davor.Duration, fortschritt); return (ergebnis, ergebnis.Success ? await _ffprobe.ProbeAsync(ziel) : null); } /// Der Beweis, auf dem das ganze Projekt ruht. /// /// /// Zwei Tonspuren und ein Untertitel gehen hinein, und alles kommt in /// der MP4 wieder heraus — samt Sprachkennung und Namen. Genau das /// kann Jellyfins Transcode-Endpunkt nicht: der bildet je Sitzung eine /// Tonspur ab, und beim Umpacken verliert er zusätzlich die /// Sprachkennungen. Dass hier beides erhalten bleibt, ist der Grund für /// diesen Server. /// [Test] public async Task Alle_Ton_und_Textspuren_ueberstehen_das_Umpacken() { var quelle = Path.Combine(_ordner, "zwei-tonspuren.mkv"); Testdateien.MitZweiTonspuren(quelle); var (ergebnis, danach) = await Umpacken(quelle); Assert.That(ergebnis.Success, Is.True, ergebnis.Error); Assert.That(danach!.Container, Is.EqualTo("mp4")); var ton = danach.Streams.Where(s => s.Kind == MediaStreamKind.Audio).ToList(); Assert.That(ton, Has.Count.EqualTo(2), "Beide Tonspuren müssen da sein"); Assert.That(ton.Select(t => t.Title), Is.EqualTo(new[] { "Deutsch", "Audiodeskription" }), "Die Namen unterscheiden die beiden — ohne sie wäre keine Auswahl möglich"); Assert.That(ton.Select(t => t.Language), Is.EqualTo(new[] { "deu", "deu" }), "Die Sprachkennung überlebt — Jellyfin verliert sie an dieser Stelle"); Assert.That(danach.Streams.Count(s => s.Kind == MediaStreamKind.Subtitle), Is.EqualTo(1)); } [Test] public async Task Der_Untertitel_liegt_danach_als_tx3g_vor() { var quelle = Path.Combine(_ordner, "mit-text.mkv"); Testdateien.MitZweiTonspuren(quelle); var (_, danach) = await Umpacken(quelle); var text = danach!.Streams.Single(s => s.Kind == MediaStreamKind.Subtitle); Assert.That(text.Codec, Is.EqualTo("mov_text"), "MP4 kennt nur diese eine Form"); Assert.That(text.Language, Is.EqualTo("deu")); } /// Nur die untaugliche Spur wird gerechnet, die andere kopiert. [Test] public async Task Eine_untaugliche_Tonspur_wird_gerechnet_die_taugliche_nicht() { var quelle = Path.Combine(_ordner, "gemischt.mkv"); Testdateien.MitUntauglichemTon(quelle); var davor = await _ffprobe.ProbeAsync(quelle); Assert.That(davor!.Streams.Select(s => s.Codec), Does.Contain("dts"), "Die Vorlage muss DTS enthalten"); var (ergebnis, danach) = await Umpacken(quelle); Assert.That(ergebnis.Success, Is.True, ergebnis.Error); var ton = danach!.Streams.Where(s => s.Kind == MediaStreamKind.Audio).ToList(); Assert.That(ton, Has.Count.EqualTo(2)); Assert.That(ton.Select(t => t.Codec), Is.EqualTo(new[] { "aac", "aac" }), "DTS wurde zu AAC, und die erste Spur blieb, was sie war"); Assert.That(ton.Select(t => t.Language), Is.EqualTo(new[] { "deu", "eng" }), "Auch die neu gerechnete Spur behält ihre Sprache"); } /// Das Bild wird nie angefasst — nachgewiesen an der Bildgrösse und am Codec. [Test] public async Task Das_Bild_bleibt_unberuehrt() { var quelle = Path.Combine(_ordner, "bild.mkv"); Testdateien.MitZweiTonspuren(quelle); var davor = await _ffprobe.ProbeAsync(quelle); var (_, danach) = await Umpacken(quelle); var vorher = davor!.Streams.Single(s => s.Kind == MediaStreamKind.Video); var nachher = danach!.Streams.Single(s => s.Kind == MediaStreamKind.Video); Assert.That(nachher.Codec, Is.EqualTo(vorher.Codec)); Assert.That((nachher.Width, nachher.Height), Is.EqualTo((vorher.Width, vorher.Height))); } [Test] public async Task Der_Fortschritt_wird_gemeldet_und_endet_bei_eins() { var quelle = Path.Combine(_ordner, "fortschritt.mkv"); Testdateien.MitZweiTonspuren(quelle); var gemeldet = new List(); var (ergebnis, _) = await Umpacken(quelle, new Progress(w => { lock (gemeldet) gemeldet.Add(w); })); Assert.That(ergebnis.Success, Is.True); // Progress meldet über den Synchronisierungskontext — kurz warten. await Task.Delay(200); lock (gemeldet) { Assert.That(gemeldet, Is.Not.Empty); Assert.That(gemeldet[^1], Is.EqualTo(1.0)); Assert.That(gemeldet, Is.Ordered, "Der Fortschritt darf nicht zurückspringen"); } } /// Eine halbe Datei ist schlimmer als keine. /// /// /// Bliebe sie liegen, hielte der nächste Aufruf sie für fertig und gäbe /// einen abgeschnittenen Film heraus — der beim Abspielen mittendrin /// abbricht. /// [Test] public async Task Nach_einem_Fehlschlag_liegt_keine_halbe_Datei_herum() { var quelle = Path.Combine(_ordner, "kaputt.mkv"); await File.WriteAllTextAsync(quelle, "das ist kein Film"); var ziel = Path.Combine(_ordner, "kaputt.mp4"); var ergebnis = await _umpacker.RunAsync(new RemuxRequest( quelle, ziel, [], new DeliveryPlan(DeliveryMethod.Remux, [], [], []))); Assert.That(ergebnis.Success, Is.False); Assert.That(ergebnis.Error, Is.Not.Null.And.Not.Empty); Assert.That(File.Exists(ziel), Is.False); } [Test] public async Task Ein_Abbruch_laesst_nichts_zurueck() { var quelle = Path.Combine(_ordner, "abbruch.mkv"); Testdateien.MitZweiTonspuren(quelle); var ziel = Path.Combine(_ordner, "abbruch.mp4"); var davor = await _ffprobe.ProbeAsync(quelle); using var abbruch = new CancellationTokenSource(); await abbruch.CancelAsync(); Assert.ThrowsAsync(() => _umpacker.RunAsync( new RemuxRequest(quelle, ziel, davor!.Streams, DeliveryDecider.ForDownload(davor.Container, davor.Streams)), ct: abbruch.Token)); Assert.That(File.Exists(ziel), Is.False); } }