From 6fe464088b4813ae1e058ee392185216a8c34876 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sat, 30 May 2026 10:40:10 +0200 Subject: [PATCH] fix(mpv): prevent UI freeze on player exit by tearing down mpv off main thread mpv_terminate_destroy() blocks until mpv's threads (including the vo_avfoundation output thread) are joined, and that teardown needs the main run loop to complete. Calling it via queue.sync from MpvPlayerView deinit (main thread) deadlocked/froze the UI on playback exit. Remove the wakeup callback synchronously while self is still alive, then run mpv_terminate_destroy on the serial queue via async so deinit returns immediately and the main thread is never blocked. Also release the PiP timebase/controller in deinit. --- modules/mpv-player/ios/MPVLayerRenderer.swift | 25 ++++++++++++------- modules/mpv-player/ios/PiPController.swift | 10 ++++++++ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/modules/mpv-player/ios/MPVLayerRenderer.swift b/modules/mpv-player/ios/MPVLayerRenderer.swift index e6686a815..7b20c990e 100644 --- a/modules/mpv-player/ios/MPVLayerRenderer.swift +++ b/modules/mpv-player/ios/MPVLayerRenderer.swift @@ -220,20 +220,27 @@ final class MPVLayerRenderer { statusObservation?.invalidate() statusObservation = nil - queue.sync { [weak self] in - guard let self, let handle = self.mpv else { return } - - mpv_set_wakeup_callback(handle, nil, nil) - mpv_terminate_destroy(handle) + if let handle = self.mpv { self.mpv = nil + // Remove the wakeup callback synchronously while `self` is still + // alive so it can never fire against a deallocated instance. + mpv_set_wakeup_callback(handle, nil, nil) + // Destroy mpv OFF the main thread. mpv_terminate_destroy() blocks + // until all mpv threads (including the vo_avfoundation output thread) + // are joined, and that teardown needs the main run loop to finish. + // Calling it via queue.sync from deinit (main thread) deadlocks/freezes + // the UI. queue.async only references `handle`, never `self`. + queue.async { + mpv_terminate_destroy(handle) + } } - DispatchQueue.main.async { [weak self] in - guard let self else { return } + let layer = self.displayLayer + DispatchQueue.main.async { if #available(iOS 18.0, *) { - self.displayLayer.sampleBufferRenderer.flush(removingDisplayedImage: true, completionHandler: nil) + layer.sampleBufferRenderer.flush(removingDisplayedImage: true, completionHandler: nil) } else { - self.displayLayer.flushAndRemoveImage() + layer.flushAndRemoveImage() } } diff --git a/modules/mpv-player/ios/PiPController.swift b/modules/mpv-player/ios/PiPController.swift index 7a58cb38e..6ad0bec51 100644 --- a/modules/mpv-player/ios/PiPController.swift +++ b/modules/mpv-player/ios/PiPController.swift @@ -150,6 +150,16 @@ final class PiPController: NSObject { CMTimebaseSetRate(tb, rate: Float64(rate)) } } + + deinit { + if let tb = timebase { + CMTimebaseSetRate(tb, rate: 0) + } + sampleBufferDisplayLayer?.controlTimebase = nil + timebase = nil + pipController?.delegate = nil + pipController = nil + } } // MARK: - AVPictureInPictureControllerDelegate