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:
parent
b2e8dfe812
commit
0d63414214
@ -11,6 +11,7 @@ struct MiniPlayerApp: App {
|
|||||||
ContentView(bridge: bridge)
|
ContentView(bridge: bridge)
|
||||||
.frame(minWidth: 900, minHeight: 600)
|
.frame(minWidth: 900, minHeight: 600)
|
||||||
.onAppear { bridge.setup() }
|
.onAppear { bridge.setup() }
|
||||||
|
.onDisappear { bridge.cleanup() }
|
||||||
}
|
}
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.commands {
|
.commands {
|
||||||
|
|||||||
@ -425,7 +425,10 @@ final class PlayerBridge: ObservableObject {
|
|||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
class FullscreenPlayerView: NSView {
|
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? {
|
var player: AVPlayer? {
|
||||||
get { playerLayer.player }
|
get { playerLayer.player }
|
||||||
@ -442,14 +445,6 @@ class FullscreenPlayerView: NSView {
|
|||||||
|
|
||||||
override var acceptsFirstResponder: Bool { true }
|
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) {
|
override func mouseDown(with event: NSEvent) {
|
||||||
if event.clickCount == 2 {
|
if event.clickCount == 2 {
|
||||||
NotificationCenter.default.post(name: .init("ExitFullscreen"), object: nil)
|
NotificationCenter.default.post(name: .init("ExitFullscreen"), object: nil)
|
||||||
|
|||||||
@ -42,19 +42,32 @@ import AppKit
|
|||||||
struct VideoPlayerRepresentable: NSViewRepresentable {
|
struct VideoPlayerRepresentable: NSViewRepresentable {
|
||||||
let player: AVPlayer
|
let player: AVPlayer
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator { Coordinator() }
|
||||||
|
|
||||||
|
class Coordinator {
|
||||||
|
weak var view: PlayerNSView?
|
||||||
|
}
|
||||||
|
|
||||||
func makeNSView(context: Context) -> PlayerNSView {
|
func makeNSView(context: Context) -> PlayerNSView {
|
||||||
let view = PlayerNSView()
|
let view = PlayerNSView()
|
||||||
view.player = player
|
view.player = player
|
||||||
|
context.coordinator.view = view
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateNSView(_ nsView: PlayerNSView, context: Context) {
|
func updateNSView(_ nsView: PlayerNSView, context: Context) {
|
||||||
nsView.player = player
|
// 只在 player 引用变化时设置(Coordinator 确保不重复设置)
|
||||||
|
if nsView.player !== player {
|
||||||
|
nsView.player = player
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlayerNSView: NSView {
|
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) {
|
override init(frame: NSRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
@ -64,16 +77,6 @@ class PlayerNSView: NSView {
|
|||||||
|
|
||||||
required init?(coder: NSCoder) { fatalError() }
|
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? {
|
var player: AVPlayer? {
|
||||||
get { playerLayer.player }
|
get { playerLayer.player }
|
||||||
set { playerLayer.player = newValue }
|
set { playerLayer.player = newValue }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user