fix: replace CoreMedia periodic time observer with pure Swift Timer to eliminate autorelease pool race

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)
This commit is contained in:
yumoqing 2026-06-22 01:06:39 +08:00
parent b2e8dfe812
commit 0d63414214
3 changed files with 20 additions and 21 deletions

View File

@ -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 {

View File

@ -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)

View File

@ -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 }