54 Commits

Author SHA1 Message Date
06345b07de fix: 视频只有一帧 + 音频长度不对
视频修复:
- 移除 hasNewPixelBuffer 依赖,改为缓存 lastPixelBuffer
- 每 1/30s 始终写入帧(新帧或复用上一帧),确保静态场景也持续输出
- 加 isRunning 检查防止停止后继续写入

音频修复:
- 用 requestMediaDataWhenReady 替代 busy-loop
- writer 控制读取节奏,不再一次性读完全部源音频
- stopRecording 时 cancelReading + markAsFinished 立即停止

音视频同步:
- 视频用 itemTime 相对 firstFrameTime 的时间戳
- 音频用源文件 pts 相对 firstFrameTime 的时间戳
- 两者共享同一时间基准
2026-06-24 21:19:10 +08:00
42f3cf2b84 fix: 音视频同步——音频从 firstFrameTime 开始读取
之前音频用 player.currentTime() 作为起始时间,视频用
AVPlayerItemVideoOutput 的 firstFrameTime,两者可能不一致
导致音视频不同步。

改为:
- prepareAudioCapture: 只预加载音频轨道,不立即开始读取
- captureVideoFrame: 第一帧时设置 firstFrameTime 后调用
  beginAudioReading(startTime: firstFrameTime)
- 音频和视频共用同一个时间基准,确保精确同步
- 移除 startRecording 的 startTime 参数
2026-06-24 21:07:07 +08:00
60a3369d82 fix: 录制停止时音频未同步停止导致输出文件超长
根因: readAudioLoop 从文件读音频速度远快于实时,每帧 dispatch 到
MainActor 造成大量 Task 积压。用户点停止时,积压的 Task 在
isRunning=false 生效前全部执行,导致整个源音频被写入。

修复:
- 音频直接在后台队列写入,不再 dispatch 到 MainActor
- 增加 nonisolated(unsafe) audioStopped 标志,stopAudioCapture
  先设标志让 loop 立即退出
- 增加 nonisolated(unsafe) audioFirstFrameTime 供后台线程读取
- audioInput 作为参数传入 readAudioLoop,避免跨 actor 访问
2026-06-24 21:01:39 +08:00
cbc03b40b4 Fix: use Timer for video capture, set isRunning before capture loop 2026-06-24 20:44:51 +08:00
6c31894a6d Fix: record audio from source URL, not microphone 2026-06-24 20:28:31 +08:00
5d9e93ba95 Record video source via AVPlayerItemVideoOutput, not screen capture 2026-06-24 20:04:08 +08:00
87c66c3c70 Recording: red icon, live duration display, save dialog with editable filename 2026-06-24 19:56:07 +08:00
35de7263e9 Fix: app quit on playlist close, request screen capture permission, playlist as panel 2026-06-24 19:50:54 +08:00
a90a60437b Set app as regular GUI app to show in Dock 2026-06-24 19:47:10 +08:00
5960c5f7c8 Move record button to front of toolbar for visibility 2026-06-24 19:45:36 +08:00
8af1140b06 Add drag-and-drop file/URL playback and screen recording with audio 2026-06-24 19:33:21 +08:00
b929b1eb6e Set metadata types after session starts (availableMetadataObjectTypes empty before) 2026-06-24 19:28:38 +08:00
e87921b59f Request camera permission before QR scanning 2026-06-24 19:25:42 +08:00
f220800262 Improve QR scanner: prefer built-in camera, add rectOfInterest, fallback metadata types 2026-06-24 19:24:25 +08:00
8236b586c5 Fix crash: check availableMetadataObjectTypes before setting QR type 2026-06-24 19:22:13 +08:00
32bf49fa0d Increase toolbar button spacing from 4 to 10 2026-06-24 19:18:45 +08:00
f7c295c653 fix: QR scanner - move camera init to background thread, window shows immediately 2026-06-23 23:03:48 +08:00
6d1ff50074 fix: QR scanner crash - use independent NSWindow instead of SwiftUI sheet 2026-06-23 09:14:23 +08:00
32f722bd34 fix: QR scanner crash - add previewLayer as sublayer in layout() instead of init 2026-06-23 09:11:48 +08:00
ae96218d20 feat: add QR code scanner for media URLs 2026-06-23 09:09:23 +08:00
9cab9dd2cb feat: add URL input button to control toolbar 2026-06-23 09:06:32 +08:00
f303832a1b feat: fullscreen uses SwiftUI with icon+toolbar, double-click toggle, single-click play/pause 2026-06-23 09:02:08 +08:00
4879f56ef1 fix: set window background to black 2026-06-23 08:55:50 +08:00
d62a3f79c1 feat: hide AVPlayerView default controls, use custom toolbar only 2026-06-23 08:44:34 +08:00
f1eca64212 fix: compact toolbar with all controls (open/volume/track/playlist)
- Replaced text buttons with SF Symbol icon buttons (28x28)
- All features: folder/prev/play/next/volume+slider/track/repeat/fullscreen/playlist
- Removed Spacer() to prevent buttons from being clipped
- Single compact row layout, no overflow
2026-06-23 08:40:01 +08:00
ad1be14a0c refactor: restore Bricks .ui for video player + native overlay controls
- miniplayer.ui: VideoPlayer 100% fullscreen (Bricks renders video)
- BricksAppView: loads .ui via BricksView, ZStack overlays native controls
- ControlToolbar: semi-transparent (0.6 opacity), slides from bottom
- 30-second auto-hide timer with mouse-hover reset
- Left-top icon toggles toolbar visibility
2026-06-23 08:36:27 +08:00
20f4dec0b9 feat: fullscreen video + click icon to toggle control toolbar
- Video fills entire window via VideoPlayerRepresentable
- MiniPlayerIcon in top-left corner toggles ControlToolbar visibility
- ControlToolbar slides up from bottom with animation
- Removed Bricks JSON UI dependency for main view
- All toolbar features preserved: open/prev/play/next/volume/tracks/repeat/fullscreen/playlist
2026-06-22 20:47:27 +08:00
e7bca7269e feat: add i18n support and app icon in top-left corner
- Added i18n JSON files (zh.json, en.json) with auto language detection
- Updated miniplayer.ui to use i18n keys for all button labels
- Added MiniPlayerIcon overlay in top-left corner of main window
- Load i18n translations on app startup based on system locale
2026-06-22 20:43:13 +08:00
49bc681cf7 chore: rename miniplayer.json to miniplayer.ui 2026-06-22 20:16:32 +08:00
62b0507a1b chore: remove duplicate Resources/miniplayer.json (keep SPM path only) 2026-06-22 09:52:53 +08:00
bdf834012a feat: SwiftBricks UI integration with shared AVPlayer
- Inject PlayerBridge AVPlayer into BricksStore for VideoPlayer widget
- Load miniplayer.json from SPM bundle resources
- Fix infinite render loop: pass JSON string instead of schema object
- Fix async closure mismatch in setupBridgeEvents
- Enhanced JSON: URL input, load/play/pause buttons, title bar
- Remove duplicate Resources/miniplayer.json (keep SPM-compliant path)
2026-06-22 09:52:28 +08:00
9db72b49aa feat: add .icns app icon + automated DMG build script
- Generated MiniPlayer.icns from existing PNG icon set via iconutil
- Added CFBundleIconFile to Info.plist so Finder/Dock shows the icon
- Created build_dmg.sh: one-command release build + .app bundle + DMG
  (handles binary, localization, icon, Info.plist automatically)
