Vier Sekunden, die ffprobe zu früh aufhörte zu lesen
Die Untertitel kamen vier Sekunden zu spät. Nicht an ihnen lag es: der
Server meldete dem Spieler einen Anfang, der vor dem lag, an dem der
Strom wirklich begann, und genau diese Differenz landet eins zu eins im
Versatz.
Die Schlüsselbildsuche las mit `-read_intervals` bis eine halbe Sekunde
hinter den Wunsch. ffprobe bricht am Ende eines Fensters auf Paketebene
ab — das letzte Schlüsselbild darin kam regelmässig gar nicht mehr
heraus, und die Suche gab das davor zurück. Bei Bildgruppen von vier
Sekunden sind das vier Sekunden.
Nachgemessen gegen die Stelle, an der ffmpeg tatsächlich anfängt, an
einer Datei mit Schlüsselbildern alle vier Sekunden:
Wunsch Suche (0,5 s) ffmpeg Abweichung
52,0 48 52,0 −4,0
73,3 68 72,0 −4,0
100,2 96 100,0 −4,0
Mit zehn Sekunden Nachlauf lag dieselbe Messung bei allen sieben Werten
auf 0,000. Was über den Wunsch hinausgelesen wird, verwirft die
Filterung ohnehin.
Die Prüfung vergleicht nicht gegen eine erwartete Zahl, sondern gegen
ffmpeg selbst — mit dem alten Wert fällt sie um.
Dazu der Filmteil der Abspannerkennung: `Abspannkante` als reine
Rechnung mit elf Prüfungen, `Bildabtaster` über ffmpeg, Marken auch an
Filmen, Knöpfe zum Suchen und Verwerfen auf der Filmseite. An einer
erzeugten Datei trifft die Kante auf vier Sekunden genau.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8112c15d4d
commit
7673c73757
@@ -151,7 +151,17 @@ public enum Markenart
|
||||
public sealed class Marke
|
||||
{
|
||||
public Guid Id { get; init; } = Guid.CreateVersion7();
|
||||
public required Guid EpisodeId { get; init; }
|
||||
/// <summary>Zu welcher Folge sie gehört — oder zu welchem Film.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Genau eines von beiden.</b> Gefunden werden sie auf
|
||||
/// verschiedenen Wegen: bei einer Folge am Ton, weil der Vorspann sich
|
||||
/// über die Staffel wiederholt, bei einem Film am Bild, weil ein Film
|
||||
/// nichts hat, womit er sich vergleichen liesse. Was dabei herauskommt,
|
||||
/// ist dasselbe — eine Stelle, an der man springen darf.
|
||||
/// </remarks>
|
||||
public Guid? EpisodeId { get; init; }
|
||||
public Guid? MovieId { get; init; }
|
||||
public required Markenart Art { get; init; }
|
||||
public required double VonSekunden { get; set; }
|
||||
public required double BisSekunden { get; set; }
|
||||
@@ -188,11 +198,22 @@ public sealed class Marke
|
||||
public sealed class Markensuche
|
||||
{
|
||||
public Guid Id { get; init; } = Guid.CreateVersion7();
|
||||
public required Guid SeriesId { get; init; }
|
||||
public required int Staffel { get; init; }
|
||||
|
||||
/// <summary>Eine Staffel einer Serie — oder ein Film.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Genau eines von beiden, wie bei <see cref="Marke"/>. Bei einer
|
||||
/// Staffel steht die Nummer daneben; ein Film hat keine.
|
||||
/// </remarks>
|
||||
public Guid? SeriesId { get; init; }
|
||||
public int? Staffel { get; init; }
|
||||
public Guid? MovieId { get; init; }
|
||||
|
||||
public required Markenart Art { get; init; }
|
||||
|
||||
/// <summary>Wie viele Folgen die Staffel hatte, als gesucht wurde.</summary>
|
||||
///
|
||||
/// <remarks>Bei einem Film immer eins — es gibt nichts zu zählen.</remarks>
|
||||
public required int Folgen { get; set; }
|
||||
|
||||
public DateTimeOffset GelaufenAm { get; set; }
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
namespace Flaro.Core.Media;
|
||||
|
||||
/// <summary>Ein Messpunkt aus dem Bild: wann, und wie hell.</summary>
|
||||
///
|
||||
/// <param name="Sekunde">Die Stelle im Film.</param>
|
||||
/// <param name="Helligkeit">Mittlere Leuchtdichte, 0 bis 255.</param>
|
||||
public readonly record struct Bildprobe(double Sekunde, double Helligkeit);
|
||||
|
||||
/// <summary>Die Schwellen, nach denen eine dunkle Strecke ein Abspann ist.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Alle an einer Stelle, damit man sie gemeinsam verschieben kann.</b>
|
||||
/// Jede einzelne ist eine Setzung; verstreut über drei Dateien liesse sich
|
||||
/// nicht mehr sagen, warum ein Fund herausfiel.
|
||||
/// </remarks>
|
||||
public readonly record struct Abspannregeln
|
||||
{
|
||||
/// <summary>Ab wann ein Bild als dunkel gilt (0–255).</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Gemessen an einem erzeugten Film: Handlung 125, Abspann 18.
|
||||
/// Fünfunddreissig liegt weit von beiden.
|
||||
/// </remarks>
|
||||
public double Dunkelschwelle { get; init; }
|
||||
|
||||
/// <summary>Wie viele helle Proben am Stück eine Strecke nicht brechen.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Eine Schrifttafel blitzt hell auf, ein Logo steht zwei Sekunden im
|
||||
/// Bild. Ohne Nachsicht zerfiele ein Abspann in fünf kurze Strecken,
|
||||
/// von denen keine lang genug wäre.
|
||||
/// </remarks>
|
||||
public int Lücke { get; init; }
|
||||
|
||||
public double Mindestlänge { get; init; }
|
||||
public double Höchstlänge { get; init; }
|
||||
|
||||
public static Abspannregeln Vorgabe => new()
|
||||
{
|
||||
Dunkelschwelle = 35,
|
||||
Lücke = 2,
|
||||
Mindestlänge = 45,
|
||||
Höchstlänge = 12 * 60,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Findet, wo der Abspann eines Films anfängt.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Warum das Bild und nicht der Ton.</b> Bei einer Serie erkennt man
|
||||
/// einen Abspann daran, dass er in jeder Folge derselbe ist. Ein Film hat
|
||||
/// nichts, womit er sich vergleichen liesse — sein Abspann kommt genau
|
||||
/// einmal vor. Was bleibt, ist, dass er dunkel ist.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Gesucht wird nicht „ein dunkles Bild", sondern die Kante.</b> Dunkle
|
||||
/// Einzelbilder gibt es in jedem Film. Ein Abspann ist eine dunkle Strecke,
|
||||
/// die bis zum Ende durchhält — und genau das Durchhalten ist das Merkmal,
|
||||
/// nicht die Dunkelheit.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Reine Rechnung, ohne Datei.</b> Das Abtasten braucht ffmpeg; das
|
||||
/// Beurteilen braucht nichts. Getrennt lässt sich das Schwierige prüfen,
|
||||
/// ohne einen Film vorzuhalten — dieselbe Naht wie bei
|
||||
/// <see cref="Tonabgleich"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class Abspannkante
|
||||
{
|
||||
/// <summary>Die dunkle Strecke, die bis zum Ende durchhält — oder nichts.</summary>
|
||||
///
|
||||
/// <param name="proben">
|
||||
/// Messpunkte über das Ende des Films, aufsteigend nach Zeit. Sie dürfen
|
||||
/// ungleichmässig liegen: Schlüsselbilder tun das.
|
||||
/// </param>
|
||||
public static double? Suchen(IReadOnlyList<Bildprobe> proben, Abspannregeln regeln)
|
||||
{
|
||||
if (proben.Count < 2) return null;
|
||||
|
||||
// **Von hinten.** Der Abspann endet am Dateiende; alles, was davor
|
||||
// dunkel war und wieder hell wurde, war eine Schwarzblende.
|
||||
var letzte = proben.Count - 1;
|
||||
if (proben[letzte].Helligkeit >= regeln.Dunkelschwelle) return null;
|
||||
|
||||
var anfang = letzte;
|
||||
var hellAmStück = 0;
|
||||
|
||||
for (var i = letzte - 1; i >= 0; i--)
|
||||
{
|
||||
if (proben[i].Helligkeit < regeln.Dunkelschwelle)
|
||||
{
|
||||
anfang = i;
|
||||
hellAmStück = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (++hellAmStück > regeln.Lücke) break;
|
||||
}
|
||||
|
||||
var länge = proben[letzte].Sekunde - proben[anfang].Sekunde;
|
||||
if (länge < regeln.Mindestlänge || länge > regeln.Höchstlänge) return null;
|
||||
|
||||
// **Die erste dunkle Probe ist nicht der Anfang.** Zwischen ihr und
|
||||
// der letzten hellen davor liegt die Kante irgendwo; wo genau, sagt
|
||||
// erst das Nachmessen. Hier steht die späteste Stelle, an der sie
|
||||
// liegen kann — und die ist als Marke schon brauchbar.
|
||||
return proben[anfang].Sekunde;
|
||||
}
|
||||
|
||||
/// <summary>Die letzte helle Probe vor dem Fund — der Suchbereich fürs Nachmessen.</summary>
|
||||
public static double? HellDavor(
|
||||
IReadOnlyList<Bildprobe> proben, double dunkelAb, Abspannregeln regeln)
|
||||
{
|
||||
double? letzteHelle = null;
|
||||
foreach (var p in proben)
|
||||
{
|
||||
if (p.Sekunde >= dunkelAb) break;
|
||||
if (p.Helligkeit >= regeln.Dunkelschwelle) letzteHelle = p.Sekunde;
|
||||
}
|
||||
return letzteHelle;
|
||||
}
|
||||
|
||||
/// <summary>Sucht die Kante zwischen hell und dunkel genauer ein.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Binäre Suche mit kurzen Sonden, nicht noch ein Durchlauf.</b> Die
|
||||
/// grobe Reihe stammt aus Schlüsselbildern, und die liegen je nach
|
||||
/// Aufnahme fünf bis zehn Sekunden auseinander. Den ganzen Bereich fein
|
||||
/// abzutasten hiesse, ihn zu dekodieren; zehn Sonden von je einer
|
||||
/// Sekunde kosten einen Bruchteil davon. Denselben Weg geht Jellyfins
|
||||
/// Intro Skipper, und hört bei denselben vier Sekunden auf.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <paramref name="messen"/> liefert die Helligkeit an einer Stelle oder
|
||||
/// <c>null</c>, wenn dort nichts zu holen war. Eine Sonde ohne Antwort
|
||||
/// beendet die Suche — dann gilt, was die grobe Reihe sagte.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static async Task<double> VerfeinernAsync(
|
||||
double hell, double dunkel, Abspannregeln regeln,
|
||||
Func<double, Task<double?>> messen, double genauigkeit = 4)
|
||||
{
|
||||
if (genauigkeit <= 0) genauigkeit = 1;
|
||||
|
||||
while (dunkel - hell > genauigkeit)
|
||||
{
|
||||
var mitte = hell + (dunkel - hell) / 2;
|
||||
var wert = await messen(mitte);
|
||||
if (wert is not { } helligkeit) return dunkel;
|
||||
|
||||
if (helligkeit < regeln.Dunkelschwelle) dunkel = mitte;
|
||||
else hell = mitte;
|
||||
}
|
||||
|
||||
// Die dunkle Seite gewinnt: lieber ein paar Sekunden Abspann zu viel
|
||||
// überspringen als eine Sekunde Handlung.
|
||||
return dunkel;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user