Meine Rundung war eine Klinge, und ich bin hineingelaufen
Du hast recht, und zwar doppelt.
**Der Versatz stand im falschen Menü.** „Später (+100 ms)" unter den
Tonspuren liest sich wie der Name einer Spur. Jetzt hat er einen eigenen
Knopf in der Leiste, mit eigenem Zeichen und festen Stufen von „Ton 1 s
früher" bis „Ton 1 s später", dazwischen „Ohne Versatz" — mit Haken, wie
bei der Qualität. Im Tonmenü stehen wieder nur Sprachen.
**Und das Schlimmere: die Rundung aufs Schlüsselbild hat es verschlechtert.**
Sie musste auf die Millisekunde stimmen, sonst nimmt ffmpeg das
Schlüsselbild davor. Gemessen an einer Datei mit Schlüsselbildern alle
fünf Sekunden:
-ss 15.015 → Bild ab 0,000 | Ton ab 0,004
-ss 15.014 → Bild ab 0,000 | Ton ab 4,983
Eine Millisekunde. Und die Zeile, die den Wert an ffmpeg gibt, rundete auf
drei Nachkommastellen — bei 23,976 Bildern je Sekunde liegen
Schlüsselbilder auf Werten wie 3,336667, und die überleben das nicht
zuverlässig. Damit konnte jede Fortsetzung schlechter werden als vorher.
Das war mein Fehler, eingebaut in E8GSRE.
**Der richtige Hebel ist `-noaccurate_seek`**, und er braucht gar keine
Genauigkeit. Damit schneidet ffmpeg auch den Ton nicht auf die Sekunde,
sondern lässt ihn beim selben Schlüsselbild anfangen wie das kopierte
Bild. Gemessen, dieselbe Datei, fünf verschiedene Sprungwerte quer durch
die Bildgruppe:
15,014 → 0,015 | 17 → 0,004 | 19,9 → 0,004 | 12,5 → 0,015 | 3,2 → 0,011
Statt bis zu fünf Sekunden also überall unter zwanzig Millisekunden, und
zwar ohne dass irgendein Wert getroffen werden muss. Fünf Testfälle quer
durch die Bildgruppe halten das jetzt fest; vorher prüfte der Sprungtest
nur, ob überhaupt Segmente entstehen.
Die Schlüsselbildsuche bleibt — aber nur noch, um dem Spieler eine
ehrliche Uhr zu geben. Vergreift sie sich, geht die Uhr falsch; Bild und
Ton bleiben zusammen. Vorher hing die Ausrichtung daran, und das war der
schlechte Tausch.
Und die Sekundenangaben gehen jetzt mit sechs Nachkommastellen hinaus
statt mit drei.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6ad4deb634
commit
dc9d1cea85
@@ -152,19 +152,31 @@ public sealed class Streamkoordinator(
|
||||
// Beim Umkodieren entfällt das: dort wird das Bild neu gerechnet und
|
||||
// schneidet auf die Sekunde genau. Dann vorzurunden hiesse, ohne Not
|
||||
// ein Stück zurückzuspringen.
|
||||
// **Gesprungen wird auf den Wunsch, gemeldet wird das Schlüsselbild.**
|
||||
//
|
||||
// Beim Kopieren springt ffmpeg ungenau — siehe `HlsArgumente` — und
|
||||
// landet damit auf dem Schlüsselbild vor dem Wunsch. Bild und Ton
|
||||
// fangen dort gemeinsam an; das ist die Zusage, und sie hängt an
|
||||
// keiner Rechnung hier.
|
||||
//
|
||||
// Wo genau das war, sagt die Suche. Sie ist dafür da, dem Spieler
|
||||
// eine ehrliche Uhr zu geben — nicht dafür, die Ausrichtung
|
||||
// herzustellen. Vergreift sie sich, geht die Uhr falsch; Bild und Ton
|
||||
// bleiben zusammen. Umgekehrt wäre es ein schlechter Tausch: einen
|
||||
// Sprungpunkt auf die Millisekunde zu treffen, ist zu viel verlangt.
|
||||
var gewuenscht = Math.Max(0, ab);
|
||||
var anfang = plan.BildKopieren
|
||||
var gemeldet = plan.BildKopieren
|
||||
? await schluesselbilder.DavorAsync(datei.Path, gewuenscht, ct)
|
||||
: gewuenscht;
|
||||
|
||||
var anfrage = new Stromanfrage(
|
||||
datei.Path, ordner, gelesen, plan, tonspur, anfang, Hardware(), stufe, tonversatz);
|
||||
datei.Path, ordner, gelesen, plan, tonspur, gewuenscht, Hardware(), stufe, tonversatz);
|
||||
|
||||
// **Die tatsächlich benutzte Spur, nicht die gewünschte.** Sonst
|
||||
// stünde im Menü bei keiner ein Haken, obwohl eine davon läuft — und
|
||||
// bei einer Spur, die es nicht mehr gibt, bei der falschen.
|
||||
var sitzung = new Stromsitzung(
|
||||
kennung, dateiId, benutzer, ordner, anfang,
|
||||
kennung, dateiId, benutzer, ordner, gemeldet,
|
||||
plan.Hoehe, HlsArgumente.Tonspur(gelesen, tonspur)?.Index, plan.Art, clock.GetUtcNow());
|
||||
|
||||
var prozess = Starten(anfrage, sitzung);
|
||||
|
||||
@@ -106,8 +106,28 @@ public static class HlsArgumente
|
||||
|
||||
if (anfrage.Ab > 0)
|
||||
{
|
||||
// **Beim Kopieren wird ungenau gesprungen, und das mit Absicht.**
|
||||
//
|
||||
// Genau gesprungen schneidet ffmpeg den Ton auf die Sekunde, das
|
||||
// kopierte Bild aber nicht — es kann erst beim Schlüsselbild davor
|
||||
// anfangen. Dazwischen liegt Bild ohne Ton, und jeder Spieler darf
|
||||
// diese Lücke deuten, wie er will; hls.js schiebt den Ton nach
|
||||
// vorn. Gemessen an einer Datei mit Schlüsselbildern alle fünf
|
||||
// Sekunden: bei einem Sprung auf 17 begann der Ton erst bei 1,96,
|
||||
// bei 15,014 sogar erst bei 4,98.
|
||||
//
|
||||
// `-noaccurate_seek` lässt auch den Ton beim Schlüsselbild
|
||||
// anfangen. Dieselben Sprünge, gemessen: 0,004 und 0,015 — und
|
||||
// zwar bei *jedem* Wert, nicht nur bei gut getroffenen. Das ist
|
||||
// der Unterschied zum Runden auf das Schlüsselbild: das muss auf
|
||||
// die Millisekunde stimmen, sonst wird es schlimmer als vorher.
|
||||
//
|
||||
// Beim Umkodieren nicht: dort wird das Bild neu gerechnet und
|
||||
// schneidet ohnehin genau.
|
||||
if (anfrage.Plan.BildKopieren) argumente.Add("-noaccurate_seek");
|
||||
|
||||
argumente.Add("-ss");
|
||||
argumente.Add(anfrage.Ab.ToString("0.###", CultureInfo.InvariantCulture));
|
||||
argumente.Add(Sekunden(anfrage.Ab));
|
||||
}
|
||||
|
||||
argumente.Add("-i");
|
||||
@@ -122,11 +142,12 @@ public static class HlsArgumente
|
||||
if (zweiteEingabe)
|
||||
{
|
||||
argumente.Add("-itsoffset");
|
||||
argumente.Add(anfrage.Tonversatz.ToString("0.###", CultureInfo.InvariantCulture));
|
||||
argumente.Add(Sekunden(anfrage.Tonversatz));
|
||||
if (anfrage.Ab > 0)
|
||||
{
|
||||
if (anfrage.Plan.BildKopieren) argumente.Add("-noaccurate_seek");
|
||||
argumente.Add("-ss");
|
||||
argumente.Add(anfrage.Ab.ToString("0.###", CultureInfo.InvariantCulture));
|
||||
argumente.Add(Sekunden(anfrage.Ab));
|
||||
}
|
||||
|
||||
argumente.Add("-i");
|
||||
@@ -196,6 +217,18 @@ public static class HlsArgumente
|
||||
/// Haken, obwohl eine davon lief.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <summary>Eine Sekundenangabe für ffmpeg — mit Punkt und voller Genauigkeit.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Sechs Nachkommastellen, nicht drei.</b> Auf drei gerundet kann ein
|
||||
/// Sprungpunkt um eine Millisekunde zu klein werden, und eine Millisekunde
|
||||
/// entscheidet, ob ffmpeg das gemeinte Schlüsselbild erwischt oder das
|
||||
/// davor. Gemessen: bei 15,015 begann der Ton nach 0,004 s, bei 15,014
|
||||
/// nach 4,98 s.
|
||||
/// </remarks>
|
||||
private static string Sekunden(double wert) =>
|
||||
wert.ToString("0.######", CultureInfo.InvariantCulture);
|
||||
|
||||
public static ProbedStream? Tonspur(IReadOnlyList<ProbedStream> spuren, int? gewuenscht)
|
||||
{
|
||||
var alle = spuren.Where(s => s.Kind == MediaStreamKind.Audio).ToList();
|
||||
|
||||
@@ -192,6 +192,39 @@ public class HlsLaufTests
|
||||
Assert.That(Directory.GetFiles(ziel, "*.m4s"), Is.Not.Empty);
|
||||
}
|
||||
|
||||
/// <summary>Bild und Ton fangen zusammen an — bei jedem Sprungwert.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Das ist der Test, der gefehlt hat.</b> Die Quelle hat genau ein
|
||||
/// Schlüsselbild, bei null; jeder Sprung landet also mitten in der
|
||||
/// Bildgruppe — der schlimmste Fall. Genau gesprungen schnitte ffmpeg den
|
||||
/// Ton auf die Sekunde, das kopierte Bild aber nicht, und dazwischen läge
|
||||
/// Bild ohne Ton.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Zuerst hatte ich dafür den Sprung auf das Schlüsselbild gerundet. Das
|
||||
/// war zu scharf geschliffen: eine Millisekunde zu klein, und ffmpeg nimmt
|
||||
/// das Schlüsselbild davor — gemessen wurden aus 0,004 s Abstand dann
|
||||
/// 4,98. `-noaccurate_seek` braucht diese Genauigkeit nicht.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[TestCase(0.0)]
|
||||
[TestCase(1.5)]
|
||||
[TestCase(3.2)]
|
||||
[TestCase(4.0)]
|
||||
[TestCase(6.75)]
|
||||
public void Bild_und_Ton_fangen_bei_jedem_Sprung_zusammen_an(double ab)
|
||||
{
|
||||
var quelle = Path.Combine(_ordner, "lang.mkv");
|
||||
Lang(quelle);
|
||||
|
||||
var (bild, ton) = Anfang(Laufen(quelle, ab: ab));
|
||||
|
||||
Assert.That(Math.Abs(bild - ton), Is.LessThan(0.2),
|
||||
$"Sprung auf {ab}: Bild beginnt bei {bild}, Ton bei {ton} — das hört man.");
|
||||
}
|
||||
|
||||
/// <summary>Bild und Ton fangen an derselben Stelle an.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
|
||||
@@ -95,6 +95,9 @@ export const icons = {
|
||||
* reserviert, was etwas wegnimmt. Wer meldet, dass ein Plakat nicht passt,
|
||||
* löst keinen Alarm aus. */
|
||||
fahne: rahmen('<path d="M6 21V4"/><path d="M6 4.8h11.6l-2.5 4 2.5 4H6"/>'),
|
||||
|
||||
/** Tonversatz — zwei Pfeile, die auseinandergehen. */
|
||||
versatz: rahmen('<path d="M3 12h18"/><path d="M7 8l-4 4 4 4"/><path d="M17 8l4 4-4 4"/>'),
|
||||
} as const;
|
||||
|
||||
export type IconName = keyof typeof icons;
|
||||
|
||||
+32
-33
@@ -85,12 +85,13 @@ 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;
|
||||
/** Die Stufen des Tonversatzes in Millisekunden.
|
||||
*
|
||||
* **Nicht jeder Hunderter von −5000 bis +5000.** Das wären einhunderteins
|
||||
* Einträge für etwas, das man im Jahr dreimal braucht. Diese elf decken ab,
|
||||
* was vorkommt — und wer mehr braucht, hat keine Einstellung zu wenig,
|
||||
* sondern eine kaputte Datei. */
|
||||
const VERSAETZE = [-1000, -500, -300, -200, -100, 0, 100, 200, 300, 500, 1000];
|
||||
|
||||
/** Wo gemerkte Tonversätze liegen — je Datei, in diesem Browser.
|
||||
*
|
||||
@@ -132,10 +133,11 @@ export function tonspracheMerken(sprache: string): void {
|
||||
try { localStorage.setItem(SPRACHSCHLUESSEL, sprache); } catch { /* Privates Fenster. */ }
|
||||
}
|
||||
|
||||
/** „läuft mit", „200 ms später", „300 ms früher". */
|
||||
/** „Ohne Versatz", „Ton 200 ms später", „Ton 1 s 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`;
|
||||
if (ms === 0) return 'Ohne Versatz';
|
||||
const betrag = Math.abs(ms) >= 1000 ? `${Math.abs(ms) / 1000} s` : `${Math.abs(ms)} ms`;
|
||||
return `Ton ${betrag} ${ms > 0 ? 'später' : 'früher'}`;
|
||||
}
|
||||
|
||||
/** Die Stufen, die im Menü stehen. Muss zu `Stromentscheid.Stufen` passen. */
|
||||
@@ -373,6 +375,7 @@ export function spieler(anfrage: Spieleranfrage): void {
|
||||
const tonmenue = menue('tonspur', 'Tonspur');
|
||||
const textmenue = menue('untertitel', 'Untertitel');
|
||||
const stufenmenue = menue('regler', 'Qualität');
|
||||
const versatzmenue = menue('versatz', 'Tonversatz');
|
||||
|
||||
const links = document.createElement('div');
|
||||
links.className = 'pl-gruppe';
|
||||
@@ -380,7 +383,8 @@ export function spieler(anfrage: Spieleranfrage): void {
|
||||
|
||||
const rechts = document.createElement('div');
|
||||
rechts.className = 'pl-gruppe';
|
||||
rechts.append(rest, tonmenue.knopf, textmenue.knopf, stufenmenue.knopf, vollbild);
|
||||
rechts.append(
|
||||
rest, tonmenue.knopf, versatzmenue.knopf, textmenue.knopf, stufenmenue.knopf, vollbild);
|
||||
|
||||
const unten = document.createElement('div');
|
||||
unten.className = 'pl-unten';
|
||||
@@ -647,30 +651,25 @@ export function spieler(anfrage: Spieleranfrage): void {
|
||||
}))
|
||||
: [{ 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.
|
||||
tonmenue.setzen(spuren);
|
||||
|
||||
// **Der Versatz hat ein eigenes Menü.** Er stand vorher unter den
|
||||
// Tonspuren, und dort las er sich wie eine: „Später (+100 ms)" sah aus
|
||||
// wie der Name einer Spur. Eine Einstellung gehört nicht in eine Liste
|
||||
// von Inhalten.
|
||||
//
|
||||
// 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]);
|
||||
// Feste Stufen statt zweier Tasten, wie bei der Qualität: so zeigt der
|
||||
// Haken, wo man steht, und man trifft einen Wert, statt sich
|
||||
// hinzutasten — jeder Schritt baut den Strom neu auf.
|
||||
versatzmenue.setzen(VERSAETZE.map(ms => ({
|
||||
text: versatzWort(ms),
|
||||
gewaehlt: ms === tonversatz,
|
||||
tun: () => {
|
||||
tonversatz = ms;
|
||||
versatzMerken(anfrage.dateiId, ms);
|
||||
void stromAufbauen(filmzeit());
|
||||
},
|
||||
})));
|
||||
|
||||
const texte = auskunft?.subtitleStreams ?? [];
|
||||
textmenue.setzen(
|
||||
|
||||
Reference in New Issue
Block a user