129 Commits

Author SHA1 Message Date
49948049c0 fix: show save dialog even on error if file has content; better default filename 2026-06-28 21:46:41 +08:00
221075ce8a refactor: replace AVAssetWriter+Tap recording with StreamRecorderKit FFmpegRecorder
- PlayerRecorder.swift: 883 lines → ~150 lines
- Recording now uses FFmpeg to download stream directly from URL
- No longer depends on AVPlayerItemVideoOutput/copyPixelBuffer/MTAudioProcessingTap
- Recording decoupled from playback (pausing playback doesn't affect recording)
- Better quality: direct stream copy/transcode instead of re-encoding captured frames
- Added StreamRecorderKit as local SPM dependency
2026-06-28 18:19:10 +08:00
70337add95 fix: remove audioMix pre-install in playIndex - breaks HLS playback
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.
2026-06-28 15:21:10 +08:00
f68476d57f fix: move video capture off main thread + filter stale end-of-playback notifications
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).
2026-06-28 10:34:50 +08:00
9160b3ce2b fix: pre-install audioMix in playIndex to prevent video freeze on record start
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.
2026-06-28 10:30:20 +08:00
cff2bd2793 fix: use kCMPersistentTrackID_Invalid for HLS audio tap
Root cause found: HLS variant switching changes audio trackID mid-recording
(e.g. 2→10). Binding to a specific trackID causes tap to never fire.
Using kCMPersistentTrackID_Invalid (wildcard) matches all audio tracks
regardless of variant switches.
2026-06-28 10:24:04 +08:00
4ba3ccde21 diag: add stop-time tracks check to detect HLS variant switching 2026-06-28 10:14:32 +08:00
1eff22a1ad fix: use playerItem.tracks for HLS audio track discovery
HLS virtual asset returns empty tracks from asset.tracks/loadTracks.
Use playerItem.tracks + assetTrack to get actual track IDs.
2026-06-28 09:52:29 +08:00
1880eeb3d3 fix: use async loadTracks for HLS stream audio tracks - HLS AVURLAsset.tracks returns empty array synchronously - must use asset.loadTracks(withMediaType: .audio) 2026-06-28 09:15:50 +08:00
70b264bcc8 fix: revert to actual trackID for audioMix (kCMPersistentTrackID_Invalid never fires tap) 2026-06-27 17:59:45 +08:00
7cfeec1f15 feat: recording diagnostic report system
- AudioTapContext tracks: peak amplitude, active/silent call counts,
  skip/error counters, last active timestamp
- tapProcess computes peak per buffer (every 16th sample for perf)
- writeDiagnosticReport() generates .txt report after each recording:
  * Verdict: OK / FAIL / WARN with specific diagnosis
  * Full tap stats, format info, skip/error breakdown
  * 'How to Read' section for quick interpretation
- Report saved alongside mp4 (same name, .txt extension)
- showSaveDialog moves both mp4 and report together
2026-06-27 17:45:00 +08:00
fb1bd32bbf fix: audio recording - double buffer + HLS variant switch support
- Replace single dataBuffer with double-buffer (dataBuffers tuple)
  to eliminate data race between real-time tapProcess thread and
  async dispatch queue reading the buffer
- Use kCMPersistentTrackID_Invalid for audioMix trackID so the tap
  survives HLS variant switches (track ID changes on quality change)
2026-06-27 15:44:55 +08:00
1733ecc7bc fix: 修复录制视频无音频问题
- audioInput 从 PCM pass-through 改为 AAC 编码 (MP4 不支持裸 Float32 PCM)
- IOProc 优先读 inOutputData (播放数据) 而非 inInputData (麦克风)
- 音频时间戳改为相对时间 (从首次回调开始计算), 与视频同步
- 复用 CMAudioFormatDescription 避免重复创建
2026-06-27 13:55:49 +08:00
f1ecbf2e91 fix: replace MTAudioProcessingTap with CoreAudio device capture for HLS audio recording
MTAudioProcessingTap with AVPlayerItem.audioMix doesn't fire its process
callback for HLS streams on macOS (known limitation). This caused recorded
videos to have no audio track when recording HLS content.

Replace with CoreAudio AudioDeviceCreateIOProcID approach that captures
PCM audio directly from the system output device. This works for all
audio sources (HLS, local files, etc).

Key changes:
- Remove MTAudioProcessingTap and all tap callbacks
- Add CoreAudio device IOProc for audio capture
- Pre-allocated buffer to avoid malloc in real-time audio thread
- Serial dispatch queue for CMSampleBuffer processing off audio thread
- AudioTimeStamp → CMTime conversion for proper timestamps
2026-06-27 10:58:09 +08:00
f0e468122b fix: trim whitespace from URL input to prevent 404 errors 2026-06-27 10:43:47 +08:00
339664ebc1 fix: 修复HLS流媒体录制无音频问题
- 存储tapPrepareCallback实际采样率,替代不可靠的timeRange.duration.timescale
- findAudioTrackID增加3路fallback: playerItem.tracks → asset.tracks → 非视频推断
- async load(.tracks)从单次尝试改为每3秒重试
- 增加muxed track fallback(HLS可能返回非标准mediaType)
2026-06-26 08:51:10 +08:00
d4a39dd7c3 fix: block app termination while recording is finalizing
- RecorderState.isFinalizing flag set during stopRecording → finishWriting
- applicationShouldTerminate returns .terminateLater if finalizing
- Shows alert '正在保存录制文件...' to inform user
- After save dialog completes, calls NSApp.reply(toApplicationShouldTerminate: true)
- All failure paths also check and resolve pendingTerminate
- Fixes: 9-min recording lost because user closed app before async finishWriting completed
2026-06-25 21:27:02 +08:00
eb3e98d567 fix: window close exits app + save dialog diagnostics
- AppDel: use NSWindow.willCloseNotification observer with visibility check
  instead of broken applicationShouldTerminateAfterLastWindowClosed logic
  (SwiftUI WindowGroup keeps hidden windows that blocked termination)
