使用 Swift 重建我们的 Electron 会议录制引擎
摘要
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)。
相似文章
全程原生,直到你需要文本
一位资深 macOS/iOS 开发者讲述了使用苹果原生框架(SwiftUI、AppKit、TextKit)实现支持 Markdown 的聊天界面的挣扎,最终发现像 Electron 这样的基于 Web 的技术为富文本渲染提供了更实用的解决方案。
@circlebackai: Circleback 现在可以捕捉会议中屏幕共享的详细信息。幻灯片、仪表盘、时间线、文档……
Circleback 现在能够捕捉会议屏幕共享中的详细信息,包括幻灯片、仪表盘、时间线和文档,确保所有重要细节都记录在笔记中。
DevRecorder
DevRecorder 是一款专为开发者设计的屏幕录制工具,支持控制台、网络、错误捕获和注释功能。
Show HN: Keen Bean — Mac 会议笔记,边说边起草 Spec
Keen Bean 是一款 macOS 应用,可在本地录制会议音频,实时生成笔记、任务、Spec 和图表,无需向通话中添加机器人。数据以 Markdown 和 JSON 格式保存在设备本地,面向需要私密且可落地的会议产出的顾问和创始人。
@NatashaTheRobot:在苹果将所有WWDC开发者大会环节改为视频形式发布后,这是第一个感觉自然而不做作、充满人情味的视频,而且它非常出色!
本推文推荐了一个WWDC视频,因其自然的演示方式而备受称赞,并为开发者提供了关于如何使用SwiftUI和UIKit为iPhone Duo的新垂直布局调整工具栏的技术指导。