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)
|
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 {
|
||||||
|
|||||||
@ -278,53 +278,54 @@ final class PlayerBridge: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 全屏(视频内容全屏,非窗口全屏)
|
// MARK: - 全屏(视频内容全屏,非窗口全屏)
|
||||||
|
// 全屏窗口只创建一次,之后只用 orderFront/orderOut 切换显示
|
||||||
|
// AVPlayerView 永不销毁,彻底避免 CoreMedia weak listener 竞态
|
||||||
func toggleFullscreen() {
|
func toggleFullscreen() {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
if let fw = fullscreenWindow {
|
if isFullscreen {
|
||||||
// 移除事件监控
|
// 退出全屏:只隐藏窗口,不销毁
|
||||||
|
fullscreenWindow?.orderOut(nil)
|
||||||
|
isFullscreen = false
|
||||||
if let monitor = fullscreenEventMonitor {
|
if let monitor = fullscreenEventMonitor {
|
||||||
NSEvent.removeMonitor(monitor)
|
NSEvent.removeMonitor(monitor)
|
||||||
fullscreenEventMonitor = nil
|
fullscreenEventMonitor = nil
|
||||||
}
|
}
|
||||||
// 先断开 player 引用再关闭窗口
|
|
||||||
if let playerView = fw.contentView as? AVPlayerView {
|
|
||||||
playerView.player = nil
|
|
||||||
}
|
|
||||||
fw.close()
|
|
||||||
fullscreenWindow = nil
|
|
||||||
isFullscreen = false
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 首次全屏:创建窗口和 AVPlayerView(仅此一次)
|
||||||
|
if fullscreenWindow == nil {
|
||||||
guard let screen = NSScreen.main else { return }
|
guard let screen = NSScreen.main else { return }
|
||||||
let win = NSWindow(contentRect: screen.frame, styleMask: .borderless, backing: .buffered, defer: false)
|
let win = NSWindow(contentRect: screen.frame, styleMask: .borderless, backing: .buffered, defer: false)
|
||||||
win.level = .screenSaver
|
win.level = .screenSaver
|
||||||
win.backgroundColor = .black
|
win.backgroundColor = .black
|
||||||
win.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
win.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
||||||
win.hasShadow = false
|
win.hasShadow = false
|
||||||
|
win.isReleasedWhenClosed = false
|
||||||
|
|
||||||
// 使用系统 AVPlayerView,Apple 内部处理所有 CoreMedia 生命周期竞态
|
|
||||||
let playerView = AVPlayerView(frame: screen.frame)
|
let playerView = AVPlayerView(frame: screen.frame)
|
||||||
playerView.player = player
|
playerView.player = player
|
||||||
playerView.controlsStyle = .none
|
playerView.controlsStyle = .none
|
||||||
playerView.videoGravity = .resizeAspect
|
playerView.videoGravity = .resizeAspect
|
||||||
win.contentView = playerView
|
win.contentView = playerView
|
||||||
win.makeKeyAndOrderFront(nil)
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
|
||||||
fullscreenWindow = win
|
fullscreenWindow = win
|
||||||
|
}
|
||||||
|
|
||||||
|
fullscreenWindow?.makeKeyAndOrderFront(nil)
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
isFullscreen = true
|
isFullscreen = true
|
||||||
|
|
||||||
// 添加本地事件监控:双击退出全屏、单击暂停/播放、Escape退出
|
// 事件监控:双击退出、单击暂停/播放、Escape退出
|
||||||
|
if fullscreenEventMonitor == nil {
|
||||||
fullscreenEventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown, .keyDown]) { [weak self] event in
|
fullscreenEventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown, .keyDown]) { [weak self] event in
|
||||||
guard let self = self, let fw = self.fullscreenWindow else { return event }
|
guard let self = self, self.isFullscreen else { return event }
|
||||||
// 只处理全屏窗口内的事件
|
guard event.window === self.fullscreenWindow else { return event }
|
||||||
if event.window !== fw { return event }
|
|
||||||
|
|
||||||
if event.type == .leftMouseDown {
|
if event.type == .leftMouseDown {
|
||||||
if event.clickCount >= 2 {
|
if event.clickCount >= 2 {
|
||||||
self.toggleFullscreen()
|
DispatchQueue.main.async { self.toggleFullscreen() }
|
||||||
return nil // 消费事件
|
return nil
|
||||||
} else {
|
} else {
|
||||||
if self.player.timeControlStatus == .playing {
|
if self.player.timeControlStatus == .playing {
|
||||||
self.player.pause()
|
self.player.pause()
|
||||||
@ -333,14 +334,13 @@ final class PlayerBridge: ObservableObject {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
} else if event.type == .keyDown {
|
} else if event.type == .keyDown && event.keyCode == 53 {
|
||||||
if event.keyCode == 53 { // Escape
|
DispatchQueue.main.async { self.toggleFullscreen() }
|
||||||
self.toggleFullscreen()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -437,24 +437,19 @@ final class PlayerBridge: ObservableObject {
|
|||||||
player.pause()
|
player.pause()
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
// 移除全屏事件监控
|
// 移除全屏事件监控(不销毁窗口,避免 teardown 竞态)
|
||||||
if let monitor = fullscreenEventMonitor {
|
if let monitor = fullscreenEventMonitor {
|
||||||
NSEvent.removeMonitor(monitor)
|
NSEvent.removeMonitor(monitor)
|
||||||
fullscreenEventMonitor = nil
|
fullscreenEventMonitor = nil
|
||||||
}
|
}
|
||||||
// 关闭全屏窗口
|
fullscreenWindow?.orderOut(nil)
|
||||||
if let fw = fullscreenWindow {
|
|
||||||
if let pv = fw.contentView as? AVPlayerView { pv.player = nil }
|
|
||||||
fw.close()
|
|
||||||
fullscreenWindow = nil
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// 1. 先停 Timer
|
// 1. 先停 Timer
|
||||||
updateTimer?.invalidate()
|
updateTimer?.invalidate()
|
||||||
updateTimer = nil
|
updateTimer = nil
|
||||||
|
|
||||||
// 2. 移除 KVO 观察者(阻止后续回调)
|
// 2. 移除 KVO 观察者
|
||||||
itemStatusObserver?.invalidate(); itemStatusObserver = nil
|
itemStatusObserver?.invalidate(); itemStatusObserver = nil
|
||||||
|
|
||||||
// 3. 移除通知观察者
|
// 3. 移除通知观察者
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user