/** * VideoKonverter TV - Video-Player * Fullscreen-Player mit Tastatur/Fernbedienung-Steuerung * Speichert Watch-Progress automatisch */ let videoEl = null; let videoId = 0; let videoDuration = 0; let progressBar = null; let timeDisplay = null; let playBtn = null; let controlsTimer = null; let saveTimer = null; let controlsVisible = true; /** * Player initialisieren * @param {number} id - Video-ID * @param {number} startPos - Startposition in Sekunden * @param {number} duration - Video-Dauer in Sekunden (Fallback) */ function initPlayer(id, startPos, duration) { videoId = id; videoDuration = duration; videoEl = document.getElementById("player-video"); progressBar = document.getElementById("player-progress-bar"); timeDisplay = document.getElementById("player-time"); playBtn = document.getElementById("btn-play"); if (!videoEl) return; // Stream-URL setzen (ffmpeg-Transcoding Endpoint) const streamUrl = `/api/library/videos/${id}/stream` + (startPos > 0 ? `?t=${Math.floor(startPos)}` : ""); videoEl.src = streamUrl; // 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)) { videoDuration = videoEl.duration; } }); // Klick auf Video -> Play/Pause videoEl.addEventListener("click", togglePlay); // Controls UI playBtn.addEventListener("click", togglePlay); document.getElementById("btn-fullscreen").addEventListener("click", toggleFullscreen); // Progress-Bar klickbar fuer Seeking document.getElementById("player-progress").addEventListener("click", onProgressClick); // Tastatur-Steuerung document.addEventListener("keydown", onKeyDown); // Maus/Touch-Bewegung -> Controls anzeigen document.addEventListener("mousemove", showControls); document.addEventListener("touchstart", showControls); // Controls nach 4 Sekunden ausblenden scheduleHideControls(); // Watch-Progress alle 10 Sekunden speichern saveTimer = setInterval(saveProgress, 10000); } // === Playback-Controls === function togglePlay() { if (!videoEl) return; if (videoEl.paused) { videoEl.play(); } else { videoEl.pause(); } } function onPlay() { if (playBtn) playBtn.innerHTML = "❚❚"; // Pause-Symbol scheduleHideControls(); } function onPause() { if (playBtn) playBtn.innerHTML = "▶"; // Play-Symbol showControls(); // Sofort speichern bei Pause saveProgress(); } function onEnded() { // Video fertig -> als "completed" speichern saveProgress(true); // Zurueck navigieren nach 2 Sekunden setTimeout(() => { window.history.back(); }, 2000); } // === Seeking === function seekRelative(seconds) { if (!videoEl) return; const newTime = Math.max(0, Math.min( videoEl.currentTime + seconds, videoEl.duration || videoDuration )); // Neue Stream-URL mit Zeitstempel const wasPlaying = !videoEl.paused; videoEl.src = `/api/library/videos/${videoId}/stream?t=${Math.floor(newTime)}`; if (wasPlaying) videoEl.play(); 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 = videoEl.duration || videoDuration; if (!dur) return; const newTime = pct * dur; // Neue Stream-URL mit Zeitstempel const wasPlaying = !videoEl.paused; videoEl.src = `/api/library/videos/${videoId}/stream?t=${Math.floor(newTime)}`; if (wasPlaying) videoEl.play(); showControls(); } // === Zeit-Anzeige und Progress === function onTimeUpdate() { if (!videoEl) return; const current = videoEl.currentTime; const dur = videoEl.duration || videoDuration; // Progress-Bar if (progressBar && dur > 0) { progressBar.style.width = ((current / dur) * 100) + "%"; } // Zeit-Anzeige 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) 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(() => {}); } } // === 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; 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": // Lautstaerke hoch (falls vom Browser unterstuetzt) if (videoEl) videoEl.volume = Math.min(1, videoEl.volume + 0.1); showControls(); e.preventDefault(); break; case "ArrowDown": // Lautstaerke runter if (videoEl) videoEl.volume = Math.max(0, videoEl.volume - 0.1); showControls(); e.preventDefault(); break; case "Escape": case "Backspace": case "Stop": // Zurueck navigieren saveProgress(); setTimeout(() => window.history.back(), 100); e.preventDefault(); break; case "f": toggleFullscreen(); e.preventDefault(); break; } } // === Watch-Progress speichern === function saveProgress(completed) { if (!videoId || !videoEl) return; const pos = videoEl.currentTime || 0; const dur = videoEl.duration || videoDuration || 0; if (pos < 5 && !completed) return; // Erst ab 5 Sekunden speichern fetch("/tv/api/watch-progress", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ video_id: videoId, position_sec: pos, duration_sec: dur, }), }).catch(() => {}); // Fehler ignorieren (nicht kritisch) } // Beim Verlassen der Seite speichern window.addEventListener("beforeunload", () => saveProgress());