From 3b351d76341e11ac53ca51aeb628bb5e29b6cb9b Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:34:30 +0100 Subject: [PATCH] fix: iOS OOM during PiP playback cycles Cancel leaked stream subscriptions in video player dispose and flush sample buffer layer on PiP teardown/cleanup to release video frames. --- ios/Runner/MpvPlayer/MpvPipController.swift | 7 +++++++ ios/Runner/MpvPlayer/MpvPlayerPlugin.swift | 1 + lib/screens/video_player_screen.dart | 12 +++++++++--- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/ios/Runner/MpvPlayer/MpvPipController.swift b/ios/Runner/MpvPlayer/MpvPipController.swift index 3c0f8466..a8ee4c89 100644 --- a/ios/Runner/MpvPlayer/MpvPipController.swift +++ b/ios/Runner/MpvPlayer/MpvPipController.swift @@ -245,10 +245,17 @@ class MpvPipController: NSObject { } pipController = nil delegateHelper = nil + sampleBufferLayer.flushAndRemoveImage() + sampleBufferLayer.controlTimebase = nil sampleBufferLayer.removeFromSuperlayer() containerView?.removeFromSuperview() containerView = nil } + + /// Flush enqueued sample buffers from the layer to free video frame memory + func flushLayer() { + sampleBufferLayer.flushAndRemoveImage() + } } // MARK: - PiP Delegate Helper (iOS 15+) diff --git a/ios/Runner/MpvPlayer/MpvPlayerPlugin.swift b/ios/Runner/MpvPlayer/MpvPlayerPlugin.swift index 510dd1aa..c564a4e2 100644 --- a/ios/Runner/MpvPlayer/MpvPlayerPlugin.swift +++ b/ios/Runner/MpvPlayer/MpvPlayerPlugin.swift @@ -171,6 +171,7 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPlayerD playerCore?.isPipActive = false isManualPipRequest = false stopPipTimebaseSync() + pipController?.flushLayer() _ = playerCore?.switchToGpuNextVO() if pause { playerCore?.setProperty("pause", value: "yes") } if notify { pipChannel?.invokeMethod("onPipChanged", arguments: false) } diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 7a0ac127..e115e283 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -141,6 +141,9 @@ class VideoPlayerScreenState extends State with WidgetsBindin StreamSubscription? _playbackRestartSubscription; StreamSubscription? _backendSwitchedSubscription; StreamSubscription? _sleepTimerSubscription; + StreamSubscription? _mediaControlsPlayingSubscription; + StreamSubscription? _mediaControlsPositionSubscription; + StreamSubscription? _mediaControlsRateSubscription; bool _isReplacingWithVideo = false; // Flag to skip orientation restoration during video-to-video navigation bool _isDisposingForNavigation = false; bool _waitingForExternalSubsTrackSelection = false; @@ -799,12 +802,12 @@ class VideoPlayerScreenState extends State with WidgetsBindin ); // Listen to playing state and update media controls - player!.streams.playing.listen((isPlaying) { + _mediaControlsPlayingSubscription = player!.streams.playing.listen((isPlaying) { _updateMediaControlsPlaybackState(); }); // Listen to position updates for media controls and Discord - player!.streams.position.listen((position) { + _mediaControlsPositionSubscription = player!.streams.position.listen((position) { _mediaControlsManager?.updatePlaybackState( isPlaying: player!.state.playing, position: position, @@ -814,7 +817,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin }); // Listen to playback rate changes for Discord Rich Presence - player!.streams.rate.listen((rate) { + _mediaControlsRateSubscription = player!.streams.rate.listen((rate) { DiscordRPCService.instance.updatePlaybackSpeed(rate); }); @@ -1740,6 +1743,9 @@ class VideoPlayerScreenState extends State with WidgetsBindin _playbackRestartSubscription?.cancel(); _backendSwitchedSubscription?.cancel(); _sleepTimerSubscription?.cancel(); + _mediaControlsPlayingSubscription?.cancel(); + _mediaControlsPositionSubscription?.cancel(); + _mediaControlsRateSubscription?.cancel(); // Cancel auto-play timer _autoPlayTimer?.cancel();