docker.videokonverter/video-konverter/app/server.py
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

228 lines
8.8 KiB
Python

"""Haupt-Server: HTTP + WebSocket + Templates in einer aiohttp-App"""
import asyncio
import logging
import os
import time
from pathlib import Path
from aiohttp import web
import aiohttp_jinja2
import jinja2
from app.config import Config
from app.services.queue import QueueService
from app.services.scanner import ScannerService
from app.services.encoder import EncoderService
from app.routes.ws import WebSocketManager
from app.services.library import LibraryService
from app.services.tvdb import TVDBService
from app.services.cleaner import CleanerService
from app.services.importer import ImporterService
from app.services.auth import AuthService
from app.services.hls import HLSSessionManager
from app.services.i18n import load_translations, setup_jinja2_i18n
from app.routes.api import setup_api_routes
from app.routes.library_api import setup_library_routes
from app.routes.pages import setup_page_routes
from app.routes.tv_api import setup_tv_routes
class VideoKonverterServer:
"""Haupt-Server - ein Port fuer HTTP, WebSocket und Admin-UI"""
def __init__(self):
self.config = Config()
self.config.setup_logging()
# Services
self.ws_manager = WebSocketManager()
self.scanner = ScannerService(self.config)
self.queue_service = QueueService(self.config, self.ws_manager)
self.ws_manager.set_queue_service(self.queue_service)
# Bibliothek-Services
self.library_service = LibraryService(self.config, self.ws_manager)
self.tvdb_service = TVDBService(self.config)
self.cleaner_service = CleanerService(self.config, self.library_service)
self.importer_service = ImporterService(
self.config, self.library_service, self.tvdb_service
)
# aiohttp App (50 GiB Upload-Limit fuer grosse Videodateien)
self.app = web.Application(
client_max_size=50 * 1024 * 1024 * 1024,
middlewares=[self._no_cache_middleware]
)
self._setup_app()
@web.middleware
async def _no_cache_middleware(self, request: web.Request,
handler) -> web.Response:
"""Verhindert Browser-Caching fuer API-Responses + Error-Logging"""
try:
response = await handler(request)
except web.HTTPException as he:
if he.status >= 500:
logging.error(f"HTTP {he.status} bei {request.method} {request.path}: {he.reason}",
exc_info=True)
raise
except Exception as e:
logging.error(f"Unbehandelte Ausnahme bei {request.method} {request.path}: {e}",
exc_info=True)
raise
if request.path.startswith("/api/"):
# Thumbnail-Bilder sollen gecacht werden (Cache-Control vom Handler)
if "/thumbnail" not in request.path:
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
return response
def _setup_app(self) -> None:
"""Konfiguriert die aiohttp-Application"""
# Jinja2 Templates (request_processor macht request in Templates verfuegbar)
template_dir = Path(__file__).parent / "templates"
aiohttp_jinja2.setup(
self.app,
loader=jinja2.FileSystemLoader(str(template_dir)),
context_processors=[aiohttp_jinja2.request_processor],
bytecode_cache=jinja2.FileSystemBytecodeCache(
directory="/tmp/jinja2_cache",
pattern="__jinja2_%s.cache",
),
auto_reload=os.environ.get("VK_DEV", "").lower() == "true",
)
# i18n: Uebersetzungen laden und Jinja2-Filter registrieren
static_dir = Path(__file__).parent / "static"
load_translations(str(static_dir))
setup_jinja2_i18n(self.app)
# Cache-Busting: Versionsstempel fuer statische Dateien
env = self.app[aiohttp_jinja2.APP_KEY]
env.globals["v"] = str(int(time.time()))
# WebSocket Route
ws_path = self.config.server_config.get("websocket_path", "/ws")
self.app.router.add_get(ws_path, self.ws_manager.handle_websocket)
# API Routes
setup_api_routes(
self.app, self.config, self.queue_service, self.scanner,
self.ws_manager
)
# Bibliothek API Routes
setup_library_routes(
self.app, self.config, self.library_service,
self.tvdb_service, self.queue_service,
self.cleaner_service, self.importer_service,
)
# Seiten Routes
setup_page_routes(self.app, self.config, self.queue_service)
# TV-App Routes (Auth-Service, DB-Pool wird in on_startup gesetzt)
async def _lazy_pool():
return self.library_service._db_pool
self.auth_service = AuthService(_lazy_pool)
self.hls_manager = HLSSessionManager(
self.library_service, self.config.gpu_device,
queue_service=self.queue_service, config=self.config)
setup_tv_routes(
self.app, self.config,
self.auth_service, self.library_service,
self.hls_manager,
)
# Statische Dateien
static_dir = Path(__file__).parent / "static"
if static_dir.exists():
self.app.router.add_static(
"/static/", path=str(static_dir), name="static"
)
# Startup/Shutdown Hooks
self.app.on_startup.append(self._on_startup)
self.app.on_shutdown.append(self._on_shutdown)
async def _on_startup(self, app: web.Application) -> None:
"""Server-Start: GPU pruefen, Queue starten"""
mode = self.config.encoding_mode
# Auto-Detection
if mode == "auto":
gpu_ok = EncoderService.detect_gpu_available()
if gpu_ok:
gpu_ok = await EncoderService.test_gpu_encoding(
self.config.gpu_device
)
if gpu_ok:
self.config.settings["encoding"]["mode"] = "gpu"
self.config.settings["encoding"]["default_preset"] = "gpu_av1"
logging.info(f"GPU erkannt ({self.config.gpu_device}), "
f"verwende GPU-Encoding")
else:
self.config.settings["encoding"]["mode"] = "cpu"
self.config.settings["encoding"]["default_preset"] = "cpu_av1"
logging.info("Keine GPU erkannt, verwende CPU-Encoding")
else:
logging.info(f"Encoding-Modus: {mode}")
# Queue starten
await self.queue_service.start()
# Bibliothek starten
await self.library_service.start()
# DB-Pool mit anderen Services teilen
if self.library_service._db_pool:
self.tvdb_service.set_db_pool(self.library_service._db_pool)
self.importer_service.set_db_pool(self.library_service._db_pool)
# WebSocket-Manager an ImporterService fuer Live-Updates
self.importer_service.set_ws_manager(self.ws_manager)
# Zusaetzliche DB-Tabellen erstellen
await self.tvdb_service.init_db()
await self.importer_service.init_db()
# TV-App Auth-Service: DB-Tabellen initialisieren (Pool kommt ueber lazy getter)
if self.library_service._db_pool:
await self.auth_service.init_db()
# HLS Session Manager starten
await self.hls_manager.start()
host = self.config.server_config.get("host", "0.0.0.0")
port = self.config.server_config.get("port", 8080)
logging.info(f"Server bereit auf http://{host}:{port}")
async def _on_shutdown(self, app: web.Application) -> None:
"""Server-Stop: Queue und Library stoppen"""
await self.hls_manager.stop()
await self.queue_service.stop()
await self.library_service.stop()
logging.info("Server heruntergefahren")
async def run(self) -> None:
"""Startet den Server"""
host = self.config.server_config.get("host", "0.0.0.0")
port = self.config.server_config.get("port", 8080)
runner = web.AppRunner(self.app)
await runner.setup()
site = web.TCPSite(runner, host, port)
await site.start()
logging.info(
f"VideoKonverter Server laeuft auf http://{host}:{port}\n"
f" Dashboard: http://{host}:{port}/\n"
f" Bibliothek: http://{host}:{port}/library\n"
f" Admin: http://{host}:{port}/admin\n"
f" Statistik: http://{host}:{port}/statistics\n"
f" TV-App: http://{host}:{port}/tv/\n"
f" WebSocket: ws://{host}:{port}/ws\n"
f" API: http://{host}:{port}/api/convert (POST)"
)
# Endlos laufen bis Interrupt
await asyncio.Event().wait()