- 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>
1034 lines
42 KiB
HTML
1034 lines
42 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 {
|
|
position: absolute;
|
|
top: 0; left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 100;
|
|
background: #0f0f0f;
|
|
}
|
|
.setup-inner {
|
|
text-align: center;
|
|
max-width: 600px;
|
|
margin: 0 auto;
|
|
padding: 2rem;
|
|
position: relative;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
}
|
|
.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; }
|
|
.error-msg { color: #ef5350; font-size: 1.1rem; margin-top: 0.5rem; margin-bottom: 1rem; display: none; }
|
|
/* Verbindungs-Overlay */
|
|
#connecting-overlay {
|
|
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
|
background: #0f0f0f; z-index: 50;
|
|
display: none; align-items: center; justify-content: center;
|
|
flex-direction: column;
|
|
}
|
|
#connecting-overlay .spinner {
|
|
width: 48px; height: 48px; border: 4px solid #333;
|
|
border-top-color: #64b5f6; border-radius: 50%;
|
|
animation: spin 0.8s linear infinite; margin-bottom: 1rem;
|
|
}
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
#connecting-overlay .msg { color: #aaa; font-size: 1.2rem; }
|
|
#connecting-overlay .btn-cancel {
|
|
margin-top: 2rem; padding: 0.8rem 2rem; font-size: 1rem;
|
|
background: #c62828; color: #fff; border: none; border-radius: 8px; cursor: pointer;
|
|
}
|
|
/* Debug-Panel: Gruene Taste (Fernbedienung) toggled Sichtbarkeit */
|
|
#dbg {
|
|
position: fixed; bottom: 0; left: 0; right: 0; z-index: 99999;
|
|
background: rgba(0,0,0,0.92); color: #0f0; font-family: monospace;
|
|
font-size: 13px; padding: 6px 10px; max-height: 40vh; overflow-y: auto;
|
|
border-top: 2px solid #0a0; display: none;
|
|
}
|
|
#dbg .e { color: #f44; } #dbg .w { color: #ff0; } #dbg .i { color: #0f0; }
|
|
</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>
|
|
<p class="error-msg" id="errorMsg"></p>
|
|
<button id="connectBtn" data-focusable>Verbinden</button>
|
|
<p class="hint">Nur IP:Port eingeben (z.B. 192.168.155.12:8080).<br>
|
|
http:// und /tv/ werden automatisch ergaenzt.</p>
|
|
<p class="hint" style="margin-top:2rem;color:#555">v5.8.0 | Gruene Taste = Debug-Log</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Verbindungs-Overlay (zeigt Spinner waehrend Verbindungstest) -->
|
|
<div id="connecting-overlay">
|
|
<div class="spinner"></div>
|
|
<div class="msg" id="connecting-msg">Verbinde mit Server...</div>
|
|
<button class="btn-cancel" id="cancelConnect" data-focusable onclick="resetToSetup()">Abbrechen</button>
|
|
</div>
|
|
|
|
<!-- Debug-Panel (Gruene Taste toggled) -->
|
|
<div id="dbg"></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.8
|
|
* Architektur: iframe (Server-UI) + AVPlay (Direct-Play im Parent-Frame)
|
|
* Kommunikation: postMessage zwischen iframe <-> Parent
|
|
* Debug: Gruene Taste = Log-Panel, Remote-Logging an /api/tizen-log
|
|
*/
|
|
|
|
// === Debug-Logger: Faengt alle console-Ausgaben ab und zeigt sie im Panel ===
|
|
var _dbgLog = [];
|
|
var _dbgMax = 200;
|
|
var _dbgEl = document.getElementById("dbg");
|
|
var _dbgVisible = false;
|
|
function _log(level, msg) {
|
|
var ts = new Date().toTimeString().substr(0,8);
|
|
var entry = {t: ts, l: level, m: String(msg)};
|
|
_dbgLog.push(entry);
|
|
if (_dbgLog.length > _dbgMax) _dbgLog.shift();
|
|
if (_dbgVisible && _dbgEl) {
|
|
var cls = level === "E" ? "e" : level === "W" ? "w" : "i";
|
|
_dbgEl.innerHTML += '<div class="' + cls + '">[' + ts + '] ' + msg.replace(/</g,"<") + '</div>';
|
|
_dbgEl.scrollTop = _dbgEl.scrollHeight;
|
|
}
|
|
}
|
|
// Console abfangen
|
|
var _origLog = console.log, _origInfo = console.info, _origWarn = console.warn, _origErr = console.error;
|
|
console.log = function() { var m = Array.prototype.join.call(arguments, " "); _origLog.apply(console, arguments); _log("I", m); };
|
|
console.info = function() { var m = Array.prototype.join.call(arguments, " "); _origInfo.apply(console, arguments); _log("I", m); };
|
|
console.warn = function() { var m = Array.prototype.join.call(arguments, " "); _origWarn.apply(console, arguments); _log("W", m); };
|
|
console.error = function() { var m = Array.prototype.join.call(arguments, " "); _origErr.apply(console, arguments); _log("E", m); };
|
|
console.debug = function() { var m = Array.prototype.join.call(arguments, " "); _log("I", m); };
|
|
// Uncaught Errors abfangen
|
|
window.onerror = function(msg, src, line) { _log("E", "UNCAUGHT: " + msg + " @" + (src||"?") + ":" + line); };
|
|
|
|
function _dbgToggle() {
|
|
_dbgVisible = !_dbgVisible;
|
|
_dbgEl.style.display = _dbgVisible ? "block" : "none";
|
|
if (_dbgVisible) {
|
|
// Gesamten Log rendern
|
|
_dbgEl.innerHTML = "";
|
|
_dbgLog.forEach(function(e) {
|
|
var cls = e.l === "E" ? "e" : e.l === "W" ? "w" : "i";
|
|
_dbgEl.innerHTML += '<div class="' + cls + '">[' + e.t + '] ' + e.m.replace(/</g,"<") + '</div>';
|
|
});
|
|
_dbgEl.scrollTop = _dbgEl.scrollHeight;
|
|
}
|
|
}
|
|
|
|
// === Remote-Logging: Logs per XHR an Server senden ===
|
|
var _remoteQueue = [];
|
|
var _remoteTimer = null;
|
|
function _flushRemoteLogs() {
|
|
var url = _serverUrl || localStorage.getItem("vk_server_url");
|
|
if (!url || _remoteQueue.length === 0) return;
|
|
var payload = JSON.stringify({
|
|
entries: _remoteQueue.splice(0, 50),
|
|
userAgent: navigator.userAgent
|
|
});
|
|
try {
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open("POST", url + "/api/tizen-log", true);
|
|
xhr.setRequestHeader("Content-Type", "application/json");
|
|
xhr.send(payload);
|
|
} catch (e) { /* ignorieren */ }
|
|
}
|
|
// Alle 3 Sekunden gesammelte Logs senden
|
|
_remoteTimer = setInterval(_flushRemoteLogs, 3000);
|
|
// Remote-Queue fuellen (zusaetzlich zum Panel)
|
|
var _origLogFn = _log;
|
|
_log = function(level, msg) {
|
|
_origLogFn(level, msg);
|
|
_remoteQueue.push({l: level, m: String(msg).substr(0, 2000), t: new Date().toTimeString().substr(0,8)});
|
|
};
|
|
|
|
console.info("[TizenApp] v5.8.0 gestartet. Gruene Taste = Debug-Log. Remote-Logging aktiv.");
|
|
console.info("[TizenApp] localStorage=" + JSON.stringify(localStorage));
|
|
console.info("[TizenApp] userAgent=" + navigator.userAgent);
|
|
|
|
var STORAGE_KEY = "vk_server_url";
|
|
var _iframe = null;
|
|
var _serverUrl = "";
|
|
var _avplayActive = false;
|
|
var _playing = false;
|
|
var _duration = 0;
|
|
var _timeUpdateId = null;
|
|
|
|
// Debug-Shorthand (nutzt den Console-Override -> landet im Panel + Remote)
|
|
function _dbg(msg) {
|
|
console.info("[DBG] " + msg);
|
|
}
|
|
|
|
// 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 _connectTimeout = null;
|
|
var CONNECT_TIMEOUT_MS = 20000; // 20 Sekunden Timeout (TV-App braucht Zeit fuer CSS/JS/Fonts)
|
|
|
|
var savedUrl = localStorage.getItem(STORAGE_KEY);
|
|
_dbg("savedUrl=" + (savedUrl || "NONE"));
|
|
if (savedUrl) {
|
|
// Gespeicherte URL bereinigen (fuer Abwaertskompatibilitaet)
|
|
var cleanUrl = cleanServerUrl(savedUrl);
|
|
if (cleanUrl !== savedUrl) {
|
|
localStorage.setItem(STORAGE_KEY, cleanUrl);
|
|
}
|
|
_dbg("startApp: " + cleanUrl);
|
|
startApp(cleanUrl);
|
|
} else {
|
|
_dbg("setup sichtbar (kein savedUrl)");
|
|
}
|
|
|
|
/** URL bereinigen: Nur Schema + Host + Port behalten (konsistent mit Android-App) */
|
|
function cleanServerUrl(raw) {
|
|
var url = raw.trim();
|
|
if (!url) return url;
|
|
// Protokoll ergaenzen
|
|
if (url.indexOf("://") === -1) url = "http://" + url;
|
|
// Trailing slashes entfernen
|
|
url = url.replace(/\/+$/, "");
|
|
// /tv oder /tv/ am Ende entfernen (wird automatisch angehaengt)
|
|
url = url.replace(/\/tv\/?$/, "");
|
|
// Pfade entfernen: Nur Schema + Host + Port behalten
|
|
try {
|
|
var a = document.createElement("a");
|
|
a.href = url;
|
|
var port = a.port ? ":" + a.port : "";
|
|
url = a.protocol + "//" + a.hostname + port;
|
|
} catch (e) {}
|
|
// Port ergaenzen falls nicht vorhanden (Standard: 8080)
|
|
if (url.indexOf("://") !== -1) {
|
|
var afterProto = url.split("://")[1] || "";
|
|
if (afterProto.indexOf(":") === -1) {
|
|
url = url + ":8080";
|
|
}
|
|
}
|
|
return url;
|
|
}
|
|
|
|
function connect() {
|
|
var input = document.getElementById("serverUrl");
|
|
var url = input.value.trim();
|
|
if (!url) return;
|
|
|
|
// Fehlermeldung zuruecksetzen
|
|
_hideError();
|
|
|
|
// URL bereinigen (konsistent mit Android-App)
|
|
url = cleanServerUrl(url);
|
|
|
|
// URL speichern und direkt starten
|
|
// (Validierung ueber iframe-Timeout + vknative_probe statt XHR,
|
|
// da XHR von WGT-Apps durch CORS blockiert wird)
|
|
localStorage.setItem(STORAGE_KEY, url);
|
|
startApp(url);
|
|
}
|
|
|
|
var _appVerified = false; // Wird true wenn iframe die TV-App tatsaechlich geladen hat
|
|
|
|
function startApp(serverUrl) {
|
|
_serverUrl = serverUrl;
|
|
_appVerified = false;
|
|
|
|
// Setup ausblenden, Connecting anzeigen
|
|
document.getElementById("setup").style.display = "none";
|
|
_showConnecting("Lade " + serverUrl + "/tv/ ...");
|
|
|
|
// iframe erstellen und Server-URL laden
|
|
_iframe = document.getElementById("app-iframe");
|
|
_iframe.style.display = "block";
|
|
_iframe.src = serverUrl + "/tv/";
|
|
|
|
// iframe.onload: Seite hat geladen (auch Fehlerseiten)
|
|
// -> Connecting-Overlay ausblenden, iframe anzeigen
|
|
// Falls Fehlerseite: User sieht sie und kann mit Back zurueck
|
|
_iframe.onload = function() {
|
|
console.info("[TizenApp] iframe.onload gefeuert");
|
|
_markAppVerified();
|
|
};
|
|
|
|
// Timeout: Wenn iframe nach X Sekunden nicht mal onload feuert
|
|
// -> Netzwerk-Problem oder Server komplett down
|
|
_connectTimeout = setTimeout(function() {
|
|
if (!_appVerified) {
|
|
console.warn("[TizenApp] iframe Timeout - kein onload");
|
|
resetToSetup("Server nicht erreichbar. Bitte Adresse pruefen.");
|
|
}
|
|
}, CONNECT_TIMEOUT_MS);
|
|
|
|
_iframe.onerror = function() {
|
|
if (_connectTimeout) {
|
|
clearTimeout(_connectTimeout);
|
|
_connectTimeout = null;
|
|
}
|
|
resetToSetup("Seite konnte nicht geladen werden. Bitte Adresse pruefen.");
|
|
};
|
|
|
|
console.info("[TizenApp] iframe laedt: " + serverUrl + "/tv/");
|
|
}
|
|
|
|
/** Zurueck zum Setup-Bildschirm (URL aus localStorage loeschen) */
|
|
function resetToSetup(errorMsg) {
|
|
_dbg("resetToSetup: " + (errorMsg || "kein Fehler"));
|
|
// Timeout abbrechen falls aktiv
|
|
if (_connectTimeout) {
|
|
clearTimeout(_connectTimeout);
|
|
_connectTimeout = null;
|
|
}
|
|
|
|
// Status zuruecksetzen
|
|
localStorage.removeItem(STORAGE_KEY);
|
|
_serverUrl = "";
|
|
_appVerified = false;
|
|
|
|
// iframe stoppen und verstecken
|
|
_iframe = document.getElementById("app-iframe");
|
|
if (_iframe) {
|
|
_iframe.src = "about:blank";
|
|
_iframe.style.display = "none";
|
|
}
|
|
|
|
// Connecting-Overlay verstecken
|
|
_hideConnecting();
|
|
|
|
// Setup-Bildschirm anzeigen (KEIN flex! CSS nutzt position+transform fuer Centering)
|
|
var setupEl = document.getElementById("setup");
|
|
setupEl.style.display = ""; // CSS-Default wiederherstellen
|
|
|
|
// Fehlermeldung anzeigen
|
|
if (errorMsg) {
|
|
_showError(errorMsg);
|
|
}
|
|
|
|
// Input-Feld fokussieren (mit Delay fuer Tizen IME)
|
|
setTimeout(function() {
|
|
var inputEl = document.getElementById("serverUrl");
|
|
if (inputEl) inputEl.focus();
|
|
}, 300);
|
|
|
|
console.info("[TizenApp] Zurueck zum Setup-Bildschirm");
|
|
}
|
|
|
|
function _showError(msg) {
|
|
var el = document.getElementById("errorMsg");
|
|
el.textContent = msg;
|
|
el.style.display = "block";
|
|
}
|
|
|
|
function _hideError() {
|
|
var el = document.getElementById("errorMsg");
|
|
el.textContent = "";
|
|
el.style.display = "none";
|
|
}
|
|
|
|
function _showConnecting(msg) {
|
|
var overlay = document.getElementById("connecting-overlay");
|
|
document.getElementById("connecting-msg").textContent = msg || "Verbinde...";
|
|
overlay.style.display = "flex";
|
|
}
|
|
|
|
function _hideConnecting() {
|
|
document.getElementById("connecting-overlay").style.display = "none";
|
|
}
|
|
|
|
/** App als verifiziert markieren (Server erreichbar, Seite geladen) */
|
|
function _markAppVerified() {
|
|
if (_appVerified) return; // Nur einmal
|
|
_appVerified = true;
|
|
_dbg("verifiziert! iframe geladen");
|
|
if (_connectTimeout) {
|
|
clearTimeout(_connectTimeout);
|
|
_connectTimeout = null;
|
|
}
|
|
_hideConnecting();
|
|
console.info("[TizenApp] App verifiziert - Server erreichbar");
|
|
}
|
|
|
|
// === postMessage Handler ===
|
|
|
|
window.addEventListener("message", function(event) {
|
|
var data = event.data;
|
|
if (!data || !data.type) return;
|
|
|
|
// Nur Nachrichten vom iframe akzeptieren (mit null-Check)
|
|
if (!_iframe || !_iframe.contentWindow || event.source !== _iframe.contentWindow) return;
|
|
|
|
switch (data.type) {
|
|
case "vk_app_loaded":
|
|
// TV-App (Login oder Home) hat geladen -> Server erreichbar!
|
|
_markAppVerified();
|
|
break;
|
|
|
|
case "vknative_probe":
|
|
// iframe fragt ob VKNative verfuegbar ist (Player geladen)
|
|
_markAppVerified();
|
|
_sendToIframe({
|
|
type: "vknative_ready",
|
|
platform: "tizen",
|
|
videoCodecs: SUPPORTED_VIDEO,
|
|
audioCodecs: SUPPORTED_AUDIO,
|
|
});
|
|
break;
|
|
|
|
case "vknative_call":
|
|
_handleCall(data);
|
|
break;
|
|
|
|
case "vknative_reset":
|
|
// Server fordert Reset an (z.B. bei Logout)
|
|
resetToSetup();
|
|
break;
|
|
|
|
case "vknative_get_stats":
|
|
// Debug-Overlay fragt AVPlay-Stats ab
|
|
var stats = { active: _avplayActive, playing: _playing };
|
|
if (_avplayActive) {
|
|
try { stats.state = webapis.avplay.getState(); } catch (ex) { stats.state = "?"; }
|
|
try { stats.time_ms = webapis.avplay.getCurrentTime(); } catch (ex) {}
|
|
stats.duration_ms = _duration || 0;
|
|
try {
|
|
var si = webapis.avplay.getCurrentStreamInfo();
|
|
stats.streams = [];
|
|
for (var s = 0; s < si.length; s++) {
|
|
stats.streams.push({ type: si[s].type, extra: si[s].extra_info });
|
|
}
|
|
} catch (ex) {}
|
|
}
|
|
_sendToIframe({ type: "vknative_stats", stats: stats });
|
|
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: Pointer deaktivieren (D-Pad kommt per postMessage)
|
|
// Hintergruende transparent machen damit AVPlay-Hardware-Layer durchscheint
|
|
// iframe z-index ueber avplayer (10) setzen damit Controls sichtbar bleiben
|
|
if (_iframe) {
|
|
_iframe.style.pointerEvents = "none";
|
|
_iframe.style.background = "transparent";
|
|
_iframe.style.zIndex = "20";
|
|
}
|
|
document.body.style.background = "transparent";
|
|
document.documentElement.style.background = "transparent";
|
|
|
|
// 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: Pointer deaktivieren + Hintergruende transparent
|
|
// iframe z-index ueber avplayer (10) setzen damit Controls sichtbar bleiben
|
|
if (_iframe) {
|
|
_iframe.style.pointerEvents = "none";
|
|
_iframe.style.background = "transparent";
|
|
_iframe.style.zIndex = "20";
|
|
}
|
|
document.body.style.background = "transparent";
|
|
document.documentElement.style.background = "transparent";
|
|
|
|
// 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 + Hintergrund/z-index wiederherstellen
|
|
if (_iframe) {
|
|
_iframe.style.pointerEvents = "auto";
|
|
_iframe.style.background = "";
|
|
_iframe.style.zIndex = "1";
|
|
}
|
|
document.body.style.background = "#0f0f0f";
|
|
document.documentElement.style.background = "";
|
|
}
|
|
|
|
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) {
|
|
// Gruene Taste = Debug-Panel toggled (keyCode 404)
|
|
if (e.keyCode === 404) {
|
|
_dbgToggle();
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
|
|
// Samsung Remote: Return/Back = 10009
|
|
if (e.keyCode === 10009) {
|
|
if (_avplayActive) {
|
|
// AVPlay aktiv -> stoppen, zurueck zum iframe
|
|
_avplay_stop();
|
|
_sendEvent("stopped");
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
|
|
// Connecting-Overlay sichtbar -> Reset zum Setup
|
|
if (document.getElementById("connecting-overlay").style.display === "flex") {
|
|
resetToSetup();
|
|
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) {}
|
|
} else if (action.indexOf("color") === 0) {
|
|
// Farbtasten an iframe weiterleiten (Audio/Subs/Quality/Debug)
|
|
if (_iframe && _iframe.contentWindow) {
|
|
_sendToIframe({
|
|
type: "vknative_keyevent",
|
|
keyCode: e.keyCode
|
|
});
|
|
}
|
|
}
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
|
|
// AVPlay nicht aktiv -> Key-Event an iframe weiterleiten (nur wenn App verifiziert)
|
|
if (_appVerified && _iframe && _iframe.contentWindow) {
|
|
_sendToIframe({
|
|
type: "vknative_keyevent",
|
|
keyCode: e.keyCode
|
|
});
|
|
e.preventDefault();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Pfeiltasten, Enter, Escape an iframe weiterleiten
|
|
// NUR wenn App verifiziert (iframe hat TV-App geladen)
|
|
// Sonst werden D-Pad-Tasten auf Setup-/Connecting-Screen geschluckt
|
|
if (_appVerified && (e.keyCode in FORWARD_KEYCODES) && _iframe && _iframe.contentWindow) {
|
|
_sendToIframe({
|
|
type: "vknative_keyevent",
|
|
keyCode: e.keyCode
|
|
});
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
|
|
// Enter-Taste auf Input-Feld:
|
|
// - Wenn leer: NICHT preventDefault -> Tizen IME (Tastatur) oeffnet sich
|
|
// - Wenn Text vorhanden: Verbinden ausloesen
|
|
document.getElementById("serverUrl").addEventListener("keydown", function(e) {
|
|
if (e.keyCode === 13) {
|
|
var val = this.value.trim();
|
|
if (val) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
connect();
|
|
}
|
|
// Sonst: Default-Verhalten (IME oeffnen auf Tizen)
|
|
}
|
|
});
|
|
|
|
// Button-Click per JS (statt onclick-Attribut, zuverlaessiger auf Tizen)
|
|
document.getElementById("connectBtn").addEventListener("click", function() {
|
|
connect();
|
|
});
|
|
// D-Pad Enter auf Button
|
|
document.getElementById("connectBtn").addEventListener("keydown", function(e) {
|
|
if (e.keyCode === 13) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
connect();
|
|
}
|
|
});
|
|
|
|
// Tizen IME: autofocus oeffnet die Tastatur nicht zuverlaessig
|
|
// -> Expliziter focus() mit Delay nach Rendering
|
|
(function() {
|
|
var setupEl = document.getElementById("setup");
|
|
var inputEl = document.getElementById("serverUrl");
|
|
if (setupEl && inputEl && setupEl.style.display !== "none") {
|
|
setTimeout(function() {
|
|
inputEl.focus();
|
|
// Samsung Tizen: Zweiter focus()-Versuch nach IME-Init
|
|
setTimeout(function() { inputEl.focus(); }, 500);
|
|
}, 300);
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|