diff --git a/Sources/MiniPlayerApp.swift b/Sources/MiniPlayerApp.swift index 2a9e165..47454dd 100644 --- a/Sources/MiniPlayerApp.swift +++ b/Sources/MiniPlayerApp.swift @@ -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 { diff --git a/Sources/PlayerBridge.swift b/Sources/PlayerBridge.swift index f1325ae..3485b56 100644 --- a/Sources/PlayerBridge.swift +++ b/Sources/PlayerBridge.swift @@ -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. 移除通知观察者