diff --git a/web/frontend/index.html b/web/frontend/index.html index 250f06b..8fd9aec 100644 --- a/web/frontend/index.html +++ b/web/frontend/index.html @@ -278,6 +278,13 @@ img { display: block; } transition: background .15s ease; } .pl-icon:hover { background: rgba(40,40,46,.75); } +/* Eine Vorgabegröße für alle Symbole, damit sie zueinander passen; einzelne + überschreiben sie über `icon(name, size)`. */ +.pl-icon svg, .pl-round svg, .tiny svg, .hero-nav svg { width: 18px; height: 18px; } +.pl-round svg { width: 22px; height: 22px; } +.btn svg { width: 15px; height: 15px; vertical-align: -2px; margin-right: 2px; } +.btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; } +.tiny, .pl-icon, .pl-round, .hero-nav { display: inline-flex; align-items: center; justify-content: center; } .pl-center { display: flex; align-items: center; justify-content: center; gap: var(--s-section); } /* Dunkel statt hell durchscheinend: über einer sonnigen Szene verschwanden @@ -331,6 +338,11 @@ img { display: block; } .pl-rate { font-size: 13px; color: var(--text-2); display: flex; align-items: center; gap: var(--s-snug); } .pl-rate .pick { width: auto; padding: 6px var(--s-regular); font-size: 13px; } +/* Während geladen wird, treten die Transportknöpfe zurück: sie tun ohnehin + nichts, und der Kreisel lag vorher genau hinter dem größten von ihnen. */ +#player.loading .pl-center { opacity: 0; pointer-events: none; } +.pl-center { transition: opacity .2s ease; } + .pl-busy { position: absolute; display: none; pointer-events: none; } @@ -350,6 +362,11 @@ img { display: block; } .pl-error strong { color: #F08A8D; font-size: 16px; font-weight: 600; } .pl-error span { color: var(--text-2); font-size: 14px; } .pl-error .btn { align-self: flex-start; } +.pl-error code { + font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 11px; + color: var(--text-3); background: rgba(0,0,0,.4); padding: var(--s-snug); + border-radius: 6px; word-break: break-all; line-height: 1.5; +} @media (max-width: 600px) { .pl-top, .pl-bottom { padding-left: var(--s-loose); padding-right: var(--s-loose); } @@ -1281,7 +1298,7 @@ function buildHero(items) {
${esc(item.Overview || '')}
Diese Serie hat keine Folgen, die sich abspielen ließen.
'); @@ -1535,7 +1552,7 @@ function openPlayer(item) {${esc(adresse.replace(/api_key=[^&]*/, 'api_key=…'))}` : ''}
`;
box.querySelector('button').onclick = close;
layer.appendChild(box);
@@ -1602,14 +1621,17 @@ function openPlayer(item) {
v.addEventListener('click', toggle);
const paint = () => {
- el2('pl-toggle').textContent = v.paused ? '▶' : '❚❚';
+ el2('pl-toggle').innerHTML = icon(v.paused ? 'play' : 'pause', 30);
el2('pl-toggle').setAttribute('aria-label', v.paused ? 'Abspielen' : 'Anhalten');
show();
};
v.addEventListener('play', paint);
v.addEventListener('pause', paint);
v.addEventListener('waiting', () => el2('pl-busy').classList.add('on'));
- v.addEventListener('playing', () => el2('pl-busy').classList.remove('on'));
+ v.addEventListener('playing', () => {
+ el2('pl-busy').classList.remove('on');
+ layer.classList.remove('loading');
+ });
// --- Fortschrittsleiste ---
const track = el2('pl-track');
@@ -1653,7 +1675,7 @@ function openPlayer(item) {
vol.oninput = () => { v.volume = +vol.value; v.muted = +vol.value === 0; };
el2('pl-mute').onclick = () => {
v.muted = !v.muted;
- el2('pl-mute').textContent = v.muted ? '🔇' : '🔊';
+ el2('pl-mute').innerHTML = icon(v.muted ? 'muted' : 'sound');
};
const rate = el2('pl-rate');
@@ -1698,6 +1720,7 @@ function openPlayer(item) {
el2('pl-close').onclick = close;
show();
+ layer.classList.add('loading');
el2('pl-busy').classList.add('on');
startStream(item, v, fail);
}
@@ -1716,11 +1739,14 @@ function episodeLabel(item) {
mitführen. Die Adresse selbst zusammenzubauen wäre eine Vermutung darüber,
was der Server kopieren kann und was er rechnen muss. */
async function startStream(item, v, fail) {
+ // Erst nach dem Aushandeln bekannt — deshalb hier eine Schachtel, die
+ // später gefüllt wird.
+ const diagnose = { text: '', url: '' };
v.addEventListener('error', () => {
const c = v.error?.code;
fail('Der Browser kann diesen Strom nicht abspielen.',
- c === 4 ? 'Das Format wird nicht unterstützt — der Server sollte umrechnen, tat es aber nicht.'
- : `Fehlercode ${c ?? '?'}. Im Netzwerk-Tab steht, was der Server geantwortet hat.`);
+ (c === 4 ? 'Das Format wird nicht unterstützt. ' : `Fehlercode ${c ?? '?'}. `)
+ + diagnose.text, diagnose.url);
});
let info;
@@ -1733,13 +1759,40 @@ async function startStream(item, v, fail) {
const source = info.MediaSources?.[0];
if (!source) return fail('Der Server nennt keine abspielbare Quelle.');
+ // **Der Rückfall muss Umrechnung erzwingen, nicht sie umgehen.**
+ //
+ // Hier stand `static=true` — das schickt die Datei so, wie sie auf der
+ // Platte liegt. Bei einem MKV mit DTS ist das für einen Browser unlesbar,
+ // und er meldet bloß „Format nicht unterstützt". Genau dieser Rückfall war
+ // der Grund, warum nichts lief: er tat das Gegenteil dessen, was er sollte.
+ //
+ // `master.m3u8` verlangt dagegen ausdrücklich einen HLS-Strom. Nennt der
+ // Server keine eigene Adresse, ist das die richtige Bitte — nicht das
+ // Original.
const url = source.TranscodingUrl
? new URL(source.TranscodingUrl, S.base).href
- : streamURL(`/Videos/${item.Id}/stream`, {
- static: 'true', mediaSourceId: source.Id || item.Id,
- api_key: S.token, PlaySessionId: info.PlaySessionId || '',
+ : streamURL(`/Videos/${item.Id}/master.m3u8`, {
+ mediaSourceId: source.Id || item.Id,
+ api_key: S.token,
+ PlaySessionId: info.PlaySessionId || '',
+ DeviceId: S.deviceId,
+ VideoCodec: 'h264', AudioCodec: 'aac,mp3',
+ TranscodingMaxAudioChannels: '2', SegmentContainer: 'ts',
+ MinSegments: '1', BreakOnNonKeyFrames: 'true',
+ TranscodingProtocol: 'hls',
});
+ // Was der Server geantwortet hat — damit im Fehlerfall nicht geraten werden
+ // muss, sondern dasteht, woran es lag.
+ const auskunft = [
+ source.TranscodingUrl ? 'Server nannte eine Adresse' : 'Server nannte keine Adresse (Rückfall auf HLS)',
+ source.Container ? `Container ${source.Container}` : null,
+ source.SupportsTranscoding === false ? 'Server sagt: kann nicht umrechnen' : null,
+ ].filter(Boolean).join(' · ');
+
+ diagnose.text = auskunft;
+ diagnose.url = url;
+
v.addEventListener('loadedmetadata', () => { v.playbackRate = P.rate; v.play(); }, { once: true });
wireProgress(v, { id: info.PlaySessionId, sourceID: source.Id || item.Id, itemID: item.Id });
@@ -1838,6 +1891,35 @@ function streamURL(path, query) {
return u.href;
}
+/* Die Symbole der Oberfläche.
+ ----------------------------------------------------------------------------
+ Als Vektor, nicht als Schriftzeichen. Ein ▶ oder ein 🔊 wird auf jeder
+ Plattform anders gezeichnet, trägt fremde Farben, sitzt schief auf der
+ Grundlinie und lässt sich weder in der Strichstärke noch in der Farbe
+ anpassen — es sieht nach Platzhalter aus, weil es einer ist.
+
+ `currentColor` erbt Farbe und Zustand vom Knopf darum; `stroke-width` bleibt
+ überall gleich, damit die Symbole zueinander passen. */
+const ICONS = {
+ play: '