docker.videokonverter/tizen-app/index.html
data 8302ff953a feat: VideoKonverter v5.3 - Android APK Fix, Tizen HLS-Surround, Native Player Verbesserungen
Android-App v1.2.0:
- Fix: 404-Fehler durch doppelten /tv/tv/ Pfad (URL-Bereinigung in SetupActivity)
- Fix: Kein Ton - AudioAttributes (AUDIO_CONTENT_TYPE_MOVIE + handleAudioFocus)
- Neu: ExoPlayer HLS-Support (playHLS) fuer DTS/TrueHD-Audio Fallback
- Neu: Back-Taste auf Root-Seite -> zurueck zum Setup (Server aendern)
- VKWebViewClient: playHLS in JS-Bridge exponiert

Tizen-App:
- Fix: Tonausfaelle bei Opus 6ch (Akte X) - canDirectPlay blockt Opus >2ch
- Neu: AVPlay HLS-Fallback (playHLS) mit AAC 5.1 Surround-Erhalt
- Neu: Buffer-Konfiguration (setBufferingParam) fuer stabilere Wiedergabe
- VKNative-Bridge v2.0: playHLS in beiden Modi (postMessage + Direct AVPlay)

Player:
- Native-HLS Default Sound auf "surround" (AVPlay/ExoPlayer koennen 5.1)
- PWA Direct-Play, Template-Fixes, UX-Verbesserungen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:18:07 +01:00

