feat: VideoKonverter v5.6 - Player-Overlay, Immersive Fullscreen, Audio-Normalisierung
Tizen TV: Transparenter iframe-Overlay statt opacity:0 - Player-Controls (Progress-Bar, Buttons, Popup-Menue) jetzt sichtbar ueber dem AVPlay-Video. CSS-Klasse "vknative-playing" macht Hintergruende transparent, AVPlay-Video scheint durch den iframe hindurch. Android App: Immersive Sticky Fullscreen mit WindowInsetsControllerCompat. Status- und Navigationsleiste komplett versteckt, per Swipe vom Rand temporaer einblendbar. Audio-Normalisierung (3 Stufen): - Server-seitig: EBU R128 loudnorm (I=-14 LUFS) im HLS-Transcoding - Server-seitig: dynaudnorm (dynamische Szenen-Anpassung) im HLS-Transcoding - Client-seitig: DynamicsCompressorNode im Browser-Player Alle Optionen konfigurierbar: loudnorm/dynaudnorm im TV Admin-Center, Audio-Kompressor pro Client in den Einstellungen. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
00d8f6b982
commit
956b7b9ac8
15 changed files with 153 additions and 10 deletions
|
|
@ -8,6 +8,9 @@ import android.webkit.CookieManager
|
||||||
import android.webkit.WebSettings
|
import android.webkit.WebSettings
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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.media3.ui.PlayerView
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
|
||||||
|
|
@ -24,6 +27,13 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
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)
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
// Server-URL aus Intent oder Preferences
|
// Server-URL aus Intent oder Preferences
|
||||||
|
|
@ -93,6 +103,17 @@ class MainActivity : AppCompatActivity() {
|
||||||
return super.onKeyDown(keyCode, event)
|
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() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
// ExoPlayer pausieren wenn App in den Hintergrund geht
|
// ExoPlayer pausieren wenn App in den Hintergrund geht
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
<resources>
|
<resources>
|
||||||
<style name="Theme.VideoKonverter" parent="Theme.AppCompat.NoActionBar">
|
<style name="Theme.VideoKonverter" parent="Theme.AppCompat.NoActionBar">
|
||||||
<item name="android:windowBackground">@android:color/black</item>
|
<item name="android:windowBackground">@android:color/black</item>
|
||||||
<item name="android:statusBarColor">@android:color/black</item>
|
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||||
<item name="android:navigationBarColor">@android:color/black</item>
|
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||||
<item name="android:windowFullscreen">false</item>
|
<item name="android:windowFullscreen">true</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -524,10 +524,10 @@
|
||||||
var avEl = document.getElementById("avplayer");
|
var avEl = document.getElementById("avplayer");
|
||||||
if (avEl) avEl.style.display = "block";
|
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) {
|
if (_iframe) {
|
||||||
_iframe.style.pointerEvents = "none";
|
_iframe.style.pointerEvents = "none";
|
||||||
_iframe.style.opacity = "0";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AVPlay oeffnen
|
// AVPlay oeffnen
|
||||||
|
|
@ -655,10 +655,9 @@
|
||||||
var avEl = document.getElementById("avplayer");
|
var avEl = document.getElementById("avplayer");
|
||||||
if (avEl) avEl.style.display = "block";
|
if (avEl) avEl.style.display = "block";
|
||||||
|
|
||||||
// iframe deaktivieren
|
// iframe: Pointer deaktivieren (D-Pad kommt per postMessage)
|
||||||
if (_iframe) {
|
if (_iframe) {
|
||||||
_iframe.style.pointerEvents = "none";
|
_iframe.style.pointerEvents = "none";
|
||||||
_iframe.style.opacity = "0";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AVPlay mit HLS-URL oeffnen
|
// AVPlay mit HLS-URL oeffnen
|
||||||
|
|
@ -758,7 +757,6 @@
|
||||||
// iframe wieder aktivieren
|
// iframe wieder aktivieren
|
||||||
if (_iframe) {
|
if (_iframe) {
|
||||||
_iframe.style.pointerEvents = "auto";
|
_iframe.style.pointerEvents = "auto";
|
||||||
_iframe.style.opacity = "1";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,10 @@ def setup_page_routes(app: web.Application, config: Config,
|
||||||
data.get("pause_batch_on_stream") == "on")
|
data.get("pause_batch_on_stream") == "on")
|
||||||
settings["tv"]["force_transcode"] = (
|
settings["tv"]["force_transcode"] = (
|
||||||
data.get("force_transcode") == "on")
|
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(
|
settings["tv"]["watched_threshold_pct"] = int(
|
||||||
data.get("watched_threshold_pct", 90))
|
data.get("watched_threshold_pct", 90))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -963,6 +963,7 @@ def setup_tv_routes(app: web.Application, config: Config,
|
||||||
"series_detail_url": series_detail_url,
|
"series_detail_url": series_detail_url,
|
||||||
"client_sound_mode": client.get("sound_mode", "stereo") if client else "stereo",
|
"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_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"]
|
client_kwargs["sound_mode"] = data["sound_mode"]
|
||||||
if "stream_quality" in data:
|
if "stream_quality" in data:
|
||||||
client_kwargs["stream_quality"] = data["stream_quality"]
|
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:
|
if client_kwargs:
|
||||||
await auth_service.update_client_settings(
|
await auth_service.update_client_settings(
|
||||||
client_id, **client_kwargs)
|
client_id, **client_kwargs)
|
||||||
|
|
|
||||||
|
|
@ -76,12 +76,19 @@ class AuthService:
|
||||||
DEFAULT 'stereo',
|
DEFAULT 'stereo',
|
||||||
stream_quality ENUM('uhd','hd','sd','low')
|
stream_quality ENUM('uhd','hd','sd','low')
|
||||||
DEFAULT 'hd',
|
DEFAULT 'hd',
|
||||||
|
audio_compressor BOOLEAN DEFAULT FALSE,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
last_active TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
last_active TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
ON UPDATE CURRENT_TIMESTAMP
|
ON UPDATE CURRENT_TIMESTAMP
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
) 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)
|
# Merkliste (Watchlist)
|
||||||
await cur.execute("""
|
await cur.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS tv_watchlist (
|
CREATE TABLE IF NOT EXISTS tv_watchlist (
|
||||||
|
|
@ -575,7 +582,7 @@ class AuthService:
|
||||||
pool = await self._get_pool()
|
pool = await self._get_pool()
|
||||||
if not pool:
|
if not pool:
|
||||||
return False
|
return False
|
||||||
allowed = {"name", "sound_mode", "stream_quality"}
|
allowed = {"name", "sound_mode", "stream_quality", "audio_compressor"}
|
||||||
updates = []
|
updates = []
|
||||||
values = []
|
values = []
|
||||||
for key, val in kwargs.items():
|
for key, val in kwargs.items():
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,12 @@ class HLSSessionManager:
|
||||||
# Audio-Transcoding noetig?
|
# Audio-Transcoding noetig?
|
||||||
needs_audio_transcode = audio_codec not in BROWSER_AUDIO_CODECS
|
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
|
# Force-Transcode: Immer transcodieren fuer maximale Kompatibilitaet
|
||||||
if self._tv_setting("force_transcode", False):
|
if self._tv_setting("force_transcode", False):
|
||||||
needs_video_transcode = True
|
needs_video_transcode = True
|
||||||
|
|
@ -346,8 +352,18 @@ class HLSSessionManager:
|
||||||
if needs_audio_transcode:
|
if needs_audio_transcode:
|
||||||
bitrate = {1: "96k", 2: "192k"}.get(
|
bitrate = {1: "96k", 2: "192k"}.get(
|
||||||
out_channels, f"{out_channels * 64}k")
|
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),
|
cmd += ["-c:a", "aac", "-ac", str(out_channels),
|
||||||
"-b:a", bitrate]
|
"-b:a", bitrate]
|
||||||
|
if af_parts:
|
||||||
|
cmd += ["-af", ",".join(af_parts)]
|
||||||
else:
|
else:
|
||||||
cmd += ["-c:a", "copy"]
|
cmd += ["-c:a", "copy"]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -948,6 +948,24 @@ a { color: var(--accent); text-decoration: none; }
|
||||||
background: #000;
|
background: #000;
|
||||||
overflow: hidden;
|
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 {
|
.player-wrapper {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,7 @@
|
||||||
"sound_surround": "Surround (5.1/7.1)",
|
"sound_surround": "Surround (5.1/7.1)",
|
||||||
"sound_original": "Original",
|
"sound_original": "Original",
|
||||||
"stream_quality": "Standard-Qualität",
|
"stream_quality": "Standard-Qualität",
|
||||||
|
"audio_compressor": "Audio-Kompression (gleicht Lautstärke-Schwankungen aus)",
|
||||||
"on": "An",
|
"on": "An",
|
||||||
"off": "Aus"
|
"off": "Aus"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,7 @@
|
||||||
"sound_surround": "Surround (5.1/7.1)",
|
"sound_surround": "Surround (5.1/7.1)",
|
||||||
"sound_original": "Original",
|
"sound_original": "Original",
|
||||||
"stream_quality": "Default Quality",
|
"stream_quality": "Default Quality",
|
||||||
|
"audio_compressor": "Audio Compression (levels out volume differences)",
|
||||||
"on": "On",
|
"on": "On",
|
||||||
"off": "Off"
|
"off": "Off"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,10 @@ let nativePlayTimeout = null; // Master-Timeout fuer VKNative-Start
|
||||||
// Legacy AVPlay Direct-Play State (Rueckwaerts-Kompatibilitaet)
|
// Legacy AVPlay Direct-Play State (Rueckwaerts-Kompatibilitaet)
|
||||||
let useDirectPlay = false; // Alter AVPlayBridge-Pfad aktiv?
|
let useDirectPlay = false; // Alter AVPlayBridge-Pfad aktiv?
|
||||||
|
|
||||||
|
// Audio-Kompressor (DynamicsCompressorNode)
|
||||||
|
let _audioCtx = null;
|
||||||
|
let _audioCompressorSetup = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Player initialisieren
|
* Player initialisieren
|
||||||
* @param {Object} opts - Konfiguration
|
* @param {Object} opts - Konfiguration
|
||||||
|
|
@ -300,6 +304,10 @@ function hideLoading() {
|
||||||
function onPlaying() {
|
function onPlaying() {
|
||||||
hideLoading();
|
hideLoading();
|
||||||
hlsRetryCount = 0; // Reset bei erfolgreichem Start
|
hlsRetryCount = 0; // Reset bei erfolgreichem Start
|
||||||
|
// Audio-Kompressor aktivieren (nur bei Browser-Wiedergabe, nicht VKNative)
|
||||||
|
if (cfg.audioCompressor && !useNativePlayer && videoEl) {
|
||||||
|
_setupAudioCompressor(videoEl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onVideoError() {
|
function onVideoError() {
|
||||||
|
|
@ -569,6 +577,7 @@ async function _tryNativeDirectPlay(startPosSec) {
|
||||||
}
|
}
|
||||||
|
|
||||||
useNativePlayer = true;
|
useNativePlayer = true;
|
||||||
|
document.body.classList.add("vknative-playing");
|
||||||
console.info("[Player] VKNative.play() aufrufen...");
|
console.info("[Player] VKNative.play() aufrufen...");
|
||||||
var ok = window.VKNative.play(directUrl, info, {
|
var ok = window.VKNative.play(directUrl, info, {
|
||||||
seekMs: Math.floor(startPosSec * 1000)
|
seekMs: Math.floor(startPosSec * 1000)
|
||||||
|
|
@ -603,6 +612,7 @@ function _cleanupNativePlayer() {
|
||||||
}
|
}
|
||||||
useNativePlayer = false;
|
useNativePlayer = false;
|
||||||
nativePlayStarted = false;
|
nativePlayStarted = false;
|
||||||
|
document.body.classList.remove("vknative-playing");
|
||||||
// Callbacks entfernen
|
// Callbacks entfernen
|
||||||
window._vkOnReady = null;
|
window._vkOnReady = null;
|
||||||
window._vkOnTimeUpdate = null;
|
window._vkOnTimeUpdate = null;
|
||||||
|
|
@ -629,6 +639,7 @@ function _nativeFallbackToHLS(startPosSec) {
|
||||||
// Kein VKNative HLS (Tizen etc.) -> WebView-HLS
|
// Kein VKNative HLS (Tizen etc.) -> WebView-HLS
|
||||||
useNativePlayer = false;
|
useNativePlayer = false;
|
||||||
nativePlayStarted = false;
|
nativePlayStarted = false;
|
||||||
|
document.body.classList.remove("vknative-playing");
|
||||||
if (videoEl) videoEl.style.display = "";
|
if (videoEl) videoEl.style.display = "";
|
||||||
var infoReady = loadVideoInfo();
|
var infoReady = loadVideoInfo();
|
||||||
startHLSStream(startPosSec);
|
startHLSStream(startPosSec);
|
||||||
|
|
@ -713,6 +724,7 @@ async function _startNativeHLS(startPosSec) {
|
||||||
|
|
||||||
// ExoPlayer HLS starten
|
// ExoPlayer HLS starten
|
||||||
useNativePlayer = true;
|
useNativePlayer = true;
|
||||||
|
document.body.classList.add("vknative-playing");
|
||||||
var ok = window.VKNative.playHLS(playlistUrl, {});
|
var ok = window.VKNative.playHLS(playlistUrl, {});
|
||||||
console.info("[Player] VKNative.playHLS() Ergebnis: " + ok);
|
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 <video>-Element (nicht bei VKNative/AVPlay).
|
||||||
|
*/
|
||||||
|
function _setupAudioCompressor(el) {
|
||||||
|
if (_audioCompressorSetup || !el) return;
|
||||||
|
try {
|
||||||
|
_audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
var source = _audioCtx.createMediaElementSource(el);
|
||||||
|
var compressor = _audioCtx.createDynamicsCompressor();
|
||||||
|
compressor.threshold.value = -30; // Ab -30dB komprimieren
|
||||||
|
compressor.knee.value = 20; // Weicher Uebergang
|
||||||
|
compressor.ratio.value = 4; // 4:1 Kompression
|
||||||
|
compressor.attack.value = 0.005; // 5ms Ansprechzeit
|
||||||
|
compressor.release.value = 0.1; // 100ms Abklingzeit
|
||||||
|
source.connect(compressor);
|
||||||
|
compressor.connect(_audioCtx.destination);
|
||||||
|
_audioCompressorSetup = true;
|
||||||
|
console.info("[Player] Audio-Kompressor aktiviert (DynamicsCompressor)");
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[Player] Audio-Kompressor nicht verfuegbar:", e.message || e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// === Browser Direct-Play (MP4 mit Range-Requests) ===
|
// === Browser Direct-Play (MP4 mit Range-Requests) ===
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
* Kein Offline-Caching noetig (Streaming braucht Netzwerk)
|
* Kein Offline-Caching noetig (Streaming braucht Netzwerk)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CACHE_NAME = "vk-tv-v15";
|
const CACHE_NAME = "vk-tv-v16";
|
||||||
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",
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@
|
||||||
subtitlesEnabled: {{ 'true' if user.subtitles_enabled else 'false' }},
|
subtitlesEnabled: {{ 'true' if user.subtitles_enabled else 'false' }},
|
||||||
soundMode: "{{ client_sound_mode or 'stereo' }}",
|
soundMode: "{{ client_sound_mode or 'stereo' }}",
|
||||||
streamQuality: "{{ client_stream_quality or 'hd' }}",
|
streamQuality: "{{ client_stream_quality or 'hd' }}",
|
||||||
|
audioCompressor: {{ 'true' if client_audio_compressor else 'false' }},
|
||||||
seriesDetailUrl: "{{ series_detail_url or '' }}",
|
seriesDetailUrl: "{{ series_detail_url or '' }}",
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,12 @@
|
||||||
<option value="low" {% if client.stream_quality == 'low' %}selected{% endif %}>{{ t('player.quality_low') }}</option>
|
<option value="low" {% if client.stream_quality == 'low' %}selected{% endif %}>{{ t('player.quality_low') }}</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="settings-check">
|
||||||
|
<input type="checkbox" name="audio_compressor"
|
||||||
|
{% if client.audio_compressor %}checked{% endif %}
|
||||||
|
data-focusable>
|
||||||
|
{{ t('settings.audio_compressor') }}
|
||||||
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,30 @@
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- Audio-Normalisierung -->
|
||||||
|
<fieldset>
|
||||||
|
<legend>Audio-Normalisierung</legend>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="audio_loudnorm" id="audio_loudnorm"
|
||||||
|
{% if tv.audio_loudnorm | default(false) %}checked{% endif %}>
|
||||||
|
EBU R128 Loudnorm (Lautstaerke-Normalisierung)
|
||||||
|
</label>
|
||||||
|
<span class="text-muted" style="font-size:0.8rem">Normalisiert Audio auf -14 LUFS (Broadcast-Standard). Alle Filme/Serien haben die gleiche Grundlautstaerke. Nur bei HLS-Streaming aktiv.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="audio_dynaudnorm" id="audio_dynaudnorm"
|
||||||
|
{% if tv.audio_dynaudnorm | default(false) %}checked{% endif %}>
|
||||||
|
Dynamische Audio-Normalisierung (dynaudnorm)
|
||||||
|
</label>
|
||||||
|
<span class="text-muted" style="font-size:0.8rem">Passt Lautstaerke Szene fuer Szene an. Leise Dialoge werden lauter, Explosionen leiser. Ergaenzt Loudnorm. Nur bei HLS-Streaming aktiv.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<!-- Watch-Status -->
|
<!-- Watch-Status -->
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Watch-Status</legend>
|
<legend>Watch-Status</legend>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue