fix: use AVPlayerView (AVKit) instead of custom AVPlayerLayer
Root cause: custom AVPlayerLayer lifecycle management causes CoreMedia FigNotificationCenter weak listener race condition. Apple's AVPlayerView handles all internal teardown correctly. Changes: - VideoPlayerView.swift: NSViewRepresentable wrapping AVPlayerView (macOS) and UIViewControllerRepresentable wrapping AVPlayerViewController (iOS) - PlayerBridge.swift: fullscreen uses AVPlayerView, removed FullscreenPlayerView class entirely - Fullscreen interaction via NSEvent.addLocalMonitorForEvents: double-click exits, single-click toggles play/pause, Escape exits - cleanup() now closes fullscreen window and removes event monitor - Removed ExitFullscreen NotificationCenter listener from MiniPlayerApp
This commit is contained in:
parent
6bbd86006a
commit
85f5699878
@ -139,12 +139,7 @@ struct ContentView: View {
|
||||
toolbarVisible = false
|
||||
}
|
||||
}
|
||||
// 全屏退出通知 — async 避免在 SwiftUI 更新周期内操作 NSWindow
|
||||
.onReceive(NotificationCenter.default.publisher(for: .init("ExitFullscreen"))) { _ in
|
||||
DispatchQueue.main.async { [bridge] in
|
||||
bridge.toggleFullscreen()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func touchInteraction() {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import SwiftUI
|
||||
import AVFoundation
|
||||
import AVKit
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
@ -80,6 +81,7 @@ final class PlayerBridge: ObservableObject {
|
||||
#if os(macOS)
|
||||
private var fullscreenWindow: NSWindow?
|
||||
private var playlistWindow: NSWindow?
|
||||
private var fullscreenEventMonitor: Any?
|
||||
#endif
|
||||
|
||||
// MARK: - 初始化
|
||||
@ -279,9 +281,13 @@ final class PlayerBridge: ObservableObject {
|
||||
func toggleFullscreen() {
|
||||
#if os(macOS)
|
||||
if let fw = fullscreenWindow {
|
||||
// 先断开 AVPlayerLayer → player 引用,避免 autorelease pool drain 时
|
||||
// 与 CoreMedia 后台线程竞争 sFigNotificationCenterWeakListenerLinks
|
||||
if let playerView = fw.contentView as? FullscreenPlayerView {
|
||||
// 移除事件监控
|
||||
if let monitor = fullscreenEventMonitor {
|
||||
NSEvent.removeMonitor(monitor)
|
||||
fullscreenEventMonitor = nil
|
||||
}
|
||||
// 先断开 player 引用再关闭窗口
|
||||
if let playerView = fw.contentView as? AVPlayerView {
|
||||
playerView.player = nil
|
||||
}
|
||||
fw.close()
|
||||
@ -298,14 +304,43 @@ final class PlayerBridge: ObservableObject {
|
||||
win.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
||||
win.hasShadow = false
|
||||
|
||||
let playerView = FullscreenPlayerView(frame: screen.frame)
|
||||
// 使用系统 AVPlayerView,Apple 内部处理所有 CoreMedia 生命周期竞态
|
||||
let playerView = AVPlayerView(frame: screen.frame)
|
||||
playerView.player = player
|
||||
playerView.controlsStyle = .none
|
||||
playerView.videoGravity = .resizeAspect
|
||||
win.contentView = playerView
|
||||
win.makeKeyAndOrderFront(nil)
|
||||
win.makeFirstResponder(playerView)
|
||||
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()
|
||||
} else {
|
||||
self.player.play()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
} else if event.type == .keyDown {
|
||||
if event.keyCode == 53 { // Escape
|
||||
self.toggleFullscreen()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return event
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -401,6 +436,20 @@ final class PlayerBridge: ObservableObject {
|
||||
isTearingDown = true
|
||||
player.pause()
|
||||
|
||||
#if os(macOS)
|
||||
// 移除全屏事件监控
|
||||
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
|
||||
}
|
||||
#endif
|
||||
|
||||
// 1. 先停 Timer
|
||||
updateTimer?.invalidate()
|
||||
updateTimer = nil
|
||||
@ -413,9 +462,6 @@ final class PlayerBridge: ObservableObject {
|
||||
NotificationCenter.default.removeObserver(token)
|
||||
endObserverToken = nil
|
||||
}
|
||||
|
||||
// 4. 不调用 replaceCurrentItem(with: nil)
|
||||
// 让 playerItem 随 player 自然释放,避免 CoreMedia 内部状态不一致
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -424,55 +470,3 @@ final class PlayerBridge: ObservableObject {
|
||||
updateTimer?.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 全屏专用 NSView(纯 AppKit,不经过 SwiftUI)
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
|
||||
class FullscreenPlayerView: NSView {
|
||||
// 使用 makeBackingLayer 让 AppKit 管理 AVPlayerLayer
|
||||
override func makeBackingLayer() -> CALayer { AVPlayerLayer() }
|
||||
|
||||
private var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer }
|
||||
|
||||
var player: AVPlayer? {
|
||||
get { playerLayer.player }
|
||||
set { playerLayer.player = newValue }
|
||||
}
|
||||
|
||||
override init(frame: NSRect) {
|
||||
super.init(frame: frame)
|
||||
wantsLayer = true
|
||||
playerLayer.videoGravity = .resizeAspect
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
override var acceptsFirstResponder: Bool { true }
|
||||
|
||||
// 安全网:视图从窗口移除时主动断开 player 引用
|
||||
// 避免 CoreMedia 后台线程与 autorelease pool drain 竞争
|
||||
override func viewWillMove(toWindow newWindow: NSWindow?) {
|
||||
if newWindow == nil {
|
||||
playerLayer.player = nil
|
||||
}
|
||||
super.viewWillMove(toWindow: newWindow)
|
||||
}
|
||||
|
||||
override func mouseDown(with event: NSEvent) {
|
||||
if event.clickCount == 2 {
|
||||
NotificationCenter.default.post(name: .init("ExitFullscreen"), object: nil)
|
||||
} else {
|
||||
if let p = player {
|
||||
if p.timeControlStatus == .playing { p.pause() } else { p.play() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func keyDown(with event: NSEvent) {
|
||||
if event.keyCode == 53 { // Escape
|
||||
NotificationCenter.default.post(name: .init("ExitFullscreen"), object: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -1,37 +1,26 @@
|
||||
import SwiftUI
|
||||
import AVFoundation
|
||||
import AVKit
|
||||
|
||||
// MARK: - 跨平台AVPlayer渲染
|
||||
// MARK: - 跨平台 AVPlayer 渲染(使用系统 AVPlayerView / AVPlayerViewController)
|
||||
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
|
||||
struct VideoPlayerRepresentable: UIViewRepresentable {
|
||||
struct VideoPlayerRepresentable: UIViewControllerRepresentable {
|
||||
let player: AVPlayer
|
||||
|
||||
func makeUIView(context: Context) -> PlayerUIView {
|
||||
let view = PlayerUIView()
|
||||
view.player = player
|
||||
return view
|
||||
func makeUIViewController(context: Context) -> AVPlayerViewController {
|
||||
let vc = AVPlayerViewController()
|
||||
vc.player = player
|
||||
vc.showsPlaybackControls = false
|
||||
vc.videoGravity = .resizeAspect
|
||||
return vc
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: PlayerUIView, context: Context) {
|
||||
uiView.player = player
|
||||
}
|
||||
}
|
||||
|
||||
class PlayerUIView: UIView {
|
||||
override static var layerClass: AnyClass { AVPlayerLayer.self }
|
||||
|
||||
var player: AVPlayer? {
|
||||
get { (layer as? AVPlayerLayer)?.player }
|
||||
set { (layer as? AVPlayerLayer)?.player = newValue }
|
||||
}
|
||||
|
||||
override var contentMode: UIView.ContentMode {
|
||||
get { (layer as? AVPlayerLayer)?.videoGravity == .resizeAspectFill ? .scaleAspectFill : .scaleAspectFit }
|
||||
set {
|
||||
(layer as? AVPlayerLayer)?.videoGravity = newValue == .scaleAspectFill ? .resizeAspectFill : .resizeAspect
|
||||
func updateUIViewController(_ vc: AVPlayerViewController, context: Context) {
|
||||
if vc.player !== player {
|
||||
vc.player = player
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -42,44 +31,18 @@ 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()
|
||||
func makeNSView(context: Context) -> AVPlayerView {
|
||||
let view = AVPlayerView()
|
||||
view.player = player
|
||||
context.coordinator.view = view
|
||||
view.controlsStyle = .none
|
||||
view.videoGravity = .resizeAspect
|
||||
return view
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: PlayerNSView, context: Context) {
|
||||
// 只在 player 引用变化时设置(Coordinator 确保不重复设置)
|
||||
func updateNSView(_ nsView: AVPlayerView, context: Context) {
|
||||
if nsView.player !== player {
|
||||
nsView.player = player
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PlayerNSView: NSView {
|
||||
// 使用 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)
|
||||
wantsLayer = true
|
||||
playerLayer.videoGravity = .resizeAspect
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
var player: AVPlayer? {
|
||||
get { playerLayer.player }
|
||||
set { playerLayer.player = newValue }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user