664 lines
25 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"];
// === Media-Keys registrieren (Tizen erfordert explizite Registrierung) ===
try {
var keysToRegister = [
"MediaPlayPause", "MediaPlay", "MediaPause", "MediaStop",
"MediaFastForward", "MediaRewind", "MediaTrackPrevious", "MediaTrackNext",
"ColorF0Red", "ColorF1Green", "ColorF2Yellow", "ColorF3Blue"
];
keysToRegister.forEach(function(key) {
try { tizen.tvinputdevice.registerKey(key); } catch (e) {}
});
console.info("[TizenApp] Media-Keys registriert: " + keysToRegister.join(", "));
} catch (e) {
console.warn("[TizenApp] tvinputdevice nicht verfuegbar:", e);
}
// === 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 "playHLS":
_avplay_playHLS(args[0], args[1]);
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);
// Buffer-Konfiguration fuer stabilere Wiedergabe
try {
// Initial-Buffer: 8 Sekunden vorpuffern bevor Wiedergabe startet
webapis.avplay.setBufferingParam("PLAYER_BUFFER_FOR_PLAY", "PLAYER_BUFFER_SIZE_IN_SECOND", 8);
// Resume-Buffer: 5 Sekunden nach Unterbrechung puffern
webapis.avplay.setBufferingParam("PLAYER_BUFFER_FOR_RESUME", "PLAYER_BUFFER_SIZE_IN_SECOND", 5);
console.info("[TizenApp] Buffer-Params gesetzt: Play=8s, Resume=5s");
} catch (e) {
console.debug("[TizenApp] setBufferingParam nicht moeglich:", e.message || e);
}
// 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) });
}
}
/**
* HLS-Stream ueber AVPlay abspielen (Fallback fuer Opus-Surround).
* Server transkodiert Audio zu AAC 5.1, AVPlay spielt HLS nativ.
*/
function _avplay_playHLS(playlistUrl, opts) {
opts = opts || {};
// Relative URL -> Absolute URL
var fullUrl = playlistUrl;
if (playlistUrl.indexOf("://") === -1) {
fullUrl = _serverUrl + playlistUrl;
}
try {
// Vorherige Session bereinigen
_avplay_stop();
// AVPlay-Display einblenden
var avEl = document.getElementById("avplayer");
if (avEl) avEl.style.display = "block";
// iframe deaktivieren
if (_iframe) {
_iframe.style.pointerEvents = "none";
_iframe.style.opacity = "0";
}
// AVPlay mit HLS-URL oeffnen
console.info("[TizenApp] AVPlay HLS oeffne: " + fullUrl);
webapis.avplay.open(fullUrl);
webapis.avplay.setDisplayRect(0, 0, window.innerWidth, window.innerHeight);
// HLS-Streaming: Groesserer Buffer fuer stabilen Surround-Sound
try {
webapis.avplay.setBufferingParam("PLAYER_BUFFER_FOR_PLAY", "PLAYER_BUFFER_SIZE_IN_SECOND", 10);
webapis.avplay.setBufferingParam("PLAYER_BUFFER_FOR_RESUME", "PLAYER_BUFFER_SIZE_IN_SECOND", 6);
console.info("[TizenApp] HLS Buffer-Params gesetzt: Play=10s, Resume=6s");
} catch (e) {
console.debug("[TizenApp] HLS setBufferingParam:", e.message || e);
}
// 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 HLS Fehler:", eventType);
_playing = false;
_sendEvent("error", { msg: String(eventType) });
},
onevent: function(eventType, eventData) {
console.debug("[TizenApp] AVPlay HLS Event:", eventType, eventData);
},
onsubtitlechange: function() {},
});
// Async vorbereiten und abspielen
console.info("[TizenApp] AVPlay HLS prepareAsync...");
webapis.avplay.prepareAsync(
function() {
try {
_duration = webapis.avplay.getDuration();
} catch (e) {
_duration = 0;
}
console.info("[TizenApp] AVPlay HLS bereit, Dauer: " + _duration + "ms");
_sendEvent("duration", { ms: _duration });
try {
webapis.avplay.play();
_playing = true;
_avplayActive = true;
_startTimeUpdates();
_sendEvent("playstatechanged", { playing: true });
_sendEvent("ready");
console.info("[TizenApp] AVPlay HLS Wiedergabe gestartet (Surround)");
} catch (e) {
console.error("[TizenApp] AVPlay HLS play() Fehler:", e);
_playing = false;
_sendEvent("error", { msg: e.message || String(e) });
}
},
function(error) {
console.error("[TizenApp] AVPlay HLS prepareAsync fehlgeschlagen:", error);
_sendEvent("error", { msg: String(error) });
}
);
} catch (e) {
console.error("[TizenApp] AVPlay HLS 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 ===
// Media-Key-Codes die bei AVPlay direkt behandelt oder an iframe weitergeleitet werden
var MEDIA_KEYCODES = {
415: "play", 19: "pause", 413: "stop",
417: "fastforward", 412: "rewind",
10252: "playpause", // MediaPlayPause
403: "colorred", 404: "colorgreen", 405: "coloryellow", 406: "colorblue"
};
// Tasten die bei aktivem AVPlay an den iframe weitergeleitet werden
// (fuer Player-Controls: Seek, Menue, Debug-Info etc.)
var FORWARD_KEYCODES = {
13: true, // Enter
37: true, // ArrowLeft
38: true, // ArrowUp
39: true, // ArrowRight
40: true, // ArrowDown
27: true, // Escape
8: true, // Backspace
};
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 -> Key an iframe weiterleiten
// (FocusManager behandelt 10009 als Escape -> history.back())
if (_iframe && _iframe.contentWindow) {
_sendToIframe({
type: "vknative_keyevent",
keyCode: e.keyCode
});
}
e.preventDefault();
return;
}
// Media-Keys behandeln
if (e.keyCode in MEDIA_KEYCODES) {
if (_avplayActive) {
// AVPlay aktiv -> direkt steuern
var action = MEDIA_KEYCODES[e.keyCode];
if (action === "play" || action === "playpause") {
if (!_playing) _avplay_resume(); else _avplay_pause();
} else if (action === "pause") {
_avplay_pause();
} else if (action === "stop") {
_avplay_stop();
_sendEvent("stopped");
} else if (action === "fastforward") {
// +10 Sekunden
try {
var cur = webapis.avplay.getCurrentTime();
_avplay_seek(cur + 10000);
} catch (ex) {}
} else if (action === "rewind") {
// -10 Sekunden
try {
var cur2 = webapis.avplay.getCurrentTime();
_avplay_seek(Math.max(0, cur2 - 10000));
} catch (ex) {}
}
e.preventDefault();
return;
}
// AVPlay nicht aktiv -> Key-Event an iframe weiterleiten
if (_iframe && _iframe.contentWindow) {
_sendToIframe({
type: "vknative_keyevent",
keyCode: e.keyCode
});
e.preventDefault();
}
return;
}
// Pfeiltasten, Enter, Escape immer an iframe weiterleiten
// (iframe bekommt auf Tizen keinen eigenen Keyboard-Focus)
if ((e.keyCode in FORWARD_KEYCODES) && _iframe && _iframe.contentWindow) {
_sendToIframe({
type: "vknative_keyevent",
keyCode: e.keyCode
});
e.preventDefault();
}
});
// Enter-Taste zum Verbinden (Setup-Bildschirm)
document.getElementById("serverUrl").addEventListener("keydown", function(e) {
if (e.keyCode === 13) connect();
});
</script>
</body>
</html>