From d0f212294438265b8ddf2275e82dedf61397d8fa Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 1 Mar 2026 04:02:36 +0100 Subject: [PATCH] feat: add tunneled playback setting + frame watchdog for HDR black screen Adds a user-facing toggle for MediaCodec tunneling (Android/ExoPlayer) and a rendered-frame watchdog that auto-falls back to MPV when ExoPlayer produces 0 video frames after 8 seconds of audio playback. --- .../edde746/plezy/exoplayer/ExoPlayerCore.kt | 73 ++++++++++++++++++- .../plezy/exoplayer/ExoPlayerPlugin.kt | 8 +- lib/i18n/de.i18n.json | 2 + lib/i18n/en.i18n.json | 2 + lib/i18n/es.i18n.json | 2 + lib/i18n/fr.i18n.json | 2 + lib/i18n/it.i18n.json | 2 + lib/i18n/ko.i18n.json | 2 + lib/i18n/nl.i18n.json | 2 + lib/i18n/strings_de.g.dart | 4 + lib/i18n/strings_en.g.dart | 8 ++ lib/i18n/strings_es.g.dart | 4 + lib/i18n/strings_fr.g.dart | 4 + lib/i18n/strings_it.g.dart | 4 + lib/i18n/strings_ko.g.dart | 4 + lib/i18n/strings_nl.g.dart | 4 + lib/i18n/strings_sv.g.dart | 4 + lib/i18n/strings_zh.g.dart | 4 + lib/i18n/sv.i18n.json | 2 + lib/i18n/zh.i18n.json | 2 + lib/mpv/player/platform/player_android.dart | 5 ++ lib/screens/settings/settings_screen.dart | 17 +++++ lib/screens/video_player_screen.dart | 4 + lib/services/settings_service.dart | 11 +++ 24 files changed, 171 insertions(+), 5 deletions(-) diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt index 3505b372..987b1f12 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt @@ -80,6 +80,8 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { companion object { private const val TAG = "ExoPlayerCore" private const val SHORT_VIDEO_LENGTH_MS = 300000L // 5 minutes + private const val WATCHDOG_CHECK_INTERVAL_MS = 1000L + private const val WATCHDOG_TIMEOUT_MS = 8000L } private var surfaceView: SurfaceView? = null @@ -90,8 +92,13 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { private var lastVideoSize: VideoSize? = null private var exoPlayer: ExoPlayer? = null private var trackSelector: DefaultTrackSelector? = null + private var tunnelingUserEnabled: Boolean = true private var tunnelingDisabledForCodec: Boolean = false private var pendingStartPositionMs: Long = 0L + + // Frame watchdog: detects black screen (audio plays but 0 video frames rendered) + private var frameWatchdogRunnable: Runnable? = null + private var frameWatchdogStartTime: Long = 0L var delegate: ExoPlayerDelegate? = null var isInitialized: Boolean = false private set @@ -212,12 +219,14 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { } } - fun initialize(bufferSizeBytes: Int? = null): Boolean { + fun initialize(bufferSizeBytes: Int? = null, tunnelingEnabled: Boolean = true): Boolean { if (isInitialized) { Log.d(TAG, "Already initialized") return true } + tunnelingUserEnabled = tunnelingEnabled + try { audioManager = activity.getSystemService(Context.AUDIO_SERVICE) as AudioManager @@ -302,7 +311,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { trackSelector = DefaultTrackSelector(activity).apply { setParameters( buildUponParameters() - .setTunnelingEnabled(true) + .setTunnelingEnabled(tunnelingUserEnabled) .setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false) .setPreferredTextLanguage("en") ) @@ -533,8 +542,12 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { delegate?.onPropertyChange("paused-for-cache", false) delegate?.onEvent("playback-restart", null) emitTrackList() + + // Start frame watchdog to detect black screen (HDR tunneling issue) + startFrameWatchdog() } Player.STATE_ENDED -> { + stopFrameWatchdog() delegate?.onPropertyChange("eof-reached", true) delegate?.onEvent("end-file", mapOf("reason" to "eof")) } @@ -549,6 +562,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { override fun onPlayerError(error: PlaybackException) { Log.e(TAG, "Player error: ${error.message} (code: ${error.errorCode})", error) + stopFrameWatchdog() if (currentMediaUri != null) { Log.w(TAG, "ExoPlayer error (code ${error.errorCode}) - attempting fallback to MPV") @@ -763,7 +777,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { val selector = trackSelector ?: return val player = exoPlayer ?: return val currentSpeed = player.playbackParameters.speed - val shouldTunnel = (currentSpeed == 1f) && !tunnelingDisabledForCodec + val shouldTunnel = tunnelingUserEnabled && (currentSpeed == 1f) && !tunnelingDisabledForCodec val currentTunneling = selector.parameters.tunnelingEnabled if (shouldTunnel == currentTunneling) return // No change needed Log.d(TAG, "updateTunnelingState: tunneling $currentTunneling -> $shouldTunnel") @@ -795,6 +809,55 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { } } + // Frame watchdog: detects when ExoPlayer plays audio but renders 0 video frames + // (common with HDR tunneling on unsupported devices — black screen, no error) + + private fun startFrameWatchdog() { + stopFrameWatchdog() + frameWatchdogStartTime = System.currentTimeMillis() + frameWatchdogRunnable = object : Runnable { + override fun run() { + val player = exoPlayer ?: return + val renderedFrames = player.videoDecoderCounters?.renderedOutputBufferCount ?: 0 + + if (renderedFrames > 0) { + Log.d(TAG, "Frame watchdog: $renderedFrames frames rendered, stopping watchdog") + stopFrameWatchdog() + return + } + + val elapsed = System.currentTimeMillis() - frameWatchdogStartTime + + // Check if we have a video track selected + val hasVideoTrack = player.currentTracks.groups.any { + it.type == C.TRACK_TYPE_VIDEO && it.isSelected + } + + if (elapsed >= WATCHDOG_TIMEOUT_MS && player.isPlaying && hasVideoTrack) { + Log.w(TAG, "Frame watchdog: 0 frames rendered after ${elapsed}ms with playing video — triggering MPV fallback") + stopFrameWatchdog() + // Trigger fallback via the same delegate path as player errors + val uri = currentMediaUri ?: return + delegate?.onFormatUnsupported( + uri = uri, + headers = currentHeaders, + positionMs = player.currentPosition, + errorMessage = "Black screen detected: 0 video frames rendered after ${elapsed}ms" + ) + return + } + + handler.postDelayed(this, WATCHDOG_CHECK_INTERVAL_MS) + } + } + handler.postDelayed(frameWatchdogRunnable!!, WATCHDOG_CHECK_INTERVAL_MS) + } + + private fun stopFrameWatchdog() { + frameWatchdogRunnable?.let { handler.removeCallbacks(it) } + frameWatchdogRunnable = null + } + // Public API fun open(uri: String, headers: Map?, startPositionMs: Long, autoPlay: Boolean, isLive: Boolean = false) { @@ -866,6 +929,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { } fun stop() { + stopFrameWatchdog() exoPlayer?.stop() setVisible(false) } @@ -910,7 +974,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { } val currentSpeed = player.playbackParameters.speed - val shouldTunnel = (currentSpeed == 1f) && !tunnelingDisabledForCodec + val shouldTunnel = tunnelingUserEnabled && (currentSpeed == 1f) && !tunnelingDisabledForCodec selector.parameters = selector.buildUponParameters() .setOverrideForType(TrackSelectionOverride(group.mediaTrackGroup, 0)) @@ -1389,6 +1453,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { fun dispose() { Log.d(TAG, "Disposing") + stopFrameWatchdog() stopPositionUpdates() clearVideoFrameRate() abandonAudioFocus() diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt index 3f35bf28..072eae55 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerPlugin.kt @@ -34,6 +34,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, private var activityBinding: ActivityPluginBinding? = null private val nameToId = mutableMapOf() private var configuredBufferSizeBytes: Int? = null + private var configuredTunnelingEnabled: Boolean = true // FlutterPlugin @@ -148,14 +149,19 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, } val bufferSizeBytes = call.argument("bufferSizeBytes") + val tunnelingEnabled = call.argument("tunnelingEnabled") ?: true configuredBufferSizeBytes = bufferSizeBytes + configuredTunnelingEnabled = tunnelingEnabled currentActivity.runOnUiThread { try { playerCore = ExoPlayerCore(currentActivity).apply { delegate = this@ExoPlayerPlugin } - val success = playerCore?.initialize(bufferSizeBytes = bufferSizeBytes) ?: false + val success = playerCore?.initialize( + bufferSizeBytes = bufferSizeBytes, + tunnelingEnabled = tunnelingEnabled, + ) ?: false // Start hidden playerCore?.setVisible(false) diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index 7809526b..5330581f 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -199,6 +199,8 @@ "discordRichPresenceDescription": "Zeige auf Discord, was du gerade schaust", "matchContentFrameRate": "Inhalts-Bildrate anpassen", "matchContentFrameRateDescription": "Bildwiederholfrequenz des Displays an den Videoinhalt anpassen, reduziert Ruckeln und spart Akku", + "tunneledPlayback": "Tunneled Playback", + "tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content", "requireProfileSelectionOnOpen": "Profil beim Öffnen abfragen", "requireProfileSelectionOnOpenDescription": "Profilauswahl bei jedem Öffnen der App anzeigen", "confirmExitOnBack": "Vor dem Beenden bestätigen", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index 66d6d471..bdd987ab 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -199,6 +199,8 @@ "discordRichPresenceDescription": "Show what you're watching on Discord", "matchContentFrameRate": "Match Content Frame Rate", "matchContentFrameRateDescription": "Adjust display refresh rate to match video content, reducing judder and saving battery", + "tunneledPlayback": "Tunneled Playback", + "tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content", "requireProfileSelectionOnOpen": "Ask for profile on app open", "requireProfileSelectionOnOpenDescription": "Show profile selection every time the app is opened", "confirmExitOnBack": "Confirm before exiting", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index fbcc5040..7bb302ee 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -199,6 +199,8 @@ "discordRichPresenceDescription": "Mostrar lo que estás viendo en Discord", "matchContentFrameRate": "Ajustar frecuencia de actualización", "matchContentFrameRateDescription": "Ajustar la frecuencia de actualización de la pantalla para que coincida con el video, reduciendo tirones y ahorrando batería", + "tunneledPlayback": "Tunneled Playback", + "tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content", "requireProfileSelectionOnOpen": "Pedir perfil al abrir la app", "requireProfileSelectionOnOpenDescription": "Mostrar selección de perfil cada vez que se abre la aplicación", "confirmExitOnBack": "Confirmar antes de salir", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index e716530f..ed0d490b 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -199,6 +199,8 @@ "discordRichPresenceDescription": "Montrez ce que vous regardez sur Discord", "matchContentFrameRate": "Fréquence d'images du contenu correspondant", "matchContentFrameRateDescription": "Ajustez la fréquence de rafraîchissement de l'écran en fonction du contenu vidéo, ce qui réduit les saccades et économise la batterie", + "tunneledPlayback": "Tunneled Playback", + "tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content", "requireProfileSelectionOnOpen": "Demander le profil à l'ouverture", "requireProfileSelectionOnOpenDescription": "Afficher la sélection de profil à chaque ouverture de l'application", "confirmExitOnBack": "Confirmer avant de quitter", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 990d31fd..ecfdeaf4 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -199,6 +199,8 @@ "discordRichPresenceDescription": "Mostra su Discord cosa stai guardando", "matchContentFrameRate": "Adatta frequenza fotogrammi", "matchContentFrameRateDescription": "Regola la frequenza di aggiornamento del display in base al contenuto video, riducendo i tremolii e risparmiando batteria", + "tunneledPlayback": "Tunneled Playback", + "tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content", "requireProfileSelectionOnOpen": "Chiedi profilo all'apertura", "requireProfileSelectionOnOpenDescription": "Mostra la selezione del profilo ogni volta che l'app viene aperta", "confirmExitOnBack": "Conferma prima di uscire", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index d275569a..6bf5b667 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -199,6 +199,8 @@ "discordRichPresenceDescription": "Discord에서 시청 중인 콘텐츠 표시", "matchContentFrameRate": "콘텐츠 프레임 레이트 맞춤", "matchContentFrameRateDescription": "비디오 콘텐츠에 맞게 디스플레이 주사율을 조정하여 떨림을 줄이고 배터리를 절약합니다", + "tunneledPlayback": "Tunneled Playback", + "tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content", "requireProfileSelectionOnOpen": "앱 실행 시 프로필 선택", "requireProfileSelectionOnOpenDescription": "앱을 열 때마다 프로필 선택 화면을 표시합니다", "confirmExitOnBack": "종료 전 확인", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index e929d31b..afbe1774 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -199,6 +199,8 @@ "discordRichPresenceDescription": "Toon op Discord wat je aan het kijken bent", "matchContentFrameRate": "Inhoudsframesnelheid afstemmen", "matchContentFrameRateDescription": "Pas de schermverversingssnelheid aan op de video-inhoud, vermindert haperingen en bespaart batterij", + "tunneledPlayback": "Tunneled Playback", + "tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content", "requireProfileSelectionOnOpen": "Vraag om profiel bij openen", "requireProfileSelectionOnOpenDescription": "Toon profielselectie telkens wanneer de app wordt geopend", "confirmExitOnBack": "Bevestigen voor afsluiten", diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index 7523dd31..cd7dbf3a 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -318,6 +318,8 @@ class _TranslationsSettingsDe implements TranslationsSettingsEn { @override String get discordRichPresenceDescription => 'Zeige auf Discord, was du gerade schaust'; @override String get matchContentFrameRate => 'Inhalts-Bildrate anpassen'; @override String get matchContentFrameRateDescription => 'Bildwiederholfrequenz des Displays an den Videoinhalt anpassen, reduziert Ruckeln und spart Akku'; + @override String get tunneledPlayback => 'Tunneled Playback'; + @override String get tunneledPlaybackDescription => 'Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content'; @override String get requireProfileSelectionOnOpen => 'Profil beim Öffnen abfragen'; @override String get requireProfileSelectionOnOpenDescription => 'Profilauswahl bei jedem Öffnen der App anzeigen'; @override String get confirmExitOnBack => 'Vor dem Beenden bestätigen'; @@ -1485,6 +1487,8 @@ extension on TranslationsDe { 'settings.discordRichPresenceDescription' => 'Zeige auf Discord, was du gerade schaust', 'settings.matchContentFrameRate' => 'Inhalts-Bildrate anpassen', 'settings.matchContentFrameRateDescription' => 'Bildwiederholfrequenz des Displays an den Videoinhalt anpassen, reduziert Ruckeln und spart Akku', + 'settings.tunneledPlayback' => 'Tunneled Playback', + 'settings.tunneledPlaybackDescription' => 'Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content', 'settings.requireProfileSelectionOnOpen' => 'Profil beim Öffnen abfragen', 'settings.requireProfileSelectionOnOpenDescription' => 'Profilauswahl bei jedem Öffnen der App anzeigen', 'settings.confirmExitOnBack' => 'Vor dem Beenden bestätigen', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index d4a20f84..a3c76800 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -700,6 +700,12 @@ class TranslationsSettingsEn { /// en: 'Adjust display refresh rate to match video content, reducing judder and saving battery' String get matchContentFrameRateDescription => 'Adjust display refresh rate to match video content, reducing judder and saving battery'; + /// en: 'Tunneled Playback' + String get tunneledPlayback => 'Tunneled Playback'; + + /// en: 'Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content' + String get tunneledPlaybackDescription => 'Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content'; + /// en: 'Ask for profile on app open' String get requireProfileSelectionOnOpen => 'Ask for profile on app open'; @@ -3091,6 +3097,8 @@ extension on Translations { 'settings.discordRichPresenceDescription' => 'Show what you\'re watching on Discord', 'settings.matchContentFrameRate' => 'Match Content Frame Rate', 'settings.matchContentFrameRateDescription' => 'Adjust display refresh rate to match video content, reducing judder and saving battery', + 'settings.tunneledPlayback' => 'Tunneled Playback', + 'settings.tunneledPlaybackDescription' => 'Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content', 'settings.requireProfileSelectionOnOpen' => 'Ask for profile on app open', 'settings.requireProfileSelectionOnOpenDescription' => 'Show profile selection every time the app is opened', 'settings.confirmExitOnBack' => 'Confirm before exiting', diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index ecde52e4..4d6db313 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -318,6 +318,8 @@ class _TranslationsSettingsEs implements TranslationsSettingsEn { @override String get discordRichPresenceDescription => 'Mostrar lo que estás viendo en Discord'; @override String get matchContentFrameRate => 'Ajustar frecuencia de actualización'; @override String get matchContentFrameRateDescription => 'Ajustar la frecuencia de actualización de la pantalla para que coincida con el video, reduciendo tirones y ahorrando batería'; + @override String get tunneledPlayback => 'Tunneled Playback'; + @override String get tunneledPlaybackDescription => 'Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content'; @override String get requireProfileSelectionOnOpen => 'Pedir perfil al abrir la app'; @override String get requireProfileSelectionOnOpenDescription => 'Mostrar selección de perfil cada vez que se abre la aplicación'; @override String get confirmExitOnBack => 'Confirmar antes de salir'; @@ -1485,6 +1487,8 @@ extension on TranslationsEs { 'settings.discordRichPresenceDescription' => 'Mostrar lo que estás viendo en Discord', 'settings.matchContentFrameRate' => 'Ajustar frecuencia de actualización', 'settings.matchContentFrameRateDescription' => 'Ajustar la frecuencia de actualización de la pantalla para que coincida con el video, reduciendo tirones y ahorrando batería', + 'settings.tunneledPlayback' => 'Tunneled Playback', + 'settings.tunneledPlaybackDescription' => 'Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content', 'settings.requireProfileSelectionOnOpen' => 'Pedir perfil al abrir la app', 'settings.requireProfileSelectionOnOpenDescription' => 'Mostrar selección de perfil cada vez que se abre la aplicación', 'settings.confirmExitOnBack' => 'Confirmar antes de salir', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index 81cc059f..02c1e74f 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -318,6 +318,8 @@ class _TranslationsSettingsFr implements TranslationsSettingsEn { @override String get discordRichPresenceDescription => 'Montrez ce que vous regardez sur Discord'; @override String get matchContentFrameRate => 'Fréquence d\'images du contenu correspondant'; @override String get matchContentFrameRateDescription => 'Ajustez la fréquence de rafraîchissement de l\'écran en fonction du contenu vidéo, ce qui réduit les saccades et économise la batterie'; + @override String get tunneledPlayback => 'Tunneled Playback'; + @override String get tunneledPlaybackDescription => 'Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content'; @override String get requireProfileSelectionOnOpen => 'Demander le profil à l\'ouverture'; @override String get requireProfileSelectionOnOpenDescription => 'Afficher la sélection de profil à chaque ouverture de l\'application'; @override String get confirmExitOnBack => 'Confirmer avant de quitter'; @@ -1485,6 +1487,8 @@ extension on TranslationsFr { 'settings.discordRichPresenceDescription' => 'Montrez ce que vous regardez sur Discord', 'settings.matchContentFrameRate' => 'Fréquence d\'images du contenu correspondant', 'settings.matchContentFrameRateDescription' => 'Ajustez la fréquence de rafraîchissement de l\'écran en fonction du contenu vidéo, ce qui réduit les saccades et économise la batterie', + 'settings.tunneledPlayback' => 'Tunneled Playback', + 'settings.tunneledPlaybackDescription' => 'Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content', 'settings.requireProfileSelectionOnOpen' => 'Demander le profil à l\'ouverture', 'settings.requireProfileSelectionOnOpenDescription' => 'Afficher la sélection de profil à chaque ouverture de l\'application', 'settings.confirmExitOnBack' => 'Confirmer avant de quitter', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index a8b63ac8..1242b7f3 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -318,6 +318,8 @@ class _TranslationsSettingsIt implements TranslationsSettingsEn { @override String get discordRichPresenceDescription => 'Mostra su Discord cosa stai guardando'; @override String get matchContentFrameRate => 'Adatta frequenza fotogrammi'; @override String get matchContentFrameRateDescription => 'Regola la frequenza di aggiornamento del display in base al contenuto video, riducendo i tremolii e risparmiando batteria'; + @override String get tunneledPlayback => 'Tunneled Playback'; + @override String get tunneledPlaybackDescription => 'Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content'; @override String get requireProfileSelectionOnOpen => 'Chiedi profilo all\'apertura'; @override String get requireProfileSelectionOnOpenDescription => 'Mostra la selezione del profilo ogni volta che l\'app viene aperta'; @override String get confirmExitOnBack => 'Conferma prima di uscire'; @@ -1485,6 +1487,8 @@ extension on TranslationsIt { 'settings.discordRichPresenceDescription' => 'Mostra su Discord cosa stai guardando', 'settings.matchContentFrameRate' => 'Adatta frequenza fotogrammi', 'settings.matchContentFrameRateDescription' => 'Regola la frequenza di aggiornamento del display in base al contenuto video, riducendo i tremolii e risparmiando batteria', + 'settings.tunneledPlayback' => 'Tunneled Playback', + 'settings.tunneledPlaybackDescription' => 'Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content', 'settings.requireProfileSelectionOnOpen' => 'Chiedi profilo all\'apertura', 'settings.requireProfileSelectionOnOpenDescription' => 'Mostra la selezione del profilo ogni volta che l\'app viene aperta', 'settings.confirmExitOnBack' => 'Conferma prima di uscire', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index 856103ec..730da709 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -318,6 +318,8 @@ class _TranslationsSettingsKo implements TranslationsSettingsEn { @override String get discordRichPresenceDescription => 'Discord에서 시청 중인 콘텐츠 표시'; @override String get matchContentFrameRate => '콘텐츠 프레임 레이트 맞춤'; @override String get matchContentFrameRateDescription => '비디오 콘텐츠에 맞게 디스플레이 주사율을 조정하여 떨림을 줄이고 배터리를 절약합니다'; + @override String get tunneledPlayback => 'Tunneled Playback'; + @override String get tunneledPlaybackDescription => 'Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content'; @override String get requireProfileSelectionOnOpen => '앱 실행 시 프로필 선택'; @override String get requireProfileSelectionOnOpenDescription => '앱을 열 때마다 프로필 선택 화면을 표시합니다'; @override String get confirmExitOnBack => '종료 전 확인'; @@ -1485,6 +1487,8 @@ extension on TranslationsKo { 'settings.discordRichPresenceDescription' => 'Discord에서 시청 중인 콘텐츠 표시', 'settings.matchContentFrameRate' => '콘텐츠 프레임 레이트 맞춤', 'settings.matchContentFrameRateDescription' => '비디오 콘텐츠에 맞게 디스플레이 주사율을 조정하여 떨림을 줄이고 배터리를 절약합니다', + 'settings.tunneledPlayback' => 'Tunneled Playback', + 'settings.tunneledPlaybackDescription' => 'Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content', 'settings.requireProfileSelectionOnOpen' => '앱 실행 시 프로필 선택', 'settings.requireProfileSelectionOnOpenDescription' => '앱을 열 때마다 프로필 선택 화면을 표시합니다', 'settings.confirmExitOnBack' => '종료 전 확인', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index e7aad0f8..b83be9f9 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -318,6 +318,8 @@ class _TranslationsSettingsNl implements TranslationsSettingsEn { @override String get discordRichPresenceDescription => 'Toon op Discord wat je aan het kijken bent'; @override String get matchContentFrameRate => 'Inhoudsframesnelheid afstemmen'; @override String get matchContentFrameRateDescription => 'Pas de schermverversingssnelheid aan op de video-inhoud, vermindert haperingen en bespaart batterij'; + @override String get tunneledPlayback => 'Tunneled Playback'; + @override String get tunneledPlaybackDescription => 'Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content'; @override String get requireProfileSelectionOnOpen => 'Vraag om profiel bij openen'; @override String get requireProfileSelectionOnOpenDescription => 'Toon profielselectie telkens wanneer de app wordt geopend'; @override String get confirmExitOnBack => 'Bevestigen voor afsluiten'; @@ -1485,6 +1487,8 @@ extension on TranslationsNl { 'settings.discordRichPresenceDescription' => 'Toon op Discord wat je aan het kijken bent', 'settings.matchContentFrameRate' => 'Inhoudsframesnelheid afstemmen', 'settings.matchContentFrameRateDescription' => 'Pas de schermverversingssnelheid aan op de video-inhoud, vermindert haperingen en bespaart batterij', + 'settings.tunneledPlayback' => 'Tunneled Playback', + 'settings.tunneledPlaybackDescription' => 'Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content', 'settings.requireProfileSelectionOnOpen' => 'Vraag om profiel bij openen', 'settings.requireProfileSelectionOnOpenDescription' => 'Toon profielselectie telkens wanneer de app wordt geopend', 'settings.confirmExitOnBack' => 'Bevestigen voor afsluiten', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index c94897e5..c60a4334 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -318,6 +318,8 @@ class _TranslationsSettingsSv implements TranslationsSettingsEn { @override String get discordRichPresenceDescription => 'Visa vad du tittar på i Discord'; @override String get matchContentFrameRate => 'Matcha innehållets bildfrekvens'; @override String get matchContentFrameRateDescription => 'Justera skärmens uppdateringsfrekvens för att matcha videoinnehållet, minskar hackighet och sparar batteri'; + @override String get tunneledPlayback => 'Tunneled Playback'; + @override String get tunneledPlaybackDescription => 'Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content'; @override String get requireProfileSelectionOnOpen => 'Fråga efter profil vid appstart'; @override String get requireProfileSelectionOnOpenDescription => 'Visa profilval varje gång appen öppnas'; @override String get confirmExitOnBack => 'Bekräfta innan avslut'; @@ -1485,6 +1487,8 @@ extension on TranslationsSv { 'settings.discordRichPresenceDescription' => 'Visa vad du tittar på i Discord', 'settings.matchContentFrameRate' => 'Matcha innehållets bildfrekvens', 'settings.matchContentFrameRateDescription' => 'Justera skärmens uppdateringsfrekvens för att matcha videoinnehållet, minskar hackighet och sparar batteri', + 'settings.tunneledPlayback' => 'Tunneled Playback', + 'settings.tunneledPlaybackDescription' => 'Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content', 'settings.requireProfileSelectionOnOpen' => 'Fråga efter profil vid appstart', 'settings.requireProfileSelectionOnOpenDescription' => 'Visa profilval varje gång appen öppnas', 'settings.confirmExitOnBack' => 'Bekräfta innan avslut', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index f6dbcb8e..676a0684 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -318,6 +318,8 @@ class _TranslationsSettingsZh implements TranslationsSettingsEn { @override String get discordRichPresenceDescription => '在 Discord 上显示您正在观看的内容'; @override String get matchContentFrameRate => '匹配内容帧率'; @override String get matchContentFrameRateDescription => '调整显示刷新率以匹配视频内容,减少画面抖动并节省电量'; + @override String get tunneledPlayback => 'Tunneled Playback'; + @override String get tunneledPlaybackDescription => 'Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content'; @override String get requireProfileSelectionOnOpen => '打开应用时询问配置文件'; @override String get requireProfileSelectionOnOpenDescription => '每次打开应用时显示配置文件选择'; @override String get confirmExitOnBack => '退出前确认'; @@ -1485,6 +1487,8 @@ extension on TranslationsZh { 'settings.discordRichPresenceDescription' => '在 Discord 上显示您正在观看的内容', 'settings.matchContentFrameRate' => '匹配内容帧率', 'settings.matchContentFrameRateDescription' => '调整显示刷新率以匹配视频内容,减少画面抖动并节省电量', + 'settings.tunneledPlayback' => 'Tunneled Playback', + 'settings.tunneledPlaybackDescription' => 'Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content', 'settings.requireProfileSelectionOnOpen' => '打开应用时询问配置文件', 'settings.requireProfileSelectionOnOpenDescription' => '每次打开应用时显示配置文件选择', 'settings.confirmExitOnBack' => '退出前确认', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 6a08c73f..ffe11772 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -199,6 +199,8 @@ "discordRichPresenceDescription": "Visa vad du tittar på i Discord", "matchContentFrameRate": "Matcha innehållets bildfrekvens", "matchContentFrameRateDescription": "Justera skärmens uppdateringsfrekvens för att matcha videoinnehållet, minskar hackighet och sparar batteri", + "tunneledPlayback": "Tunneled Playback", + "tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content", "requireProfileSelectionOnOpen": "Fråga efter profil vid appstart", "requireProfileSelectionOnOpenDescription": "Visa profilval varje gång appen öppnas", "confirmExitOnBack": "Bekräfta innan avslut", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index 3976017e..1466a6b5 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -199,6 +199,8 @@ "discordRichPresenceDescription": "在 Discord 上显示您正在观看的内容", "matchContentFrameRate": "匹配内容帧率", "matchContentFrameRateDescription": "调整显示刷新率以匹配视频内容,减少画面抖动并节省电量", + "tunneledPlayback": "Tunneled Playback", + "tunneledPlaybackDescription": "Use hardware-accelerated video tunneling. Disable if you see a black screen with audio on HDR content", "requireProfileSelectionOnOpen": "打开应用时询问配置文件", "requireProfileSelectionOnOpenDescription": "每次打开应用时显示配置文件选择", "confirmExitOnBack": "退出前确认", diff --git a/lib/mpv/player/platform/player_android.dart b/lib/mpv/player/platform/player_android.dart index ec1f2250..098a1c19 100644 --- a/lib/mpv/player/platform/player_android.dart +++ b/lib/mpv/player/platform/player_android.dart @@ -10,6 +10,7 @@ class PlayerAndroid extends PlayerBase { static const _eventChannel = EventChannel('com.plezy/exo_player/events'); int? _bufferSizeBytes; + bool _tunnelingEnabled = true; @override MethodChannel get methodChannel => _methodChannel; @@ -50,6 +51,7 @@ class PlayerAndroid extends PlayerBase { try { final result = await methodChannel.invokeMethod('initialize', { 'bufferSizeBytes': _bufferSizeBytes, + 'tunnelingEnabled': _tunnelingEnabled, }); initialized = result == true; if (!initialized) { @@ -188,6 +190,9 @@ class PlayerAndroid extends PlayerBase { case 'demuxer-max-bytes': _bufferSizeBytes = int.tryParse(value); break; + case 'tunneled-playback': + _tunnelingEnabled = value != 'no'; + break; // Other properties are no-ops for ExoPlayer } } diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index bb844658..196a90f0 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -78,6 +78,7 @@ class _SettingsScreenState extends State with FocusableTab { static const _kExternalPlayer = 'external_player'; static const _kHardwareDecoding = 'hardware_decoding'; static const _kMatchContentFrameRate = 'match_content_frame_rate'; + static const _kTunneledPlayback = 'tunneled_playback'; static const _kBufferSize = 'buffer_size'; static const _kSubtitleStyling = 'subtitle_styling'; static const _kMpvConfig = 'mpv_config'; @@ -121,6 +122,7 @@ class _SettingsScreenState extends State with FocusableTab { int _maxVolume = 100; bool _enableDiscordRPC = false; bool _matchContentFrameRate = false; + bool _tunneledPlayback = true; bool _useExoPlayer = true; // Android only: ExoPlayer vs MPV bool _requireProfileSelectionOnOpen = false; bool _useExternalPlayer = false; @@ -195,6 +197,7 @@ class _SettingsScreenState extends State with FocusableTab { _maxVolume = _settingsService.getMaxVolume(); _enableDiscordRPC = _settingsService.getEnableDiscordRPC(); _matchContentFrameRate = _settingsService.getMatchContentFrameRate(); + _tunneledPlayback = _settingsService.getTunneledPlayback(); _useExoPlayer = _settingsService.getUseExoPlayer(); _requireProfileSelectionOnOpen = _settingsService.getRequireProfileSelectionOnOpen(); _useExternalPlayer = _settingsService.getUseExternalPlayer(); @@ -498,6 +501,20 @@ class _SettingsScreenState extends State with FocusableTab { await _settingsService.setMatchContentFrameRate(value); }, ), + if (Platform.isAndroid && _useExoPlayer) + SwitchListTile( + focusNode: _focusTracker.get(_kTunneledPlayback), + secondary: const AppIcon(Symbols.tv_options_input_settings_rounded, fill: 1), + title: Text(t.settings.tunneledPlayback), + subtitle: Text(t.settings.tunneledPlaybackDescription), + value: _tunneledPlayback, + onChanged: (value) async { + setState(() { + _tunneledPlayback = value; + }); + await _settingsService.setTunneledPlayback(value); + }, + ), ListTile( focusNode: _focusTracker.get(_kBufferSize), leading: const AppIcon(Symbols.memory_rounded, fill: 1), diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 0aa9f2aa..ba769f72 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -403,6 +403,10 @@ class VideoPlayerScreenState extends State with WidgetsBindin player = Player(useExoPlayer: useExoPlayer); await player!.setProperty('sub-ass', 'yes'); // Enable libass + if (Platform.isAndroid && useExoPlayer) { + final tunneledPlayback = settingsService.getTunneledPlayback(); + await player!.setProperty('tunneled-playback', tunneledPlayback ? 'yes' : 'no'); + } if (bufferSizeMB > 0) { final bufferSizeBytes = bufferSizeMB * 1024 * 1024; await player!.setProperty('demuxer-max-bytes', bufferSizeBytes.toString()); diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 9e4afabc..f050dcd8 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -65,6 +65,7 @@ class SettingsService extends BaseSharedPreferencesService { static const String _keyMaxVolume = 'max_volume'; static const String _keyEnableDiscordRPC = 'enable_discord_rpc'; static const String _keyMatchContentFrameRate = 'match_content_frame_rate'; + static const String _keyTunneledPlayback = 'tunneled_playback'; static const String _keyDefaultPlaybackSpeed = 'default_playback_speed'; static const String _keyAutoPlayNextEpisode = 'auto_play_next_episode'; static const String _keyUseExoPlayer = 'use_exoplayer'; @@ -993,6 +994,15 @@ class SettingsService extends BaseSharedPreferencesService { return prefs.getBool(_keyMatchContentFrameRate) ?? false; // Default disabled } + // Tunneled Playback (Android ExoPlayer only) + Future setTunneledPlayback(bool enabled) async { + await prefs.setBool(_keyTunneledPlayback, enabled); + } + + bool getTunneledPlayback() { + return prefs.getBool(_keyTunneledPlayback) ?? true; // Default: enabled + } + // Default Playback Speed (0.5 to 3.0) Future setDefaultPlaybackSpeed(double speed) async { await prefs.setDouble(_keyDefaultPlaybackSpeed, speed.clamp(0.5, 3.0)); @@ -1202,6 +1212,7 @@ class SettingsService extends BaseSharedPreferencesService { prefs.remove(_keyMpvConfigPresets), prefs.remove(_keyEnableDiscordRPC), prefs.remove(_keyMatchContentFrameRate), + prefs.remove(_keyTunneledPlayback), prefs.remove(_keyDefaultPlaybackSpeed), prefs.remove(_keyAutoPlayNextEpisode), prefs.remove(_keyUseExoPlayer),