diff --git a/android-app/app/src/main/java/de/datait/videokonverter/MainActivity.kt b/android-app/app/src/main/java/de/datait/videokonverter/MainActivity.kt index c3e950e..709771b 100644 --- a/android-app/app/src/main/java/de/datait/videokonverter/MainActivity.kt +++ b/android-app/app/src/main/java/de/datait/videokonverter/MainActivity.kt @@ -8,6 +8,9 @@ import android.webkit.CookieManager import android.webkit.WebSettings import android.webkit.WebView import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat import androidx.media3.ui.PlayerView import androidx.preference.PreferenceManager @@ -24,6 +27,13 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + // Immersive Fullscreen: Status- und Navigationsleiste verstecken + WindowCompat.setDecorFitsSystemWindows(window, false) + val insetsController = WindowInsetsControllerCompat(window, window.decorView) + insetsController.hide(WindowInsetsCompat.Type.systemBars()) + insetsController.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + setContentView(R.layout.activity_main) // Server-URL aus Intent oder Preferences @@ -93,6 +103,17 @@ class MainActivity : AppCompatActivity() { return super.onKeyDown(keyCode, event) } + override fun onWindowFocusChanged(hasFocus: Boolean) { + super.onWindowFocusChanged(hasFocus) + // Immersive Mode nach Focus-Wechsel neu setzen (Android setzt es zurueck) + if (hasFocus) { + val insetsController = WindowInsetsControllerCompat(window, window.decorView) + insetsController.hide(WindowInsetsCompat.Type.systemBars()) + insetsController.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + } + override fun onPause() { super.onPause() // ExoPlayer pausieren wenn App in den Hintergrund geht diff --git a/android-app/app/src/main/res/values/themes.xml b/android-app/app/src/main/res/values/themes.xml index 04fd467..f70e5be 100644 --- a/android-app/app/src/main/res/values/themes.xml +++ b/android-app/app/src/main/res/values/themes.xml @@ -2,8 +2,8 @@ diff --git a/tizen-app/index.html b/tizen-app/index.html index c38e867..b75d67c 100644 --- a/tizen-app/index.html +++ b/tizen-app/index.html @@ -524,10 +524,10 @@ var avEl = document.getElementById("avplayer"); if (avEl) avEl.style.display = "block"; - // iframe deaktivieren (keine Events abfangen) + // iframe: Pointer deaktivieren (D-Pad kommt per postMessage) + // Opacity bleibt 1 → transparenter Hintergrund im iframe zeigt AVPlay-Video durch if (_iframe) { _iframe.style.pointerEvents = "none"; - _iframe.style.opacity = "0"; } // AVPlay oeffnen @@ -655,10 +655,9 @@ var avEl = document.getElementById("avplayer"); if (avEl) avEl.style.display = "block"; - // iframe deaktivieren + // iframe: Pointer deaktivieren (D-Pad kommt per postMessage) if (_iframe) { _iframe.style.pointerEvents = "none"; - _iframe.style.opacity = "0"; } // AVPlay mit HLS-URL oeffnen @@ -758,7 +757,6 @@ // iframe wieder aktivieren if (_iframe) { _iframe.style.pointerEvents = "auto"; - _iframe.style.opacity = "1"; } } diff --git a/video-konverter/app/routes/pages.py b/video-konverter/app/routes/pages.py index e982311..e47b6b2 100644 --- a/video-konverter/app/routes/pages.py +++ b/video-konverter/app/routes/pages.py @@ -156,6 +156,10 @@ def setup_page_routes(app: web.Application, config: Config, data.get("pause_batch_on_stream") == "on") settings["tv"]["force_transcode"] = ( data.get("force_transcode") == "on") + settings["tv"]["audio_loudnorm"] = ( + data.get("audio_loudnorm") == "on") + settings["tv"]["audio_dynaudnorm"] = ( + data.get("audio_dynaudnorm") == "on") settings["tv"]["watched_threshold_pct"] = int( data.get("watched_threshold_pct", 90)) diff --git a/video-konverter/app/routes/tv_api.py b/video-konverter/app/routes/tv_api.py index c97bd92..93d1b69 100644 --- a/video-konverter/app/routes/tv_api.py +++ b/video-konverter/app/routes/tv_api.py @@ -963,6 +963,7 @@ def setup_tv_routes(app: web.Application, config: Config, "series_detail_url": series_detail_url, "client_sound_mode": client.get("sound_mode", "stereo") if client else "stereo", "client_stream_quality": client.get("stream_quality", "hd") if client else "hd", + "client_audio_compressor": client.get("audio_compressor", False) if client else False, } ) @@ -1300,6 +1301,12 @@ def setup_tv_routes(app: web.Application, config: Config, client_kwargs["sound_mode"] = data["sound_mode"] if "stream_quality" in data: client_kwargs["stream_quality"] = data["stream_quality"] + # audio_compressor: Checkbox-Handling + if "audio_compressor" in data: + client_kwargs["audio_compressor"] = ( + data["audio_compressor"] == "on") + elif not is_ajax: + client_kwargs["audio_compressor"] = False if client_kwargs: await auth_service.update_client_settings( client_id, **client_kwargs) diff --git a/video-konverter/app/services/auth.py b/video-konverter/app/services/auth.py index 6c9c613..4284e3e 100644 --- a/video-konverter/app/services/auth.py +++ b/video-konverter/app/services/auth.py @@ -76,12 +76,19 @@ class AuthService: DEFAULT 'stereo', stream_quality ENUM('uhd','hd','sd','low') DEFAULT 'hd', + audio_compressor BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_active TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 """) + # Spalte audio_compressor hinzufuegen (Migration fuer bestehende DBs) + await cur.execute(""" + ALTER TABLE tv_clients + ADD COLUMN IF NOT EXISTS audio_compressor BOOLEAN DEFAULT FALSE + """) + # Merkliste (Watchlist) await cur.execute(""" CREATE TABLE IF NOT EXISTS tv_watchlist ( @@ -575,7 +582,7 @@ class AuthService: pool = await self._get_pool() if not pool: return False - allowed = {"name", "sound_mode", "stream_quality"} + allowed = {"name", "sound_mode", "stream_quality", "audio_compressor"} updates = [] values = [] for key, val in kwargs.items(): diff --git a/video-konverter/app/services/hls.py b/video-konverter/app/services/hls.py index b57cc4a..fd3f70e 100644 --- a/video-konverter/app/services/hls.py +++ b/video-konverter/app/services/hls.py @@ -194,6 +194,12 @@ class HLSSessionManager: # Audio-Transcoding noetig? needs_audio_transcode = audio_codec not in BROWSER_AUDIO_CODECS + # Audio-Normalisierung erzwingt Transcoding + audio_loudnorm = self._tv_setting("audio_loudnorm", False) + audio_dynaudnorm = self._tv_setting("audio_dynaudnorm", False) + if audio_loudnorm or audio_dynaudnorm: + needs_audio_transcode = True + # Force-Transcode: Immer transcodieren fuer maximale Kompatibilitaet if self._tv_setting("force_transcode", False): needs_video_transcode = True @@ -346,8 +352,18 @@ class HLSSessionManager: if needs_audio_transcode: bitrate = {1: "96k", 2: "192k"}.get( out_channels, f"{out_channels * 64}k") + # Audio-Filter aufbauen (loudnorm, dynaudnorm) + af_parts = [] + audio_loudnorm = self._tv_setting("audio_loudnorm", False) + audio_dynaudnorm = self._tv_setting("audio_dynaudnorm", False) + if audio_loudnorm: + af_parts.append("loudnorm=I=-14:LRA=7:TP=-1") + if audio_dynaudnorm: + af_parts.append("dynaudnorm=f=250:g=15:p=0.95") cmd += ["-c:a", "aac", "-ac", str(out_channels), "-b:a", bitrate] + if af_parts: + cmd += ["-af", ",".join(af_parts)] else: cmd += ["-c:a", "copy"] diff --git a/video-konverter/app/static/tv/css/tv.css b/video-konverter/app/static/tv/css/tv.css index 2036741..880db7c 100644 --- a/video-konverter/app/static/tv/css/tv.css +++ b/video-konverter/app/static/tv/css/tv.css @@ -948,6 +948,24 @@ a { color: var(--accent); text-decoration: none; } background: #000; overflow: hidden; } +/* VKNative (Tizen AVPlay): Transparenter Hintergrund damit + das AVPlay-Video (Hardware-Layer unter HTML) durchscheint. + Controls behalten ihre Gradient-Hintergruende. */ +body.vknative-playing { + background: transparent !important; +} +body.vknative-playing .player-wrapper { + background: transparent !important; +} +body.vknative-playing #player-video { + display: none !important; +} +body.vknative-playing .player-loading { + background: rgba(0, 0, 0, 0.6) !important; +} +body.vknative-playing .player-body { + background: transparent !important; +} .player-wrapper { position: fixed; inset: 0; diff --git a/video-konverter/app/static/tv/i18n/de.json b/video-konverter/app/static/tv/i18n/de.json index a19bef4..1ee2651 100644 --- a/video-konverter/app/static/tv/i18n/de.json +++ b/video-konverter/app/static/tv/i18n/de.json @@ -129,6 +129,7 @@ "sound_surround": "Surround (5.1/7.1)", "sound_original": "Original", "stream_quality": "Standard-Qualität", + "audio_compressor": "Audio-Kompression (gleicht Lautstärke-Schwankungen aus)", "on": "An", "off": "Aus" }, diff --git a/video-konverter/app/static/tv/i18n/en.json b/video-konverter/app/static/tv/i18n/en.json index 9ecc2af..0dfe2c2 100644 --- a/video-konverter/app/static/tv/i18n/en.json +++ b/video-konverter/app/static/tv/i18n/en.json @@ -129,6 +129,7 @@ "sound_surround": "Surround (5.1/7.1)", "sound_original": "Original", "stream_quality": "Default Quality", + "audio_compressor": "Audio Compression (levels out volume differences)", "on": "On", "off": "Off" }, diff --git a/video-konverter/app/static/tv/js/player.js b/video-konverter/app/static/tv/js/player.js index 52dbb2b..a98e379 100644 --- a/video-konverter/app/static/tv/js/player.js +++ b/video-konverter/app/static/tv/js/player.js @@ -42,6 +42,10 @@ let nativePlayTimeout = null; // Master-Timeout fuer VKNative-Start // Legacy AVPlay Direct-Play State (Rueckwaerts-Kompatibilitaet) let useDirectPlay = false; // Alter AVPlayBridge-Pfad aktiv? +// Audio-Kompressor (DynamicsCompressorNode) +let _audioCtx = null; +let _audioCompressorSetup = false; + /** * Player initialisieren * @param {Object} opts - Konfiguration @@ -300,6 +304,10 @@ function hideLoading() { function onPlaying() { hideLoading(); hlsRetryCount = 0; // Reset bei erfolgreichem Start + // Audio-Kompressor aktivieren (nur bei Browser-Wiedergabe, nicht VKNative) + if (cfg.audioCompressor && !useNativePlayer && videoEl) { + _setupAudioCompressor(videoEl); + } } function onVideoError() { @@ -569,6 +577,7 @@ async function _tryNativeDirectPlay(startPosSec) { } useNativePlayer = true; + document.body.classList.add("vknative-playing"); console.info("[Player] VKNative.play() aufrufen..."); var ok = window.VKNative.play(directUrl, info, { seekMs: Math.floor(startPosSec * 1000) @@ -603,6 +612,7 @@ function _cleanupNativePlayer() { } useNativePlayer = false; nativePlayStarted = false; + document.body.classList.remove("vknative-playing"); // Callbacks entfernen window._vkOnReady = null; window._vkOnTimeUpdate = null; @@ -629,6 +639,7 @@ function _nativeFallbackToHLS(startPosSec) { // Kein VKNative HLS (Tizen etc.) -> WebView-HLS useNativePlayer = false; nativePlayStarted = false; + document.body.classList.remove("vknative-playing"); if (videoEl) videoEl.style.display = ""; var infoReady = loadVideoInfo(); startHLSStream(startPosSec); @@ -713,6 +724,7 @@ async function _startNativeHLS(startPosSec) { // ExoPlayer HLS starten useNativePlayer = true; + document.body.classList.add("vknative-playing"); var ok = window.VKNative.playHLS(playlistUrl, {}); console.info("[Player] VKNative.playHLS() Ergebnis: " + ok); @@ -733,6 +745,33 @@ async function _startNativeHLS(startPosSec) { } } +// === Audio-Kompressor (Client-seitige Lautstaerke-Nivellierung) === + +/** + * DynamicsCompressorNode: Reduziert Lautstaerke-Schwankungen im Browser. + * Laute Passagen (Explosionen) werden komprimiert, leise (Dialoge) angehoben. + * Funktioniert nur bei