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:
co-authored by
Claude Opus 5
parent
3fe0013be1
commit
cf272f20aa
+72
-7
@@ -85,6 +85,43 @@ export function geraeteprofil(): {
|
||||
/** Wie oft der Stand zum Server geht. */
|
||||
const MELDEABSTAND = 10_000;
|
||||
|
||||
/** In welchen Schritten sich der Tonversatz verstellen lässt. */
|
||||
const VERSATZSCHRITT = 100;
|
||||
|
||||
/** Wie weit in beide Richtungen. Darüber hinaus ist es kein Versatz mehr,
|
||||
* sondern eine kaputte Datei. */
|
||||
const VERSATZGRENZE = 5000;
|
||||
|
||||
/** Wo gemerkte Tonversätze liegen — je Datei, in diesem Browser.
|
||||
*
|
||||
* **Je Datei, weil es eine Eigenschaft der Datei ist.** Wer einen Film einmal
|
||||
* geradegerückt hat, soll das beim nächsten Mal nicht wieder tun. Und in
|
||||
* diesem Browser, weil ein Versatz auch an der Anlage liegen kann — über
|
||||
* Bluetooth-Kopfhörer stimmt etwas anderes als am Fernseher. */
|
||||
const VERSATZSCHLUESSEL = 'flaro.tonversatz';
|
||||
|
||||
function versatzLesen(datei: string): number {
|
||||
try {
|
||||
const roh = JSON.parse(localStorage.getItem(VERSATZSCHLUESSEL) ?? '{}');
|
||||
const wert = Number(roh?.[datei]);
|
||||
return Number.isFinite(wert) ? wert : 0;
|
||||
} catch { return 0; }
|
||||
}
|
||||
|
||||
function versatzMerken(datei: string, wert: number): void {
|
||||
try {
|
||||
const roh = JSON.parse(localStorage.getItem(VERSATZSCHLUESSEL) ?? '{}');
|
||||
if (wert === 0) delete roh[datei]; else roh[datei] = wert;
|
||||
localStorage.setItem(VERSATZSCHLUESSEL, JSON.stringify(roh));
|
||||
} catch { /* Privates Fenster — dann gilt er eben nur für diesen Film. */ }
|
||||
}
|
||||
|
||||
/** „läuft mit", „200 ms später", „300 ms früher". */
|
||||
function versatzWort(ms: number): string {
|
||||
if (ms === 0) return 'läuft mit dem Bild';
|
||||
return ms > 0 ? `${ms} ms später` : `${-ms} ms früher`;
|
||||
}
|
||||
|
||||
/** Die Stufen, die im Menü stehen. Muss zu `Stromentscheid.Stufen` passen. */
|
||||
const STUFEN: { wert: number | null; text: string }[] = [
|
||||
{ wert: null, text: 'Original' },
|
||||
@@ -145,6 +182,9 @@ export function spieler(anfrage: Spieleranfrage): void {
|
||||
/** Wohin gesprungen wird, sobald die Metadaten da sind. Nur bei Direktwiedergabe. */
|
||||
let zuSpringen = 0;
|
||||
|
||||
/** Der Tonversatz in Millisekunden — gemerkt je Datei. */
|
||||
let tonversatz = versatzLesen(anfrage.dateiId);
|
||||
|
||||
/** Die gewählte Stufe und Spuren — sie überleben einen Neustart des Stroms. */
|
||||
let stufe: number | null = null;
|
||||
let tonspur: number | null = null;
|
||||
@@ -458,6 +498,7 @@ export function spieler(anfrage: Spieleranfrage): void {
|
||||
audioStream: tonspur,
|
||||
startSeconds: ab,
|
||||
transcode: umkodierenErzwingen,
|
||||
audioOffsetMs: tonversatz,
|
||||
});
|
||||
} catch (fehler) {
|
||||
zeigeFehler(meldung(fehler));
|
||||
@@ -576,13 +617,37 @@ export function spieler(anfrage: Spieleranfrage): void {
|
||||
*/
|
||||
function menuesAuffrischen(): void {
|
||||
const toene = auskunft?.audioStreams ?? [];
|
||||
tonmenue.setzen(
|
||||
toene.length > 0
|
||||
? toene.map(s => ({
|
||||
text: s.label, gewaehlt: s.index === tonspur,
|
||||
tun: () => { tonspur = s.index; void stromAufbauen(filmzeit()); },
|
||||
}))
|
||||
: [{ text: 'Diese Datei hat keinen Ton.', gewaehlt: false, tun: () => {} }]);
|
||||
const spuren = toene.length > 0
|
||||
? toene.map(s => ({
|
||||
text: s.label, gewaehlt: s.index === tonspur,
|
||||
tun: () => { tonspur = s.index; void stromAufbauen(filmzeit()); },
|
||||
}))
|
||||
: [{ text: 'Diese Datei hat keinen Ton.', gewaehlt: false, tun: () => {} }];
|
||||
|
||||
// **Der Versatz steht im Tonmenü, nicht in einem eigenen.** Wer ihn
|
||||
// sucht, sucht ihn beim Ton — und ein vierter Knopf in der Leiste wäre
|
||||
// einer für etwas, das man im Jahr dreimal braucht.
|
||||
//
|
||||
// Zwei Schritte und ein Zurück statt einer Liste: eine Aufzählung von
|
||||
// −5000 bis +5000 in Hundertern wären einhunderteins Einträge, und man
|
||||
// sucht ohnehin nicht einen Wert, sondern rückt ihn, bis es passt.
|
||||
const verstellen = (um: number): void => {
|
||||
tonversatz = Math.max(-VERSATZGRENZE, Math.min(VERSATZGRENZE, tonversatz + um));
|
||||
versatzMerken(anfrage.dateiId, tonversatz);
|
||||
void stromAufbauen(filmzeit());
|
||||
};
|
||||
|
||||
// Ohne Tonspur gibt es nichts zu verschieben.
|
||||
const versatzpunkte = toene.length === 0 ? [] : [
|
||||
{ text: `Ton ${versatzWort(tonversatz)}`, gewaehlt: false, tun: () => {} },
|
||||
{ text: `Später (+${VERSATZSCHRITT} ms)`, gewaehlt: false, tun: () => verstellen(VERSATZSCHRITT) },
|
||||
{ text: `Früher (−${VERSATZSCHRITT} ms)`, gewaehlt: false, tun: () => verstellen(-VERSATZSCHRITT) },
|
||||
...(tonversatz !== 0
|
||||
? [{ text: 'Versatz zurücksetzen', gewaehlt: false, tun: () => verstellen(-tonversatz) }]
|
||||
: []),
|
||||
];
|
||||
|
||||
tonmenue.setzen([...spuren, ...versatzpunkte]);
|
||||
|
||||
const texte = auskunft?.subtitleStreams ?? [];
|
||||
textmenue.setzen(
|
||||
|
||||
Reference in New Issue
Block a user