使用 Swift 重建我们的 Electron 会议录制引擎

Hacker News Top 产品

摘要

Circleback 使用 Swift 重建了其 Electron 会议录制引擎,以提升可靠性。他们采用了 ScreenCaptureKit 和 libobs 等原生录制方法,并借助内部工具 Atomic 在 Swift 和 React 之间架起桥梁。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/08/21 19:29

# 我们如何用Swift重写Electron录制引擎 来源:https://circleback.ai/blog/how-we-rebuilt-our-electron-recording-engine-in-swift 我们的桌面应用无需机器人即可捕获会议内容,并将其流式传输至云端。数月来,录制引擎一直是产品中最难以保证可靠性的部分。我们修复了一类边界情况并发布后,下一周又会出现新的问题。虽然根因各异,但模式相似。 该引擎原本运行在Electron应用的渲染进程中。我们尝试了常规解决方案:更严格的生命周期管理、将任务移出主线程、使其与React渲染循环隔离。每次调整都略有改善,但未触及核心问题——渲染进程并非进行实时音视频捕获的合适环境。捕获引擎无法容忍垃圾回收暂停、节流或其他浏览器运行时为保持响应性而执行的操作。 因此我们转向原生方案:在macOS上使用ScreenCaptureKit,Windows上使用libobs,并用共享的Swift层将它们整合。 ### Atomic:我们的Combine-to-Jotai桥接方案 将原生运行时与React桥接通常需要手动编写原生插件绑定。你需要序列化所有跨越边界的值,通过字符串类型化名称路由事件,并且每添加一个属性就要修改三个文件:Swift类、C++绑定和TypeScript包装器。这种方法虽然可行,但一旦有人遗漏步骤就会立即失步。 如果Swift中的每个`@Published`属性都能自动转化为React中的Jotai原子呢?完全响应式、类型安全、无需粘合代码。这就是我们内部工具Atomic的实现效果。 ```swift @NodeExport public final class AudioPlayer { @Published public var isPlaying: Bool = false @Published public var volume: Float = 1.0 public func play() { isPlaying = true } public func pause() { isPlaying = false } } #AtomicExport(AudioPlayer.self) ``` ```typescript const player = new AudioPlayer(); const volumeAtom = atomWithNativeState(player.volume); store.set(volumeAtom, 0.5); // 数据流入Swift端 player.play(); // 更新回流至React端 ``` 对React而言,这些原子与任何其他Jotai原子毫无区别。数据实际存储于不同线程的Swift运行时这一事实完全透明。 `@NodeExport`宏在编译时生成整个桥接层。类型自动映射(`Int`→`number`,`String?`→`string | null`)。Swift中的值变更会在Node事件循环上调度回调。我们在Swift端新增的每个属性都能立即在React中使用。并且由于Atomic基于Swift而非Apple框架构建(在Windows上使用OpenCombine (https://github.com/OpenCombine/OpenCombine)),同一桥接方案可在双平台运行。 ### 双捕获引擎,统一接口 在macOS上,ScreenCaptureKit提供硬件加速捕获和原生内容选择器。在Windows上,我们通过名为OBSKit的Swift包装器使用libobs。两个引擎架构截然不同: macOS端从三个独立源接收原始样本缓冲区并自行组装文件;Windows端则将捕获、混音、编码和封装运行在单一处理图中。我们配置该引擎后,文件监视器会将新写入的字节流传输至上传会话。 Windows捕获面临独特挑战:我们采用Windows图形捕获(WGC)作为主要方案,若其无法及时交付帧,则回退至BitBlt。我们还会检测全黑帧(常见于某些模拟窗口或游戏),并在录制中途切换捕获方式。 ### 当时钟不同步时 这正是macOS引擎展现其复杂性的场景:三个捕获源、三套硬件时钟、三种不同的时间基准。 两个音频源都会添加时间戳并转换为全局帧索引。混音器严格同步排空两个队列,仅当双方都有足够数据时才输出。若某源卡顿(静音麦克风、冻结的虚拟设备),混音器会在500毫秒后检测到并切换至单源模式,直到其恢复。 还存在更微妙的问题:某些音频驱动会谎报采样率。虚拟驱动可能报告48kHz却实际以44.1kHz传输缓冲区。在30分钟的会议中,这种漂移会导致明显听觉差异。我们的解决方案是基于置信度的校正:通过测量实际缓冲区节奏,若连续三个缓冲区与格式报告持续不符,则以正确速率重新解释音频流,并使用交叉淡入淡出避免爆音。 在Windows上,大部分复杂性被捕获引擎内部混音器抽象处理。这种权衡在于控制粒度:macOS端我们自行检测并修复驱动谎报等边界情况;Windows端则以简单性换取精细控制。 ### 能在崩溃中幸存的录制 常规MP4文件在末尾写入元数据。若在完成前崩溃,录制内容便会丢失。在双平台中,我们改用分片MP4格式。 每个分片都是自包含的。即使在30分钟时崩溃,最多只会丢失最后1秒内容。分片会同时存储至本地和云端。若网络中断,分片会在本地持久保存,待连接恢复后自动续传。 桌面录制曾经是我们最常见的支持工单来源之一,如今已成为稳定可靠的应用功能。整个重写工作在两个月内完成,Atomic功不可没:一旦桥接建立,添加功能只需编写Swift代码并实时观察UI更新。 并非所有任务都应置于渲染进程中。有时你需要转向原生方案。若对此类挑战感兴趣,欢迎加入我们 (https://circleback.ai/jobs)。

相似文章

全程原生,直到你需要文本

Hacker News Top

一位资深 macOS/iOS 开发者讲述了使用苹果原生框架(SwiftUI、AppKit、TextKit)实现支持 Markdown 的聊天界面的挣扎,最终发现像 Electron 这样的基于 Web 的技术为富文本渲染提供了更实用的解决方案。

DevRecorder

Product Hunt

DevRecorder 是一款专为开发者设计的屏幕录制工具,支持控制台、网络、错误捕获和注释功能。

Show HN: Keen Bean — Mac 会议笔记,边说边起草 Spec

Hacker News Top

Keen Bean 是一款 macOS 应用,可在本地录制会议音频,实时生成笔记、任务、Spec 和图表,无需向通话中添加机器人。数据以 Markdown 和 JSON 格式保存在设备本地,面向需要私密且可落地的会议产出的顾问和创始人。