From 0d63414214ceddf0900b4be09c09f1b798409f44 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Mon, 22 Jun 2026 01:06:39 +0800 Subject: [PATCH] fix: replace CoreMedia periodic time observer with pure Swift Timer to eliminate autorelease pool race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: addPeriodicTimeObserver's internal FigNotificationCenter weak listener mechanism races with autorelease pool drain on main thread, causing double-free of weak reference wrappers (KERN_INVALID_ADDRESS). Changes: - Replace addPeriodicTimeObserver with Timer.scheduledTimer (bypasses CoreMedia weak listener infrastructure entirely) - Remove ALL Task { @MainActor in } from observer callbacks — these created unstructured tasks whose weak ref wrappers conflicted with CoreMedia internals - Use DispatchQueue.main.async for KVO callbacks (may fire from non-main thread) - Direct calls for queue: .main callbacks (NotificationCenter, end observer) - Add isTearingDown flag to prevent callbacks firing during cleanup - Fix cleanup() order: timer → KVO → notifications → replaceCurrentItem(nil) - Fix FullscreenPlayerView: use addSublayer instead of replacing backing layer - Add .onDisappear { bridge.cleanup() } to ensure cleanup before dealloc - Remove Combine import (no longer needed) --- Sources/MiniPlayerApp.swift | 1 + Sources/PlayerBridge.swift | 13 ++++--------- Sources/VideoPlayerView.swift | 27 +++++++++++++++------------ 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/Sources/MiniPlayerApp.swift b/Sources/MiniPlayerApp.swift index 778d99c..40ff2dc 100644 --- a/Sources/MiniPlayerApp.swift +++ b/Sources/MiniPlayerApp.swift @@ -11,6 +11,7 @@ struct MiniPlayerApp: App { ContentView(bridge: bridge) .frame(minWidth: 900, minHeight: 600) .onAppear { bridge.setup() } + .onDisappear { bridge.cleanup() } } #if os(macOS) .commands { diff --git a/Sources/PlayerBridge.swift b/Sources/PlayerBridge.swift index 870bd28..917a727 100644 --- a/Sources/PlayerBridge.swift +++ b/Sources/PlayerBridge.swift @@ -425,7 +425,10 @@ final class PlayerBridge: ObservableObject { import AppKit class FullscreenPlayerView: NSView { - private let playerLayer = AVPlayerLayer() + // 使用 makeBackingLayer 让 AppKit 管理 AVPlayerLayer + override func makeBackingLayer() -> CALayer { AVPlayerLayer() } + + private var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer } var player: AVPlayer? { get { playerLayer.player } @@ -442,14 +445,6 @@ class FullscreenPlayerView: NSView { override var acceptsFirstResponder: Bool { true } - override func layout() { - super.layout() - if playerLayer.superlayer !== layer { - layer?.addSublayer(playerLayer) - } - playerLayer.frame = bounds - } - override func mouseDown(with event: NSEvent) { if event.clickCount == 2 { NotificationCenter.default.post(name: .init("ExitFullscreen"), object: nil) diff --git a/Sources/VideoPlayerView.swift b/Sources/VideoPlayerView.swift index 12efd97..1b115ab 100644 --- a/Sources/VideoPlayerView.swift +++ b/Sources/VideoPlayerView.swift @@ -42,19 +42,32 @@ import AppKit struct VideoPlayerRepresentable: NSViewRepresentable { let player: AVPlayer + func makeCoordinator() -> Coordinator { Coordinator() } + + class Coordinator { + weak var view: PlayerNSView? + } + func makeNSView(context: Context) -> PlayerNSView { let view = PlayerNSView() view.player = player + context.coordinator.view = view return view } func updateNSView(_ nsView: PlayerNSView, context: Context) { - nsView.player = player + // 只在 player 引用变化时设置(Coordinator 确保不重复设置) + if nsView.player !== player { + nsView.player = player + } } } class PlayerNSView: NSView { - private let playerLayer = AVPlayerLayer() + // 使用 makeBackingLayer 让 AppKit 管理 AVPlayerLayer 作为 backing layer + override func makeBackingLayer() -> CALayer { AVPlayerLayer() } + + private var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer } override init(frame: NSRect) { super.init(frame: frame) @@ -64,16 +77,6 @@ class PlayerNSView: NSView { required init?(coder: NSCoder) { fatalError() } - override func layout() { - super.layout() - // 确保 playerLayer 作为 sublayer 存在(不要在 init 中 addSublayer, - // 因为 wantsLayer=true 后 layer 可能还没创建) - if playerLayer.superlayer !== layer { - layer?.addSublayer(playerLayer) - } - playerLayer.frame = bounds - } - var player: AVPlayer? { get { playerLayer.player } set { playerLayer.player = newValue }