98 Commits

Author SHA1 Message Date
4bfd4e5565 fix: .ipa 复制到项目根目录,Xcode Add App 弹窗可见 2026-07-01 22:05:33 +08:00
6c21bdbef7 fix: iOS .ipa 打包成功 - 修复 Team ID、Bundle.module、@main 入口点
- build-ios.sh: 设置正确 Team ID (LR8YT5M53U), 添加 GENERATE_INFOPLIST_FILE
- MiniPlayerApp.swift: Bundle.module → Bundle.main (非SPM), @main 条件编译
- 签名: Apple Development + Personal Team provisioning profile
- 产物: MiniPlayer.ipa (584K)
2026-07-01 21:38:52 +08:00
d22a5037b8 feat: iOS/macOS 双平台打包支持
- Package.swift: 重构为 MiniPlayerCore(共享库) + MiniPlayer(macOS) + MiniPlayeriOS(iOS) 三 target
- 所有源文件添加 #if os(macOS) / #if os(iOS) 条件编译
- HLSRecorder.swift: iOS 纯 Swift HLS 录制器(下载TS+合并+AVAssetExportSession转MP4)
- scripts/build-macos.sh: macOS 打包脚本 (.app + .dmg)
- scripts/build-ios.sh: iOS 打包脚本 (xcodegen + xcodebuild archive → .ipa)
- .gitignore: 排除构建产物
2026-07-01 08:29:16 +08:00
103b994c60 debug: 添加录制流程日志 2026-07-01 08:01:45 +08:00
16fdd72271 fix: 用 NSEvent.addLocalMonitorForEvents 替代 .onHover/tracking area
之前的方案(.onHover 和 .background tracking area)都不可靠:
- .onHover 在 macOS 上行为不稳定
- .background() 里的 NSView 被 AVPlayerLayer 挡住收不到鼠标事件

新方案:用 NSEvent.addLocalMonitorForEvents 在应用层监听 mouseMoved 事件,
直接检测鼠标是否在窗口 frame 内,完全绕过 SwiftUI 的 hover 机制。
加上双层防抖(lastState + currentlyVisible 检查)避免重复设置。
2026-07-01 07:48:16 +08:00
fde83a87bc fix: 用 NSTrackingArea 替代 .onHover 解决标题栏闪烁和红绿灯丢失问题
.onHover 在 macOS 上不可靠,鼠标离开后会误触发 true 导致标题栏短暂回来但没有红绿灯按钮。
改用 NSTrackingArea 的 mouseEntered/mouseExited 做可靠的鼠标追踪。
2026-07-01 07:42:16 +08:00
ec8c3bd51c fix: title bar shows/hides based on window hover, not just top strip
- Remove 28pt overlay hover detection
- Add .onHover to entire window content (ZStack)
- Extract setWindowTitleBarVisible() helper function
- Mouse enters window → show title bar, mouse leaves → hide
2026-06-30 23:54:36 +08:00
5a031a57ea fix: fully hide title bar including traffic lights
- Hide close/minimize/zoom buttons on startup
- Show all buttons + title on hover, hide on exit
- Use asyncAfter to ensure window is ready
- Find visible window instead of first window
- Enable window drag by background when title bar hidden
2026-06-30 23:49:32 +08:00
03b8ef8d40 feat: URL deduplication for media library and queue
- On load: merge duplicate URLs (keep first, merge tags/liked/position)
- addItem: check queue for existing URL, show toast if duplicate
- addItemNoPlay: skip if URL already in queue
- Add hasItem(url:) helper
- Playlist references updated when merging duplicates
2026-06-30 23:45:02 +08:00
7c1a84c413 feat: title bar hover to show/hide
- Title bar hidden by default (titleVisibility + titlebarAppearsTransparent)
- Mouse enter top 28pt area: show title bar
- Mouse exit: hide title bar
- Decouple title visibility from toolbar state
2026-06-30 23:27:14 +08:00
610f619a35 feat: add player status overlay (loading/buffering/error)
- 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
2026-06-30 23:10:20 +08:00
b1937bfc16 fix: 修复新增availability字段导致JSON解码失败
自定义init(from decoder:),为新增字段提供默认值:
- availability默认.unknown
- 兼容旧版JSON数据,避免解码失败清空媒体库
2026-06-30 22:43:11 +08:00
af710e4e4f feat: 流媒体URL并发可用性检测
- 后台并发检测所有远程流URL(最多10并发)
- 5秒超时,403标记forbidden,连接失败标记unavailable
- 默认过滤不可用频道,眼睛图标切换显示
- 进度条+状态文本实时显示检测进度
- 行内状态图标:绿✓可用/红🔒无权限/灰✗不可用
- 支持取消检测
2026-06-30 22:41:04 +08:00
48011dd4f0 fix: 修复M3U导入后UI冻结问题
- save()改为async+debounce(500ms),不阻塞主线程
- URL查找从O(n)线性扫描改为O(1)索引
- 播放位置保存增加节流(30秒/5秒差值)
- 列表分页渲染(先100条,滚动加载)
- 退出时强制同步保存位置
- 直播流跳过seek恢复
2026-06-30 08:49:18 +08:00
05afadfd11 feat: 媒体库 - 播放记忆、多播放列表、M3U导入、模糊搜索、喜欢/标签系统
- MediaLibrary.swift: 数据模型(JSON持久化) + 播放列表管理 + 标签/喜欢
- M3UParser.swift: 解析IPTV格式M3U播放列表(EXTINF属性)
- MediaLibraryView.swift: NavigationSplitView侧边栏+内容区UI
- PlayerBridge.swift: 播放位置记忆(暂停/切歌/退出保存,播放恢复)
- 本地视频自动入库,搜索结果临时播放列表,所有操作不中断播放
- 快捷键: Cmd+Shift+L 媒体库, Cmd+Shift+I 导入M3U
2026-06-30 00:28:14 +08:00
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