/** * 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(); } } function openOverlaySection(section) { const overlay = document.getElementById("player-overlay"); if (!overlay) return; if (overlayOpen) { // Bereits offen -> schliessen overlayOpen = false; overlay.style.display = "none"; return; } overlayOpen = true; overlay.style.display = ""; renderOverlay(); showControls(); if (section) { var el = document.getElementById("overlay-" + section); if (el) el.scrollIntoView({ behavior: "smooth" }); } } 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); } // === 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", }; const key = keyMap[e.keyCode] || e.key; // Overlay offen? -> Navigation im Overlay if (overlayOpen && (key === "Escape" || key === "Backspace")) { toggleOverlay(); e.preventDefault(); return; } switch (key) { case " ": case "Enter": case "Play": case "Pause": togglePlay(); e.preventDefault(); break; case "ArrowLeft": case "Rewind": seekRelative(-10); e.preventDefault(); break; case "ArrowRight": case "FastForward": seekRelative(10); e.preventDefault(); break; case "ArrowUp": if (videoEl) videoEl.volume = Math.min(1, videoEl.volume + 0.1); showControls(); e.preventDefault(); break; case "ArrowDown": if (videoEl) videoEl.volume = Math.max(0, videoEl.volume - 0.1); 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; } } // === 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 || ""; }