QuickRecorder 录屏管线解析:系统音免驱录制原理

发布时间:2026/9/20 20:46:00
QuickRecorder 录屏管线解析:系统音免驱录制原理 QuickRecorder 录屏管线解析系统音免驱录制原理【免费下载链接】QuickRecorderA lightweight screen recorder based on ScreenCapture Kit for macOS / 基于 ScreenCapture Kit 的轻量化多功能 macOS 录屏工具项目地址: https://gitcode.com/GitHub_Trending/qu/QuickRecorderQuickRecorder 是一个基于 ScreenCaptureKitmacOS 屏幕捕获框架下文简称 SCKit的轻量级 macOS 录屏工具。整个仓库没有庞大的抽象层而是一条清晰可见的管线从SCShareableContent枚举可捕获对象到按录制类型拼装内容过滤器SCContentFilter再到SCStream把视频帧与系统音、麦克风音送进AVAssetWriter落盘。读完这篇解析你能够独立看懂一条 SCKit 录屏链路的完整数据流并理解免驱动录制系统音暂停/续录时间缝合这两个高频难点的解法。背景与问题域该仓库的角色是单一录制会话型的桌面工具同一时刻只有一条捕获流、一个输出文件、一套浮窗控制面板围绕这三件事组织全部代码。它要解决的核心问题有三个如何在不同录制对象整屏、区域、窗口、应用、纯音频之间统一建模捕获源如何让系统声音系统音回环即 loopback不需要用户安装 BlackHole 之类的虚拟声卡驱动以及如何在暂停、Presenter OverlaymacOS 14 的相机叠加等状态下保证产物的时间轴连续、音画同步。架构全景仓库的源码文件不多先给一份按角色划分的清单QuickRecorder/QuickRecorderApp.swift应用入口与AppDelegate负责权限探测、单实例检查、全局快捷键、主面板与各浮窗的创建QuickRecorder/SCContext.swift全局静态状态中心持有SCStream、文件路径、AVAssetWriter并实现停止/收尾/混音流水线QuickRecorder/RecordEngine.swift录屏引擎本体prepRecord构造捕获过滤器record构造流配置并驱动AVAssetWriterQuickRecorder/AVContext.swift摄像头与 iOS 设备iDevice捕获路径走AVCaptureSession与 SCKit 主路并行QuickRecorder/ViewModel/SwiftUI 界面层含各类选择器屏幕/窗口/应用/区域、放大镜、鼠标高亮、设置、剪辑器、qma 播放器QuickRecorder/Supports/外围能力防休眠SleepPreventer、窗口访问器、Sparkle 更新视图等QuickRecorder/Info.plist声明.qma文档类型与 Sparkle 更新源appcast.xmlSparkle 版本清单appcastQuickRecorder.xcodeproj/project.pbxproj构建配置与 SPM 依赖Sparkle、KeyboardShortcuts、SwiftLAME、AECAudioStream下面按状态 → 捕获 → 编码 → 收尾的依赖顺序拆解四个核心机制。全局状态枢纽整条管线挂在静态 SCContext 上之所以选择static全家桶而不是层层传递的依赖注入是因为录制会话天然全局唯一SCStream只有一条输出文件只有一个。把状态集中到一个静态类里回调分散在AppDelegate与引擎两处与 UI状态栏、设置面板才能以最小成本共享同一份真相。// QuickRecorder/SCContext.swift class SCContext { static var stream: SCStream! static var streamType: StreamType? // screen/window/application/systemaudio/idevice... static var screen: SCDisplay? static var window: [SCWindow]? static var application: [SCRunningApplication]? static var vW: AVAssetWriter! static var vwInput, awInput, micInput: AVAssetWriterInput! static let audioEngine AVAudioEngine() // ... }StreamTypeQuickRecorderApp.swift L657枚举了 8 种录制对象类型它是后面过滤器构造、文件后缀选择、停止分支的共同开关AppDelegate同时实现SCStreamDelegate、SCStreamOutput、AVCaptureVideoDataOutputSampleBufferDelegateQuickRecorderApp.swift L108意味着所有采样缓冲回调最终汇入同一个类捕获侧prepRecord 按类型拼装 SCContentFilterprepRecord是捕获侧唯一入口它把用户想录什么翻译成SCContentFilter。核心是三种构造方式的选择// QuickRecorder/RecordEngine.swift if SCContext.streamType .window || SCContext.streamType .windows { if includ.count 1 { SCContext.filter SCContentFilter(display: screen, including: includ camLayer) } else { SCContext.filter SCContentFilter(desktopIndependentWindow: includ[0]) } } else { // .screen / .screenarea排除式 SCContext.filter SCContentFilter(display: screen, excludingApplications: excluded, exceptingWindows: except) // .application包含式 SCContext.filter SCContentFilter(display: screen, including: includ, exceptingWindows: except) } if SCContext.streamType .systemaudio { SCContext.filter SCContentFilter(display: screen, excludingApplications: [], exceptingWindows: []) prepareAudioRecording() }见 RecordEngine.swift L87-L136录窗口用including只捕获指定窗口录整屏用excludingApplications exceptingWindows排除黑名单应用与指定窗口。整屏录制必须排除自己因为主面板、鼠标高亮窗mousePointer、相机浮窗都是本 App 的浮层不排除就会把自己录进去形成套娃用户的黑名单从UserDefaults里的 JSON 解码出来RecordEngine.swift L80-L85与SCContext.excludedApps中的系统应用Dock、控制中心、通知中心SCContext.swift L53合并systemaudio纯系统音录制也走SCContentFilter但排除列表为空——它只关心流的音频通道视频部分被丢弃编码侧SCStreamConfiguration 到 AVAssetWriterrecord(filter:)负责把过滤器和用户配置帧率、分辨率、编码器、HDR翻译成SCStreamConfiguration与AVAssetWriter// QuickRecorder/RecordEngine.swift if #available(macOS 13, *) { conf.capturesAudio recordWinSound || fastStart || audioOnly conf.sampleRate 48000 conf.channelCount 2 } conf.minimumFrameInterval CMTime(value: 1, timescale: audioOnly ? CMTimeScale.max : (frameRate 60 ? 0 : CMTimeScale(frameRate)))见 RecordEngine.swift L216-L224minimumFrameInterval的timescale直接当帧率用CMTime(value: 1, timescale: 30)就是每帧至少 1/30 秒帧率 ≥60 时传 0 表示不节流由系统按变化驱动交付。作者在注释里专门留了调试输出佐证这一点RecordEngine.swift L239timescale is FPS. 0 means no throttlinginitVideo里根据withAlpha选择AVVideoCodecType.hevcWithAlphaHEVC with Alpha带 alpha 通道的 H.265对应 README 宣称的可输出含透明通道的视频供 iMovie/FCPX 使用RecordEngine.swift L396-L406收尾侧stopRecording 的多分支出口stopRecording是全仓库最长的函数因为录完去哪取决于录制类型与设置的组合SCContext.swift L329-L477vW.finishWriting { // 视频写盘完成后 if ud.bool(forKey: recordMic) ud.bool(forKey: recordWinSound) ud.bool(forKey: remuxAudio) { mixAudioTracks(videoURL: filePath.url) { ... } // 两轨混成单轨 } } // 纯系统音 麦克风时 // .qma 包 → 加载 qmaPackageHandle → AudioPlayerManager 导出 mp3/m4a常规视频录制vwInput/awInput走AVAssetWriter完成即通知或弹预览窗系统音 麦克风且开启混音先用AVAssetExportSession把两条音轨混成一条再合成mixAudioTracksSCContext.swift L714-L838README 的 QA 解释了为什么要留开关——关闭后系统音与麦克风音各自成轨可在剪辑器中独立调音量纯系统音 麦克风默认写.qma下文深挖m4a2mp3则借 SwiftLAME 在进程内完成 AAC→MP3 转码SCContext.swift L494-L507关键机制深挖这一节讲两个最容易抄错的细节暂停/续录的时间缝合以及免驱动系统音的版本分支。暂停续录如何缝合时间轴timeOffset 与 adjustTime 的双重处理⚠️ 先看错误做法很多实现把暂停简单地写成暂停时丢弃帧。但 SCKit 的行为是——流不会停屏幕上有变化就持续交付帧。如果暂停期间只丢帧、不做任何补偿恢复录制后AVAssetWriter收到的首帧 PTS显示时间戳presentation time stamp会带着暂停时长的跳变成片里就出现一段与暂停时长等长的空白或音画错位。该工程的处理分两步。第一步恢复时用timeOffset累积间隙RecordEngine.swift L547-L558if SCContext.isResume { SCContext.isResume false var pts CMSampleBufferGetPresentationTimeStamp(SampleBuffer) guard let last SCContext.lastPTS else { return } // ... let off CMTimeSubtract(pts, last) // 新帧与最后一帧的 PTS 差 暂停时长 if SCContext.timeOffset.value 0 { SCContext.timeOffset off } else { SCContext.timeOffset CMTimeAdd(SCContext.timeOffset, off) } }第二步之后每一帧都用adjustTime把timeOffset从时间戳中减掉SCContext.swift L685-L700内部走CMSampleBufferCreateCopyWithNewTiming重建时序。因为 offset 是持续累积的多次暂停—恢复也不会产生二次跳变。反过来说如果只减不累积每次用单帧差第二次恢复就会把之前已缝合的部分再次偏移——这正是这个机制容易被误解的地方它缝合的不是一帧而是输出时间轴与真实时间轴的累计差。配合它还有两个守卫frameQueue用最近 20 帧的 PTS 做去重Skip this frameRecordEngine.swift L576防止 SCKit 在边界时刻重发同一帧firstFrame缓存首帧供缩略图预览SCContext.swift L483。免驱动系统音为什么成立capturesAudio 与 CMIO 开关的分野QuickRecorder supports driver-free audio loopback recording, mouse highlighting, screen magnifier and many more useful features.README.md L31免驱动在两个 macOS 版本上的实现完全不同。macOS 13 起SCKit 的原生能力直接给出了答案SCStreamConfiguration.capturesAudio true流的.audio输出通道里就是系统混音结果RecordEngine.swift L216-L220 的#available(macOS 13, *)分支只做这一件事。而 macOS 12 上系统音要经过 CoreMediaIO 的屏幕捕获虚拟设备该设备默认被硬件层关闭。工程在启动时applicationWillFinishLaunching直接把这个全局开关拨上去QuickRecorderApp.swift L255-L262var allow: UInt32 1 var prop CMIOObjectPropertyAddress( mSelector: CMIOObjectPropertySelector(kCMIOHardwarePropertyAllowScreenCaptureDevices), mScope: kCMIOObjectPropertyScopeGlobal, mElement: kCMIOObjectPropertyElementMain) CMIOObjectSetPropertyData(CMIOObjectID(kCMIOObjectSystemObject), prop, zero, nil, dataSize, allow)反事实推理如果去掉这段代码macOS 12 用户录系统音会得到一个音轨恒为静音的文件而且排查起来极具迷惑性——权限都在、流也正常只有声音是零。作者没有为它写注释但配套的默认值注册暴露了意图remuxAudio与recordWinSound在 macOS 12 上默认为falseQuickRecorderApp.swift L234-L235即老系统上更保守地关闭音频链路组合降低这条 CMIO 路径上的变量。这也是为什么这样写的答案不是不想录而是老系统的音画合成链路更脆默认关掉最易出错的组合。验证与闭环✅ 这个仓库没有 CI 工作流与测试 target仓库内未见.github或 XCTest 目录需确认它的验证闭环由三件东西构成Xcode scheme、Sparkle 更新链路、以及运行时权限流程本身。谁在用它、怎么触发构建入口是 QuickRecorder.xcodeproj/xcshareddata/xcschemes/QuickRecorder.xcschemeSPM 四个依赖在首次构建时联网解析发布侧形成自更新闭环Info.plist的SUFeedURL/SUPublicEDKeyInfo.plist L47-L50指向版本清单仓库根目录的 appcast.xml 即其内容入口init里SPUStandardUpdaterController(startingUpdater: true, ...)QuickRecorderApp.swift L48让每次启动都自动检查运行时验证靠权限拒绝路径自我检查SCShareableContent拿不到显示器就重连、userDeclined时弹授权窗并终止SCContext.swift L68-L91最小复现序列所有操作在你的本地副本中进行不改仓库git clone https://gitcode.com/GitHub_Trending/qu/QuickRecorder cd QuickRecorder xcodebuild -project QuickRecorder.xcodeproj -scheme QuickRecorder -configuration Release build预期产物DerivedData下的QuickRecorder.app。首次启动需授予屏幕录制必须、麦克风可选、相机可选权限在默认保存目录桌面会得到形如Recording at y-MM-dd HH.mm.ss.mp4的文件命名规则见 SCContext.swift L180-L184。前置条件macOS 12.3README 声明project.pbxproj中MACOSX_DEPLOYMENT_TARGET同时出现 12.3 与 14.2 两组取值需确认能联网拉取 SPM 依赖HDR 预设需 macOS 15 且 Swift 6 编译器#if compiler(6.0)分支RecordEngine.swift L148-L161Presenter Overlay 相关判定需 macOS 14.2。迁移价值与适用边界可以直接搬走的设计SCContentFilter三构造法including/excludingApplicationsexceptingWindows/desktopIndependentWindow按录制对象切换的分支结构是任何 SCKit 录屏应用都能直接套用的骨架用CMTime(value: 1, timescale: fps)表达帧率上限、0 表示不节流的写法附带帧只在内容变化时交付这一 SCKit 特性源码注释 RecordEngine.swift L229ScreenCaptureKit only delivers frames when something changes暂停/续录的timeOffset累积 CMSampleBufferCreateCopyWithNewTiming重写时序模式可平移到任何流不停、但用户要暂停的实时采集场景.qma多轨音频包的做法系统音与麦克风音分文件写入 info.jsonUTType声明成 macOS 包Info.plist L51-L79双击即打开自研播放器既保留后期独立调音量能力又不污染 Finder 目录用VTCompressionSessionCreate(..., RequireHardwareAcceleratedVideoEncoder: true)预检硬件编码器、失败时引导用户换 H.265 的降级模式RecordEngine.swift L257-L283适用边界全局静态状态 单实例守卫NSWorkspace按 bundle id 查重QuickRecorderApp.swift L204-L210是为同时只录一路量身定做的想扩展到多路并发录制需要重构且该 App 明确不做沙盒README QA 自述不上 App Store依赖用户手动授予屏幕录制权限的权限模型不适用于需要上架分发的场景。结语一句话定位QuickRecorder 用不到十个 Swift 文件把 SCKit 录屏的捕获—编码—收尾三段式写成了教科书式的直线结构SCContext是状态RecordEngine是管线而免驱动系统音与时间缝合这两个细节正是它与调个 API 就能录的演示代码之间真正的分水岭。【免费下载链接】QuickRecorderA lightweight screen recorder based on ScreenCapture Kit for macOS / 基于 ScreenCapture Kit 的轻量化多功能 macOS 录屏工具项目地址: https://gitcode.com/GitHub_Trending/qu/QuickRecorder创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考