docker.videokonverter/video-konverter/app/static/tv/js/avplay-bridge.js
data 93983cf6ee fix: Tizen-App iframe + Cookie-Fix für Cross-Origin
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>
2026-03-07 08:36:13 +01:00

340 lines
11 KiB
JavaScript

/**
* VideoKonverter TV - AVPlay Bridge v1.0
* Abstraktionsschicht fuer Samsung AVPlay API (Tizen WebApps).
* Ermoeglicht Direct-Play von MKV/WebM/MP4 mit Hardware-Decodern.
*
* AVPlay kann: H.264, HEVC, AV1, VP9, EAC3, AC3, AAC, Opus
* AVPlay kann NICHT: DTS (seit Samsung 2022 entfernt)
*
* Wird nur geladen wenn Tizen-Umgebung erkannt wird.
*/
const AVPlayBridge = {
available: false,
_playing: false,
_duration: 0,
_listener: null,
_displayEl: null, // <object> Element fuer AVPlay-Rendering
_timeUpdateId: null, // Interval fuer periodische Zeit-Updates
/**
* Initialisierung: Prueft ob AVPlay API verfuegbar ist
* @returns {boolean} true wenn AVPlay nutzbar
*/
init() {
try {
this.available = typeof webapis !== "undefined"
&& typeof webapis.avplay !== "undefined";
} catch (e) {
this.available = false;
}
if (this.available) {
console.info("[AVPlay] API verfuegbar");
}
return this.available;
},
/**
* Prueft ob ein Video direkt abgespielt werden kann (ohne Transcoding)
* @param {Object} videoInfo - Video-Infos vom Server
* @returns {boolean} true wenn Direct-Play moeglich
*/
canPlay(videoInfo) {
if (!this.available) return false;
// Video-Codec pruefen
const vc = (videoInfo.video_codec_normalized || "").toLowerCase();
const supportedVideo = ["h264", "hevc", "av1", "vp9"];
if (!supportedVideo.includes(vc)) {
console.info(`[AVPlay] Video-Codec '${vc}' nicht unterstuetzt`);
return false;
}
// Container pruefen
const container = (videoInfo.container || "").toLowerCase();
const supportedContainers = ["mkv", "matroska", "mp4", "webm", "avi", "ts"];
if (container && !supportedContainers.some(c => container.includes(c))) {
console.info(`[AVPlay] Container '${container}' nicht unterstuetzt`);
return false;
}
// Audio-Codecs pruefen - DTS ist nicht unterstuetzt
const audioCodecs = videoInfo.audio_codecs || [];
const unsupported = ["dts", "dca", "dts_hd", "dts-hd", "truehd"];
const hasUnsupported = audioCodecs.some(
ac => unsupported.includes(ac.toLowerCase())
);
if (hasUnsupported) {
console.info("[AVPlay] DTS-Audio erkannt -> kein Direct-Play");
return false;
}
console.info(`[AVPlay] Direct-Play moeglich: ${vc}/${container}`);
return true;
},
/**
* Video abspielen via AVPlay
* @param {string} url - Direct-Stream-URL
* @param {Object} opts - {seekMs, onTimeUpdate, onComplete, onError, onBuffering}
*/
play(url, opts = {}) {
if (!this.available) return false;
try {
// Vorherige Session bereinigen
this.stop();
// Display-Element setzen
this._displayEl = document.getElementById("avplayer");
if (this._displayEl) {
this._displayEl.style.display = "block";
}
// AVPlay oeffnen
webapis.avplay.open(url);
// Display-Bereich setzen (Vollbild)
webapis.avplay.setDisplayRect(
0, 0, window.innerWidth, window.innerHeight
);
// Event-Listener registrieren
this._listener = opts;
webapis.avplay.setListener({
onbufferingstart: () => {
console.debug("[AVPlay] Buffering gestartet");
if (this._listener && this._listener.onBuffering) {
this._listener.onBuffering(true);
}
},
onbufferingcomplete: () => {
console.debug("[AVPlay] Buffering abgeschlossen");
if (this._listener && this._listener.onBuffering) {
this._listener.onBuffering(false);
}
},
oncurrentplaytime: (ms) => {
// Wird von AVPlay periodisch aufgerufen
if (this._listener && this._listener.onTimeUpdate) {
this._listener.onTimeUpdate(ms);
}
},
onstreamcompleted: () => {
console.info("[AVPlay] Wiedergabe abgeschlossen");
this._playing = false;
if (this._listener && this._listener.onComplete) {
this._listener.onComplete();
}
},
onerror: (eventType) => {
console.error("[AVPlay] Fehler:", eventType);
this._playing = false;
if (this._listener && this._listener.onError) {
this._listener.onError(eventType);
}
},
onevent: (eventType, eventData) => {
console.debug("[AVPlay] Event:", eventType, eventData);
},
onsubtitlechange: (duration, text, dataSize, jsonData) => {
// Untertitel-Events (optional)
console.debug("[AVPlay] Subtitle:", text);
},
});
// Async vorbereiten und starten
webapis.avplay.prepareAsync(
() => {
// Erfolg: Wiedergabe starten
this._duration = webapis.avplay.getDuration();
console.info(`[AVPlay] Bereit, Dauer: ${this._duration}ms`);
// Seeking vor dem Start
if (opts.seekMs && opts.seekMs > 0) {
webapis.avplay.seekTo(opts.seekMs,
() => {
console.info(`[AVPlay] Seek zu ${opts.seekMs}ms`);
webapis.avplay.play();
this._playing = true;
this._startTimeUpdates();
},
(e) => {
console.warn("[AVPlay] Seek fehlgeschlagen:", e);
webapis.avplay.play();
this._playing = true;
this._startTimeUpdates();
}
);
} else {
webapis.avplay.play();
this._playing = true;
this._startTimeUpdates();
}
// Buffering-Ende signalisieren
if (this._listener && this._listener.onBuffering) {
this._listener.onBuffering(false);
}
if (this._listener && this._listener.onReady) {
this._listener.onReady();
}
},
(error) => {
console.error("[AVPlay] Prepare fehlgeschlagen:", error);
if (this._listener && this._listener.onError) {
this._listener.onError(error);
}
}
);
return true;
} catch (e) {
console.error("[AVPlay] Fehler beim Starten:", e);
if (this._listener && this._listener.onError) {
this._listener.onError(e.message || e);
}
return false;
}
},
/**
* Pause/Resume umschalten
*/
togglePlay() {
if (!this.available) return;
try {
const state = webapis.avplay.getState();
if (state === "PLAYING") {
webapis.avplay.pause();
this._playing = false;
} else if (state === "PAUSED" || state === "READY") {
webapis.avplay.play();
this._playing = true;
}
} catch (e) {
console.error("[AVPlay] togglePlay Fehler:", e);
}
},
pause() {
if (!this.available) return;
try {
if (this._playing) {
webapis.avplay.pause();
this._playing = false;
}
} catch (e) {
console.error("[AVPlay] pause Fehler:", e);
}
},
resume() {
if (!this.available) return;
try {
webapis.avplay.play();
this._playing = true;
} catch (e) {
console.error("[AVPlay] resume Fehler:", e);
}
},
/**
* Seeking zu Position in Millisekunden
* @param {number} positionMs - Zielposition in ms
* @param {function} onSuccess - Callback bei Erfolg
* @param {function} onError - Callback bei Fehler
*/
seek(positionMs, onSuccess, onError) {
if (!this.available) return;
try {
webapis.avplay.seekTo(
Math.max(0, Math.floor(positionMs)),
() => {
console.debug(`[AVPlay] Seek zu ${positionMs}ms`);
if (onSuccess) onSuccess();
},
(e) => {
console.warn("[AVPlay] Seek Fehler:", e);
if (onError) onError(e);
}
);
} catch (e) {
console.error("[AVPlay] seek Fehler:", e);
if (onError) onError(e);
}
},
/**
* Wiedergabe stoppen und AVPlay bereinigen
*/
stop() {
this._stopTimeUpdates();
this._playing = false;
try {
const state = webapis.avplay.getState();
if (state !== "IDLE" && state !== "NONE") {
webapis.avplay.stop();
}
webapis.avplay.close();
} catch (e) {
// Ignorieren wenn bereits gestoppt
}
if (this._displayEl) {
this._displayEl.style.display = "none";
}
},
/**
* Aktuelle Wiedergabeposition in Millisekunden
* @returns {number} Position in ms
*/
getCurrentTime() {
if (!this.available) return 0;
try {
return webapis.avplay.getCurrentTime();
} catch (e) {
return 0;
}
},
/**
* Gesamtdauer in Millisekunden
* @returns {number} Dauer in ms
*/
getDuration() {
if (!this.available) return 0;
try {
return this._duration || webapis.avplay.getDuration();
} catch (e) {
return 0;
}
},
/**
* Prueft ob gerade abgespielt wird
* @returns {boolean}
*/
isPlaying() {
return this._playing;
},
/**
* Periodische Zeit-Updates starten (fuer Progress-Bar)
*/
_startTimeUpdates() {
this._stopTimeUpdates();
this._timeUpdateId = setInterval(() => {
if (this._playing && this._listener && this._listener.onTimeUpdate) {
this._listener.onTimeUpdate(this.getCurrentTime());
}
}, 500);
},
_stopTimeUpdates() {
if (this._timeUpdateId) {
clearInterval(this._timeUpdateId);
this._timeUpdateId = null;
}
},
};