/** * VideoKonverter TV - Video-Player v5.0 * VKNative Bridge fuer Direct-Play (Tizen AVPlay / Android ExoPlayer), * HLS-Streaming mit hls.js als Fallback, * kompaktes Popup-Menue, 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 popupOpen = false; // Popup-Menue offen? let popupSection = null; // Aktive Popup-Sektion (null = Hauptmenue) let nextCountdown = null; let episodesWatched = 0; // HLS-State let hlsInstance = null; // hls.js Instanz let hlsSessionId = null; // Aktive HLS-Session-ID let hlsReady = false; // HLS-Playback bereit? let hlsSeekOffset = 0; // Server-seitiger Seek: echte Position im Video let clientCodecs = null; // Vom Client unterstuetzte Video-Codecs let hlsRetryCount = 0; // Retry-Zaehler fuer gesamten Stream-Start let loadingTimeout = null; // Timeout fuer Loading-Spinner // Native Player State (VKNative Bridge: Tizen AVPlay / Android ExoPlayer) let useNativePlayer = false; // VKNative Direct-Play aktiv? let nativePlayStarted = false; // VKNative _vkOnReady wurde aufgerufen? let nativePlayTimeout = null; // Master-Timeout fuer VKNative-Start // Legacy AVPlay Direct-Play State (Rueckwaerts-Kompatibilitaet) let useDirectPlay = false; // Alter AVPlayBridge-Pfad aktiv? // Audio-Kompressor (DynamicsCompressorNode) let _audioCtx = null; let _audioCompressorSetup = false; /** * Player initialisieren * @param {Object} opts - Konfiguration */ function initPlayer(opts) { cfg = opts; currentQuality = opts.streamQuality || "hd"; // Client-Codec-Erkennung (welche Video-Codecs kann dieser Browser?) clientCodecs = detectSupportedCodecs(); console.info("Client-Codecs:", clientCodecs.join(", ")); videoEl = document.getElementById("player-video"); progressBar = document.getElementById("player-progress-bar"); timeDisplay = document.getElementById("player-time"); playBtn = document.getElementById("btn-play"); if (!videoEl) return; // Prioritaet 1: VKNative Bridge (Tizen AVPlay / Android ExoPlayer) if (window.VKNative) { console.info("[Player] VKNative erkannt (Platform: " + window.VKNative.platform + ")"); showLoading(); // Master-Timeout: Falls VKNative nach 15s nicht gestartet hat -> HLS Fallback nativePlayTimeout = setTimeout(function() { if (!nativePlayStarted) { console.warn("[Player] VKNative Master-Timeout (15s) - Fallback auf HLS"); _cleanupNativePlayer(); _nativeFallbackToHLS(opts.startPos || 0); } }, 15000); _tryNativeDirectPlay(opts.startPos || 0); } // Prioritaet 2: Legacy AVPlayBridge (alte Tizen-App ohne VKNative) else if (isTizenTV() && typeof AVPlayBridge !== "undefined" && AVPlayBridge.init()) { showLoading(); _tryDirectPlay(opts.startPos || 0).then(success => { if (!success) { console.info("[Player] AVPlay Fallback -> HLS"); useDirectPlay = false; const infoReady = loadVideoInfo(); startHLSStream(opts.startPos || 0); infoReady.then(() => updatePlayerButtons()); } }); } // Prioritaet 3: Browser/PWA - Direct-Play (MP4) bevorzugt, HLS als Fallback else { showLoading(); const startPos = opts.startPos || 0; loadVideoInfo().then(() => { updatePlayerButtons(); _tryBrowserDirectPlay(startPos); }).catch(() => { startHLSStream(startPos); }); } // Events videoEl.addEventListener("timeupdate", onTimeUpdate); videoEl.addEventListener("play", onPlay); videoEl.addEventListener("pause", onPause); videoEl.addEventListener("ended", onEnded); videoEl.addEventListener("click", togglePlay); // Loading ausblenden sobald Video laeuft (mehrere Events als Sicherheit) videoEl.addEventListener("playing", onPlaying); videoEl.addEventListener("canplay", hideLoading); videoEl.addEventListener("loadeddata", hideLoading); // Video-Error: Automatisch Retry mit Fallback videoEl.addEventListener("error", onVideoError); // Controls UI playBtn.addEventListener("click", togglePlay); const btnFs = document.getElementById("btn-fullscreen"); if (btnFs) btnFs.addEventListener("click", toggleFullscreen); document.getElementById("player-progress").addEventListener("click", onProgressClick); // Einstellungen-Button -> Popup-Hauptmenue const btnSettings = document.getElementById("btn-settings"); if (btnSettings) btnSettings.addEventListener("click", () => togglePopup()); // Direkt-Buttons: Audio, Untertitel, Qualitaet const btnAudio = document.getElementById("btn-audio"); if (btnAudio) btnAudio.addEventListener("click", () => openPopupSection("audio")); const btnSubs = document.getElementById("btn-subs"); if (btnSubs) btnSubs.addEventListener("click", () => openPopupSection("subs")); const btnQuality = document.getElementById("btn-quality"); if (btnQuality) btnQuality.addEventListener("click", () => openPopupSection("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(); goBack(); }); // Tastatur-Steuerung document.addEventListener("keydown", onKeyDown); document.addEventListener("mousemove", showControls); document.addEventListener("touchstart", showControls); // Fullscreen nur auf Desktop/Handy anzeigen (nicht auf Samsung TV) if (btnFs && isTizenTV()) { btnFs.style.display = "none"; } scheduleHideControls(); saveTimer = setInterval(saveProgress, 10000); } // === Erkennung: Samsung Tizen TV === function isTizenTV() { return typeof tizen !== "undefined" || /Tizen/i.test(navigator.userAgent); } // === 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); }); updateSubtitleTrack(); } } catch (e) { console.warn("Video-Info laden fehlgeschlagen:", e); } } // === Codec-Erkennung === /** * Erkennt automatisch welche Video-Codecs der Browser/TV decodieren kann. * Wird beim HLS-Start an den Server geschickt -> Server entscheidet copy vs transcode. * * WICHTIG: Unterscheidung zwischen nativem HLS (Tizen/Safari) und MSE (hls.js): * - Natives HLS: canPlayType() meldet oft AV1/VP9, aber der native HLS-Player * unterstuetzt diese Codecs NICHT zuverlaessig in fMP4-Segmenten. * -> Konservativ: nur H.264 (+ evtl. HEVC) * - MSE/hls.js: MediaSource.isTypeSupported() ist zuverlaessig * -> Alle unterstuetzten Codecs melden */ function detectSupportedCodecs() { // VKNative Bridge: Codec-Liste vom nativen Player abfragen if (window.VKNative && window.VKNative.getSupportedVideoCodecs) { var nativeCodecs = window.VKNative.getSupportedVideoCodecs(); if (nativeCodecs && nativeCodecs.length) { console.info("[Player] VKNative Video-Codecs:", nativeCodecs.join(", ")); return nativeCodecs; } } const codecs = []; const el = document.createElement("video"); const hasNativeHLS = !!el.canPlayType("application/vnd.apple.mpegurl"); const hasMSE = typeof MediaSource !== "undefined" && MediaSource.isTypeSupported; if (hasMSE) { // MSE-basiert: zuverlaessige Codec-Erkennung // (auch auf Tizen, da wir hls.js dem nativen Player vorziehen) if (MediaSource.isTypeSupported('video/mp4; codecs="avc1.640028"')) codecs.push("h264"); if (MediaSource.isTypeSupported('video/mp4; codecs="hev1.1.6.L93.B0"')) codecs.push("hevc"); if (MediaSource.isTypeSupported('video/mp4; codecs="av01.0.05M.08"')) codecs.push("av1"); if (MediaSource.isTypeSupported('video/mp4; codecs="vp09.00.10.08"')) codecs.push("vp9"); } else { // Kein MSE: konservativ nur H.264 + HEVC melden codecs.push("h264"); if (el.canPlayType('video/mp4; codecs="hev1.1.6.L93.B0"') || el.canPlayType('video/mp4; codecs="hvc1.1.6.L93.B0"')) { codecs.push("hevc"); } } if (!codecs.length) codecs.push("h264"); return codecs; } // === Loading-Indikator === let loadingTimer = null; function showLoading() { var el = document.getElementById("player-loading"); if (el) { el.classList.remove("hidden"); el.classList.remove("fade-out"); el.style.display = ""; } // Fallback: Loading nach 8 Sekunden ausblenden und Controls zeigen clearTimeout(loadingTimer); loadingTimer = setTimeout(function() { hideLoading(); // Falls Video noch nicht spielt: Controls anzeigen damit User manuell starten kann if (videoEl && videoEl.paused) { showControls(); if (playBtn) playBtn.innerHTML = "▶"; } }, 8000); } function hideLoading() { clearTimeout(loadingTimer); clearTimeout(loadingTimeout); loadingTimeout = null; var el = document.getElementById("player-loading"); if (!el || el.classList.contains("fade-out")) return; // Sanfter Uebergang: Loading-Overlay blendet ueber 1.5s aus // Puffert gleichzeitig das Video waehrend der Ueberblendung el.classList.add("fade-out"); setTimeout(function() { el.style.display = "none"; }, 1600); } function onPlaying() { hideLoading(); hlsRetryCount = 0; // Reset bei erfolgreichem Start // Audio-Kompressor aktivieren (nur bei Browser-Wiedergabe, nicht VKNative) if (cfg.audioCompressor && !useNativePlayer && videoEl) { _setupAudioCompressor(videoEl); } } function onVideoError() { // Video-Element Fehler: Retry oder Fallback var err = videoEl.error; var msg = err ? "Video-Fehler: Code " + err.code : "Video-Fehler"; console.error(msg, err); if (hlsRetryCount < 2) { hlsRetryCount++; if (typeof showToast === "function") showToast("Stream-Fehler, Retry " + hlsRetryCount + "/2...", "error"); var seekPos = getCurrentTime(); setTimeout(function() { startHLSStream(seekPos); }, 1000 * hlsRetryCount); } else { if (typeof showToast === "function") showToast("Video konnte nicht gestartet werden", "error"); hideLoading(); } } // === AVPlay Direct-Play === /** * Versucht Direct-Play via AVPlay (nur auf Tizen). * Laedt erweiterte Video-Info vom Server und prueft Kompatibilitaet. * @returns {boolean} true wenn Direct-Play gestartet wurde */ async function _tryDirectPlay(startPosSec) { try { // Erweiterte Video-Info laden (inkl. audio_codecs, video_codec_normalized) const resp = await fetch(`/tv/api/video-info/${cfg.videoId}`); if (!resp.ok) return false; const info = await resp.json(); videoInfo = info; // Video-Info global setzen // Bevorzugte Audio-/Untertitel-Spur finden if (info.audio_tracks) { const prefIdx = info.audio_tracks.findIndex( a => a.lang === cfg.preferredAudio); if (prefIdx >= 0) currentAudio = prefIdx; } if (cfg.subtitlesEnabled && cfg.preferredSub && info.subtitle_tracks) { const subIdx = info.subtitle_tracks.findIndex( s => s.lang === cfg.preferredSub); if (subIdx >= 0) currentSub = subIdx; } // AVPlay Kompatibilitaet pruefen if (!AVPlayBridge.canPlay(info)) { console.info("[Player] Direct-Play nicht kompatibel"); return false; } // Direct-Play starten useDirectPlay = true; const directUrl = info.direct_play_url; if (!directUrl) return false; // Video-Element verstecken, AVPlay-Object anzeigen if (videoEl) videoEl.style.display = "none"; const ok = AVPlayBridge.play(directUrl, { seekMs: Math.floor(startPosSec * 1000), onReady: () => { hideLoading(); showControls(); scheduleHideControls(); }, onTimeUpdate: (ms) => { // Progress-Bar und Zeit-Anzeige aktualisieren const current = ms / 1000; const dur = getDuration(); if (progressBar && dur > 0) { progressBar.style.width = ((current / dur) * 100) + "%"; } if (timeDisplay) { timeDisplay.textContent = formatTime(current) + " / " + formatTime(dur); } }, onComplete: () => { onEnded(); }, onError: (err) => { console.error("[Player] AVPlay Fehler:", err); if (typeof showToast === "function") showToast("Direct-Play Fehler, wechsle zu HLS...", "error"); // Fallback auf HLS _cleanupDirectPlay(); useDirectPlay = false; if (videoEl) videoEl.style.display = ""; startHLSStream(startPosSec); }, onBuffering: (buffering) => { if (buffering) showLoading(); else hideLoading(); }, }); if (!ok) { _cleanupDirectPlay(); return false; } updatePlayerButtons(); return true; } catch (e) { console.error("[Player] Direct-Play Init fehlgeschlagen:", e); _cleanupDirectPlay(); return false; } } /** AVPlay Direct-Play bereinigen */ function _cleanupDirectPlay() { if (typeof AVPlayBridge !== "undefined") { AVPlayBridge.stop(); } useDirectPlay = false; if (videoEl) videoEl.style.display = ""; } // === VKNative Direct-Play === /** * Versucht Direct-Play ueber VKNative Bridge (Tizen AVPlay / Android ExoPlayer). * Laedt Video-Info und prueft Kompatibilitaet. Faellt bei Fehler auf HLS zurueck. */ async function _tryNativeDirectPlay(startPosSec) { console.info("[Player] _tryNativeDirectPlay start (videoId=" + cfg.videoId + ", startPos=" + startPosSec + ")"); try { // Erweiterte Video-Info laden console.info("[Player] Lade Video-Info..."); var resp = await fetch("/tv/api/video-info/" + cfg.videoId); console.info("[Player] Video-Info Response: HTTP " + resp.status); if (!resp.ok) { console.warn("[Player] Video-Info nicht ladbar (HTTP " + resp.status + ")"); _nativeFallbackToHLS(startPosSec); return; } var info = await resp.json(); videoInfo = info; console.info("[Player] Video-Info geladen: " + (info.video_codec_normalized || "?") + "/" + (info.container || "?") + ", Audio: " + (info.audio_codecs || []).join(",")); // Bevorzugte Audio-/Untertitel-Spur finden if (info.audio_tracks) { var prefIdx = info.audio_tracks.findIndex( function(a) { return a.lang === cfg.preferredAudio; }); if (prefIdx >= 0) currentAudio = prefIdx; } if (cfg.subtitlesEnabled && cfg.preferredSub && info.subtitle_tracks) { var subIdx = info.subtitle_tracks.findIndex( function(s) { return s.lang === cfg.preferredSub; }); if (subIdx >= 0) currentSub = subIdx; } // Untertitel als hinzufuegen if (info.subtitle_tracks) { info.subtitle_tracks.forEach(function(sub, i) { var 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); }); updateSubtitleTrack(); } // VKNative Kompatibilitaets-Pruefung var canPlay = window.VKNative.canDirectPlay(info); console.info("[Player] VKNative canDirectPlay: " + canPlay); if (!canPlay) { console.info("[Player] VKNative: Codec nicht kompatibel -> HLS Fallback"); _nativeFallbackToHLS(startPosSec); return; } // VKNative Callbacks registrieren window._vkOnReady = function() { nativePlayStarted = true; clearTimeout(nativePlayTimeout); nativePlayTimeout = null; hideLoading(); showControls(); scheduleHideControls(); console.info("[Player] VKNative Direct-Play gestartet"); }; window._vkOnTimeUpdate = function(ms) { var current = ms / 1000; var dur = getDuration(); if (progressBar && dur > 0) { progressBar.style.width = ((current / dur) * 100) + "%"; } if (timeDisplay) { timeDisplay.textContent = formatTime(current) + " / " + formatTime(dur); } }; window._vkOnComplete = function() { onEnded(); }; window._vkOnError = function(msg) { console.error("[Player] VKNative Fehler:", msg); clearTimeout(nativePlayTimeout); nativePlayTimeout = null; if (typeof showToast === "function") showToast("Direct-Play Fehler, wechsle zu HLS...", "error"); _cleanupNativePlayer(); startHLSStream(startPosSec); }; window._vkOnBuffering = function(buffering) { // Nur Loading-Text aendern, nicht den ganzen Spinner neu starten // (wuerde den Master-Timeout zuruecksetzen) var el = document.getElementById("player-loading"); if (el) { var textEl = el.querySelector(".player-loading-text"); if (textEl) textEl.textContent = buffering ? "Puffert..." : "Stream wird geladen..."; } }; window._vkOnPlayStateChanged = function(playing) { if (playing) { nativePlayStarted = true; clearTimeout(nativePlayTimeout); nativePlayTimeout = null; onPlay(); } else { onPause(); } }; // Return/Back-Taste auf Fernbedienung -> zurueck ins Menue navigieren window._vkOnStopped = function() { saveProgress(); _cleanupNativePlayer(); goBack(); }; // Direct-Play starten var directUrl = info.direct_play_url; console.info("[Player] Direct-Play URL: " + directUrl); if (!directUrl) { console.warn("[Player] Keine Direct-Play-URL vorhanden"); _nativeFallbackToHLS(startPosSec); return; } // Stream-Token holen (fuer AVPlay, das keinen Cookie-Jar hat) try { var tokenResp = await fetch("/tv/api/stream-token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ video_id: cfg.videoId }), }); if (tokenResp.ok) { var tokenData = await tokenResp.json(); if (tokenData.token) { directUrl += (directUrl.indexOf("?") >= 0 ? "&" : "?") + "token=" + encodeURIComponent(tokenData.token); console.info("[Player] Stream-Token angehaengt"); } } } catch (tokenErr) { console.warn("[Player] Stream-Token Fehler (ignoriert):", tokenErr); } useNativePlayer = true; document.body.classList.add("vknative-playing"); console.info("[Player] VKNative.play() aufrufen..."); var ok = window.VKNative.play(directUrl, info, { seekMs: Math.floor(startPosSec * 1000) }); console.info("[Player] VKNative.play() Ergebnis: " + ok); if (!ok) { console.warn("[Player] VKNative.play() fehlgeschlagen"); _cleanupNativePlayer(); _nativeFallbackToHLS(startPosSec); return; } updatePlayerButtons(); } catch (e) { console.error("[Player] VKNative Init fehlgeschlagen:", e); clearTimeout(nativePlayTimeout); nativePlayTimeout = null; _cleanupNativePlayer(); _nativeFallbackToHLS(startPosSec); } } /** VKNative bereinigen */ function _cleanupNativePlayer() { console.info("[Player] _cleanupNativePlayer()"); clearTimeout(nativePlayTimeout); nativePlayTimeout = null; if (window.VKNative) { try { window.VKNative.stop(); } catch (e) { console.warn("[Player] VKNative.stop() Fehler:", e); } } useNativePlayer = false; nativePlayStarted = false; document.body.classList.remove("vknative-playing"); // Callbacks entfernen window._vkOnReady = null; window._vkOnTimeUpdate = null; window._vkOnComplete = null; window._vkOnError = null; window._vkOnBuffering = null; window._vkOnPlayStateChanged = null; window._vkOnStopped = null; } /** Fallback von VKNative auf HLS */ function _nativeFallbackToHLS(startPosSec) { console.info("[Player] Fallback auf HLS (startPos=" + startPosSec + ")"); clearTimeout(nativePlayTimeout); nativePlayTimeout = null; // Android: ExoPlayer fuer HLS nutzen (WebView-Audio hat Probleme) if (window.VKNative && window.VKNative.playHLS) { console.info("[Player] VKNative.playHLS() verfuegbar - nutze ExoPlayer fuer HLS"); _startNativeHLS(startPosSec); return; } // Kein VKNative HLS (Tizen etc.) -> WebView-HLS useNativePlayer = false; nativePlayStarted = false; document.body.classList.remove("vknative-playing"); if (videoEl) videoEl.style.display = ""; var infoReady = loadVideoInfo(); startHLSStream(startPosSec); infoReady.then(function() { updatePlayerButtons(); }); } /** HLS ueber VKNative (ExoPlayer) abspielen - Audio laeuft zuverlaessig */ async function _startNativeHLS(startPosSec) { try { // HLS-Session vom Server anfordern var hlsSeek = startPosSec > 0 ? Math.floor(startPosSec) : 0; var resp = await fetch("/tv/api/hls/start", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ video_id: cfg.videoId, quality: currentQuality, audio: currentAudio, // Native Player (AVPlay/ExoPlayer) koennen Surround -> Default "surround" sound: cfg.soundMode || "surround", t: hlsSeek, codecs: clientCodecs || ["h264"], }), }); if (!resp.ok) { console.warn("[Player] HLS-Session fehlgeschlagen (HTTP " + resp.status + ")"); useNativePlayer = false; nativePlayStarted = false; if (videoEl) videoEl.style.display = ""; startHLSStream(startPosSec); return; } var data = await resp.json(); hlsSessionId = data.session_id; var playlistUrl = data.playlist_url; hlsSeekOffset = hlsSeek; // VKNative Callbacks window._vkOnReady = function() { nativePlayStarted = true; clearTimeout(nativePlayTimeout); nativePlayTimeout = null; hideLoading(); showControls(); scheduleHideControls(); console.info("[Player] VKNative HLS gestartet"); }; window._vkOnTimeUpdate = function(ms) { // ExoPlayer meldet HLS-Stream-Position + Server-Seek-Offset var current = hlsSeekOffset + (ms / 1000); var dur = getDuration(); if (progressBar && dur > 0) { progressBar.style.width = ((current / dur) * 100) + "%"; } if (timeDisplay) { timeDisplay.textContent = formatTime(current) + " / " + formatTime(dur); } }; window._vkOnComplete = function() { onEnded(); }; window._vkOnError = function(msg) { console.error("[Player] VKNative HLS Fehler:", msg); _cleanupNativePlayer(); if (videoEl) videoEl.style.display = ""; startHLSStream(startPosSec); }; window._vkOnBuffering = function(buffering) { if (buffering) showLoading(); else hideLoading(); }; window._vkOnPlayStateChanged = function(playing) { if (playing) { nativePlayStarted = true; clearTimeout(nativePlayTimeout); nativePlayTimeout = null; onPlay(); } else { onPause(); } }; // ExoPlayer HLS starten useNativePlayer = true; document.body.classList.add("vknative-playing"); var ok = window.VKNative.playHLS(playlistUrl, {}); console.info("[Player] VKNative.playHLS() Ergebnis: " + ok); if (!ok) { _cleanupNativePlayer(); if (videoEl) videoEl.style.display = ""; startHLSStream(startPosSec); return; } loadVideoInfo().then(function() { updatePlayerButtons(); }); } catch (e) { console.error("[Player] _startNativeHLS Fehler:", e); _cleanupNativePlayer(); if (videoEl) videoEl.style.display = ""; startHLSStream(startPosSec); } } // === Audio-Kompressor (Client-seitige Lautstaerke-Nivellierung) === /** * DynamicsCompressorNode: Reduziert Lautstaerke-Schwankungen im Browser. * Laute Passagen (Explosionen) werden komprimiert, leise (Dialoge) angehoben. * Funktioniert nur bei