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:
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "종료 전 확인",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' => '종료 전 확인',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' => '退出前确认',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user