Root cause: when FullscreenPlayerView deallocates during window close,
AVPlayerLayer.player reference is released via autorelease pool drain
on main thread. CoreMedia background threads simultaneously access
sFigNotificationCenterWeakListenerLinks dictionary (weak listener
cleanup), causing use-after-free race condition.
Fix:
- toggleFullscreen(): set playerView.player = nil BEFORE fw.close()
- FullscreenPlayerView.viewWillMove(toWindow: nil): safety net to
nil out player when view is removed from window by any means
This ensures the AVPlayer reference is released synchronously on the
main thread, before any autorelease pool drain can race with CoreMedia
internal cleanup threads.
Root cause: addPeriodicTimeObserver's internal FigNotificationCenter weak
listener mechanism races with autorelease pool drain on main thread, causing
double-free of weak reference wrappers (KERN_INVALID_ADDRESS).
Changes:
- Replace addPeriodicTimeObserver with Timer.scheduledTimer (bypasses CoreMedia
weak listener infrastructure entirely)
- Remove ALL Task { @MainActor in } from observer callbacks — these created
unstructured tasks whose weak ref wrappers conflicted with CoreMedia internals
- Use DispatchQueue.main.async for KVO callbacks (may fire from non-main thread)
- Direct calls for queue: .main callbacks (NotificationCenter, end observer)
- Add isTearingDown flag to prevent callbacks firing during cleanup
- Fix cleanup() order: timer → KVO → notifications → replaceCurrentItem(nil)
- Fix FullscreenPlayerView: use addSublayer instead of replacing backing layer
- Add .onDisappear { bridge.cleanup() } to ensure cleanup before dealloc
- Remove Combine import (no longer needed)
- Remove KVO on player.timeControlStatus (fires from CoreMedia bg threads)
- Stop accessing item.duration in timer callback (triggers FigNotificationCenter weak listener ops)
- Check player.rate in timer callback instead for isPlaying state
- Remove replaceCurrentItem(nil) from cleanup to prevent CoreMedia state inconsistency
- Use cachedDuration exclusively in time display updates
- Fix actor isolation warning in endObserver callback
- PlayerNSView: AVPlayerLayer as sublayer (not layer= replacement)
NSView internally manages a sublayer array; replacing the backing
layer directly causes dangling refs during autorelease pool drain
- cleanup(): only pause + remove observers, no replaceCurrentItem
CoreMedia internal state gets corrupted when item is replaced
during view teardown; let it release naturally with deinit
- Remove Combine import and cancellables
- Use NSKeyValueObservation for timeControlStatus (controlled teardown)
- Invalidate itemStatusObserver before replacing playerItem
- Store endObserver token for proper removal
- Add cleanup() method called on view disappear
- Proper deinit with direct property cleanup (nonisolated-safe)
- UI defined in player.json (Bricks JSON schema)
- Custom widgets: VideoPlayer (AVPlayer layer), ProgressSlider (seek bar)
- PlayerBridge connects AVPlayer to BricksEngine event bus
- All interactions via binds/events (no imperative UI code)
- Depends on SwiftBricks SPM package