Ton und Bild: ein Versatz zum Einstellen, und zwei gemessene Fehler weniger

Du meldest Ton, der drei, vier Sekunden neben dem Bild liegt.
**Nachstellen konnte ich es nicht** — und das sage ich lieber, als eine
Vermutung als Befund zu verkaufen.

Gemessen habe ich alle vier Wege, jeweils mit ffprobe am Ergebnis, erster
Zeitstempel je Spur:

| Weg | Bild ab | Ton ab |
|---|---|---|
| Umpacken, ab 0 | 0,021 | 0,000 |
| Umpacken, ab 12 | 0,000 | 1,979 |
| Umkodieren, ab 0 | 0,021 | 0,000 |
| Umkodieren, ab 12 | 0,021 | 0,000 |

Alle vier sind **zueinander richtig**. Auch eine Quelle mit einem echten
Loch in der Tonspur blieb ausgerichtet. Die naheliegende Erklärung — `-ss`
vor `-i` mit kopiertem Bild und neu gerechnetem Ton — ist damit widerlegt.

**Zwei echte Fehler hat dieselbe Messung aber gefunden**, und beide sehen
aus wie „um Sekunden daneben":

Zeile zwei der Tabelle. Beim Umpacken fängt das Bild beim Schlüsselbild
*vor* dem Sprung an, der Ton genau beim Sprung — bei Sekunde 12 also zwei
Sekunden Bild **ohne Ton** am Anfang. Zueinander stimmen beide, aber jeder
Spieler darf diese Lücke deuten, wie er will. Und der Server meldete dem
Spieler „beginnt bei 12", obwohl er bei 10 beginnt: die Uhr, die
Zeitleiste und der gespeicherte Stand lagen um diesen Unterschied daneben.

Beides geht weg, indem der Sprung **auf das Schlüsselbild gerundet** wird
und derselbe Wert zurückgemeldet wird. Dann fangen Bild und Ton zusammen
an, es gibt keine Stille davor, und die Uhr stimmt. Beim Umkodieren
entfällt das Runden — dort schneidet das neu gerechnete Bild genau.

**Und: ffmpeg durfte bisher nichts sagen.** `-loglevel error` versteckt
genau die Meldungen, die einen Tonversatz ankündigen, und der
Fehlerkanal wurde bei Rückgabewert 0 weggeworfen. Jetzt steht er auf
`warning`, und Warnungen landen auch bei einem geglückten Lauf im
Protokoll. Wenn es bei dir wieder auftritt, steht diesmal etwas da.

**Der Versatz zum Einstellen** sitzt im Tonmenü des Spielers: „Später"
und „Früher" in Hundertmillisekundenschritten, bis ±5 s, dazu
„Zurücksetzen". Gemerkt wird er **je Datei in diesem Browser** — ein
Versatz ist meist eine Eigenschaft der Datei, kann aber auch an der
Anlage liegen, und über Bluetooth stimmt etwas anderes als am Fernseher.

Verschoben wird serverseitig: die Datei wird ein zweites Mal mit
`-itsoffset` geöffnet und von dort nur der Ton genommen. Mit einer
Eingabe verschöbe sich beides gemeinsam. Der Preis: der Strom startet neu,
und eine durchgereichte Datei muss dafür umgepackt werden — ohne ffmpeg
lässt sich nichts verschieben.

Zwei neue Läufe halten das fest: dass Bild und Ton zusammen anfangen, und
dass ein halbe-Sekunde-Versatz den Ton um eine halbe Sekunde verschiebt
und das Bild gar nicht. Der Sprungtest daneben lief bisher genau durch
diesen Fall und sah nur nach, ob überhaupt Segmente entstehen.

Ein Test musste umgeschrieben werden: er stand auf „Einstiegspunkt ist
genau 2" und hielt damit die falsche Meldung fest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-07 19:37:21 +02:00
co-authored by Claude Opus 5
parent 3fe0013be1
commit cf272f20aa
10 changed files with 374 additions and 20 deletions
+37 -3
View File
@@ -32,7 +32,12 @@ public sealed record Stromanfrage(
int? Tonspur = null,
double Ab = 0,
Beschleunigung Hardware = Beschleunigung.Software,
Kodierstufe Stufe = Kodierstufe.Ausgewogen);
Kodierstufe Stufe = Kodierstufe.Ausgewogen,
/// <param name="Tonversatz">
/// Um wie viele Sekunden der Ton gegenüber dem Bild verschoben wird.
/// Positiv heisst später, negativ früher. <c>0</c> heisst: nichts tun.
/// </param>
double Tonversatz = 0);
/// <summary>Baut die ffmpeg-Zeile für einen laufenden Strom.</summary>
///
@@ -80,7 +85,12 @@ public static class HlsArgumente
public static List<string> Fuer(Stromanfrage anfrage)
{
var argumente = new List<string> { "-y", "-loglevel", "error" };
// **`warning`, nicht `error`.** Genau die Meldungen, die einen
// Tonversatz ankündigen — „Non-monotonous DTS", „first pts value …
// forcing" —, sind Warnungen. Auf `error` waren sie unsichtbar, und
// ein Lauf, der schief aussieht, aber mit 0 endet, hinterliess keine
// Spur.
var argumente = new List<string> { "-y", "-loglevel", "warning" };
// **Die Beschleunigung steht vor der Eingabe.** Danach wäre sie eine
// Angabe über die Ausgabe und bliebe wirkungslos — ffmpeg sagt dazu
@@ -103,6 +113,26 @@ public static class HlsArgumente
argumente.Add("-i");
argumente.Add(anfrage.Quelle);
// **Der Tonversatz braucht eine zweite Eingabe.** `-itsoffset` gilt
// für alles, was hinter ihm steht; mit nur einer Eingabe verschöbe es
// Bild und Ton gemeinsam und wäre damit wirkungslos. Dieselbe Datei
// ein zweites Mal zu öffnen kostet einen Demuxer und sonst nichts —
// gelesen wird ohnehin nur eine Spur daraus.
var zweiteEingabe = Math.Abs(anfrage.Tonversatz) > 0.0005;
if (zweiteEingabe)
{
argumente.Add("-itsoffset");
argumente.Add(anfrage.Tonversatz.ToString("0.###", CultureInfo.InvariantCulture));
if (anfrage.Ab > 0)
{
argumente.Add("-ss");
argumente.Add(anfrage.Ab.ToString("0.###", CultureInfo.InvariantCulture));
}
argumente.Add("-i");
argumente.Add(anfrage.Quelle);
}
var bild = anfrage.Spuren.FirstOrDefault(s => s.Kind == MediaStreamKind.Video);
var ton = Tonspur(anfrage.Spuren, anfrage.Tonspur);
@@ -111,7 +141,11 @@ public static class HlsArgumente
// deshalb über eine neue Sitzung. Untertitel kommen als eigene
// WebVTT-Datei daneben und brauchen den Strom gar nicht anzufassen.
if (bild is not null) { argumente.Add("-map"); argumente.Add($"0:{bild.Index}"); }
if (ton is not null) { argumente.Add("-map"); argumente.Add($"0:{ton.Index}"); }
if (ton is not null)
{
argumente.Add("-map");
argumente.Add($"{(zweiteEingabe ? 1 : 0)}:{ton.Index}");
}
argumente.AddRange(Bildargumente(anfrage, bild));
argumente.AddRange(Tonargumente(anfrage, ton));