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.
This commit is contained in:
edde746
2026-03-01 04:02:36 +01:00
parent 1d739c74ec
commit d0f2122944
24 changed files with 171 additions and 5 deletions
@@ -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<String, String>?, 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()
@@ -34,6 +34,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
private var activityBinding: ActivityPluginBinding? = null
private val nameToId = mutableMapOf<String, Int>()
private var configuredBufferSizeBytes: Int? = null
private var configuredTunnelingEnabled: Boolean = true
// FlutterPlugin
@@ -148,14 +149,19 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
}
val bufferSizeBytes = call.argument<Int>("bufferSizeBytes")
val tunnelingEnabled = call.argument<Boolean>("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)
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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": "종료 전 확인",
+2
View File
@@ -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",
+4
View File
@@ -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',
+8
View File
@@ -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',
+4
View File
@@ -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',
+4
View File
@@ -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',
+4
View File
@@ -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',
+4
View File
@@ -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' => '종료 전 확인',
+4
View File
@@ -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',
+4
View File
@@ -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',
+4
View File
@@ -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' => '退出前确认',
+2
View File
@@ -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",
+2
View File
@@ -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": "退出前确认",
@@ -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<bool>('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
}
}
+17
View File
@@ -78,6 +78,7 @@ class _SettingsScreenState extends State<SettingsScreen> 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<SettingsScreen> 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<SettingsScreen> 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<SettingsScreen> 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),
+4
View File
@@ -403,6 +403,10 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> 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());
+11
View File
@@ -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<void> 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<void> 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),