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>
451 lines
16 KiB
HTML
451 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>VideoKonverter</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
background: #0f0f0f;
|
|
color: #fff;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
overflow: hidden;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
}
|
|
#app-iframe {
|
|
border: none;
|
|
width: 100%;
|
|
height: 100%;
|
|
position: absolute;
|
|
top: 0; left: 0;
|
|
z-index: 1;
|
|
}
|
|
/* AVPlay Overlay: ueber dem iframe wenn Direct-Play aktiv */
|
|
#avplayer {
|
|
position: absolute;
|
|
top: 0; left: 0;
|
|
width: 100%; height: 100%;
|
|
z-index: 10;
|
|
display: none;
|
|
}
|
|
/* Setup-Bildschirm */
|
|
.setup {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100vh;
|
|
position: absolute;
|
|
top: 0; left: 0;
|
|
width: 100%;
|
|
z-index: 100;
|
|
}
|
|
.setup-inner {
|
|
text-align: center;
|
|
max-width: 600px;
|
|
padding: 2rem;
|
|
}
|
|
.setup h1 { font-size: 2rem; margin-bottom: 1rem; color: #64b5f6; }
|
|
.setup p { font-size: 1.2rem; color: #aaa; margin-bottom: 2rem; }
|
|
.setup input {
|
|
width: 100%; padding: 1rem; font-size: 1.5rem;
|
|
background: #1a1a1a; border: 2px solid #333; border-radius: 8px;
|
|
color: #fff; text-align: center; margin-bottom: 1rem;
|
|
}
|
|
.setup input:focus { border-color: #64b5f6; outline: none; }
|
|
.setup button {
|
|
padding: 1rem 3rem; font-size: 1.3rem;
|
|
background: #1976d2; color: #fff; border: none; border-radius: 8px; cursor: pointer;
|
|
}
|
|
.setup button:focus { outline: 3px solid #64b5f6; outline-offset: 4px; }
|
|
.hint { margin-top: 1.5rem; font-size: 0.9rem; color: #666; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Setup-Bildschirm (nur beim ersten Start sichtbar) -->
|
|
<div class="setup" id="setup">
|
|
<div class="setup-inner">
|
|
<h1>VideoKonverter TV</h1>
|
|
<p>Server-Adresse eingeben:</p>
|
|
<input type="text" id="serverUrl" placeholder="z.B. 192.168.155.12:8080"
|
|
data-focusable autofocus>
|
|
<br>
|
|
<button id="connectBtn" onclick="connect()" data-focusable>Verbinden</button>
|
|
<p class="hint">Die Adresse wird gespeichert und beim naechsten Start automatisch geladen.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Full-Screen iframe fuer Server-Content -->
|
|
<iframe id="app-iframe" style="display:none" allow="autoplay; fullscreen"></iframe>
|
|
|
|
<!-- AVPlay Container: rendert ueber den iframe wenn Direct-Play aktiv -->
|
|
<object id="avplayer" type="application/avplayer"
|
|
style="position:absolute;top:0;left:0;width:100%;height:100%;display:none;z-index:10">
|
|
</object>
|
|
|
|
<script>
|
|
/**
|
|
* VideoKonverter Tizen App v5.0
|
|
* Architektur: iframe (Server-UI) + AVPlay (Direct-Play im Parent-Frame)
|
|
* Kommunikation: postMessage zwischen iframe <-> Parent
|
|
*/
|
|
var STORAGE_KEY = "vk_server_url";
|
|
var _iframe = null;
|
|
var _serverUrl = "";
|
|
var _avplayActive = false;
|
|
var _playing = false;
|
|
var _duration = 0;
|
|
var _timeUpdateId = null;
|
|
|
|
// Unterstuetzte Codecs (Samsung Tizen 9.0+, AV1 HW-Decoder)
|
|
var SUPPORTED_VIDEO = ["h264", "hevc", "av1", "vp9"];
|
|
var SUPPORTED_AUDIO = ["aac", "opus", "ac3", "eac3", "flac", "mp3", "vorbis", "pcm"];
|
|
|
|
// === Setup ===
|
|
|
|
var savedUrl = localStorage.getItem(STORAGE_KEY);
|
|
if (savedUrl) {
|
|
startApp(savedUrl);
|
|
}
|
|
|
|
function connect() {
|
|
var input = document.getElementById("serverUrl");
|
|
var url = input.value.trim();
|
|
if (!url) return;
|
|
|
|
if (url.indexOf("://") === -1) url = "http://" + url;
|
|
url = url.replace(/\/+$/, ""); // Trailing slashes entfernen
|
|
|
|
localStorage.setItem(STORAGE_KEY, url);
|
|
startApp(url);
|
|
}
|
|
|
|
function startApp(serverUrl) {
|
|
_serverUrl = serverUrl;
|
|
|
|
// Setup ausblenden
|
|
document.getElementById("setup").style.display = "none";
|
|
|
|
// iframe erstellen und Server-URL laden
|
|
_iframe = document.getElementById("app-iframe");
|
|
_iframe.style.display = "block";
|
|
_iframe.src = serverUrl + "/tv/";
|
|
|
|
console.info("[TizenApp] iframe laedt: " + serverUrl + "/tv/");
|
|
}
|
|
|
|
// === postMessage Handler ===
|
|
|
|
window.addEventListener("message", function(event) {
|
|
var data = event.data;
|
|
if (!data || !data.type) return;
|
|
|
|
// Nur Nachrichten vom iframe akzeptieren
|
|
if (event.source !== _iframe.contentWindow) return;
|
|
|
|
switch (data.type) {
|
|
case "vknative_probe":
|
|
// iframe fragt ob VKNative verfuegbar ist
|
|
_sendToIframe({
|
|
type: "vknative_ready",
|
|
platform: "tizen",
|
|
videoCodecs: SUPPORTED_VIDEO,
|
|
audioCodecs: SUPPORTED_AUDIO,
|
|
});
|
|
break;
|
|
|
|
case "vknative_call":
|
|
_handleCall(data);
|
|
break;
|
|
}
|
|
});
|
|
|
|
function _sendToIframe(msg) {
|
|
if (_iframe && _iframe.contentWindow) {
|
|
_iframe.contentWindow.postMessage(msg, "*");
|
|
}
|
|
}
|
|
|
|
function _sendEvent(event, detail) {
|
|
_sendToIframe({
|
|
type: "vknative_event",
|
|
event: event,
|
|
detail: detail || {},
|
|
});
|
|
}
|
|
|
|
// === AVPlay Controller ===
|
|
|
|
function _handleCall(data) {
|
|
var method = data.method;
|
|
var args = data.args || [];
|
|
|
|
switch (method) {
|
|
case "play":
|
|
_avplay_play(args[0], args[1], args[2]);
|
|
break;
|
|
case "stop":
|
|
_avplay_stop();
|
|
break;
|
|
case "togglePlay":
|
|
_avplay_togglePlay();
|
|
break;
|
|
case "pause":
|
|
_avplay_pause();
|
|
break;
|
|
case "resume":
|
|
_avplay_resume();
|
|
break;
|
|
case "seek":
|
|
_avplay_seek(args[0]);
|
|
break;
|
|
case "setPlaybackSpeed":
|
|
_avplay_setSpeed(args[0]);
|
|
break;
|
|
default:
|
|
console.warn("[TizenApp] Unbekannter VKNative-Aufruf:", method);
|
|
}
|
|
}
|
|
|
|
function _avplay_play(url, videoInfo, opts) {
|
|
opts = opts || {};
|
|
var seekMs = opts.seekMs || 0;
|
|
|
|
// Relative URL -> Absolute URL
|
|
var fullUrl = url;
|
|
if (url.indexOf("://") === -1) {
|
|
fullUrl = _serverUrl + url;
|
|
}
|
|
|
|
try {
|
|
// Vorherige Session bereinigen
|
|
_avplay_stop();
|
|
|
|
// AVPlay-Display einblenden (ueber iframe)
|
|
var avEl = document.getElementById("avplayer");
|
|
if (avEl) avEl.style.display = "block";
|
|
|
|
// iframe deaktivieren (keine Events abfangen)
|
|
if (_iframe) {
|
|
_iframe.style.pointerEvents = "none";
|
|
_iframe.style.opacity = "0";
|
|
}
|
|
|
|
// AVPlay oeffnen
|
|
console.info("[TizenApp] AVPlay oeffne: " + fullUrl);
|
|
webapis.avplay.open(fullUrl);
|
|
webapis.avplay.setDisplayRect(0, 0, window.innerWidth, window.innerHeight);
|
|
|
|
// Event-Listener
|
|
webapis.avplay.setListener({
|
|
onbufferingstart: function() {
|
|
_sendEvent("buffering", { buffering: true });
|
|
},
|
|
onbufferingcomplete: function() {
|
|
_sendEvent("buffering", { buffering: false });
|
|
},
|
|
oncurrentplaytime: function(ms) {
|
|
_sendEvent("timeupdate", { ms: ms });
|
|
},
|
|
onstreamcompleted: function() {
|
|
_playing = false;
|
|
_sendEvent("playstatechanged", { playing: false });
|
|
_sendEvent("complete");
|
|
},
|
|
onerror: function(eventType) {
|
|
console.error("[TizenApp] AVPlay Fehler:", eventType);
|
|
_playing = false;
|
|
_sendEvent("error", { msg: String(eventType) });
|
|
},
|
|
onevent: function(eventType, eventData) {
|
|
console.debug("[TizenApp] AVPlay Event:", eventType, eventData);
|
|
},
|
|
onsubtitlechange: function() {},
|
|
});
|
|
|
|
// Hilfsfunktion: Wiedergabe starten
|
|
function _startPlayback() {
|
|
try {
|
|
webapis.avplay.play();
|
|
_playing = true;
|
|
_avplayActive = true;
|
|
_startTimeUpdates();
|
|
_sendEvent("playstatechanged", { playing: true });
|
|
_sendEvent("ready");
|
|
console.info("[TizenApp] AVPlay Wiedergabe gestartet");
|
|
} catch (e) {
|
|
console.error("[TizenApp] play() Fehler:", e);
|
|
_playing = false;
|
|
_sendEvent("error", { msg: e.message || String(e) });
|
|
}
|
|
}
|
|
|
|
// Async vorbereiten
|
|
console.info("[TizenApp] AVPlay prepareAsync...");
|
|
webapis.avplay.prepareAsync(
|
|
function() {
|
|
try {
|
|
_duration = webapis.avplay.getDuration();
|
|
} catch (e) {
|
|
_duration = 0;
|
|
}
|
|
console.info("[TizenApp] AVPlay bereit, Dauer: " + _duration + "ms");
|
|
_sendEvent("duration", { ms: _duration });
|
|
|
|
if (seekMs > 0) {
|
|
try {
|
|
webapis.avplay.seekTo(seekMs,
|
|
function() {
|
|
console.info("[TizenApp] Seek zu " + seekMs + "ms");
|
|
_startPlayback();
|
|
},
|
|
function(e) {
|
|
console.warn("[TizenApp] Seek fehlgeschlagen:", e);
|
|
_startPlayback();
|
|
}
|
|
);
|
|
} catch (e) {
|
|
console.warn("[TizenApp] seekTo Exception:", e);
|
|
_startPlayback();
|
|
}
|
|
} else {
|
|
_startPlayback();
|
|
}
|
|
},
|
|
function(error) {
|
|
console.error("[TizenApp] prepareAsync fehlgeschlagen:", error);
|
|
_sendEvent("error", { msg: String(error) });
|
|
}
|
|
);
|
|
|
|
} catch (e) {
|
|
console.error("[TizenApp] AVPlay Start-Fehler:", e);
|
|
_sendEvent("error", { msg: e.message || String(e) });
|
|
}
|
|
}
|
|
|
|
function _avplay_stop() {
|
|
_stopTimeUpdates();
|
|
_playing = false;
|
|
_avplayActive = false;
|
|
try {
|
|
var state = webapis.avplay.getState();
|
|
if (state !== "IDLE" && state !== "NONE") {
|
|
webapis.avplay.stop();
|
|
}
|
|
webapis.avplay.close();
|
|
} catch (e) { /* ignorieren */ }
|
|
|
|
var avEl = document.getElementById("avplayer");
|
|
if (avEl) avEl.style.display = "none";
|
|
|
|
// iframe wieder aktivieren
|
|
if (_iframe) {
|
|
_iframe.style.pointerEvents = "auto";
|
|
_iframe.style.opacity = "1";
|
|
}
|
|
}
|
|
|
|
function _avplay_togglePlay() {
|
|
try {
|
|
var state = webapis.avplay.getState();
|
|
if (state === "PLAYING") {
|
|
webapis.avplay.pause();
|
|
_playing = false;
|
|
_sendEvent("playstatechanged", { playing: false });
|
|
} else if (state === "PAUSED" || state === "READY") {
|
|
webapis.avplay.play();
|
|
_playing = true;
|
|
_sendEvent("playstatechanged", { playing: true });
|
|
}
|
|
} catch (e) {
|
|
console.error("[TizenApp] togglePlay Fehler:", e);
|
|
}
|
|
}
|
|
|
|
function _avplay_pause() {
|
|
try {
|
|
if (_playing) {
|
|
webapis.avplay.pause();
|
|
_playing = false;
|
|
_sendEvent("playstatechanged", { playing: false });
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
function _avplay_resume() {
|
|
try {
|
|
webapis.avplay.play();
|
|
_playing = true;
|
|
_sendEvent("playstatechanged", { playing: true });
|
|
} catch (e) {}
|
|
}
|
|
|
|
function _avplay_seek(positionMs) {
|
|
try {
|
|
webapis.avplay.seekTo(
|
|
Math.max(0, Math.floor(positionMs)),
|
|
function() { console.debug("[TizenApp] Seek OK: " + positionMs + "ms"); },
|
|
function(e) { console.warn("[TizenApp] Seek Fehler:", e); }
|
|
);
|
|
} catch (e) {}
|
|
}
|
|
|
|
function _avplay_setSpeed(speed) {
|
|
try {
|
|
webapis.avplay.setSpeed(speed);
|
|
} catch (e) {}
|
|
}
|
|
|
|
// Periodische Zeit-Updates (als Backup fuer oncurrentplaytime)
|
|
function _startTimeUpdates() {
|
|
_stopTimeUpdates();
|
|
_timeUpdateId = setInterval(function() {
|
|
if (_playing) {
|
|
try {
|
|
var ms = webapis.avplay.getCurrentTime();
|
|
_sendEvent("timeupdate", { ms: ms });
|
|
} catch (e) {}
|
|
}
|
|
}, 500);
|
|
}
|
|
|
|
function _stopTimeUpdates() {
|
|
if (_timeUpdateId) {
|
|
clearInterval(_timeUpdateId);
|
|
_timeUpdateId = null;
|
|
}
|
|
}
|
|
|
|
// === Tastatur-Handling ===
|
|
|
|
document.addEventListener("keydown", function(e) {
|
|
// Samsung Remote: Return/Back = 10009
|
|
if (e.keyCode === 10009) {
|
|
if (_avplayActive) {
|
|
// AVPlay aktiv -> stoppen, zurueck zum iframe
|
|
_avplay_stop();
|
|
_sendEvent("stopped");
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
|
|
// Setup-Bildschirm sichtbar -> App beenden
|
|
if (document.getElementById("setup").style.display !== "none") {
|
|
try { tizen.application.getCurrentApplication().exit(); } catch (ex) {}
|
|
return;
|
|
}
|
|
|
|
// iframe sichtbar -> History-Back im iframe
|
|
// (wird vom iframe selbst gehandelt via keydown event)
|
|
}
|
|
});
|
|
|
|
// Enter-Taste zum Verbinden (Setup-Bildschirm)
|
|
document.getElementById("serverUrl").addEventListener("keydown", function(e) {
|
|
if (e.keyCode === 13) connect();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|