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
|
toolbarVisible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 全屏退出通知 — async 避免在 SwiftUI 更新周期内操作 NSWindow
|
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .init("ExitFullscreen"))) { _ in
|
|
||||||
DispatchQueue.main.async { [bridge] in
|
|
||||||
bridge.toggleFullscreen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func touchInteraction() {
|
private func touchInteraction() {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
import AVKit
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
import AppKit
|
import AppKit
|
||||||
#endif
|
#endif
|
||||||
@ -80,6 +81,7 @@ final class PlayerBridge: ObservableObject {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
private var fullscreenWindow: NSWindow?
|
private var fullscreenWindow: NSWindow?
|
||||||
private var playlistWindow: NSWindow?
|
private var playlistWindow: NSWindow?
|
||||||
|
private var fullscreenEventMonitor: Any?
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// MARK: - 初始化
|
// MARK: - 初始化
|
||||||
@ -279,9 +281,13 @@ final class PlayerBridge: ObservableObject {
|
|||||||
func toggleFullscreen() {
|
func toggleFullscreen() {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
if let fw = fullscreenWindow {
|
if let fw = fullscreenWindow {
|
||||||
// 先断开 AVPlayerLayer → player 引用,避免 autorelease pool drain 时
|
// 移除事件监控
|
||||||
// 与 CoreMedia 后台线程竞争 sFigNotificationCenterWeakListenerLinks
|
if let monitor = fullscreenEventMonitor {
|
||||||
if let playerView = fw.contentView as? FullscreenPlayerView {
|
NSEvent.removeMonitor(monitor)
|
||||||
|
fullscreenEventMonitor = nil
|
||||||
|
}
|
||||||
|
// 先断开 player 引用再关闭窗口
|
||||||
|
if let playerView = fw.contentView as? AVPlayerView {
|
||||||
playerView.player = nil
|
playerView.player = nil
|
||||||
}
|
}
|
||||||
fw.close()
|
fw.close()
|
||||||
@ -298,14 +304,43 @@ final class PlayerBridge: ObservableObject {
|
|||||||
win.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
win.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
||||||
win.hasShadow = false
|
win.hasShadow = false
|
||||||
|
|
||||||
let playerView = FullscreenPlayerView(frame: screen.frame)
|
// 使用系统 AVPlayerView,Apple 内部处理所有 CoreMedia 生命周期竞态
|
||||||
|
let playerView = AVPlayerView(frame: screen.frame)
|
||||||
playerView.player = player
|
playerView.player = player
|
||||||
|
playerView.controlsStyle = .none
|
||||||
|
playerView.videoGravity = .resizeAspect
|
||||||
win.contentView = playerView
|
win.contentView = playerView
|
||||||
win.makeKeyAndOrderFront(nil)
|
win.makeKeyAndOrderFront(nil)
|
||||||
win.makeFirstResponder(playerView)
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
fullscreenWindow = win
|
fullscreenWindow = win
|
||||||
isFullscreen = true
|
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
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -401,6 +436,20 @@ final class PlayerBridge: ObservableObject {
|
|||||||
isTearingDown = true
|
isTearingDown = true
|
||||||
player.pause()
|
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
|
// 1. 先停 Timer
|
||||||
updateTimer?.invalidate()
|
updateTimer?.invalidate()
|
||||||
updateTimer = nil
|
updateTimer = nil
|
||||||
@ -413,9 +462,6 @@ final class PlayerBridge: ObservableObject {
|
|||||||
NotificationCenter.default.removeObserver(token)
|
NotificationCenter.default.removeObserver(token)
|
||||||
endObserverToken = nil
|
endObserverToken = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 不调用 replaceCurrentItem(with: nil)
|
|
||||||
// 让 playerItem 随 player 自然释放,避免 CoreMedia 内部状态不一致
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@ -424,55 +470,3 @@ final class PlayerBridge: ObservableObject {
|
|||||||
updateTimer?.invalidate()
|
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 SwiftUI
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
import AVKit
|
||||||
|
|
||||||
// MARK: - 跨平台AVPlayer渲染
|
// MARK: - 跨平台 AVPlayer 渲染(使用系统 AVPlayerView / AVPlayerViewController)
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
struct VideoPlayerRepresentable: UIViewRepresentable {
|
struct VideoPlayerRepresentable: UIViewControllerRepresentable {
|
||||||
let player: AVPlayer
|
let player: AVPlayer
|
||||||
|
|
||||||
func makeUIView(context: Context) -> PlayerUIView {
|
func makeUIViewController(context: Context) -> AVPlayerViewController {
|
||||||
let view = PlayerUIView()
|
let vc = AVPlayerViewController()
|
||||||
view.player = player
|
vc.player = player
|
||||||
return view
|
vc.showsPlaybackControls = false
|
||||||
|
vc.videoGravity = .resizeAspect
|
||||||
|
return vc
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIView(_ uiView: PlayerUIView, context: Context) {
|
func updateUIViewController(_ vc: AVPlayerViewController, context: Context) {
|
||||||
uiView.player = player
|
if vc.player !== player {
|
||||||
}
|
vc.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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,44 +31,18 @@ import AppKit
|
|||||||
struct VideoPlayerRepresentable: NSViewRepresentable {
|
struct VideoPlayerRepresentable: NSViewRepresentable {
|
||||||
let player: AVPlayer
|
let player: AVPlayer
|
||||||
|
|
||||||
func makeCoordinator() -> Coordinator { Coordinator() }
|
func makeNSView(context: Context) -> AVPlayerView {
|
||||||
|
let view = AVPlayerView()
|
||||||
class Coordinator {
|
|
||||||
weak var view: PlayerNSView?
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeNSView(context: Context) -> PlayerNSView {
|
|
||||||
let view = PlayerNSView()
|
|
||||||
view.player = player
|
view.player = player
|
||||||
context.coordinator.view = view
|
view.controlsStyle = .none
|
||||||
|
view.videoGravity = .resizeAspect
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateNSView(_ nsView: PlayerNSView, context: Context) {
|
func updateNSView(_ nsView: AVPlayerView, context: Context) {
|
||||||
// 只在 player 引用变化时设置(Coordinator 确保不重复设置)
|
|
||||||
if nsView.player !== player {
|
if nsView.player !== player {
|
||||||
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
|
#endif
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user