docker.videokonverter/video-konverter/app/static/tv/js/vknative-bridge.js
data 95df4d7a90 feat: VideoKonverter v5.8 - AVPlay-Overlay Fix, Debug-Stats, Focus-Ring Fix
- Tizen: Parent-Frame Transparenz + iframe z-index Fix fuer sichtbare Player-Controls ueber AVPlay
- Tizen: Farbtasten (Rot/Gruen/Gelb/Blau) werden bei aktivem AVPlay an iframe weitergeleitet
- Tizen: AVPlay Debug-Stats (State, Stream-Info, Codec, Bitrate) per postMessage abrufbar
- VKNative Bridge: requestStats() + vknative_stats Handler fuer AVPlay-Monitoring
- Player: Debug-Overlay zeigt AVPlay-spezifische Infos (Blaue Taste auf Fernbedienung)
- CSS: Episoden-Karten Focus-Ring von outline auf box-shadow umgestellt (kein Clipping mehr)
- CSS: Episode-Grid padding fuer Scale-Transform Platz
- SW Cache v16 -> v17

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 20:20:01 +01:00

567 lines
24 KiB
JavaScript

/**
* 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");
})();