using Flaro.Core.Delivery;
using Flaro.Core.Library;
using Microsoft.Extensions.Logging.Abstractions;
namespace Flaro.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(), "flaro-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);
}
}