using Flaro.Core.Media; namespace Flaro.Core.Tests; /// Wo der Abspann eines Films anfängt. /// /// /// Mit erfundenen Messreihen statt mit Filmen. Geprüft wird das /// Beurteilen, nicht ob ffmpeg rechnen kann. Die Zahlen sind der Anteil /// dunkler Pixel je Bild und stammen aus einer Messung an erzeugtem /// 1080p-Material: Handlung 0 %, Abspann 84 bis 87 %, dunkle Nachtszene /// 37 %. /// public class AbspannkanteTests { private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe; /// Eine Reihe von Messpunkten im Abstand von zehn Sekunden. /// /// Ab wann dunkel. null heisst: nie. private static List Reihe(double von, double bis, double? abspannAb) { var proben = new List(); for (var t = von; t <= bis; t += 10) proben.Add(new Bildprobe(t, abspannAb is { } ab && t >= ab ? 85 : 0)); return proben; } [Test] public void Eine_dunkle_Strecke_bis_zum_Ende_ist_der_Abspann() { var proben = Reihe(0, 600, abspannAb: 420); Assert.That(Abspannkante.Suchen(proben, Regeln), Is.EqualTo(420)); } /// Ein Film, der hell endet, hat keinen erkennbaren Abspann. /// /// /// Namen über laufendem Bild — Marvel, Pixar, jede zweite Komödie. Kein /// Fund ist hier die richtige Antwort: ein geratener Knopf schnitte /// Handlung weg. /// [Test] public void Ohne_dunkles_Ende_kein_Fund() { Assert.That(Abspannkante.Suchen(Reihe(0, 600, abspannAb: null), Regeln), Is.Null); } /// Eine Schwarzblende mitten im Film zählt nicht. [Test] public void Eine_Schwarzblende_in_der_Mitte_zaehlt_nicht() { var proben = Reihe(0, 600, abspannAb: null); for (var i = 20; i < 30; i++) proben[i] = proben[i] with { Dunkelanteil = 92 }; Assert.That(Abspannkante.Suchen(proben, Regeln), Is.Null, "sie hält nicht bis zum Ende durch"); } /// Eine Szene nach dem Abspann nimmt den Fund weg. /// /// /// Lieber keine Marke als eine, die über die Szene hinwegspringt. /// [Test] public void Eine_Szene_nach_dem_Abspann_nimmt_den_Fund_weg() { var proben = Reihe(0, 600, abspannAb: 420); for (var i = proben.Count - 5; i < proben.Count; i++) proben[i] = proben[i] with { Dunkelanteil = 0 }; Assert.That(Abspannkante.Suchen(proben, Regeln), Is.Null); } /// Eine helle Schrifttafel mittendrin bricht die Strecke nicht. /// /// /// Eine Probe, also zehn Sekunden. Hier standen zwei, und das /// war unter der alten Zählweise gedacht: nachgesehen wurde, wie viele /// Proben am Stück hell sind, nicht wie lange. Zwanzig Sekunden /// helles Bild sind keine Schrifttafel mehr, sondern eine Szene — dass /// die Suche dort abbricht, ist richtig und steht in /// AbspannNachsichtTests. /// [Test] public void Eine_kurze_helle_Tafel_bricht_nichts() { var proben = Reihe(0, 600, abspannAb: 420); proben[50] = proben[50] with { Dunkelanteil = 0 }; Assert.That(Abspannkante.Suchen(proben, Regeln), Is.EqualTo(420)); } /// Zwanzig Sekunden Dunkelheit sind kein Abspann. [Test] public void Zu_kurz_zaehlt_nicht() { Assert.That(Abspannkante.Suchen(Reihe(0, 600, abspannAb: 580), Regeln), Is.Null); } /// Ein halber Film im Dunkeln ist ein Fehlfund. [Test] public void Zu_lang_zaehlt_auch_nicht() { Assert.That(Abspannkante.Suchen(Reihe(0, 3600, abspannAb: 600), Regeln), Is.Null); } [Test] public void Die_letzte_helle_Probe_begrenzt_die_Suche() { var proben = Reihe(0, 600, abspannAb: 420); Assert.That(Abspannkante.HellDavor(proben, 420, Regeln), Is.EqualTo(410)); } // ---------- Nachmessen ---------- /// Die binäre Suche findet die Kante auf vier Sekunden genau. [Test] public async Task Die_Kante_wird_eingegrenzt() { const double echteKante = 417.3; var sonden = 0; var kante = await Abspannkante.VerfeinernAsync(410, 420, Regeln.Mindestanteil, stelle => { sonden++; return Task.FromResult(stelle >= echteKante ? 85 : 0); }); Assert.Multiple(() => { Assert.That(kante, Is.EqualTo(echteKante).Within(4)); Assert.That(kante, Is.GreaterThanOrEqualTo(echteKante), "lieber ein paar Sekunden Abspann zu viel als eine Sekunde Handlung"); Assert.That(sonden, Is.LessThanOrEqualTo(4), "zehn Sekunden auf vier: zwei Halbierungen"); }); } /// Antwortet eine Sonde nicht, gilt der grobe Fund. [Test] public async Task Ohne_Antwort_bleibt_es_beim_groben_Fund() { var kante = await Abspannkante.VerfeinernAsync( 410, 420, Regeln.Mindestanteil, _ => Task.FromResult(null)); Assert.That(kante, Is.EqualTo(420)); } /// Ein schon enger Bereich braucht keine Sonde. [Test] public async Task Ein_enger_Bereich_wird_nicht_angefasst() { var sonden = 0; var kante = await Abspannkante.VerfeinernAsync(418, 420, Regeln.Mindestanteil, _ => { sonden++; return Task.FromResult(85); }); Assert.Multiple(() => { Assert.That(kante, Is.EqualTo(420)); Assert.That(sonden, Is.Zero); }); } } /// Der Anteil dunkler Pixel, an gemessenen Werten. /// /// /// /// Wofür diese Prüfungen stehen. Gemessen wurde lange der /// Mittelwert der Helligkeit, und daran scheiterte die Erkennung /// wiederholt: derselbe Abspann misst 19, wenn wenig Schrift darin /// steht, und 37, wenn viel darin steht — die Schwelle musste zwischen /// diesen Wert und die Handlung passen, und der Abstand schrumpfte mit /// jedem Namen. Zweimal nachgezogen, zweimal wieder getroffen. /// /// /// Der Anteil dunkler Pixel kennt dieses Problem nicht. Schrift /// belegt ein bis zehn Prozent der Fläche; der Rest bleibt schwarz. /// Nachgemessen an erzeugtem 1080p-Material mit zwei Spalten Namen, /// durch dieselbe ffmpeg-Kette wie im Betrieb /// (format=gray,blackframe=amount=0:threshold=24): /// /// /// Handlung: 0 % /// Abspann, schriftreich: 84 % /// Abspann, schlank: 87 % /// dunkle Nachtszene: 37 % /// /// /// Sechzig Prozent trennen das sauber, und zwar nach beiden Seiten. /// /// public class AbspannAnteilTests { private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe; /// Ein Suchfenster, wie es im Betrieb entsteht. /// /// /// Zwanzig Minuten vor dem Ende einer Folge von fünfundvierzig, an den /// Schlüsselbildern abgetastet — also alle fünf Sekunden eine Probe. /// private static List Fenster( double abspannAb, double imAbspann, double imInhalt = 0) { var proben = new List(); for (var t = 1500.0; t <= 2700; t += 5) proben.Add(new Bildprobe(t, t < abspannAb ? imInhalt : imAbspann)); return proben; } /// Der schriftreiche Abspann — der Fall, an dem es scheiterte. [Test] public void Ein_dichter_Abspann_bei_vierundachtzig_Prozent_wird_gefunden() { var proben = Fenster(abspannAb: 2340, imAbspann: 84); Assert.That(Abspannkante.Suchen(proben, Regeln), Is.EqualTo(2340.0).Within(5)); } [Test] public void Ein_schlanker_Abspann_bei_siebenundachtzig_Prozent_auch() { var proben = Fenster(abspannAb: 2340, imAbspann: 87); Assert.That(Abspannkante.Suchen(proben, Regeln), Is.EqualTo(2340.0).Within(5)); } /// **Eine dunkle Nachtszene ist kein Abspann.** /// /// /// Mit dem Mittelwert war das die heikelste Grenze: eine Nachtszene /// misst 28, ein schlanker Abspann 19 — zwischen diesen neun Punkten /// musste die Schwelle liegen. Nach Pixelanteilen sind es 37 gegen 87, /// und die Frage stellt sich nicht mehr. /// [Test] public void Eine_duestere_Schlussszene_ist_kein_Abspann() { var proben = Fenster(abspannAb: 2340, imAbspann: 37, imInhalt: 12); Assert.That(Abspannkante.Suchen(proben, Regeln), Is.Null); } /// Und ein heller Film bis zum Ende bleibt Handlung. [Test] public void Ohne_dunkles_Ende_kein_Fund() { var proben = Fenster(abspannAb: double.MaxValue, imAbspann: 99); Assert.That(Abspannkante.Suchen(proben, Regeln), Is.Null); } } /// Was hinter dem Abspann noch kommt. /// /// /// Der Fehler, für den diese Prüfungen stehen. Die Suche verlangte, /// dass die allerletzte Probe dunkel ist. Hinter dem Abspann steht bei /// vielen Produktionen aber noch eine Logotafel — Sender, Studio, /// Förderer —, oft hell. Ein einziges weisses Schlussbild machte damit den /// ganzen Abspann unsichtbar. /// public class AbspannSchlusstafelTests { private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe; private static List Fenster(double abspannAb, double tafelAb = double.MaxValue) { var proben = new List(); for (var t = 1500.0; t <= 2700; t += 5) { var dunkel = t >= tafelAb ? 0 : t < abspannAb ? 0 : 85; proben.Add(new Bildprobe(t, dunkel)); } return proben; } /// Eine helle Tafel am Schluss verdeckt den Abspann nicht. [Test] public void Eine_Logotafel_am_Ende_zaehlt_nicht_dagegen() { // Fünfzehn Sekunden weiss hinter dem Abspann. var proben = Fenster(abspannAb: 2340, tafelAb: 2690); Assert.That(Abspannkante.Suchen(proben, Regeln), Is.EqualTo(2340).Within(5)); } /// Eine lange helle Strecke am Ende schon. /// /// /// Die Nachsicht hat eine Grenze, sonst fände die Suche hinter jeder /// Schwarzblende einen Abspann und hinge die halbe Handlung daran. /// [Test] public void Eine_lange_helle_Strecke_am_Ende_bricht_ab() { var proben = Fenster(abspannAb: 2340, tafelAb: 2600); Assert.That(Abspannkante.Suchen(proben, Regeln), Is.Null); } } /// Dieselbe Szene, zweierlei Schlüsselbilddichte. /// /// /// /// Der Fehler, für den diese Prüfungen stehen. Die Nachsicht /// gegenüber hellen Stellen mitten im Abspann war in Proben /// angegeben, nicht in Sekunden. Was das bedeutet, hängt allein daran, /// wie dicht eine Datei ihre Schlüsselbilder setzt — und das schwankt um /// eine Zehnerpotenz. /// /// /// Gemessen am Bestand: „(T)Raumschiff Surprise" liefert 1318 Proben auf /// zwanzig Minuten, also eine alle 0,9 Sekunden. Zwei Proben Nachsicht /// waren dort 1,8 Sekunden; bei einer Datei mit einem Schlüsselbild alle /// zehn Sekunden waren dieselben zwei Proben zwanzig Sekunden. Ein /// fünfsekündiges Logo mitten im Abspann zerriss die Strecke deshalb nur /// bei der dicht abgetasteten Datei — und genau die fiel durch. /// /// public class AbspannNachsichtTests { private static readonly Abspannregeln Regeln = Abspannregeln.Vorgabe; /// Abspann ab 2340 s, darin fünf Sekunden helles Logo. private static List Fenster(double takt) { var proben = new List(); for (var t = 1500.0; t <= 2700; t += takt) { var imLogo = t >= 2500 && t < 2505; proben.Add(new Bildprobe(t, t < 2340 ? 0 : imLogo ? 5 : 85)); } return proben; } /// Dicht abgetastet — der Fall, der im Bestand durchfiel. [Test] public void Ein_Logo_zerreisst_den_Abspann_nicht_bei_dichten_Proben() { Assert.That(Abspannkante.Suchen(Fenster(takt: 0.9), Regeln), Is.EqualTo(2340.0).Within(2)); } /// Dünn abgetastet — derselbe Film, dieselbe Antwort. [Test] public void Und_auch_nicht_bei_duennen_Proben() { Assert.That(Abspannkante.Suchen(Fenster(takt: 10), Regeln), Is.EqualTo(2340.0).Within(11)); } /// Eine lange helle Strecke bricht weiterhin ab. /// /// /// Sonst liefe die Suche rückwärts durch jede Nachtszene und meldete /// den halben Film als Abspann. /// [Test] public void Eine_lange_helle_Strecke_bricht_die_Suche() { var proben = new List(); for (var t = 1500.0; t <= 2700; t += 1) { // Dunkel ab 2340, aber von 2400 bis 2460 eine Minute hell. var hell = t < 2340 || (t >= 2400 && t < 2460); proben.Add(new Bildprobe(t, hell ? 0 : 85)); } // Gefunden wird nur das Stück hinter der hellen Minute. Assert.That(Abspannkante.Suchen(proben, Regeln), Is.EqualTo(2460.0).Within(2)); } /// Und der Grund steht für das Protokoll bereit. [Test] public void Der_Grund_wird_benannt() { var hell = new List(); for (var t = 1500.0; t <= 2700; t += 5) hell.Add(new Bildprobe(t, 0)); Abspannkante.Suchen(hell, Regeln, out var grund); Assert.That(grund, Does.Contain("hell")); var kurz = new List(); for (var t = 1500.0; t <= 2700; t += 5) kurz.Add(new Bildprobe(t, t < 2680 ? 0 : 85)); Abspannkante.Suchen(kurz, Regeln, out var zuKurz); Assert.That(zuKurz, Does.Contain("kurz")); } }