/** * VideoKonverter TV - AVPlay Bridge v1.0 * Abstraktionsschicht fuer Samsung AVPlay API (Tizen WebApps). * Ermoeglicht Direct-Play von MKV/WebM/MP4 mit Hardware-Decodern. * * AVPlay kann: H.264, HEVC, AV1, VP9, EAC3, AC3, AAC, Opus * AVPlay kann NICHT: DTS (seit Samsung 2022 entfernt) * * Wird nur geladen wenn Tizen-Umgebung erkannt wird. */ const AVPlayBridge = { available: false, _playing: false, _duration: 0, _listener: null, _displayEl: null, // Element fuer AVPlay-Rendering _timeUpdateId: null, // Interval fuer periodische Zeit-Updates /** * Initialisierung: Prueft ob AVPlay API verfuegbar ist * @returns {boolean} true wenn AVPlay nutzbar */ init() { try { this.available = typeof webapis !== "undefined" && typeof webapis.avplay !== "undefined"; } catch (e) { this.available = false; } if (this.available) { console.info("[AVPlay] API verfuegbar"); } return this.available; }, /** * Prueft ob ein Video direkt abgespielt werden kann (ohne Transcoding) * @param {Object} videoInfo - Video-Infos vom Server * @returns {boolean} true wenn Direct-Play moeglich */ canPlay(videoInfo) { if (!this.available) return false; // Video-Codec pruefen const vc = (videoInfo.video_codec_normalized || "").toLowerCase(); const supportedVideo = ["h264", "hevc", "av1", "vp9"]; if (!supportedVideo.includes(vc)) { console.info(`[AVPlay] Video-Codec '${vc}' nicht unterstuetzt`); return false; } // Container pruefen const container = (videoInfo.container || "").toLowerCase(); const supportedContainers = ["mkv", "matroska", "mp4", "webm", "avi", "ts"]; if (container && !supportedContainers.some(c => container.includes(c))) { console.info(`[AVPlay] Container '${container}' nicht unterstuetzt`); return false; } // Audio-Codecs pruefen - DTS ist nicht unterstuetzt const audioCodecs = videoInfo.audio_codecs || []; const unsupported = ["dts", "dca", "dts_hd", "dts-hd", "truehd"]; const hasUnsupported = audioCodecs.some( ac => unsupported.includes(ac.toLowerCase()) ); if (hasUnsupported) { console.info("[AVPlay] DTS-Audio erkannt -> kein Direct-Play"); return false; } console.info(`[AVPlay] Direct-Play moeglich: ${vc}/${container}`); return true; }, /** * Video abspielen via AVPlay * @param {string} url - Direct-Stream-URL * @param {Object} opts - {seekMs, onTimeUpdate, onComplete, onError, onBuffering} */ play(url, opts = {}) { if (!this.available) return false; try { // Vorherige Session bereinigen this.stop(); // Display-Element setzen this._displayEl = document.getElementById("avplayer"); if (this._displayEl) { this._displayEl.style.display = "block"; } // AVPlay oeffnen webapis.avplay.open(url); // Display-Bereich setzen (Vollbild) webapis.avplay.setDisplayRect( 0, 0, window.innerWidth, window.innerHeight ); // Event-Listener registrieren this._listener = opts; webapis.avplay.setListener({ onbufferingstart: () => { console.debug("[AVPlay] Buffering gestartet"); if (this._listener && this._listener.onBuffering) { this._listener.onBuffering(true); } }, onbufferingcomplete: () => { console.debug("[AVPlay] Buffering abgeschlossen"); if (this._listener && this._listener.onBuffering) { this._listener.onBuffering(false); } }, oncurrentplaytime: (ms) => { // Wird von AVPlay periodisch aufgerufen if (this._listener && this._listener.onTimeUpdate) { this._listener.onTimeUpdate(ms); } }, onstreamcompleted: () => { console.info("[AVPlay] Wiedergabe abgeschlossen"); this._playing = false; if (this._listener && this._listener.onComplete) { this._listener.onComplete(); } }, onerror: (eventType) => { console.error("[AVPlay] Fehler:", eventType); this._playing = false; if (this._listener && this._listener.onError) { this._listener.onError(eventType); } }, onevent: (eventType, eventData) => { console.debug("[AVPlay] Event:", eventType, eventData); }, onsubtitlechange: (duration, text, dataSize, jsonData) => { // Untertitel-Events (optional) console.debug("[AVPlay] Subtitle:", text); }, }); // Async vorbereiten und starten webapis.avplay.prepareAsync( () => { // Erfolg: Wiedergabe starten this._duration = webapis.avplay.getDuration(); console.info(`[AVPlay] Bereit, Dauer: ${this._duration}ms`); // Seeking vor dem Start if (opts.seekMs && opts.seekMs > 0) { webapis.avplay.seekTo(opts.seekMs, () => { console.info(`[AVPlay] Seek zu ${opts.seekMs}ms`); webapis.avplay.play(); this._playing = true; this._startTimeUpdates(); }, (e) => { console.warn("[AVPlay] Seek fehlgeschlagen:", e); webapis.avplay.play(); this._playing = true; this._startTimeUpdates(); } ); } else { webapis.avplay.play(); this._playing = true; this._startTimeUpdates(); } // Buffering-Ende signalisieren if (this._listener && this._listener.onBuffering) { this._listener.onBuffering(false); } if (this._listener && this._listener.onReady) { this._listener.onReady(); } }, (error) => { console.error("[AVPlay] Prepare fehlgeschlagen:", error); if (this._listener && this._listener.onError) { this._listener.onError(error); } } ); return true; } catch (e) { console.error("[AVPlay] Fehler beim Starten:", e); if (this._listener && this._listener.onError) { this._listener.onError(e.message || e); } return false; } }, /** * Pause/Resume umschalten */ togglePlay() { if (!this.available) return; try { const state = webapis.avplay.getState(); if (state === "PLAYING") { webapis.avplay.pause(); this._playing = false; } else if (state === "PAUSED" || state === "READY") { webapis.avplay.play(); this._playing = true; } } catch (e) { console.error("[AVPlay] togglePlay Fehler:", e); } }, pause() { if (!this.available) return; try { if (this._playing) { webapis.avplay.pause(); this._playing = false; } } catch (e) { console.error("[AVPlay] pause Fehler:", e); } }, resume() { if (!this.available) return; try { webapis.avplay.play(); this._playing = true; } catch (e) { console.error("[AVPlay] resume Fehler:", e); } }, /** * Seeking zu Position in Millisekunden * @param {number} positionMs - Zielposition in ms * @param {function} onSuccess - Callback bei Erfolg * @param {function} onError - Callback bei Fehler */ seek(positionMs, onSuccess, onError) { if (!this.available) return; try { webapis.avplay.seekTo( Math.max(0, Math.floor(positionMs)), () => { console.debug(`[AVPlay] Seek zu ${positionMs}ms`); if (onSuccess) onSuccess(); }, (e) => { console.warn("[AVPlay] Seek Fehler:", e); if (onError) onError(e); } ); } catch (e) { console.error("[AVPlay] seek Fehler:", e); if (onError) onError(e); } }, /** * Wiedergabe stoppen und AVPlay bereinigen */ stop() { this._stopTimeUpdates(); this._playing = false; try { const state = webapis.avplay.getState(); if (state !== "IDLE" && state !== "NONE") { webapis.avplay.stop(); } webapis.avplay.close(); } catch (e) { // Ignorieren wenn bereits gestoppt } if (this._displayEl) { this._displayEl.style.display = "none"; } }, /** * Aktuelle Wiedergabeposition in Millisekunden * @returns {number} Position in ms */ getCurrentTime() { if (!this.available) return 0; try { return webapis.avplay.getCurrentTime(); } catch (e) { return 0; } }, /** * Gesamtdauer in Millisekunden * @returns {number} Dauer in ms */ getDuration() { if (!this.available) return 0; try { return this._duration || webapis.avplay.getDuration(); } catch (e) { return 0; } }, /** * Prueft ob gerade abgespielt wird * @returns {boolean} */ isPlaying() { return this._playing; }, /** * Periodische Zeit-Updates starten (fuer Progress-Bar) */ _startTimeUpdates() { this._stopTimeUpdates(); this._timeUpdateId = setInterval(() => { if (this._playing && this._listener && this._listener.onTimeUpdate) { this._listener.onTimeUpdate(this.getCurrentTime()); } }, 500); }, _stopTimeUpdates() { if (this._timeUpdateId) { clearInterval(this._timeUpdateId); this._timeUpdateId = null; } }, };