/** * VideoKonverter TV - VKNative Bridge v2.0 * Einheitliches Interface fuer native Video-Player auf allen Plattformen. * * Modus 1 - Tizen iframe (postMessage): * Laeuft im iframe auf Tizen-TV. Kommuniziert per postMessage mit dem * Parent-Frame (tizen-app/index.html), der AVPlay steuert. * webapis.avplay ist im iframe NICHT verfuegbar. * * Modus 2 - Tizen direkt (legacy, falls webapis doch verfuegbar): * Direkter Zugriff auf webapis.avplay (Fallback). * * Android: window.VKNative wird von der Kotlin-App per @JavascriptInterface injiziert. * Diese Bridge erkennt das und ueberspringt sich selbst. * * Interface: window.VKNative * Callbacks: window._vkOnReady, _vkOnTimeUpdate, _vkOnComplete, * _vkOnError, _vkOnBuffering, _vkOnPlayStateChanged */ (function() { "use strict"; // Bereits von Android injiziert? Dann nichts tun if (window.VKNative) { console.info("[VKNative] Bridge bereits vorhanden (platform: " + window.VKNative.platform + ")"); return; } // Tizen-Erkennung (User-Agent, auch im iframe gueltig) var isTizen = /Tizen/i.test(navigator.userAgent); if (!isTizen) { // Kein Tizen -> Bridge nicht noetig (Desktop/Handy nutzt HLS) return; } // Im iframe? (Parent-Frame hat AVPlay) var inIframe = (window.parent !== window); // AVPlay direkt verfuegbar? (nur im Parent-Frame oder bei altem Redirect-Ansatz) var avplayDirect = false; try { avplayDirect = typeof webapis !== "undefined" && typeof webapis.avplay !== "undefined"; } catch (e) { avplayDirect = false; } // === MODUS 1: iframe + postMessage === if (inIframe && !avplayDirect) { console.info("[VKNative] Tizen iframe-Modus: postMessage Bridge wird initialisiert"); var _parentReady = false; var _videoCodecs = ["h264", "hevc", "av1", "vp9"]; var _audioCodecs = ["aac", "opus", "ac3", "eac3", "flac", "mp3", "vorbis", "pcm"]; var _playing = false; var _currentTimeMs = 0; var _durationMs = 0; // Unterstuetzte/Nicht-unterstuetzte Codecs (fuer canDirectPlay) var UNSUPPORTED_AUDIO = ["dts", "dca", "dts_hd", "dts-hd", "truehd"]; var SUPPORTED_CONTAINERS = ["mkv", "matroska", "mp4", "webm", "avi", "ts"]; // Events vom Parent empfangen window.addEventListener("message", function(event) { // Nur Nachrichten vom Parent akzeptieren if (event.source !== window.parent) return; var data = event.data; if (!data || !data.type) return; switch (data.type) { case "vknative_ready": // Parent bestaetigt: AVPlay ist verfuegbar _parentReady = true; if (data.videoCodecs) _videoCodecs = data.videoCodecs; if (data.audioCodecs) _audioCodecs = data.audioCodecs; console.info("[VKNative] Parent bereit, Codecs: " + _videoCodecs.join(",") + " / " + _audioCodecs.join(",")); break; case "vknative_event": _handleParentEvent(data.event, data.detail || {}); break; case "vknative_keyevent": // Media-Key vom Parent weitergeleitet -> als KeyboardEvent dispatchen if (data.keyCode) { var keyEvt = new KeyboardEvent("keydown", { keyCode: data.keyCode, which: data.keyCode, bubbles: true, }); document.dispatchEvent(keyEvt); } break; } }); // Events vom Parent an die Callbacks weiterleiten function _handleParentEvent(event, detail) { switch (event) { case "ready": _playing = true; if (window._vkOnReady) window._vkOnReady(); break; case "timeupdate": _currentTimeMs = detail.ms || 0; if (window._vkOnTimeUpdate) window._vkOnTimeUpdate(_currentTimeMs); break; case "complete": _playing = false; if (window._vkOnComplete) window._vkOnComplete(); break; case "error": _playing = false; if (window._vkOnError) window._vkOnError(detail.msg || "Unbekannter Fehler"); break; case "buffering": if (window._vkOnBuffering) window._vkOnBuffering(detail.buffering); break; case "playstatechanged": _playing = !!detail.playing; if (window._vkOnPlayStateChanged) window._vkOnPlayStateChanged(_playing); break; case "stopped": _playing = false; _currentTimeMs = 0; if (window._vkOnPlayStateChanged) window._vkOnPlayStateChanged(false); // Zurueck-Navigation ausloesen (Return-Taste auf Fernbedienung) if (window._vkOnStopped) window._vkOnStopped(); break; case "duration": _durationMs = detail.ms || 0; break; } } // Nachricht an Parent senden function _callParent(method, args) { window.parent.postMessage({ type: "vknative_call", method: method, args: args || [], }, "*"); } // Probe senden: "Bist du bereit?" function _probeParent() { window.parent.postMessage({ type: "vknative_probe" }, "*"); } // === VKNative Interface (postMessage-Modus) === window.VKNative = { platform: "tizen", version: "2.0.0", getSupportedVideoCodecs: function() { return _videoCodecs.slice(); }, getSupportedAudioCodecs: function() { return _audioCodecs.slice(); }, canDirectPlay: function(videoInfo) { // Video-Codec pruefen var vc = (videoInfo.video_codec_normalized || "").toLowerCase(); if (_videoCodecs.indexOf(vc) === -1) { console.info("[VKNative] Video-Codec '" + vc + "' nicht unterstuetzt"); return false; } // Container pruefen var container = (videoInfo.container || "").toLowerCase(); if (container) { var containerOk = false; for (var i = 0; i < SUPPORTED_CONTAINERS.length; i++) { if (container.indexOf(SUPPORTED_CONTAINERS[i]) !== -1) { containerOk = true; break; } } if (!containerOk) { console.info("[VKNative] Container '" + container + "' nicht unterstuetzt"); return false; } } // Audio-Codecs pruefen - DTS/TrueHD blockieren var audioCodecs = videoInfo.audio_codecs || []; for (var j = 0; j < audioCodecs.length; j++) { var ac = audioCodecs[j].toLowerCase(); if (UNSUPPORTED_AUDIO.indexOf(ac) !== -1) { console.info("[VKNative] Audio-Codec '" + ac + "' nicht unterstuetzt -> kein Direct-Play"); return false; } } console.info("[VKNative] Direct-Play moeglich: " + vc + "/" + container); return true; }, play: function(url, videoInfo, opts) { console.info("[VKNative] play() per postMessage: " + url); _callParent("play", [url, videoInfo, opts]); return true; }, togglePlay: function() { _callParent("togglePlay"); }, pause: function() { _callParent("pause"); }, resume: function() { _callParent("resume"); }, seek: function(positionMs) { _callParent("seek", [positionMs]); }, getCurrentTime: function() { // Letzte bekannte Position (wird per timeupdate-Event aktualisiert) return _currentTimeMs; }, getDuration: function() { return _durationMs; }, isPlaying: function() { return _playing; }, stop: function() { _playing = false; _currentTimeMs = 0; _callParent("stop"); }, setAudioTrack: function(index) { console.info("[VKNative] Audio-Track-Wechsel auf Tizen nicht moeglich"); return false; }, setSubtitleTrack: function(index) { return false; }, setPlaybackSpeed: function(speed) { _callParent("setPlaybackSpeed", [speed]); return true; }, }; // Parent proben (wiederholt, falls Parent noch nicht bereit) _probeParent(); var _probeRetries = 0; var _probeInterval = setInterval(function() { if (_parentReady || _probeRetries > 20) { clearInterval(_probeInterval); if (!_parentReady) { console.warn("[VKNative] Parent hat nach 10s nicht geantwortet"); } return; } _probeRetries++; _probeParent(); }, 500); console.info("[VKNative] Tizen postMessage Bridge bereit"); return; } // === MODUS 2: Direkter AVPlay-Zugriff (Legacy-Fallback) === if (avplayDirect) { console.info("[VKNative] Tizen Direct-AVPlay Bridge wird initialisiert"); var _playing2 = false; var _duration2 = 0; var _displayEl2 = null; var _timeUpdateId2 = null; var SUPPORTED_VIDEO = ["h264", "hevc", "av1", "vp9"]; var SUPPORTED_AUDIO = ["aac", "opus", "ac3", "eac3", "flac", "mp3", "vorbis", "pcm"]; var UNSUPPORTED_AUDIO2 = ["dts", "dca", "dts_hd", "dts-hd", "truehd"]; var SUPPORTED_CONTAINERS2 = ["mkv", "matroska", "mp4", "webm", "avi", "ts"]; function _startTimeUpdates2() { _stopTimeUpdates2(); _timeUpdateId2 = setInterval(function() { if (_playing2 && window._vkOnTimeUpdate) { try { window._vkOnTimeUpdate(webapis.avplay.getCurrentTime()); } catch (e) {} } }, 500); } function _stopTimeUpdates2() { if (_timeUpdateId2) { clearInterval(_timeUpdateId2); _timeUpdateId2 = null; } } function _resolveUrl2(url) { if (url.indexOf("://") !== -1) return url; return window.location.origin + url; } window.VKNative = { platform: "tizen", version: "2.0.0", getSupportedVideoCodecs: function() { return SUPPORTED_VIDEO.slice(); }, getSupportedAudioCodecs: function() { return SUPPORTED_AUDIO.slice(); }, canDirectPlay: function(videoInfo) { var vc = (videoInfo.video_codec_normalized || "").toLowerCase(); if (SUPPORTED_VIDEO.indexOf(vc) === -1) return false; var container = (videoInfo.container || "").toLowerCase(); if (container) { var ok = false; for (var i = 0; i < SUPPORTED_CONTAINERS2.length; i++) { if (container.indexOf(SUPPORTED_CONTAINERS2[i]) !== -1) { ok = true; break; } } if (!ok) return false; } var ac2 = videoInfo.audio_codecs || []; for (var j = 0; j < ac2.length; j++) { if (UNSUPPORTED_AUDIO2.indexOf(ac2[j].toLowerCase()) !== -1) return false; } return true; }, play: function(url, videoInfo, opts) { opts = opts || {}; var seekMs = opts.seekMs || 0; var fullUrl = _resolveUrl2(url); try { this.stop(); _displayEl2 = document.getElementById("avplayer"); if (_displayEl2) _displayEl2.style.display = "block"; var videoEl = document.getElementById("player-video"); if (videoEl) videoEl.style.display = "none"; webapis.avplay.open(fullUrl); webapis.avplay.setDisplayRect(0, 0, window.innerWidth, window.innerHeight); webapis.avplay.setListener({ onbufferingstart: function() { if (window._vkOnBuffering) window._vkOnBuffering(true); }, onbufferingcomplete: function() { if (window._vkOnBuffering) window._vkOnBuffering(false); }, oncurrentplaytime: function(ms) { if (window._vkOnTimeUpdate) window._vkOnTimeUpdate(ms); }, onstreamcompleted: function() { _playing2 = false; if (window._vkOnPlayStateChanged) window._vkOnPlayStateChanged(false); if (window._vkOnComplete) window._vkOnComplete(); }, onerror: function(evt) { _playing2 = false; if (window._vkOnError) window._vkOnError(String(evt)); }, onevent: function() {}, onsubtitlechange: function() {}, }); function _start() { try { webapis.avplay.play(); _playing2 = true; _startTimeUpdates2(); if (window._vkOnPlayStateChanged) window._vkOnPlayStateChanged(true); if (window._vkOnReady) window._vkOnReady(); } catch (e) { _playing2 = false; if (window._vkOnError) window._vkOnError(e.message || String(e)); } } webapis.avplay.prepareAsync( function() { try { _duration2 = webapis.avplay.getDuration(); } catch (e) { _duration2 = 0; } if (seekMs > 0) { try { webapis.avplay.seekTo(seekMs, function() { _start(); }, function() { _start(); }); } catch (e) { _start(); } } else { _start(); } }, function(err) { if (window._vkOnError) window._vkOnError(String(err)); } ); return true; } catch (e) { if (window._vkOnError) window._vkOnError(e.message || String(e)); return false; } }, togglePlay: function() { try { var state = webapis.avplay.getState(); if (state === "PLAYING") { webapis.avplay.pause(); _playing2 = false; if (window._vkOnPlayStateChanged) window._vkOnPlayStateChanged(false); } else if (state === "PAUSED" || state === "READY") { webapis.avplay.play(); _playing2 = true; if (window._vkOnPlayStateChanged) window._vkOnPlayStateChanged(true); } } catch (e) {} }, pause: function() { try { if (_playing2) { webapis.avplay.pause(); _playing2 = false; if (window._vkOnPlayStateChanged) window._vkOnPlayStateChanged(false); } } catch (e) {} }, resume: function() { try { webapis.avplay.play(); _playing2 = true; if (window._vkOnPlayStateChanged) window._vkOnPlayStateChanged(true); } catch (e) {} }, seek: function(ms) { try { webapis.avplay.seekTo(Math.max(0, Math.floor(ms)), function(){}, function(){}); } catch (e) {} }, getCurrentTime: function() { try { return webapis.avplay.getCurrentTime(); } catch (e) { return 0; } }, getDuration: function() { try { return _duration2 || webapis.avplay.getDuration(); } catch (e) { return 0; } }, isPlaying: function() { return _playing2; }, stop: function() { _stopTimeUpdates2(); _playing2 = false; try { var s = webapis.avplay.getState(); if (s !== "IDLE" && s !== "NONE") webapis.avplay.stop(); webapis.avplay.close(); } catch (e) {} if (_displayEl2) { _displayEl2.style.display = "none"; _displayEl2 = null; } var v = document.getElementById("player-video"); if (v) v.style.display = ""; }, setAudioTrack: function() { return false; }, setSubtitleTrack: function() { return false; }, setPlaybackSpeed: function(speed) { try { webapis.avplay.setSpeed(speed); return true; } catch (e) { return false; } }, }; console.info("[VKNative] Tizen Direct-AVPlay Bridge bereit"); return; } // Weder iframe noch AVPlay verfuegbar -> kein VKNative console.warn("[VKNative] Tizen erkannt, aber weder iframe-Parent noch webapis.avplay verfuegbar"); })();