- import json in library_api.py ergänzt (fehlte, Video-Info-API crashte) - Player: D-Pad-Navigation für Samsung TV Fernbedienung eingebaut - Player: Samsung Farbtasten (Rot=Audio, Grün=Subs, Gelb=Qualität, Blau=Speed) - Player: Overlay zeigt nur noch die zum Button passende Sektion - Player: Auto-Fokus beim Öffnen von Overlays Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
693 lines
24 KiB
JavaScript
693 lines
24 KiB
JavaScript
/**
|
|
* 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 <track> 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 = "<h3>Audio</h3>";
|
|
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 += `<button class="overlay-option${active}" data-focusable onclick="switchAudio(${i})">${label}${ch}</button>`;
|
|
});
|
|
audioEl.innerHTML = html;
|
|
}
|
|
|
|
// Untertitel
|
|
const subsEl = document.getElementById("overlay-subs");
|
|
if (subsEl && videoInfo) {
|
|
let html = "<h3>Untertitel</h3>";
|
|
html += `<button class="overlay-option${currentSub === -1 ? ' active' : ''}" data-focusable onclick="switchSub(-1)">Aus</button>`;
|
|
if (videoInfo.subtitle_tracks) {
|
|
videoInfo.subtitle_tracks.forEach((s, i) => {
|
|
const label = langName(s.lang) || `Spur ${i + 1}`;
|
|
const active = i === currentSub ? " active" : "";
|
|
html += `<button class="overlay-option${active}" data-focusable onclick="switchSub(${i})">${label}</button>`;
|
|
});
|
|
}
|
|
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 = "<h3>Qualit\u00e4t</h3>";
|
|
qualities.forEach(([val, label]) => {
|
|
const active = val === currentQuality ? " active" : "";
|
|
html += `<button class="overlay-option${active}" data-focusable onclick="switchQuality('${val}')">${label}</button>`;
|
|
});
|
|
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 = "<h3>Geschwindigkeit</h3>";
|
|
speeds.forEach(s => {
|
|
const active = s === currentSpeed ? " active" : "";
|
|
html += `<button class="overlay-option${active}" data-focusable onclick="switchSpeed(${s})">${s}x</button>`;
|
|
});
|
|
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 || "";
|
|
}
|