SwiftUI multiple .sheet modifiers can conflict — replaced the pendingExportURL
sheet binding with a direct UIKit UIActivityViewController presentation from
the key window's root VC. This bypasses any SwiftUI sheet stack issues.
Also added copyLocalFile() for local file recording (FileManager.copyItem
instead of URLSession.download which fails on file:// URLs).
macOS:
- Replace deprecated NSSavePanel.begin(completionHandler:) with async begin() API
- The old API has been deprecated since macOS 12 and may silently fail on macOS 26
- Added diagnostic logging throughout recording completion flow
iOS:
- Added diagnostic logging to track onRecordingSaved → pendingExportURL → sheet chain
- Logs file existence, size after recording completes
Diagnostic logging added to help verify the save dialog flow:
- macOS: file size check, showSaveDialog entry, NSSavePanel response
- iOS: recordStream output verification, onRecordingSaved call, sheet binding activation
Recording:
- stopRecording() now only sets stopRequested=true, no longer cancels task
- Task exits polling loop naturally → merges TS segments → exports MP4
- System share sheet + Photos save shown on completion
- Added HEAD request to detect HLS Content-Type for non-.m3u8 URLs
- Faster exit when stopRequested && segments exist
Playback:
- Copy imported files to app Documents sandbox (security-scoped URLs expire after defer)
- Remove Referer header that blocks some streams; use iPhone UA only
- AVURLAsset options only for http/https schemes
Media Library UI (iOS):
- Replace NavigationSplitView with VStack + horizontal category chips
- Search bar at top, swipe actions for queue/delete
- scrollDismissesKeyboard(.interactively) on list
- Tap gesture sends resignFirstResponder to dismiss keyboard
- Compact rows with context menu, inline play button
- Close button in navigation bar
- presentationDetents(.medium) for tag/playlist sheets
- Add PlayerStatus enum with loading/buffering/playing/error states
- Track loading elapsed time with countdown timer
- Detect buffering via timeControlStatus
- Handle playback errors with error message + retry button
- Show timeout warning after 15s of loading
- Buffering indicator in top-right corner (non-blocking)
- Localization strings for all status messages
Setting audioMix with MTAudioProcessingTap on HLS playerItem before
stream is ready blocks the audio pipeline entirely, preventing m3u8
streams from playing at all.
Revert to lazy install in startRecording() only.
Two fixes for video freeze during recording:
1. Video capture loop (30fps) moved from main-thread Timer to a
dedicated DispatchSourceTimer on 'miniplayer.videoCapture' queue.
copyPixelBuffer() on the main thread was competing with AVPlayer's
rendering pipeline for CPU time, causing frame stalls.
2. AVPlayerItemDidPlayToEndTime observer now checks notification.object
against player.currentItem. Previously object:nil caught stale
notifications from replaced items, causing spurious playNext() calls
that interrupted playback (visible as duplicate playIndex in logs).
Setting playerItem.audioMix while AVPlayer is actively playing causes
the audio/video pipeline to reconfigure, stalling video playback.
Fix: install the MTAudioProcessingTap audioMix at item creation time
(in playIndex, before replaceCurrentItem), so the pipeline is already
configured when playback begins. startRecording now only flips the
isRecording flag and skips re-installation if audioMix exists.
Also removed the guard that blocked tap installation when tracks
weren't available yet (HLS streams) — the wildcard trackID
(kCMPersistentTrackID_Invalid) handles this case.
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
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