PROBLEME BEHOBEN: - Schwarzes Bild beim Video-Abspielen (z-index & iframe-Overlap) - Login-Cookie wurde nicht gesetzt (Third-Party-Cookie-Blocking) ÄNDERUNGEN: Tizen-App (tizen-app/index.html): - z-index AVPlay von 0 auf 10 erhöht (über iframe) - iframe wird beim AVPlay-Start ausgeblendet (opacity: 0, pointerEvents: none) - iframe wird beim AVPlay-Stop wieder eingeblendet - Fix: <object id="avplayer"> nur im Parent, NICHT im iframe Player-Template (video-konverter/app/templates/tv/player.html): - <object id="avplayer"> entfernt (existiert nur im Parent-Frame) - AVPlay läuft ausschließlich im Tizen-App Parent-Frame Cookie-Fix (video-konverter/app/routes/tv_api.py): - SameSite=Lax → SameSite=None (4 Stellen) - Ermöglicht Session-Cookies im Cross-Origin-iframe - Login funktioniert jetzt in Tizen-App (tizen:// → http://) Neue Features: - VKNative Bridge (vknative-bridge.js): postMessage-Kommunikation iframe ↔ Parent - AVPlay Bridge (avplay-bridge.js): Legacy Direct-Play Support - Android-App Scaffolding (android-app/) TESTERGEBNIS: - ✅ Login erfolgreich (SameSite=None Cookie) - ✅ AVPlay Direct-Play funktioniert (samsung-agent/1.1) - ✅ Bildqualität gut (Hardware-Decoding) - ✅ Keine Stream-Unterbrechungen - ✅ Watch-Progress-Tracking funktioniert Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
442 lines
17 KiB
JavaScript
442 lines
17 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;
|
|
}
|
|
});
|
|
|
|
// 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);
|
|
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");
|
|
})();
|