/** * VideoKonverter TV - Video-Player v4.1 * HLS-Streaming mit hls.js, kompaktes Popup-Menue statt Panel-Overlay, * Fullscreen-Player mit Audio/Untertitel/Qualitaets-Auswahl, * Naechste-Episode-Countdown und Tastatur/Fernbedienung-Steuerung. */ // === State === let videoEl = null; let cfg = {}; // Konfiguration aus initPlayer() let videoInfo = null; // Audio/Subtitle-Tracks vom Server let currentAudio = 0; let currentSub = -1; // -1 = aus let currentQuality = "hd"; let currentSpeed = 1.0; let progressBar = null; let timeDisplay = null; let playBtn = null; let controlsTimer = null; let saveTimer = null; let controlsVisible = true; let popupOpen = false; // Popup-Menue offen? let popupSection = null; // Aktive Popup-Sektion (null = Hauptmenue) let nextCountdown = null; let episodesWatched = 0; // HLS-State let hlsInstance = null; // hls.js Instanz let hlsSessionId = null; // Aktive HLS-Session-ID let hlsReady = false; // HLS-Playback bereit? let hlsSeekOffset = 0; // Server-seitiger Seek: echte Position im Video let clientCodecs = null; // Vom Client unterstuetzte Video-Codecs let hlsRetryCount = 0; // Retry-Zaehler fuer gesamten Stream-Start let loadingTimeout = null; // Timeout fuer Loading-Spinner /** * Player initialisieren * @param {Object} opts - Konfiguration */ function initPlayer(opts) { cfg = opts; currentQuality = opts.streamQuality || "hd"; // Client-Codec-Erkennung (welche Video-Codecs kann dieser Browser?) clientCodecs = detectSupportedCodecs(); console.info("Client-Codecs:", clientCodecs.join(", ")); videoEl = document.getElementById("player-video"); progressBar = document.getElementById("player-progress-bar"); timeDisplay = document.getElementById("player-time"); playBtn = document.getElementById("btn-play"); if (!videoEl) return; // Video-Info + HLS-Stream PARALLEL starten (nicht sequentiell warten!) const infoReady = loadVideoInfo(); startHLSStream(opts.startPos || 0); infoReady.then(() => updatePlayerButtons()); // Events videoEl.addEventListener("timeupdate", onTimeUpdate); videoEl.addEventListener("play", onPlay); videoEl.addEventListener("pause", onPause); videoEl.addEventListener("ended", onEnded); videoEl.addEventListener("click", togglePlay); // Loading ausblenden sobald Video laeuft (mehrere Events als Sicherheit) videoEl.addEventListener("playing", onPlaying); videoEl.addEventListener("canplay", hideLoading, {once: true}); // Video-Error: Automatisch Retry mit Fallback videoEl.addEventListener("error", onVideoError); // Controls UI playBtn.addEventListener("click", togglePlay); const btnFs = document.getElementById("btn-fullscreen"); if (btnFs) btnFs.addEventListener("click", toggleFullscreen); document.getElementById("player-progress").addEventListener("click", onProgressClick); // Einstellungen-Button -> Popup-Hauptmenue const btnSettings = document.getElementById("btn-settings"); if (btnSettings) btnSettings.addEventListener("click", () => togglePopup()); // Direkt-Buttons: Audio, Untertitel, Qualitaet const btnAudio = document.getElementById("btn-audio"); if (btnAudio) btnAudio.addEventListener("click", () => openPopupSection("audio")); const btnSubs = document.getElementById("btn-subs"); if (btnSubs) btnSubs.addEventListener("click", () => openPopupSection("subs")); const btnQuality = document.getElementById("btn-quality"); if (btnQuality) btnQuality.addEventListener("click", () => openPopupSection("quality")); // Naechste-Episode-Button const btnNext = document.getElementById("btn-next"); if (btnNext) btnNext.addEventListener("click", playNextEpisode); // Naechste-Episode Overlay Buttons const btnNextPlay = document.getElementById("btn-next-play"); if (btnNextPlay) btnNextPlay.addEventListener("click", playNextEpisode); const btnNextCancel = document.getElementById("btn-next-cancel"); if (btnNextCancel) btnNextCancel.addEventListener("click", cancelNext); // Schaust du noch? const btnStillYes = document.getElementById("btn-still-yes"); if (btnStillYes) btnStillYes.addEventListener("click", () => { document.getElementById("still-watching-overlay").style.display = "none"; episodesWatched = 0; videoEl.play(); }); const btnStillNo = document.getElementById("btn-still-no"); if (btnStillNo) btnStillNo.addEventListener("click", () => { saveProgress(); window.history.back(); }); // Tastatur-Steuerung document.addEventListener("keydown", onKeyDown); document.addEventListener("mousemove", showControls); document.addEventListener("touchstart", showControls); // Fullscreen nur auf Desktop/Handy anzeigen (nicht auf Samsung TV) if (btnFs && isTizenTV()) { btnFs.style.display = "none"; } scheduleHideControls(); saveTimer = setInterval(saveProgress, 10000); } // === Erkennung: Samsung Tizen TV === function isTizenTV() { return typeof tizen !== "undefined" || /Tizen/i.test(navigator.userAgent); } // === Video-Info laden === async function loadVideoInfo() { try { const resp = await fetch(`/api/library/videos/${cfg.videoId}/info`); videoInfo = await resp.json(); // Bevorzugte Audio-Spur finden if (videoInfo.audio_tracks) { const prefIdx = videoInfo.audio_tracks.findIndex( a => a.lang === cfg.preferredAudio); if (prefIdx >= 0) currentAudio = prefIdx; } // Bevorzugte Untertitel-Spur finden if (cfg.subtitlesEnabled && cfg.preferredSub && videoInfo.subtitle_tracks) { const subIdx = videoInfo.subtitle_tracks.findIndex( s => s.lang === cfg.preferredSub); if (subIdx >= 0) currentSub = subIdx; } // Untertitel-Tracks als hinzufuegen if (videoInfo.subtitle_tracks) { videoInfo.subtitle_tracks.forEach((sub, i) => { const track = document.createElement("track"); track.kind = "subtitles"; track.src = `/api/library/videos/${cfg.videoId}/subtitles/${i}`; track.srclang = sub.lang || "und"; track.label = langName(sub.lang) || `Spur ${i + 1}`; if (i === currentSub) track.default = true; videoEl.appendChild(track); }); updateSubtitleTrack(); } } catch (e) { console.warn("Video-Info laden fehlgeschlagen:", e); } } // === Codec-Erkennung === /** * Erkennt automatisch welche Video-Codecs der Browser/TV decodieren kann. * Wird beim HLS-Start an den Server geschickt -> Server entscheidet copy vs transcode. * * WICHTIG: Unterscheidung zwischen nativem HLS (Tizen/Safari) und MSE (hls.js): * - Natives HLS: canPlayType() meldet oft AV1/VP9, aber der native HLS-Player * unterstuetzt diese Codecs NICHT zuverlaessig in fMP4-Segmenten. * -> Konservativ: nur H.264 (+ evtl. HEVC) * - MSE/hls.js: MediaSource.isTypeSupported() ist zuverlaessig * -> Alle unterstuetzten Codecs melden */ function detectSupportedCodecs() { const codecs = []; const el = document.createElement("video"); const hasNativeHLS = !!el.canPlayType("application/vnd.apple.mpegurl"); const hasMSE = typeof MediaSource !== "undefined" && MediaSource.isTypeSupported; if (!hasNativeHLS && hasMSE) { // MSE-basiert (hls.js auf Chrome/Firefox/Edge): zuverlaessige Erkennung if (MediaSource.isTypeSupported('video/mp4; codecs="avc1.640028"')) codecs.push("h264"); if (MediaSource.isTypeSupported('video/mp4; codecs="hev1.1.6.L93.B0"')) codecs.push("hevc"); if (MediaSource.isTypeSupported('video/mp4; codecs="av01.0.05M.08"')) codecs.push("av1"); if (MediaSource.isTypeSupported('video/mp4; codecs="vp09.00.10.08"')) codecs.push("vp9"); } else { // Natives HLS (Samsung Tizen, Safari, iOS): // Konservativ - nur H.264 melden, da AV1/VP9 in HLS-fMP4 nicht zuverlaessig codecs.push("h264"); if (el.canPlayType('video/mp4; codecs="hev1.1.6.L93.B0"') || el.canPlayType('video/mp4; codecs="hvc1.1.6.L93.B0"')) { codecs.push("hevc"); } } if (!codecs.length) codecs.push("h264"); return codecs; } // === Loading-Indikator === let loadingTimer = null; function showLoading() { var el = document.getElementById("player-loading"); if (el) { el.classList.remove("hidden"); el.style.display = ""; } // Fallback: Loading nach 8 Sekunden ausblenden (falls Events nicht feuern) clearTimeout(loadingTimer); loadingTimer = setTimeout(hideLoading, 8000); } function hideLoading() { clearTimeout(loadingTimer); clearTimeout(loadingTimeout); loadingTimeout = null; var el = document.getElementById("player-loading"); if (!el) return; el.style.display = "none"; } function onPlaying() { hideLoading(); hlsRetryCount = 0; // Reset bei erfolgreichem Start } function onVideoError() { // Video-Element Fehler: Retry oder Fallback var err = videoEl.error; var msg = err ? "Video-Fehler: Code " + err.code : "Video-Fehler"; console.error(msg, err); if (hlsRetryCount < 2) { hlsRetryCount++; if (typeof showToast === "function") showToast("Stream-Fehler, Retry " + hlsRetryCount + "/2...", "error"); var seekPos = getCurrentTime(); setTimeout(function() { startHLSStream(seekPos); }, 1000 * hlsRetryCount); } else { if (typeof showToast === "function") showToast("Video konnte nicht gestartet werden", "error"); hideLoading(); } } // === HLS Streaming === async function startHLSStream(seekSec) { // Loading-Spinner anzeigen showLoading(); // Vorherige Session beenden await cleanupHLS(); // Seek-Offset merken (ffmpeg -ss schneidet serverseitig) hlsSeekOffset = seekSec > 0 ? Math.floor(seekSec) : 0; // Neue HLS-Session vom Server anfordern try { const resp = await fetch("/tv/api/hls/start", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ video_id: cfg.videoId, quality: currentQuality, audio: currentAudio, sound: cfg.soundMode || "stereo", t: hlsSeekOffset, codecs: clientCodecs || ["h264"], }), }); if (!resp.ok) { console.error("HLS Session Start fehlgeschlagen:", resp.status); if (typeof showToast === "function") showToast("HLS-Start fehlgeschlagen (HTTP " + resp.status + ")", "error"); setStreamUrlLegacy(seekSec); return; } const data = await resp.json(); hlsSessionId = data.session_id; const playlistUrl = data.playlist_url; // Retry-Zaehler fuer Netzwerkfehler let networkRetries = 0; const MAX_RETRIES = 3; // HLS abspielen if (videoEl.canPlayType("application/vnd.apple.mpegurl")) { // Native HLS (Safari, Tizen) videoEl.src = playlistUrl; hlsReady = true; videoEl.addEventListener("playing", hideLoading, {once: true}); videoEl.play().catch(() => {}); } else if (typeof Hls !== "undefined" && Hls.isSupported()) { // hls.js Polyfill (Chrome, Firefox, Edge) hlsInstance = new Hls({ maxBufferLength: 30, maxMaxBufferLength: 60, startLevel: -1, }); hlsInstance.loadSource(playlistUrl); hlsInstance.attachMedia(videoEl); hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => { hlsReady = true; videoEl.addEventListener("playing", hideLoading, {once: true}); videoEl.play().catch(() => {}); }); hlsInstance.on(Hls.Events.ERROR, (event, data) => { if (data.fatal) { console.error("HLS fataler Fehler:", data.type, data.details); if (data.type === Hls.ErrorTypes.NETWORK_ERROR && networkRetries < MAX_RETRIES) { // Netzwerkfehler -> Retry mit Backoff networkRetries++; if (typeof showToast === "function") showToast("Netzwerkfehler, Retry " + networkRetries + "/" + MAX_RETRIES + "...", "error"); setTimeout(() => hlsInstance.startLoad(), 1000 * networkRetries); } else { // Zu viele Retries oder anderer Fehler -> Fallback if (typeof showToast === "function") showToast("Stream-Fehler: " + data.details, "error"); cleanupHLS(); setStreamUrlLegacy(seekSec); } } }); } else { // Kein HLS moeglich -> Fallback console.warn("Weder natives HLS noch hls.js verfuegbar"); setStreamUrlLegacy(seekSec); } } catch (e) { console.error("HLS Start Fehler:", e); if (typeof showToast === "function") showToast("Stream-Start fehlgeschlagen: " + e.message, "error"); hideLoading(); setStreamUrlLegacy(seekSec); } } /** Fallback: Altes Pipe-Streaming (fMP4 ueber StreamResponse) */ function setStreamUrlLegacy(seekSec) { const params = new URLSearchParams({ quality: currentQuality, audio: currentAudio, sound: cfg.soundMode || "stereo", }); if (seekSec > 0) params.set("t", Math.floor(seekSec)); videoEl.src = `/api/library/videos/${cfg.videoId}/stream?${params}`; videoEl.addEventListener("playing", hideLoading, {once: true}); videoEl.play().catch(() => {}); } /** HLS aufraumen: hls.js + Server-Session beenden */ async function cleanupHLS() { if (hlsInstance) { hlsInstance.destroy(); hlsInstance = null; } if (hlsSessionId) { // Server-Session loeschen (fire & forget) fetch(`/tv/api/hls/${hlsSessionId}`, {method: "DELETE"}).catch(() => {}); hlsSessionId = null; } hlsReady = false; hlsSeekOffset = 0; } // === Playback-Controls === function togglePlay() { if (!videoEl) return; if (videoEl.paused) videoEl.play(); else videoEl.pause(); } function onPlay() { if (playBtn) playBtn.innerHTML = "❚❚"; scheduleHideControls(); } function onPause() { if (playBtn) playBtn.innerHTML = "▶"; showControls(); saveProgress(); } function onEnded() { saveProgress(true); episodesWatched++; // Schaust du noch? (wenn Max-Episoden erreicht) if (cfg.autoplayMax > 0 && episodesWatched >= cfg.autoplayMax) { document.getElementById("still-watching-overlay").style.display = ""; return; } // Naechste Episode if (cfg.nextVideoId && cfg.autoplay) { showNextEpisodeOverlay(); } else { setTimeout(() => window.history.back(), 2000); } } // === Seeking === function seekRelative(seconds) { if (!videoEl) return; const dur = getDuration(); const cur = getCurrentTime(); const newTime = Math.max(0, Math.min(cur + seconds, dur)); if (hlsSessionId) { // HLS: nativen Seek verwenden (hls.js unterstuetzt das) videoEl.currentTime = Math.max(0, Math.min( videoEl.currentTime + seconds, videoEl.duration || Infinity)); showControls(); } else { // Legacy: neuen Stream starten startHLSStream(newTime); showControls(); } } function onProgressClick(e) { if (!videoEl) return; const rect = e.currentTarget.getBoundingClientRect(); const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); const dur = getDuration(); if (!dur) return; // Absolute Seek-Position im Video const seekTo = pct * dur; // Immer neuen HLS-Stream starten (server-seitiger Seek) startHLSStream(seekTo); showControls(); } // === Zeit-Funktionen === function getCurrentTime() { if (!videoEl) return 0; // Bei HLS mit Server-Seek: videoEl.currentTime + Offset = echte Position return hlsSeekOffset + (videoEl.currentTime || 0); } function getDuration() { // Echte Gesamtdauer des Videos (nicht der HLS-Stream-Dauer) return cfg.duration || 0; } function onTimeUpdate() { if (!videoEl) return; const current = getCurrentTime(); const dur = getDuration(); if (progressBar && dur > 0) { progressBar.style.width = ((current / dur) * 100) + "%"; } if (timeDisplay) { timeDisplay.textContent = formatTime(current) + " / " + formatTime(dur); } } function formatTime(sec) { if (!sec || !isFinite(sec)) return "0:00"; const h = Math.floor(sec / 3600); const m = Math.floor((sec % 3600) / 60); const s = Math.floor(sec % 60); if (h > 0) return h + ":" + String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0"); return m + ":" + String(s).padStart(2, "0"); } // === Controls Ein-/Ausblenden === function showControls() { const wrapper = document.getElementById("player-wrapper"); if (wrapper) wrapper.classList.remove("player-hide-controls"); controlsVisible = true; scheduleHideControls(); } function hideControls() { if (!videoEl || videoEl.paused || popupOpen) return; const wrapper = document.getElementById("player-wrapper"); if (wrapper) wrapper.classList.add("player-hide-controls"); controlsVisible = false; } function scheduleHideControls() { if (controlsTimer) clearTimeout(controlsTimer); controlsTimer = setTimeout(hideControls, 4000); } // === Fullscreen === function toggleFullscreen() { const wrapper = document.getElementById("player-wrapper"); if (!document.fullscreenElement) { (wrapper || document.documentElement).requestFullscreen().catch(() => {}); } else { document.exitFullscreen().catch(() => {}); } } // === Popup-Menue (ersetzt das grosse Overlay-Panel) === function togglePopup() { if (popupOpen) { closePopup(); } else { openPopupSection(null); } } function openPopupSection(section) { const popup = document.getElementById("player-popup"); if (!popup) return; if (popupOpen && popupSection === section) { // Gleiche Sektion nochmal -> schliessen closePopup(); return; } popupOpen = true; popupSection = section; popup.style.display = ""; popup.classList.add("popup-visible"); renderPopup(section); showControls(); // Focus auf ersten Button im Popup requestAnimationFrame(() => { const first = popup.querySelector("[data-focusable]"); if (first) first.focus(); }); } function closePopup() { const popup = document.getElementById("player-popup"); if (!popup) return; popupOpen = false; popupSection = null; popup.classList.remove("popup-visible"); popup.style.display = "none"; } function renderPopup(section) { const popup = document.getElementById("player-popup"); if (!popup) return; let html = ""; if (!section) { // Hauptmenue: Liste aller Optionen html = '"; } else if (section === "audio") { html = _renderAudioOptions(); } else if (section === "subs") { html = _renderSubOptions(); } else if (section === "quality") { html = _renderQualityOptions(); } else if (section === "speed") { html = _renderSpeedOptions(); } popup.innerHTML = html; } function _currentAudioLabel() { if (videoInfo && videoInfo.audio_tracks && videoInfo.audio_tracks[currentAudio]) { const a = videoInfo.audio_tracks[currentAudio]; const ch = a.channels > 2 ? ` ${a.channels}ch` : ""; return langName(a.lang) + ch; } return "Spur 1"; } function _currentSubLabel() { if (videoInfo && videoInfo.subtitle_tracks && videoInfo.subtitle_tracks[currentSub]) { return langName(videoInfo.subtitle_tracks[currentSub].lang); } return "Spur " + (currentSub + 1); } function _renderAudioOptions() { let html = '"; return html; } function _renderSubOptions() { let html = '"; return html; } function _renderQualityOptions() { const qualities = [ ["uhd", "Ultra HD"], ["hd", "HD"], ["sd", "SD"], ["low", "Niedrig"] ]; let html = '"; return html; } function _renderSpeedOptions() { const speeds = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0]; let html = '"; return html; } // === Audio/Sub/Quality/Speed wechseln === function switchAudio(idx) { if (idx === currentAudio) return; currentAudio = idx; // Neuen HLS-Stream mit anderer Audio-Spur starten const currentTime = getCurrentTime(); startHLSStream(currentTime); renderPopup(popupSection); updatePlayerButtons(); } function switchSub(idx) { currentSub = idx; updateSubtitleTrack(); renderPopup(popupSection); updatePlayerButtons(); } function updateSubtitleTrack() { if (!videoEl || !videoEl.textTracks) return; for (let i = 0; i < videoEl.textTracks.length; i++) { videoEl.textTracks[i].mode = (i === currentSub) ? "showing" : "hidden"; } } function switchQuality(q) { if (q === currentQuality) return; currentQuality = q; const currentTime = getCurrentTime(); startHLSStream(currentTime); renderPopup(popupSection); updatePlayerButtons(); } function switchSpeed(s) { currentSpeed = s; if (videoEl) videoEl.playbackRate = s; renderPopup(popupSection); } // === Naechste Episode === function showNextEpisodeOverlay() { const overlay = document.getElementById("next-overlay"); if (!overlay) return; overlay.style.display = ""; let remaining = cfg.autoplayCountdown || 10; const countdownEl = document.getElementById("next-countdown"); nextCountdown = setInterval(() => { remaining--; if (countdownEl) countdownEl.textContent = remaining + "s"; if (remaining <= 0) { clearInterval(nextCountdown); playNextEpisode(); } }, 1000); if (countdownEl) countdownEl.textContent = remaining + "s"; } function playNextEpisode() { if (nextCountdown) clearInterval(nextCountdown); cleanupHLS(); if (cfg.nextUrl) window.location.href = cfg.nextUrl; } function cancelNext() { if (nextCountdown) clearInterval(nextCountdown); const overlay = document.getElementById("next-overlay"); if (overlay) overlay.style.display = "none"; setTimeout(() => window.history.back(), 500); } // === D-Pad Navigation fuer Fernbedienung === function _getFocusables() { if (popupOpen) { const popup = document.getElementById("player-popup"); return popup ? Array.from(popup.querySelectorAll("[data-focusable]")) : []; } // "Naechste Episode" oder "Schaust du noch" Overlay? const nextOv = document.getElementById("next-overlay"); if (nextOv && nextOv.style.display !== "none") { return Array.from(nextOv.querySelectorAll("[data-focusable]")); } const stillOv = document.getElementById("still-watching-overlay"); if (stillOv && stillOv.style.display !== "none") { return Array.from(stillOv.querySelectorAll("[data-focusable]")); } // Player-Controls const controls = document.getElementById("player-controls"); return controls ? Array.from(controls.querySelectorAll("[data-focusable]")) : []; } function _focusNext(direction) { const items = _getFocusables(); if (!items.length) return false; const cur = items.indexOf(document.activeElement); let next; if (direction === 1) { next = cur < 0 ? 0 : Math.min(cur + 1, items.length - 1); } else { next = cur < 0 ? items.length - 1 : Math.max(cur - 1, 0); } items[next].focus(); return true; } // === Tastatur-Steuerung === function onKeyDown(e) { // Samsung Tizen Remote Keys const keyMap = { 10009: "Escape", 10182: "Escape", 415: "Play", 19: "Pause", 413: "Stop", 417: "FastForward", 412: "Rewind", // Samsung Farbtasten 403: "ColorRed", 404: "ColorGreen", 405: "ColorYellow", 406: "ColorBlue", }; const key = keyMap[e.keyCode] || e.key; const active = document.activeElement; const buttonFocused = active && active.hasAttribute("data-focusable") && active.tagName === "BUTTON"; // --- Popup offen: D-Pad navigiert im Popup --- if (popupOpen) { switch (key) { case "Escape": case "Backspace": if (popupSection) { // Zurueck zum Hauptmenue openPopupSection(null); } else { closePopup(); const btnSettings = document.getElementById("btn-settings"); if (btnSettings) btnSettings.focus(); } e.preventDefault(); return; case "ArrowUp": _focusNext(-1); e.preventDefault(); return; case "ArrowDown": _focusNext(1); e.preventDefault(); return; case "ArrowLeft": if (popupSection) { openPopupSection(null); } else { closePopup(); } e.preventDefault(); return; case "ArrowRight": case "Enter": if (buttonFocused) active.click(); e.preventDefault(); return; } } // --- "Naechste Episode" / "Schaust du noch" Overlay --- const nextOv = document.getElementById("next-overlay"); const stillOv = document.getElementById("still-watching-overlay"); const modalOpen = (nextOv && nextOv.style.display !== "none") || (stillOv && stillOv.style.display !== "none"); if (modalOpen) { switch (key) { case "ArrowLeft": case "ArrowRight": _focusNext(key === "ArrowRight" ? 1 : -1); e.preventDefault(); return; case "Enter": if (buttonFocused) active.click(); e.preventDefault(); return; } } // --- Controls sichtbar + Button fokussiert: D-Pad navigiert --- if (controlsVisible && buttonFocused) { switch (key) { case "ArrowLeft": _focusNext(-1); showControls(); e.preventDefault(); return; case "ArrowRight": _focusNext(1); showControls(); e.preventDefault(); return; case "ArrowUp": active.blur(); showControls(); e.preventDefault(); return; case "ArrowDown": active.blur(); showControls(); e.preventDefault(); return; case "Enter": active.click(); showControls(); e.preventDefault(); return; } } // --- Standard Player-Tasten --- switch (key) { case " ": case "Play": case "Pause": togglePlay(); e.preventDefault(); break; case "Enter": if (!controlsVisible) { showControls(); if (playBtn) playBtn.focus(); } else { togglePlay(); } e.preventDefault(); break; case "ArrowLeft": case "Rewind": seekRelative(-10); showControls(); e.preventDefault(); break; case "ArrowRight": case "FastForward": seekRelative(10); showControls(); e.preventDefault(); break; case "ArrowUp": if (!controlsVisible) { showControls(); if (playBtn) playBtn.focus(); } else { if (playBtn) playBtn.focus(); showControls(); } e.preventDefault(); break; case "ArrowDown": if (!controlsVisible) { showControls(); if (playBtn) playBtn.focus(); } else { if (playBtn) playBtn.focus(); showControls(); } e.preventDefault(); break; case "Escape": case "Backspace": case "Stop": saveProgress(); cleanupHLS(); setTimeout(() => window.history.back(), 100); e.preventDefault(); break; case "f": toggleFullscreen(); e.preventDefault(); break; case "s": togglePopup(); e.preventDefault(); break; case "n": if (cfg.nextVideoId) playNextEpisode(); e.preventDefault(); break; // Samsung Farbtasten: Direkt-Zugriff auf Popup-Sektionen case "ColorRed": openPopupSection("audio"); e.preventDefault(); break; case "ColorGreen": openPopupSection("subs"); e.preventDefault(); break; case "ColorYellow": openPopupSection("quality"); e.preventDefault(); break; case "ColorBlue": openPopupSection("speed"); e.preventDefault(); break; } } // === Watch-Progress speichern === function saveProgress(completed) { if (!cfg.videoId || !videoEl) return; const dur = getDuration(); // Bei completed: Position = Duration (garantiert ueber Schwelle) const pos = completed ? dur : getCurrentTime(); if (pos < 5 && !completed) return; const payload = { video_id: cfg.videoId, position_sec: pos, duration_sec: dur, }; if (completed) payload.completed = true; fetch("/tv/api/watch-progress", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify(payload), }).catch(() => {}); } window.addEventListener("beforeunload", () => { saveProgress(); cleanupHLS(); }); // === Button-Status aktualisieren === function updatePlayerButtons() { // CC-Button: aktiv wenn Untertitel an var btnSubs = document.getElementById("btn-subs"); if (btnSubs) btnSubs.classList.toggle("active", currentSub >= 0); // Quality-Badge: aktuellen Modus anzeigen var badge = document.getElementById("quality-badge"); if (badge) { var labels = {uhd: "4K", hd: "HD", sd: "SD", low: "LD"}; badge.textContent = labels[currentQuality] || "HD"; } // Audio-Button: aktuelle Sprache anzeigen (Tooltip) var btnAudio = document.getElementById("btn-audio"); if (btnAudio && videoInfo && videoInfo.audio_tracks && videoInfo.audio_tracks[currentAudio]) { var lang = videoInfo.audio_tracks[currentAudio].lang; btnAudio.title = langName(lang) || "Audio"; } } // === Hilfsfunktionen === const LANG_NAMES = { deu: "Deutsch", eng: "English", fra: "Fran\u00e7ais", spa: "Espa\u00f1ol", ita: "Italiano", jpn: "\u65e5\u672c\u8a9e", kor: "\ud55c\uad6d\uc5b4", por: "Portugu\u00eas", rus: "\u0420\u0443\u0441\u0441\u043a\u0438\u0439", zho: "\u4e2d\u6587", und: "Unbekannt", }; function langName(code) { return LANG_NAMES[code] || code || ""; }