From 2276750704a051f5fe1566db9d7e90d5865e1f63 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Tue, 24 Feb 2026 00:55:53 +0100 Subject: [PATCH] fix: memory-aware buffer limits --- .../edde746/plezy/exoplayer/ExoPlayerCore.kt | 53 ++++++++++++++++--- .../plezy/exoplayer/ExoPlayerPlugin.kt | 9 ++++ lib/mpv/player/player_android.dart | 6 +++ 3 files changed, 61 insertions(+), 7 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 76eb73f3..597e534e 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 @@ -1,7 +1,10 @@ package com.edde746.plezy.exoplayer import android.app.Activity +import android.app.ActivityManager +import android.content.ComponentCallbacks2 import android.content.Context +import android.content.res.Configuration import android.graphics.Color import android.graphics.PixelFormat import android.hardware.display.DisplayManager @@ -102,6 +105,9 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { private var hasAudioFocus: Boolean = false private var wasPlayingBeforeFocusLoss: Boolean = false + // Memory pressure detection + private var memoryCallback: ComponentCallbacks2? = null + // Track state for event emission private var lastPosition: Long = 0 private var lastDuration: Long = 0 @@ -339,15 +345,29 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { val wrappedRenderersFactory = AssRenderersFactory(handler, renderersFactory) - val loadControl = DefaultLoadControl.Builder().apply { - if (bufferSizeBytes != null && bufferSizeBytes > 0) { - setTargetBufferBytes(bufferSizeBytes) - setPrioritizeTimeOverSizeThresholds(false) - Log.d(TAG, "Buffer byte limit set to ${bufferSizeBytes / 1024 / 1024}MB") - } else { - Log.d(TAG, "Buffer in auto mode (time-based thresholds)") + // Compute memory-aware buffer limits to prevent CCodec OOM crashes + val activityManager = activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val memoryInfo = ActivityManager.MemoryInfo() + activityManager.getMemoryInfo(memoryInfo) + val availableMB = memoryInfo.availMem / (1024 * 1024) + + val targetBufferBytes = if (bufferSizeBytes != null && bufferSizeBytes > 0) { + bufferSizeBytes + } else { + // Scale buffer to available memory to reduce hardware decoder pressure + when { + availableMB < 512 -> 50 * 1024 * 1024 + availableMB < 1024 -> 75 * 1024 * 1024 + else -> 150 * 1024 * 1024 } + } + + val loadControl = DefaultLoadControl.Builder().apply { + setTargetBufferBytes(targetBufferBytes) + setPrioritizeTimeOverSizeThresholds(false) + setBufferDurationsMs(15_000, 30_000, 2_500, 5_000) }.build() + Log.d(TAG, "Buffer: ${targetBufferBytes / 1024 / 1024}MB limit, available=${availableMB}MB") exoPlayer = ExoPlayer.Builder(activity) .setTrackSelector(trackSelector!!) @@ -381,6 +401,22 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { } } + // Register memory pressure listener to detect impending OOM + memoryCallback = object : ComponentCallbacks2 { + override fun onTrimMemory(level: Int) { + if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) { + Log.w(TAG, "TRIM_MEMORY level $level - critical memory pressure") + delegate?.onEvent("memory-pressure", mapOf("level" to "critical")) + } + } + override fun onConfigurationChanged(newConfig: Configuration) {} + override fun onLowMemory() { + Log.w(TAG, "onLowMemory - system-wide memory pressure") + delegate?.onEvent("memory-pressure", mapOf("level" to "critical")) + } + } + activity.registerComponentCallbacks(memoryCallback) + // Start position update loop startPositionUpdates() @@ -1266,6 +1302,9 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { abandonAudioFocus() audioManager = null + memoryCallback?.let { activity.unregisterComponentCallbacks(it) } + memoryCallback = null + exoPlayer?.clearVideoSurface() exoPlayer?.removeListener(this) exoPlayer?.release() 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 80df9668..6675b08d 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 @@ -32,6 +32,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, private var activity: Activity? = null private var activityBinding: ActivityPluginBinding? = null private val nameToId = mutableMapOf() + private var configuredBufferSizeBytes: Int? = null // FlutterPlugin @@ -145,6 +146,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, } val bufferSizeBytes = call.argument("bufferSizeBytes") + configuredBufferSizeBytes = bufferSizeBytes currentActivity.runOnUiThread { try { @@ -598,6 +600,13 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, mpvCore?.setProperty("vo", "gpu") mpvCore?.setProperty("ao", "audiotrack") + // Forward user's buffer config to MPV fallback + configuredBufferSizeBytes?.let { bytes -> + if (bytes > 0) { + mpvCore?.setProperty("demuxer-max-bytes", bytes.toString()) + } + } + // Setup property observers mpvCore?.observeProperty("time-pos", "double") mpvCore?.observeProperty("duration", "double") diff --git a/lib/mpv/player/player_android.dart b/lib/mpv/player/player_android.dart index 98c6c269..3ee3dc4d 100644 --- a/lib/mpv/player/player_android.dart +++ b/lib/mpv/player/player_android.dart @@ -36,6 +36,12 @@ class PlayerAndroid extends PlayerBase { return; } + if (name == 'memory-pressure') { + // System memory is critically low — playback may be at risk of OOM crash + errorController.add('Low memory — playback may be unstable'); + return; + } + // Delegate to base class for common events super.handlePlayerEvent(name, data); }