docker.videokonverter/video-konverter/app/static/tv/js/player.js
data 99730f2f8f feat: VideoKonverter v3.1 - TV-App, Auth, Tizen, Log-API
TV-App (/tv/):
- Login mit bcrypt-Passwort-Hashing und DB-Sessions (30 Tage)
- Home (Weiterschauen, Serien, Filme), Serien-Detail mit Staffeln
- Film-Uebersicht und Detail, Fullscreen Video-Player
- Suche mit Live-Ergebnissen, Watch-Progress (alle 10s gespeichert)
- D-Pad/Fernbedienung-Navigation (FocusManager, Samsung Tizen Keys)
- PWA: manifest.json, Service Worker, Icons fuer Handy/Tablet
- Pro-User Berechtigungen (Serien, Filme, Admin, erlaubte Pfade)

Admin-Erweiterungen:
- QR-Code fuer TV-App URL
- User-Verwaltung (CRUD) mit Rechte-Konfiguration
- Log-API: GET /api/log?lines=100&level=INFO

Tizen-App (tizen-app/):
- Wrapper-App fuer Samsung Smart TVs (.wgt Paket)
- Einmalige Server-IP Eingabe, danach automatische Verbindung
- Installationsanleitung (INSTALL.md)

Bug-Fixes:
- executeImport: Job-ID vor resetImport() gesichert
- cursor(aiomysql.DictCursor) statt cursor(dict)
- DB-Spalten width/height statt video_width/video_height

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 09:26:19 +01:00

276 lines
7.7 KiB
JavaScript

/**
* 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 = "&#10074;&#10074;"; // Pause-Symbol
scheduleHideControls();
}
function onPause() {
if (playBtn) playBtn.innerHTML = "&#9654;"; // 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());