From 36277076bd846e768f0e4bb7655159d4cde1b6d9 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Mon, 2 Mar 2026 02:30:48 +0100 Subject: [PATCH] fix(android): cap demuxer buffers + sync mpv dispose --- .../com/edde746/plezy/mpv/MpvPlayerCore.kt | 14 +++++++++-- .../com/edde746/plezy/mpv/MpvPlayerPlugin.kt | 12 ++++++---- lib/screens/video_player_screen.dart | 24 +++++++++++++++++++ 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt index 462c6bf9..c1bc8c58 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt @@ -471,8 +471,11 @@ class MpvPlayerCore(private val activity: Activity) : // Cleanup - fun dispose() { - if (disposing) return + fun dispose(onComplete: (() -> Unit)? = null) { + if (disposing) { + onComplete?.invoke() + return + } disposing = true Log.d(TAG, "Disposing") @@ -519,6 +522,10 @@ class MpvPlayerCore(private val activity: Activity) : // Run native destroy on background thread to avoid ANR — // MPVLib.destroy() blocks on pthread_cond_wait while mpv's // internal threads (lua, demux, vo) shut down. + // The completion callback is posted to the main thread so the + // caller (MpvPlayerPlugin) can defer result.success() until + // native resources are fully released, preventing a new mpv + // instance from being created while the old one still holds memory. if (nativeReady) { Thread { synchronized(mpvLock) { @@ -531,7 +538,10 @@ class MpvPlayerCore(private val activity: Activity) : } } Log.d(TAG, "Disposed (native)") + onComplete?.let { Handler(Looper.getMainLooper()).post(it) } }.start() + } else { + onComplete?.invoke() } } } diff --git a/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerPlugin.kt b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerPlugin.kt index 37eacdac..29a0f2fc 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerPlugin.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerPlugin.kt @@ -142,10 +142,14 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, private fun handleDispose(result: MethodChannel.Result) { activity?.runOnUiThread { - playerCore?.dispose() - playerCore = null - Log.d(TAG, "Disposed") - result.success(null) + playerCore?.dispose { + playerCore = null + Log.d(TAG, "Disposed") + result.success(null) + } ?: run { + playerCore = null + result.success(null) + } } ?: result.success(null) } diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 9969e8f5..01516cf3 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:plezy/widgets/app_icon.dart'; @@ -418,6 +419,29 @@ class VideoPlayerScreenState extends State with WidgetsBindin if (bufferSizeMB > 0) { final bufferSizeBytes = bufferSizeMB * 1024 * 1024; await player!.setProperty('demuxer-max-bytes', bufferSizeBytes.toString()); + // Set back-buffer to 1/4 of forward buffer + final backBytes = bufferSizeBytes ~/ 4; + await player!.setProperty('demuxer-max-back-bytes', backBytes.toString()); + } + if (Platform.isAndroid) { + // Cap demuxer buffers based on device heap to prevent OOM crashes. + // Without limits, mpv defaults can consume 225MB+ just for demuxer + // buffering, which combined with decoded frames and GPU textures + // exhausts the process address space on memory-constrained devices. + final heapMB = await PlayerAndroid.getHeapSize(); + if (heapMB > 0) { + final autoBackMB = heapMB <= 256 ? 16 : (heapMB <= 512 ? 32 : 48); + if (bufferSizeMB == 0) { + // Auto mode: cap both forward and back buffer based on heap + final autoForwardMB = heapMB <= 256 ? 32 : (heapMB <= 512 ? 64 : 100); + await player!.setProperty('demuxer-max-bytes', '${autoForwardMB * 1024 * 1024}'); + await player!.setProperty('demuxer-max-back-bytes', '${autoBackMB * 1024 * 1024}'); + } else { + // Manual mode: cap back-buffer relative to heap if 1/4 ratio is too high + final maxBackBytes = min(bufferSizeMB * 1024 * 1024 ~/ 4, autoBackMB * 1024 * 1024); + await player!.setProperty('demuxer-max-back-bytes', maxBackBytes.toString()); + } + } } await player!.setProperty('msg-level', debugLoggingEnabled ? 'all=debug' : 'all=error'); await player!.setLogLevel(debugLoggingEnabled ? 'v' : 'warn');