/** * VideoKonverter TV - Video-Player v4.0 * 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 overlayOpen = false; let nextCountdown = null; let episodesWatched = 0; let seekOffset = 0; // Korrektur fuer Seek-basiertes Streaming /** * Player initialisieren * @param {Object} opts - Konfiguration */ function initPlayer(opts) { cfg = opts; currentQuality = opts.streamQuality || "hd"; 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 laden (Audio/Subtitle-Tracks) loadVideoInfo().then(() => { // Stream starten setStreamUrl(opts.startPos || 0); updatePlayerButtons(); }); // Events videoEl.addEventListener("timeupdate", onTimeUpdate); videoEl.addEventListener("play", onPlay); videoEl.addEventListener("pause", onPause); videoEl.addEventListener("ended", onEnded); videoEl.addEventListener("loadedmetadata", () => { if (videoEl.duration && isFinite(videoEl.duration)) { cfg.duration = videoEl.duration + seekOffset; } }); videoEl.addEventListener("click", togglePlay); // Controls UI playBtn.addEventListener("click", togglePlay); document.getElementById("btn-fullscreen").addEventListener("click", toggleFullscreen); document.getElementById("player-progress").addEventListener("click", onProgressClick); // Einstellungen-Button const btnSettings = document.getElementById("btn-settings"); if (btnSettings) btnSettings.addEventListener("click", () => openOverlaySection(null)); // Separate Buttons: Audio, Untertitel, Qualitaet const btnAudio = document.getElementById("btn-audio"); if (btnAudio) btnAudio.addEventListener("click", () => openOverlaySection("audio")); const btnSubs = document.getElementById("btn-subs"); if (btnSubs) btnSubs.addEventListener("click", () => openOverlaySection("subs")); const btnQuality = document.getElementById("btn-quality"); if (btnQuality) btnQuality.addEventListener("click", () => openOverlaySection("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); scheduleHideControls(); saveTimer = setInterval(saveProgress, 10000); } // === 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); }); // Aktiven Track setzen updateSubtitleTrack(); } } catch (e) { console.warn("Video-Info laden fehlgeschlagen:", e); } } // === Stream-URL === function setStreamUrl(seekSec) { seekOffset = seekSec || 0; const params = new URLSearchParams({ quality: currentQuality, audio: currentAudio, sound: cfg.soundMode || "stereo", }); if (seekSec > 0) params.set("t", Math.floor(seekSec)); const wasPlaying = videoEl && !videoEl.paused; videoEl.src = `/api/library/videos/${cfg.videoId}/stream?${params}`; if (wasPlaying) videoEl.play(); } // === 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 totalTime = seekOffset + videoEl.currentTime; const dur = cfg.duration || (seekOffset + (videoEl.duration || 0)); const newTime = Math.max(0, Math.min(totalTime + seconds, dur)); setStreamUrl(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 = cfg.duration || (seekOffset + (videoEl.duration || 0)); if (!dur) return; setStreamUrl(pct * dur); showControls(); } // === Zeit-Anzeige === function onTimeUpdate() { if (!videoEl) return; const current = seekOffset + videoEl.currentTime; const dur = cfg.duration || (seekOffset + (videoEl.duration || 0)); 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 || overlayOpen) 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(() => {}); } } // === Einstellungen-Overlay === function toggleOverlay() { const overlay = document.getElementById("player-overlay"); if (!overlay) return; overlayOpen = !overlayOpen; overlay.style.display = overlayOpen ? "" : "none"; if (overlayOpen) { renderOverlay(); showControls(); // Nur Geschwindigkeit (Audio/Subs/Qualitaet haben eigene Buttons) overlay.querySelectorAll(".player-overlay-section").forEach(s => { s.style.display = s.id === "overlay-speed" ? "" : "none"; }); var el = document.getElementById("overlay-speed"); if (el) { var firstBtn = el.querySelector("[data-focusable]"); if (firstBtn) firstBtn.focus(); } } } function openOverlaySection(section) { const overlay = document.getElementById("player-overlay"); if (!overlay) return; if (overlayOpen) { // Bereits offen -> schliessen overlayOpen = false; overlay.style.display = "none"; // Alle Sektionen wieder sichtbar machen overlay.querySelectorAll(".player-overlay-section").forEach( s => s.style.display = ""); return; } overlayOpen = true; overlay.style.display = ""; renderOverlay(); showControls(); if (section) { // Nur die gewaehlte Sektion anzeigen, andere verstecken overlay.querySelectorAll(".player-overlay-section").forEach(s => { s.style.display = s.id === "overlay-" + section ? "" : "none"; }); var el = document.getElementById("overlay-" + section); if (el) { var firstBtn = el.querySelector("[data-focusable]"); if (firstBtn) firstBtn.focus(); } } else { // Settings-Button: nur Geschwindigkeit (Audio/Subs/Qualitaet haben eigene Buttons) overlay.querySelectorAll(".player-overlay-section").forEach(s => { s.style.display = s.id === "overlay-speed" ? "" : "none"; }); var el = document.getElementById("overlay-speed"); if (el) { var firstBtn = el.querySelector("[data-focusable]"); if (firstBtn) firstBtn.focus(); } } } function renderOverlay() { // Audio-Spuren const audioEl = document.getElementById("overlay-audio"); if (audioEl && videoInfo && videoInfo.audio_tracks) { let html = "

Audio

"; videoInfo.audio_tracks.forEach((a, i) => { const label = langName(a.lang) || `Spur ${i + 1}`; const ch = a.channels > 2 ? ` (${a.channels}ch)` : ""; const active = i === currentAudio ? " active" : ""; html += ``; }); audioEl.innerHTML = html; } // Untertitel const subsEl = document.getElementById("overlay-subs"); if (subsEl && videoInfo) { let html = "

Untertitel

"; html += ``; if (videoInfo.subtitle_tracks) { videoInfo.subtitle_tracks.forEach((s, i) => { const label = langName(s.lang) || `Spur ${i + 1}`; const active = i === currentSub ? " active" : ""; html += ``; }); } subsEl.innerHTML = html; } // Qualitaet const qualEl = document.getElementById("overlay-quality"); if (qualEl) { const qualities = [ ["uhd", "Ultra HD"], ["hd", "HD"], ["sd", "SD"], ["low", "Niedrig"] ]; let html = "

Qualit\u00e4t

"; qualities.forEach(([val, label]) => { const active = val === currentQuality ? " active" : ""; html += ``; }); qualEl.innerHTML = html; } // Geschwindigkeit const speedEl = document.getElementById("overlay-speed"); if (speedEl) { const speeds = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0]; let html = "

Geschwindigkeit

"; speeds.forEach(s => { const active = s === currentSpeed ? " active" : ""; html += ``; }); speedEl.innerHTML = html; } } function switchAudio(idx) { if (idx === currentAudio) return; currentAudio = idx; // Neuen Stream mit anderer Audio-Spur starten const currentTime = seekOffset + (videoEl ? videoEl.currentTime : 0); setStreamUrl(currentTime); renderOverlay(); updatePlayerButtons(); } function switchSub(idx) { currentSub = idx; updateSubtitleTrack(); renderOverlay(); 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 = seekOffset + (videoEl ? videoEl.currentTime : 0); setStreamUrl(currentTime); renderOverlay(); updatePlayerButtons(); } function switchSpeed(s) { currentSpeed = s; if (videoEl) videoEl.playbackRate = s; renderOverlay(); } // === 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); 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 === /** * Fokussierbare Elemente im aktuellen Kontext finden. * Im Overlay: nur Overlay-Buttons. Sonst: Player-Control-Buttons. */ function _getFocusables() { if (overlayOpen) { const overlay = document.getElementById("player-overlay"); return overlay ? Array.from(overlay.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"; // --- Overlay offen: D-Pad navigiert im Overlay --- if (overlayOpen) { switch (key) { case "Escape": case "Backspace": toggleOverlay(); // Focus zurueck auf Settings-Button 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": _focusNext(-1); e.preventDefault(); return; case "ArrowRight": _focusNext(1); e.preventDefault(); return; 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": // Vom Button weg = Controls ausblenden, Video steuern 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": // Kein Button fokussiert: Controls einblenden + Focus auf Play 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": // Erster Druck: Controls + Focus auf Buttons if (!controlsVisible) { showControls(); if (playBtn) playBtn.focus(); } else { // Controls sichtbar aber kein Button fokussiert: Focus setzen 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(); setTimeout(() => window.history.back(), 100); e.preventDefault(); break; case "f": toggleFullscreen(); e.preventDefault(); break; case "s": toggleOverlay(); e.preventDefault(); break; case "n": if (cfg.nextVideoId) playNextEpisode(); e.preventDefault(); break; // Samsung Farbtasten: Direkt-Zugriff auf Overlay-Sektionen case "ColorRed": openOverlaySection("audio"); e.preventDefault(); break; case "ColorGreen": openOverlaySection("subs"); e.preventDefault(); break; case "ColorYellow": openOverlaySection("quality"); e.preventDefault(); break; case "ColorBlue": openOverlaySection("speed"); e.preventDefault(); break; } } // === Watch-Progress speichern === function saveProgress(completed) { if (!cfg.videoId || !videoEl) return; const pos = seekOffset + (videoEl.currentTime || 0); const dur = cfg.duration || (seekOffset + (videoEl.duration || 0)); if (pos < 5 && !completed) return; fetch("/tv/api/watch-progress", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ video_id: cfg.videoId, position_sec: pos, duration_sec: dur, }), }).catch(() => {}); } window.addEventListener("beforeunload", () => saveProgress()); // === 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 || ""; }