- Removed onDisappear cleanup that caused autorelease pool race
- DMG: /build/MiniPlayer.dmg (420KB)
2026-06-22 09:13:11 +08:00
cae63c6588 fix: persistent fullscreen window — never destroy AVPlayerView
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
2026-06-22 09:01:04 +08:00
85f5699878 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
2026-06-22 01:16:49 +08:00
6bbd86006a fix: nil AVPlayerLayer.player before closing 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.
2026-06-22 01:11:40 +08:00
0d63414214 fix: replace CoreMedia periodic time observer with pure Swift Timer to eliminate autorelease pool race
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)
2026-06-22 01:06:39 +08:00
b2e8dfe812 chore: add MiniPlayer.app to gitignore 2026-06-22 00:57:36 +08:00
c336066198 fix: eliminate CoreMedia FigNotificationCenter race condition causing SIGSEGV
- 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
2026-06-22 00:57:23 +08:00
f14d47b5c8 fix: use sublayer instead of replacing NSView backing layer to prevent autorelease crash
- 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
2026-06-22 00:47:05 +08:00
aa19ab9799 feat: i18n support, tri-color icon, fix crash
- Add i18n: Localization.swift + zh-Hans/en Localizable.strings
- Add MiniPlayerIcon SwiftUI view (tri-color play button + ring)
- Fix crash: isInteracting/lastInteraction no longer @Published
- Fix crash: ExitFullscreen notification wrapped in DispatchQueue.main.async
- Auto-hide toolbar uses local @State + Timer (not @Published)
- Replace emoji logo with MiniPlayerIcon
- Move icon sets out of Resources/ to avoid SPM conflicts
- Package.swift: add defaultLocalization, process Resources
2026-06-22 00:40:20 +08:00
3e3a990f5e fix: replace Combine KVO with NSKeyValueObservation to prevent autorelease crash
- 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)
2026-06-22 00:36:18 +08:00
1fa6f6a4bb feat: toolbar auto-hides after 60s of no interaction 2026-06-22 00:12:43 +08:00
2fd6db3a4e chore: remove unused player.ui resource (SwiftBricks schema no longer needed) 2026-06-22 00:10:51 +08:00
6811572b7e fix: rewrite UI as pure SwiftUI, fix crash/fullscreen/height issues
- Skip BricksView, render video directly in SwiftUI (fixes 1/3 height)
- Fullscreen uses plain NSView+AVPlayerLayer (fixes objc_release crash)
- Remove NSApp.hide(nil) (fixes fullscreen not showing)
- Add volume +/- buttons and volume slider indicator
- Add iOS/iPadOS support with #if os guards
- ProgressSlider decoupled from BricksEngine
- PlayerBridge no longer depends on player.ui JSON
2026-06-22 00:04:06 +08:00
e4ca9bc80a fix: fullscreen crash + video size - use system toggleFullScreen, direct VideoPlayer render 2026-06-21 23:50:54 +08:00
4b94f11664 feat: hidden toolbar with logo toggle, semi-transparent controls, playlist popup window 2026-06-21 23:43:20 +08:00
4e58377582 fix: persist audio track selection across songs (preferredTrackIndex) 2026-06-21 23:33:08 +08:00
bf4cb64286 fix: video content fullscreen (borderless window), load actual duration via KVO+async 2026-06-21 23:31:12 +08:00
2a5e3b8ad7 feat: video fills entire window, compact control bar, single-tap play/pause 2026-06-21 23:27:17 +08:00
af71d46c63 chore: add .gitignore, remove build artifacts from tracking 2026-06-21 23:20:19 +08:00