- finishWriting callback: use DispatchQueue.main.async instead of Task @MainActor
  (more reliable from non-main-thread AVFoundation callback)
- Add extensive NSLog diagnostics: callback invoked, file existence check,
  file size, status codes, nil checks
2026-06-25 21:08:59 +08:00
cf53afd7c8 fix: improve HLS audio track discovery - async load fallback + detailed diagnostics
- setupAudioTap: log all tracks at start, KVO count changes
- findAudioTrackID: log when assetTrack is nil (HLS not loaded)
- Polling: add async asset.load(.tracks) fallback after 5s
- Extend timeout from 20s to 30s
- Status log every 5s during polling
2026-06-25 20:51:27 +08:00
f5e35a6ee7 fix: close main window exits app; playlist panel close doesn't 2026-06-25 20:47:23 +08:00
8cfb072370 fix: HLS audio via KVO+polling for playerItem.tracks, diagnostic logging
- KVO观察playerItem.tracks变化(HLS tracks异步加载)
- 轮询保底(20秒超时),KVO不触发时仍能捕获
- 诊断日志:打印每个track的mediaType/trackID
- tap回调改进:始终先GetSourceAudio保证管道不阻塞
- 窗口标题随toolbar联动
2026-06-25 09:12:02 +08:00
be0cf09941 fix: HLS audio (playerItem.tracks + better tap callback) + window title sync with toolbar
- HLS录制音频: 改用playerItem.tracks查找trackID(fallback到asset.loadTracks)
- tap回调: 始终先调GetSourceAudio保证管道畅通,加详细日志
- 窗口标题: onAppear初始隐藏,随toolbar一起30秒后消失/点击图标回来
2026-06-25 09:08:02 +08:00
6e94bb46b7 feat: window title hides/shows with toolbar on idle timeout 2026-06-25 09:00:56 +08:00
fc95254500 feat: 录制完成后弹出保存对话框,支持用户改名
- 添加 showSaveDialog 方法,使用 NSSavePanel
- 默认文件名为 Recording_时间戳.mp4
- 用户可修改文件名或选择保存位置
- 点击取消(X)时删除临时文件
- 保存成功后调用 onRecordingSaved 回调
2026-06-25 08:56:47 +08:00
ff20ffe07f fix: HLS音频录制+边录边写+结束仅改名移动
1. 修复HLS无声音:
   - 统一回调函数签名,删除重复定义
   - 回调函数名改为 tapInitCallback/tapFinalizeCallback 等避免歧义
   - 添加NSLog诊断日志

2. 边录边写文件:
   - AVAssetWriter直接写入临时文件(边录边写,不缓存内存)
   - 临时文件名用时间戳,避免冲突

3. 录制结束仅改名移动:
   - finishWriting完成后直接moveItem到最终文件名
   - 不再弹NSSavePanel
   - 最终文件名格式:Recording_yyyy-MM-dd_HH-mm-ss.mp4
   - 保存到 ~/Movies/MiniPlayer/
2026-06-25 08:29:39 +08:00
d9f12ced06 fix: MTAudioProcessingTapCallbacks参数名initCallback→init
修复编译错误:Swift bridging 期望的参数名是 init 而非 initCallback
2026-06-24 23:16:08 +08:00
5ae4cc4e70 feat: HLS流音频录制——MTAudioProcessingTap替代AVAssetReader
问题: AVAssetReader不支持HLS流(m3u8),录制HLS时音频丢失。

方案: 统一使用MTAudioProcessingTap捕获音频
- 从播放管道直接获取PCM音频数据,本地文件和HLS流通用
- tap回调通过MTAudioProcessingTapGetSourceAudio获取源音频+时间范围
- AudioCaptureContext通过Unmanaged传递给C回调
- init/finalize回调管理context生命周期
- 视频仍用AVPlayerItemVideoOutput(已验证稳定)

API变更:
- startRecording(from:playerItem:startTime:) 替代 sourceURL
- PlayerBridge传入playerItem而非URL
2026-06-24 23:14:47 +08:00
c7bb63bb7e fix: HLS播放加自定义headers + 录制finishWriting错误日志
HLS播放:
- AVURLAsset替代AVPlayerItem(url:),加Referer/UA headers
- 某些HLS流服务端校验Referer,裸URL请求被拒绝

录制:
- startWriting检查返回值和error
- finishWriting回调打印status/error/frames
- 失败时删除损坏的临时文件,弹出错误提示
2026-06-24 22:59:44 +08:00
ff4902ffe6 fix: 视频只有一帧+音频超长——重写录制时间基准和音频限速
视频修复:
- startSession(atSourceTime: .zero) 替代 itemTime
- 之前 session 起点是绝对 itemTime(如120s),但 sample 时间戳
  是相对的(0, 1/30...),writer 因 0<120 丢弃帧
- 现在统一用 player.currentTime() 作为共享时间基准

音频修复:
- 用 DispatchSourceTimer(20ms) 替代 requestMediaDataWhenReady
- 每 20ms 检查实时进度(elapsed wall-clock),只写入不超前的音频
- 超前的 sample 缓存到 pendingAudioSample 等下次 tick
- 停止时 cancel timer + cancel reader,音频长度精确对齐视频

音视频同步:
- 两者共享 recordStartTime = player.currentTime()
- 视频: itemTime - recordStartTime
- 音频: audioPTS - recordStartTime
2026-06-24 22:08:09 +08:00
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