/** * 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_stats": // AVPlay-Stats vom Parent (Antwort auf requestStats) if (data.stats) window.VKNative._lastStats = data.stats; break; case "vknative_keyevent": // Key-Event vom Parent weitergeleitet -> als KeyboardEvent dispatchen if (data.keyCode) { // keyCode -> key-Name Mapping (KeyboardEvent setzt key nicht automatisch) var keyNameMap = { 13: "Enter", 37: "ArrowLeft", 38: "ArrowUp", 39: "ArrowRight", 40: "ArrowDown", 27: "Escape", 8: "Backspace", 32: " ", // Samsung-spezifische keyCodes (Media + Farbtasten) 10009: "Escape", 10182: "Escape", 415: "Play", 19: "Pause", 413: "Stop", 417: "FastForward", 412: "Rewind", 10252: "Play", 403: "ColorRed", 404: "ColorGreen", 405: "ColorYellow", 406: "ColorBlue", }; var keyEvt = new KeyboardEvent("keydown", { keyCode: data.keyCode, which: data.keyCode, key: keyNameMap[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; } } // Tizen AVPlay: Opus mit >2 Kanaelen hat Tonausfaelle -> HLS Fallback var audioTracks = videoInfo.audio_tracks || []; for (var k = 0; k < audioTracks.length; k++) { var track = audioTracks[k]; var trackCodec = (track.codec || "").toLowerCase(); var trackChannels = track.channels || 2; if (trackCodec === "opus" && trackChannels > 2) { console.info("[VKNative] Opus " + trackChannels + "ch auf Tizen -> HLS Fallback (Tonausfaelle bei AVPlay)"); 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"); }, /** * HLS-Stream ueber AVPlay abspielen (Fallback fuer Opus-Surround etc.) * AVPlay spielt HLS nativ inkl. AAC 5.1 Surround. */ playHLS: function(playlistUrl, opts) { console.info("[VKNative] playHLS() per postMessage: " + playlistUrl); _callParent("playHLS", [playlistUrl, opts]); return true; }, 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; }, /** AVPlay-Stats vom Parent abfragen (async, Ergebnis in _lastStats) */ requestStats: function() { window.parent.postMessage({ type: "vknative_get_stats" }, "*"); }, _lastStats: null, }; // 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; } // Tizen AVPlay: Opus mit >2 Kanaelen hat Tonausfaelle -> HLS Fallback var audioTracks = videoInfo.audio_tracks || []; for (var k = 0; k < audioTracks.length; k++) { var trk = audioTracks[k]; var trkCodec = (trk.codec || "").toLowerCase(); var trkCh = trk.channels || 2; if (trkCodec === "opus" && trkCh > 2) { console.info("[VKNative] Opus " + trkCh + "ch auf Tizen -> HLS Fallback"); 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 = ""; }, /** HLS ueber AVPlay abspielen (Fallback fuer Opus-Surround) */ playHLS: function(playlistUrl, opts) { opts = opts || {}; var seekMs = opts.seekMs || 0; var fullUrl = _resolveUrl2(playlistUrl); 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() {}, }); webapis.avplay.prepareAsync( function() { try { _duration2 = webapis.avplay.getDuration(); } catch (e) { _duration2 = 0; } 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)); } }, function(err) { if (window._vkOnError) window._vkOnError(String(err)); } ); return true; } catch (e) { if (window._vkOnError) window._vkOnError(e.message || String(e)); return false; } }, 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"); })();