Ein Bild, wo bisher nur eine Zahl stand
Wer zur Zeitleiste greift, sieht jetzt das Standbild der Stelle — im Web beim Überfahren, in der App beim Ziehen. **Erzeugt wird beim Abspielen, nicht auf Vorrat.** Ein Lauf über 453 Filme und 4213 Folgen wäre ein Abend ffmpeg und mehrere Gigabyte für Titel, die niemand anfasst. Startet jemand einen Titel, stellt sich die Datei hinten an; bis zum ersten Griff zur Leiste ist der Lauf durch. Gemessen an einer erzeugten Datei: 191 ms für 250 Sekunden Film, ein Zweistünder liegt damit bei etwa fünf Sekunden. **Ein ffmpeg-Aufruf, nicht siebenhundert.** `fps=1/10,scale=320:-2, tile=10x10` macht aus einem Durchlauf fertige Blätter zu hundert Kacheln. Bezahlbar ist das durch `-skip_frame nokey` — dieselbe Abkürzung wie beim Abtasten der Abspannhelligkeit: nur Keyframes dekodieren. Dass die Vorschau dadurch um zwei Sekunden danebenliegen kann, sieht niemand. **Der Spieler lädt ein Blatt und verschiebt danach nur den Ausschnitt.** Beim Fahren über die Leiste wird kein einziger Abruf mehr fällig. Die Rechnung Sekunde → Kachel steht an drei Stellen — Server, Web, App — und deshalb auch dreimal dieselbe; weicht eine ab, hält man die Vorschau für ein unscharfes Bild statt für einen Fehler. Neun Prüfungen decken die Ränder ab: Sekunde null, letzte Kachel eines Blattes, erste des nächsten, und hinter dem Ende. **Die Kachelmasse werden gemessen, nicht gerechnet.** `scale=320:-2` hält das Seitenverhältnis, ein Breitbildfilm ergibt also flachere Kacheln. Geraten wäre jede Kachel um ein paar Bildpunkte verschoben. Aufgeräumt wird mit dem Transcode-Cache: der Namensanfang trägt Kennung und Änderungszeit der Mediendatei, verwaiste Blätter sind daran ohne Tabelle erkennbar. Der Ordner taucht unter „Speicher" auf. **Und die Pfeiltasten spulen wieder.** Hatte die Zeitleiste den Fokus, überliess der Spieler die Pfeile dem Browser — der verschiebt `value` und löst `input` aus, aber `change` kommt bei Tastatureingabe nicht verlässlich, also wurde nie gesprungen. Die Ausnahme fällt weg; Pfeil heisst überall ±10 Sekunden. Nativ wären es ohnehin 100 Millisekunden gewesen, weil `step` auf 0,1 steht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ed13d5fa43
commit
68abd71074
@@ -1252,6 +1252,27 @@ export const protokolltage = () => anfrage<Protokolltag[]>('/system/logs');
|
||||
*
|
||||
* Mit `zeilen` kommt nur das Ende — das ist, was die Ansicht braucht. Ohne
|
||||
* kommt alles, und das ist der Weg zum Herunterladen. */
|
||||
/** Wie die Vorschaubilder eines Titels zusammengesetzt sind.
|
||||
*
|
||||
* **Die Kachelmasse stehen nicht fest**: skaliert wird auf eine feste
|
||||
* Breite, die Höhe folgt dem Seitenverhältnis. Der Server misst sie und
|
||||
* sagt sie — geraten wäre jede Kachel ein paar Bildpunkte verschoben. */
|
||||
export type Vorschauauskunft = {
|
||||
fertig: boolean;
|
||||
intervall: number;
|
||||
spalten: number;
|
||||
zeilen: number;
|
||||
kachelBreite: number;
|
||||
kachelHoehe: number;
|
||||
blaetter: number;
|
||||
};
|
||||
|
||||
export const vorschauauskunft = (dateiId: string) =>
|
||||
anfrage<Vorschauauskunft>(`/files/${dateiId}/vorschau`);
|
||||
|
||||
export const vorschaublattAdresse = (dateiId: string, blatt: number) =>
|
||||
`/api/v1/files/${dateiId}/vorschau/${blatt}.jpg`;
|
||||
|
||||
export const protokollAdresse = (tag: string, zeilen?: number) =>
|
||||
`/api/v1/system/logs/${tag}` + (zeilen ? `?tail=${zeilen}` : '');
|
||||
|
||||
|
||||
+77
-2
@@ -1,6 +1,7 @@
|
||||
import type HlsTyp from 'hls.js';
|
||||
import {
|
||||
stromStarten, stromBeenden, untertitelHolen, fortschritt, nachbarfolge, segmenteHolen,
|
||||
vorschauauskunft, vorschaublattAdresse, type Vorschauauskunft,
|
||||
type Stromauskunft, type Spurauskunft, type Folge, type Segment,
|
||||
} from './api';
|
||||
import { dauer, knopf, meldung, hinweisZeigen, folgenname } from './util';
|
||||
@@ -661,7 +662,12 @@ export function spieler(anfrage: Spieleranfrage): void {
|
||||
// gibt, ist keiner. Jetzt liegt er als eigene Schicht über dem Bild,
|
||||
// rechts unten, und erscheint von selbst — bis man die Bedienung einmal
|
||||
// eingeblendet hat, danach kommt und geht er mit ihr.
|
||||
bedienung.append(zeitleiste, unten);
|
||||
// **Die Vorschau hängt an der Bedienung, nicht am Bild.** Sie soll über
|
||||
// der Leiste stehen und mit ihr verschwinden; als eigene Schicht über
|
||||
// dem Film müsste ihre Lage zweimal gerechnet werden.
|
||||
const vorschau = document.createElement('div');
|
||||
vorschau.className = 'pl-vorschau hidden';
|
||||
bedienung.append(zeitleiste, vorschau, unten);
|
||||
|
||||
// **Beide Nachbarn gleich zu Beginn holen.** Zwei kleine Anfragen, einmal
|
||||
// je Wiedergabe. Erst beim Antippen zu fragen hiesse, einen Knopf zu
|
||||
@@ -786,6 +792,68 @@ export function spieler(anfrage: Spieleranfrage): void {
|
||||
// Regler stehen, bis jemand ihn wieder anfasst.
|
||||
zeitleiste.onpointercancel = () => { amRegler = false; };
|
||||
|
||||
/** Was der Server über die Vorschaubilder sagt — `null`, solange nichts da ist. */
|
||||
let bilder: Vorschauauskunft | null = null;
|
||||
|
||||
// **Einmal fragen, und bei „noch nicht" genau einmal nachfassen.** Die
|
||||
// Blätter entstehen beim Abspielen im Hintergrund; nach zehn Sekunden
|
||||
// sind sie bei den meisten Filmen da. Weiter zu pollen hiesse, für eine
|
||||
// Annehmlichkeit dauernd zu fragen.
|
||||
void (async () => {
|
||||
for (const warten of [0, 10000]) {
|
||||
if (warten) await new Promise(r => setTimeout(r, warten));
|
||||
if (geschlossen) return;
|
||||
try {
|
||||
const auskunft = await vorschauauskunft(anfrage.dateiId);
|
||||
if (auskunft.fertig) { bilder = auskunft; return; }
|
||||
} catch { return; }
|
||||
}
|
||||
})();
|
||||
|
||||
/** Zeigt die Kachel zur Stelle unter dem Zeiger. */
|
||||
function vorschauZeigen(x: number): void {
|
||||
if (!bilder || gesamt <= 0) return;
|
||||
|
||||
const kasten = zeitleiste.getBoundingClientRect();
|
||||
const anteil = Math.min(1, Math.max(0, (x - kasten.left) / kasten.width));
|
||||
const sekunde = anteil * gesamt;
|
||||
|
||||
// Dieselbe Rechnung wie im Server und in der App — siehe `Vorschauplan`.
|
||||
const proBlatt = bilder.spalten * bilder.zeilen;
|
||||
const nummer = Math.min(
|
||||
Math.floor(sekunde / bilder.intervall), proBlatt * bilder.blaetter - 1);
|
||||
const rest = nummer % proBlatt;
|
||||
|
||||
vorschau.style.width = `${bilder.kachelBreite}px`;
|
||||
vorschau.style.height = `${bilder.kachelHoehe}px`;
|
||||
vorschau.style.backgroundImage =
|
||||
`url(${vorschaublattAdresse(anfrage.dateiId, Math.floor(nummer / proBlatt))})`;
|
||||
vorschau.style.backgroundPosition =
|
||||
`-${(rest % bilder.spalten) * bilder.kachelBreite}px `
|
||||
+ `-${Math.floor(rest / bilder.spalten) * bilder.kachelHoehe}px`;
|
||||
|
||||
// **Gerechnet wird in zwei Bezugssystemen, und das ist die Falle.**
|
||||
// Die Stelle kommt aus der Leiste, das Feld hängt an der Bedienung —
|
||||
// dazwischen liegt deren Innenabstand. Ohne die Umrechnung sässe die
|
||||
// Vorschau achtzehn Bildpunkte daneben.
|
||||
const bezug = bedienung.getBoundingClientRect();
|
||||
const halb = bilder.kachelBreite / 2;
|
||||
const links = Math.min(
|
||||
kasten.right - bezug.left - halb,
|
||||
Math.max(kasten.left - bezug.left + halb, x - bezug.left));
|
||||
vorschau.style.left = `${links}px`;
|
||||
vorschau.classList.remove('hidden');
|
||||
|
||||
zeit.textContent = dauer(sekunde);
|
||||
}
|
||||
|
||||
const zeit = document.createElement('div');
|
||||
zeit.className = 'pl-vorschauzeit';
|
||||
vorschau.appendChild(zeit);
|
||||
|
||||
zeitleiste.addEventListener('pointermove', e => vorschauZeigen(e.clientX));
|
||||
zeitleiste.addEventListener('pointerleave', () => vorschau.classList.add('hidden'));
|
||||
|
||||
/** Die Gesamtlänge des Films — nicht die des Stroms. */
|
||||
const gesamt = anfrage.laenge ?? 0;
|
||||
|
||||
@@ -1309,7 +1377,14 @@ export function spieler(anfrage: Spieleranfrage): void {
|
||||
/* ---------- Tastatur ---------- */
|
||||
|
||||
function tastendruck(e: KeyboardEvent): void {
|
||||
if (e.target instanceof HTMLInputElement && e.target.type === 'range' && e.key.startsWith('Arrow')) return;
|
||||
// **Pfeiltasten gehören dem Spieler, auch auf der Leiste.**
|
||||
//
|
||||
// Hier stand eine Ausnahme: hat die Zeitleiste den Fokus, überlasse
|
||||
// die Pfeile dem Browser. Der verschiebt `value` und löst `input`
|
||||
// aus — aber `change` kommt bei Tastatureingabe nicht verlässlich,
|
||||
// und ohne `change` wird nie gesprungen. Der Regler bewegte sich
|
||||
// also und der Film nicht. Abgesehen davon steht `step` auf 0,1:
|
||||
// ein nativer Tastendruck wäre eine Zehntelsekunde.
|
||||
|
||||
switch (e.key) {
|
||||
case ' ': case 'k': e.preventDefault(); umschalten(); break;
|
||||
|
||||
@@ -987,6 +987,35 @@ textarea.feld {
|
||||
height: 20px; background: transparent; cursor: pointer; margin: 0;
|
||||
}
|
||||
.pl-zeit { width: 100%; }
|
||||
|
||||
/* **Die Vorschau steht über der Leiste, nicht unter dem Zeiger.**
|
||||
Am Zeiger klebend verdeckte sie genau die Stelle, die man sucht; eine
|
||||
feste Höhe darüber lässt die Leiste frei. `translateX(-50%)` setzt sie
|
||||
mittig über die Stelle, `left` kommt aus dem Skript. */
|
||||
.pl-vorschau {
|
||||
position: absolute;
|
||||
bottom: calc(100% + var(--s-snug));
|
||||
transform: translateX(-50%);
|
||||
border-radius: var(--r-klein);
|
||||
overflow: hidden;
|
||||
background-color: #000;
|
||||
background-repeat: no-repeat;
|
||||
box-shadow: 0 8px 24px rgb(0 0 0 / .6);
|
||||
border: 1px solid rgb(255 255 255 / .18);
|
||||
pointer-events: none;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.pl-vorschauzeit {
|
||||
position: absolute;
|
||||
inset: auto 0 0 0;
|
||||
padding: 2px 0 3px;
|
||||
text-align: center;
|
||||
font-size: .72rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #fff;
|
||||
background: linear-gradient(to top, rgb(0 0 0 / .85), transparent);
|
||||
}
|
||||
.pl-laut { width: 84px; flex: 0 0 auto; }
|
||||
|
||||
.pl-zeit::-webkit-slider-runnable-track, .pl-laut::-webkit-slider-runnable-track {
|
||||
|
||||
Reference in New Issue
Block a user