fix: persistent fullscreen window — never destroy AVPlayerView
Root cause: every fullscreen exit destroyed the AVPlayerView, triggering
CoreMedia's FigNotificationCenterRemoveWeakListeners which races with
the main thread's autorelease pool drain. No amount of nil-ing the
player reference or delaying the close could prevent this race.
Solution: the fullscreen NSWindow + AVPlayerView are created ONCE on
first use and never destroyed. Toggle uses orderFront/orderOut instead
of makeKeyAndOrderFront/close. The AVPlayerView stays alive for the
entire app lifetime, so CoreMedia teardown never happens during toggle.
Also:
- Removed onDisappear { bridge.cleanup() } — cleanup during app
termination races with autorelease pool drain. Process exit handles
all teardown correctly.
- Double-click exit uses DispatchQueue.main.async to avoid calling
toggleFullscreen from inside the event monitor callback.
- isReleasedWhenClosed = false on fullscreen window
This commit is contained in:
parent
85f5699878
commit
cae63c6588
@ -11,7 +11,6 @@ struct MiniPlayerApp: App {
|
||||
ContentView(bridge: bridge)
|
||||
.frame(minWidth: 900, minHeight: 600)
|
||||
.onAppear { bridge.setup() }
|
||||
.onDisappear { bridge.cleanup() }
|
||||
}
|
||||
#if os(macOS)
|
||||
.commands {
|
||||
|
||||
@ -278,68 +278,68 @@ final class PlayerBridge: ObservableObject {
|
||||
}
|
||||
|
||||
// MARK: - 全屏(视频内容全屏,非窗口全屏)
|
||||
// 全屏窗口只创建一次,之后只用 orderFront/orderOut 切换显示
|
||||
// AVPlayerView 永不销毁,彻底避免 CoreMedia weak listener 竞态
|
||||
func toggleFullscreen() {
|
||||
#if os(macOS)
|
||||
if let fw = fullscreenWindow {
|
||||
// 移除事件监控
|
||||
if isFullscreen {
|
||||
// 退出全屏:只隐藏窗口,不销毁
|
||||
fullscreenWindow?.orderOut(nil)
|
||||
isFullscreen = false
|
||||
if let monitor = fullscreenEventMonitor {
|
||||
NSEvent.removeMonitor(monitor)
|
||||
fullscreenEventMonitor = nil
|
||||
}
|
||||
// 先断开 player 引用再关闭窗口
|
||||
if let playerView = fw.contentView as? AVPlayerView {
|
||||
playerView.player = nil
|
||||
}
|
||||
fw.close()
|
||||
fullscreenWindow = nil
|
||||
isFullscreen = false
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
return
|
||||
}
|
||||
|
||||
guard let screen = NSScreen.main else { return }
|
||||
let win = NSWindow(contentRect: screen.frame, styleMask: .borderless, backing: .buffered, defer: false)
|
||||
win.level = .screenSaver
|
||||
win.backgroundColor = .black
|
||||
win.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
||||
win.hasShadow = false
|
||||
// 首次全屏:创建窗口和 AVPlayerView(仅此一次)
|
||||
if fullscreenWindow == nil {
|
||||
guard let screen = NSScreen.main else { return }
|
||||
let win = NSWindow(contentRect: screen.frame, styleMask: .borderless, backing: .buffered, defer: false)
|
||||
win.level = .screenSaver
|
||||
win.backgroundColor = .black
|
||||
win.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
||||
win.hasShadow = false
|
||||
win.isReleasedWhenClosed = false
|
||||
|
||||
let playerView = AVPlayerView(frame: screen.frame)
|
||||
playerView.player = player
|
||||
playerView.controlsStyle = .none
|
||||
playerView.videoGravity = .resizeAspect
|
||||
win.contentView = playerView
|
||||
fullscreenWindow = win
|
||||
}
|
||||
|
||||
// 使用系统 AVPlayerView,Apple 内部处理所有 CoreMedia 生命周期竞态
|
||||
let playerView = AVPlayerView(frame: screen.frame)
|
||||
playerView.player = player
|
||||
playerView.controlsStyle = .none
|
||||
playerView.videoGravity = .resizeAspect
|
||||
win.contentView = playerView
|
||||
win.makeKeyAndOrderFront(nil)
|
||||
fullscreenWindow?.makeKeyAndOrderFront(nil)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
fullscreenWindow = win
|
||||
isFullscreen = true
|
||||
|
||||
// 添加本地事件监控:双击退出全屏、单击暂停/播放、Escape退出
|
||||
fullscreenEventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown, .keyDown]) { [weak self] event in
|
||||
guard let self = self, let fw = self.fullscreenWindow else { return event }
|
||||
// 只处理全屏窗口内的事件
|
||||
if event.window !== fw { return event }
|
||||
|
||||
if event.type == .leftMouseDown {
|
||||
if event.clickCount >= 2 {
|
||||
self.toggleFullscreen()
|
||||
return nil // 消费事件
|
||||
} else {
|
||||
if self.player.timeControlStatus == .playing {
|
||||
self.player.pause()
|
||||
// 事件监控:双击退出、单击暂停/播放、Escape退出
|
||||
if fullscreenEventMonitor == nil {
|
||||
fullscreenEventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown, .keyDown]) { [weak self] event in
|
||||
guard let self = self, self.isFullscreen else { return event }
|
||||
guard event.window === self.fullscreenWindow else { return event }
|
||||
|
||||
if event.type == .leftMouseDown {
|
||||
if event.clickCount >= 2 {
|
||||
DispatchQueue.main.async { self.toggleFullscreen() }
|
||||
return nil
|
||||
} else {
|
||||
self.player.play()
|
||||
if self.player.timeControlStatus == .playing {
|
||||
self.player.pause()
|
||||
} else {
|
||||
self.player.play()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
} else if event.type == .keyDown && event.keyCode == 53 {
|
||||
DispatchQueue.main.async { self.toggleFullscreen() }
|
||||
return nil
|
||||
}
|
||||
} else if event.type == .keyDown {
|
||||
if event.keyCode == 53 { // Escape
|
||||
self.toggleFullscreen()
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
}
|
||||
return event
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@ -437,24 +437,19 @@ final class PlayerBridge: ObservableObject {
|
||||
player.pause()
|
||||
|
||||
#if os(macOS)
|
||||
// 移除全屏事件监控
|
||||
// 移除全屏事件监控(不销毁窗口,避免 teardown 竞态)
|
||||
if let monitor = fullscreenEventMonitor {
|
||||
NSEvent.removeMonitor(monitor)
|
||||
fullscreenEventMonitor = nil
|
||||
}
|
||||
// 关闭全屏窗口
|
||||
if let fw = fullscreenWindow {
|
||||
if let pv = fw.contentView as? AVPlayerView { pv.player = nil }
|
||||
fw.close()
|
||||
fullscreenWindow = nil
|
||||
}
|
||||
fullscreenWindow?.orderOut(nil)
|
||||
#endif
|
||||
|
||||
// 1. 先停 Timer
|
||||
updateTimer?.invalidate()
|
||||
updateTimer = nil
|
||||
|
||||
// 2. 移除 KVO 观察者(阻止后续回调)
|
||||
// 2. 移除 KVO 观察者
|
||||
itemStatusObserver?.invalidate(); itemStatusObserver = nil
|
||||
|
||||
// 3. 移除通知观察者
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user