feat: VideoKonverter v5.5 - Tizen Remote-Logging, Login D-Pad, Cookie-Fix
Tizen-App v5.5.0: - Remote-Logging: Console-Override + XHR an /api/tizen-log alle 3s - Debug-Panel: Gruene Taste toggled scrollbares Log-Panel (unten) - window.onerror Handler fuer uncaught Errors - Alle v5.4.2 Features erhalten (Connecting-Overlay, Timeout, IME-Fixes) Server (tv_api.py): - POST/GET /api/tizen-log Endpunkte (DB-Tabelle tizen_logs) - Cookie SameSite-Fix: Tizen iframe bekommt kein SameSite (Lax blockiert) Login (login.html): - D-Pad Navigation per postMessage (vknative_keyevent) - ArrowUp/Down zwischen Feldern, Enter auf Button Sonstiges: - base.html: vk_app_loaded postMessage Signal - sw.js: Cache v14 -> v15 - Altes Docker-Export entfernt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8302ff953a
commit
00d8f6b982
8 changed files with 461 additions and 26 deletions
Binary file not shown.
Binary file not shown.
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<widget xmlns="http://www.w3.org/ns/widgets" xmlns:tizen="http://tizen.org/ns/widgets"
|
<widget xmlns="http://www.w3.org/ns/widgets" xmlns:tizen="http://tizen.org/ns/widgets"
|
||||||
id="http://data-it-solution.de/videokonverter" version="5.0.0" viewmodes="maximized">
|
id="http://data-it-solution.de/videokonverter" version="5.5.0" viewmodes="maximized">
|
||||||
|
|
||||||
<name>VideoKonverter</name>
|
<name>VideoKonverter</name>
|
||||||
<description>VideoKonverter TV-App - Serien und Filme streamen mit AVPlay Direct-Play</description>
|
<description>VideoKonverter TV-App - Serien und Filme streamen mit AVPlay Direct-Play</description>
|
||||||
|
|
@ -24,6 +24,9 @@
|
||||||
<!-- Netzwerk-Zugriff erlauben (lokales Netz) -->
|
<!-- Netzwerk-Zugriff erlauben (lokales Netz) -->
|
||||||
<access origin="*" subdomains="true"/>
|
<access origin="*" subdomains="true"/>
|
||||||
|
|
||||||
|
<!-- Navigation zu externen URLs erlauben (fuer iframe-Content) -->
|
||||||
|
<tizen:allow-navigation>*</tizen:allow-navigation>
|
||||||
|
|
||||||
<!-- TV-spezifische Einstellungen -->
|
<!-- TV-spezifische Einstellungen -->
|
||||||
<tizen:setting screen-orientation="landscape" context-menu="enable" background-support="disable"
|
<tizen:setting screen-orientation="landscape" context-menu="enable" background-support="disable"
|
||||||
encryption="disable" install-location="auto" hwkey-event="enable"/>
|
encryption="disable" install-location="auto" hwkey-event="enable"/>
|
||||||
|
|
|
||||||
|
|
@ -32,19 +32,21 @@
|
||||||
}
|
}
|
||||||
/* Setup-Bildschirm */
|
/* Setup-Bildschirm */
|
||||||
.setup {
|
.setup {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100vh;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0; left: 0;
|
top: 0; left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
background: #0f0f0f;
|
||||||
}
|
}
|
||||||
.setup-inner {
|
.setup-inner {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
|
position: relative;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
.setup h1 { font-size: 2rem; margin-bottom: 1rem; color: #64b5f6; }
|
.setup h1 { font-size: 2rem; margin-bottom: 1rem; color: #64b5f6; }
|
||||||
.setup p { font-size: 1.2rem; color: #aaa; margin-bottom: 2rem; }
|
.setup p { font-size: 1.2rem; color: #aaa; margin-bottom: 2rem; }
|
||||||
|
|
@ -60,6 +62,33 @@
|
||||||
}
|
}
|
||||||
.setup button:focus { outline: 3px solid #64b5f6; outline-offset: 4px; }
|
.setup button:focus { outline: 3px solid #64b5f6; outline-offset: 4px; }
|
||||||
.hint { margin-top: 1.5rem; font-size: 0.9rem; color: #666; }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -70,12 +99,24 @@
|
||||||
<p>Server-Adresse eingeben:</p>
|
<p>Server-Adresse eingeben:</p>
|
||||||
<input type="text" id="serverUrl" placeholder="z.B. 192.168.155.12:8080"
|
<input type="text" id="serverUrl" placeholder="z.B. 192.168.155.12:8080"
|
||||||
data-focusable autofocus>
|
data-focusable autofocus>
|
||||||
<br>
|
<p class="error-msg" id="errorMsg"></p>
|
||||||
<button id="connectBtn" onclick="connect()" data-focusable>Verbinden</button>
|
<button id="connectBtn" data-focusable>Verbinden</button>
|
||||||
<p class="hint">Die Adresse wird gespeichert und beim naechsten Start automatisch geladen.</p>
|
<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.5.0 | Gruene Taste = Debug-Log</p>
|
||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Full-Screen iframe fuer Server-Content -->
|
||||||
<iframe id="app-iframe" style="display:none" allow="autoplay; fullscreen"></iframe>
|
<iframe id="app-iframe" style="display:none" allow="autoplay; fullscreen"></iframe>
|
||||||
|
|
||||||
|
|
@ -86,10 +127,82 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
/**
|
/**
|
||||||
* VideoKonverter Tizen App v5.0
|
* VideoKonverter Tizen App v5.5
|
||||||
* Architektur: iframe (Server-UI) + AVPlay (Direct-Play im Parent-Frame)
|
* Architektur: iframe (Server-UI) + AVPlay (Direct-Play im Parent-Frame)
|
||||||
* Kommunikation: postMessage zwischen iframe <-> Parent
|
* 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.5.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 STORAGE_KEY = "vk_server_url";
|
||||||
var _iframe = null;
|
var _iframe = null;
|
||||||
var _serverUrl = "";
|
var _serverUrl = "";
|
||||||
|
|
@ -98,6 +211,11 @@
|
||||||
var _duration = 0;
|
var _duration = 0;
|
||||||
var _timeUpdateId = null;
|
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)
|
// Unterstuetzte Codecs (Samsung Tizen 9.0+, AV1 HW-Decoder)
|
||||||
var SUPPORTED_VIDEO = ["h264", "hevc", "av1", "vp9"];
|
var SUPPORTED_VIDEO = ["h264", "hevc", "av1", "vp9"];
|
||||||
var SUPPORTED_AUDIO = ["aac", "opus", "ac3", "eac3", "flac", "mp3", "vorbis", "pcm"];
|
var SUPPORTED_AUDIO = ["aac", "opus", "ac3", "eac3", "flac", "mp3", "vorbis", "pcm"];
|
||||||
|
|
@ -119,9 +237,48 @@
|
||||||
|
|
||||||
// === Setup ===
|
// === 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);
|
var savedUrl = localStorage.getItem(STORAGE_KEY);
|
||||||
|
_dbg("savedUrl=" + (savedUrl || "NONE"));
|
||||||
if (savedUrl) {
|
if (savedUrl) {
|
||||||
startApp(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() {
|
function connect() {
|
||||||
|
|
@ -129,39 +286,157 @@
|
||||||
var url = input.value.trim();
|
var url = input.value.trim();
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
|
|
||||||
if (url.indexOf("://") === -1) url = "http://" + url;
|
// Fehlermeldung zuruecksetzen
|
||||||
url = url.replace(/\/+$/, ""); // Trailing slashes entfernen
|
_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);
|
localStorage.setItem(STORAGE_KEY, url);
|
||||||
startApp(url);
|
startApp(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _appVerified = false; // Wird true wenn iframe die TV-App tatsaechlich geladen hat
|
||||||
|
|
||||||
function startApp(serverUrl) {
|
function startApp(serverUrl) {
|
||||||
_serverUrl = serverUrl;
|
_serverUrl = serverUrl;
|
||||||
|
_appVerified = false;
|
||||||
|
|
||||||
// Setup ausblenden
|
// Setup ausblenden, Connecting anzeigen
|
||||||
document.getElementById("setup").style.display = "none";
|
document.getElementById("setup").style.display = "none";
|
||||||
|
_showConnecting("Lade " + serverUrl + "/tv/ ...");
|
||||||
|
|
||||||
// iframe erstellen und Server-URL laden
|
// iframe erstellen und Server-URL laden
|
||||||
_iframe = document.getElementById("app-iframe");
|
_iframe = document.getElementById("app-iframe");
|
||||||
_iframe.style.display = "block";
|
_iframe.style.display = "block";
|
||||||
_iframe.src = serverUrl + "/tv/";
|
_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/");
|
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 ===
|
// === postMessage Handler ===
|
||||||
|
|
||||||
window.addEventListener("message", function(event) {
|
window.addEventListener("message", function(event) {
|
||||||
var data = event.data;
|
var data = event.data;
|
||||||
if (!data || !data.type) return;
|
if (!data || !data.type) return;
|
||||||
|
|
||||||
// Nur Nachrichten vom iframe akzeptieren
|
// Nur Nachrichten vom iframe akzeptieren (mit null-Check)
|
||||||
if (event.source !== _iframe.contentWindow) return;
|
if (!_iframe || !_iframe.contentWindow || event.source !== _iframe.contentWindow) return;
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
|
case "vk_app_loaded":
|
||||||
|
// TV-App (Login oder Home) hat geladen -> Server erreichbar!
|
||||||
|
_markAppVerified();
|
||||||
|
break;
|
||||||
|
|
||||||
case "vknative_probe":
|
case "vknative_probe":
|
||||||
// iframe fragt ob VKNative verfuegbar ist
|
// iframe fragt ob VKNative verfuegbar ist (Player geladen)
|
||||||
|
_markAppVerified();
|
||||||
_sendToIframe({
|
_sendToIframe({
|
||||||
type: "vknative_ready",
|
type: "vknative_ready",
|
||||||
platform: "tizen",
|
platform: "tizen",
|
||||||
|
|
@ -173,6 +448,11 @@
|
||||||
case "vknative_call":
|
case "vknative_call":
|
||||||
_handleCall(data);
|
_handleCall(data);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "vknative_reset":
|
||||||
|
// Server fordert Reset an (z.B. bei Logout)
|
||||||
|
resetToSetup();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -576,6 +856,13 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("keydown", function(e) {
|
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
|
// Samsung Remote: Return/Back = 10009
|
||||||
if (e.keyCode === 10009) {
|
if (e.keyCode === 10009) {
|
||||||
if (_avplayActive) {
|
if (_avplayActive) {
|
||||||
|
|
@ -586,6 +873,13 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Connecting-Overlay sichtbar -> Reset zum Setup
|
||||||
|
if (document.getElementById("connecting-overlay").style.display === "flex") {
|
||||||
|
resetToSetup();
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Setup-Bildschirm sichtbar -> App beenden
|
// Setup-Bildschirm sichtbar -> App beenden
|
||||||
if (document.getElementById("setup").style.display !== "none") {
|
if (document.getElementById("setup").style.display !== "none") {
|
||||||
try { tizen.application.getCurrentApplication().exit(); } catch (ex) {}
|
try { tizen.application.getCurrentApplication().exit(); } catch (ex) {}
|
||||||
|
|
@ -633,8 +927,8 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// AVPlay nicht aktiv -> Key-Event an iframe weiterleiten
|
// AVPlay nicht aktiv -> Key-Event an iframe weiterleiten (nur wenn App verifiziert)
|
||||||
if (_iframe && _iframe.contentWindow) {
|
if (_appVerified && _iframe && _iframe.contentWindow) {
|
||||||
_sendToIframe({
|
_sendToIframe({
|
||||||
type: "vknative_keyevent",
|
type: "vknative_keyevent",
|
||||||
keyCode: e.keyCode
|
keyCode: e.keyCode
|
||||||
|
|
@ -644,9 +938,10 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pfeiltasten, Enter, Escape immer an iframe weiterleiten
|
// Pfeiltasten, Enter, Escape an iframe weiterleiten
|
||||||
// (iframe bekommt auf Tizen keinen eigenen Keyboard-Focus)
|
// NUR wenn App verifiziert (iframe hat TV-App geladen)
|
||||||
if ((e.keyCode in FORWARD_KEYCODES) && _iframe && _iframe.contentWindow) {
|
// Sonst werden D-Pad-Tasten auf Setup-/Connecting-Screen geschluckt
|
||||||
|
if (_appVerified && (e.keyCode in FORWARD_KEYCODES) && _iframe && _iframe.contentWindow) {
|
||||||
_sendToIframe({
|
_sendToIframe({
|
||||||
type: "vknative_keyevent",
|
type: "vknative_keyevent",
|
||||||
keyCode: e.keyCode
|
keyCode: e.keyCode
|
||||||
|
|
@ -655,10 +950,47 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enter-Taste zum Verbinden (Setup-Bildschirm)
|
// 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) {
|
document.getElementById("serverUrl").addEventListener("keydown", function(e) {
|
||||||
if (e.keyCode === 13) connect();
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -77,12 +77,17 @@ def setup_tv_routes(app: web.Application, config: Config,
|
||||||
# --- Auth-Hilfsfunktionen ---
|
# --- Auth-Hilfsfunktionen ---
|
||||||
|
|
||||||
def _cookie_params(request: web.Request) -> dict:
|
def _cookie_params(request: web.Request) -> dict:
|
||||||
"""SameSite-Parameter je nach Protokoll: None+Secure bei HTTPS, Lax bei HTTP.
|
"""SameSite-Parameter je nach Protokoll und Client.
|
||||||
Browser verwerfen SameSite=None ohne Secure-Flag stillschweigend."""
|
Tizen-WGT-App: iframe-Context -> SameSite weglassen (Lax blockiert Cookies in iframes).
|
||||||
|
HTTPS: SameSite=None+Secure. HTTP normal: Lax."""
|
||||||
|
ua = request.headers.get("User-Agent", "")
|
||||||
is_https = (request.secure
|
is_https = (request.secure
|
||||||
or request.headers.get("X-Forwarded-Proto") == "https")
|
or request.headers.get("X-Forwarded-Proto") == "https")
|
||||||
if is_https:
|
if is_https:
|
||||||
return {"samesite": "None", "secure": True}
|
return {"samesite": "None", "secure": True}
|
||||||
|
# Tizen-App laeuft im iframe -> SameSite weglassen damit Cookie gesetzt wird
|
||||||
|
if "Tizen" in ua:
|
||||||
|
return {}
|
||||||
return {"samesite": "Lax"}
|
return {"samesite": "Lax"}
|
||||||
|
|
||||||
async def get_tv_user(request: web.Request) -> dict | None:
|
async def get_tv_user(request: web.Request) -> dict | None:
|
||||||
|
|
@ -1746,6 +1751,56 @@ def setup_tv_routes(app: web.Application, config: Config,
|
||||||
await hls_manager.destroy_session(sid)
|
await hls_manager.destroy_session(sid)
|
||||||
return web.json_response({"success": True})
|
return web.json_response({"success": True})
|
||||||
|
|
||||||
|
# --- Tizen Debug-Log ---
|
||||||
|
|
||||||
|
async def post_tizen_log(request):
|
||||||
|
"""Empfaengt Log-Eintraege von der Tizen WGT-App und speichert in DB"""
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
entries = data.get("entries", [])
|
||||||
|
ua = data.get("userAgent", "")[:500]
|
||||||
|
if not entries:
|
||||||
|
return web.json_response({"ok": True, "count": 0})
|
||||||
|
pool = library_service._db_pool
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
async with conn.cursor() as cur:
|
||||||
|
for e in entries[:50]: # Max 50 pro Request
|
||||||
|
await cur.execute(
|
||||||
|
"INSERT INTO tizen_logs (level, message, user_agent, client_ts) "
|
||||||
|
"VALUES (%s, %s, %s, %s)",
|
||||||
|
(e.get("l", "I"), e.get("m", "")[:2000],
|
||||||
|
ua, e.get("t", ""))
|
||||||
|
)
|
||||||
|
await conn.commit()
|
||||||
|
return web.json_response({"ok": True, "count": len(entries)})
|
||||||
|
except Exception as ex:
|
||||||
|
logging.warning(f"Tizen-Log Fehler: {ex}")
|
||||||
|
return web.json_response({"ok": False, "error": str(ex)}, status=400)
|
||||||
|
|
||||||
|
async def get_tizen_log(request):
|
||||||
|
"""Gibt die letzten Tizen-Logs zurueck (fuer Debugging)"""
|
||||||
|
limit = int(request.query.get("limit", "100"))
|
||||||
|
level = request.query.get("level", "")
|
||||||
|
pool = library_service._db_pool
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
async with conn.cursor(aiomysql.DictCursor) as cur:
|
||||||
|
if level:
|
||||||
|
await cur.execute(
|
||||||
|
"SELECT * FROM tizen_logs WHERE level = %s "
|
||||||
|
"ORDER BY id DESC LIMIT %s", (level, limit))
|
||||||
|
else:
|
||||||
|
await cur.execute(
|
||||||
|
"SELECT * FROM tizen_logs ORDER BY id DESC LIMIT %s",
|
||||||
|
(limit,))
|
||||||
|
rows = await cur.fetchall()
|
||||||
|
# Chronologisch (aelteste zuerst)
|
||||||
|
rows.reverse()
|
||||||
|
# Timestamps serialisierbar machen
|
||||||
|
for r in rows:
|
||||||
|
if r.get("created_at"):
|
||||||
|
r["created_at"] = str(r["created_at"])
|
||||||
|
return web.json_response(rows)
|
||||||
|
|
||||||
# --- Routes registrieren ---
|
# --- Routes registrieren ---
|
||||||
|
|
||||||
# TV-Seiten (mit Auth via Decorator)
|
# TV-Seiten (mit Auth via Decorator)
|
||||||
|
|
@ -1807,3 +1862,7 @@ def setup_tv_routes(app: web.Application, config: Config,
|
||||||
app.router.add_delete("/api/tv/users/{id}", delete_user)
|
app.router.add_delete("/api/tv/users/{id}", delete_user)
|
||||||
app.router.add_get("/api/tv/hls-sessions", get_hls_sessions)
|
app.router.add_get("/api/tv/hls-sessions", get_hls_sessions)
|
||||||
app.router.add_delete("/api/tv/hls-sessions/{sid}", delete_hls_session_admin)
|
app.router.add_delete("/api/tv/hls-sessions/{sid}", delete_hls_session_admin)
|
||||||
|
|
||||||
|
# Tizen Debug-Log API (kein Auth, Daten von WGT-App)
|
||||||
|
app.router.add_post("/api/tizen-log", post_tizen_log)
|
||||||
|
app.router.add_get("/api/tizen-log", get_tizen_log)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
* Kein Offline-Caching noetig (Streaming braucht Netzwerk)
|
* Kein Offline-Caching noetig (Streaming braucht Netzwerk)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CACHE_NAME = "vk-tv-v14";
|
const CACHE_NAME = "vk-tv-v15";
|
||||||
const STATIC_ASSETS = [
|
const STATIC_ASSETS = [
|
||||||
"/static/tv/css/tv.css",
|
"/static/tv/css/tv.css",
|
||||||
"/static/tv/js/tv.js",
|
"/static/tv/js/tv.js",
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,11 @@
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Tizen iframe: Parent signalisieren dass die App geladen hat
|
||||||
|
if (window.parent !== window) {
|
||||||
|
try { window.parent.postMessage({type: "vk_app_loaded"}, "*"); } catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
// PWA Service Worker registrieren
|
// PWA Service Worker registrieren
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.register('/static/tv/sw.js', {scope: '/tv/'})
|
navigator.serviceWorker.register('/static/tv/sw.js', {scope: '/tv/'})
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,42 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Tizen iframe: Parent signalisieren dass die App geladen hat
|
||||||
|
if (window.parent !== window) {
|
||||||
|
try { window.parent.postMessage({type: "vk_app_loaded"}, "*"); } catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === D-Pad Navigation fuer Login-Formular (Tizen/Samsung Fernbedienung) ===
|
||||||
|
(function() {
|
||||||
|
// postMessage-Events vom Tizen-Parent empfangen und als KeyboardEvent dispatchen
|
||||||
|
window.addEventListener("message", function(evt) {
|
||||||
|
var d = evt.data;
|
||||||
|
if (!d || d.type !== "vknative_keyevent" || !d.keyCode) return;
|
||||||
|
var keyMap = {13:"Enter",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",27:"Escape",8:"Backspace",10009:"Escape"};
|
||||||
|
var ke = new KeyboardEvent("keydown", {keyCode:d.keyCode, which:d.keyCode, key:keyMap[d.keyCode]||"", bubbles:true});
|
||||||
|
document.dispatchEvent(ke);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Focusable Elemente sammeln und D-Pad-Navigation
|
||||||
|
function getFocusables() {
|
||||||
|
return Array.prototype.slice.call(document.querySelectorAll("[data-focusable]"));
|
||||||
|
}
|
||||||
|
document.addEventListener("keydown", function(e) {
|
||||||
|
var els = getFocusables();
|
||||||
|
if (!els.length) return;
|
||||||
|
var idx = els.indexOf(document.activeElement);
|
||||||
|
if (e.key === "ArrowDown" || e.keyCode === 40) {
|
||||||
|
e.preventDefault();
|
||||||
|
els[idx < els.length - 1 ? idx + 1 : 0].focus();
|
||||||
|
} else if (e.key === "ArrowUp" || e.keyCode === 38) {
|
||||||
|
e.preventDefault();
|
||||||
|
els[idx > 0 ? idx - 1 : els.length - 1].focus();
|
||||||
|
} else if ((e.key === "Enter" || e.keyCode === 13) && document.activeElement.tagName === "BUTTON") {
|
||||||
|
document.activeElement.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
// Pruefen ob Browser Felder vorausgefuellt hat -> automatisch absenden
|
// Pruefen ob Browser Felder vorausgefuellt hat -> automatisch absenden
|
||||||
var _autoAttempts = 0;
|
var _autoAttempts = 0;
|
||||||
var _autoInterval = setInterval(function() {
|
var _autoInterval = setInterval(function() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue