用Swift重建我们的Electron会议录音引擎
我们的桌面应用程序在没有机器人干预的情况下捕捉会议并将其流式传输到云端。几个月来,录音引擎是我们产品中最难让其稳定的部分。我们修复了一类边缘案例,发布了它,然后下周又会出现一个新的。根本原因各不相同,但模式却相同。该引擎在我们Electron应用的渲染进程中运行。我们尝试了明显的修复:更严格的生命周期管理,将工作移出主线程,使其与React的渲染周期隔离。每次更改在边缘上有所帮助,但没有解决真正的问题。渲染进程不是实时音频和视频捕获的正确位置。一个捕获引擎不能容忍GC暂停、节流或浏览器运行时为保持响应而做的其他操作。因此,我们选择了原生方案:macOS上的ScreenCaptureKit,Windows上的libobs,以及将它们连接在一起的共享Swift层。Atomic:我们的Combine到Jotai的桥接 将本地运行时与React连接通常意味着手动编写本地附加绑定。你需要序列化每个跨越边界的值,通过字符串类型的名称路由事件,并在添加属性时更新三个文件:Swift类、C++绑定和TypeScript包装。这是有效的,但一旦有人忘记步骤,立刻就会失去同步。如果每个@Published属性在Swift中自动成为React中的Jotai原子呢?完全响应的、类型安全的,没有胶水代码。这就是我们内部工具Atomic所做的。@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) const player = new AudioPlayer(); const volumeAtom = atomWithNativeState<number>(player.volume); store.set(volumeAtom, 0.5); // 流入Swift。player.play(); // 更新流回React。从React的角度来看,这些原子与任何其他Jotai原子 indistinguishable。数据在不同线程的Swift运行时中的存在是不可见的。@NodeExport宏在编译时生成整个桥。类型自动映射(Int → number,String? → string | null)。Swift中的值更改会在Node的事件循环上调度回调。我们在Swift端添加的每个新属性在React中立刻可用。并且因为Atomic是基于Swift构建的,而不是Apple框架(我们在Windows上使用OpenCombine),同样的桥可以在两个平台上运行。 两个捕获引擎,一个接口 在macOS上,ScreenCaptureKit为我们提供硬件加速捕获和本地内容选择器。在Windows上,我们使用libobs通过一个叫做OBSKit的Swift包装器。两个引擎有着根本不同的架构。在macOS上,我们从三个独立源接收原始样本缓冲区并自行组装文件。在Windows上,捕获、混合、编码和复用以单个图形运行。我们配置它,文件监视器将新写入的字节流传输到我们的上传会话。Windows捕获有其自身的挑战。我们使用Windows Graphics Capture(WGC)作为主要方法,如果它无法及时提供帧,我们会回落到BitBlt。我们还会检测所有黑色帧(某些仿真窗口或游戏中常见),并在录制中途切换方法。 当时钟不一致 这是macOS引擎获得其复杂性的地方。三个捕获源,三个硬件时钟,对时间有三种不同的理解。两个音频源都被时间戳并转化为全局帧索引。混合器以锁步的方式排出两个队列,只有在两者都有足够数据时才会产生输出。如果一个源停滞(静音麦克风、虚拟设备冻结),混合器在500毫秒后检测到并切换到单源模式,直到它恢复。然后,还有一个更微妙的问题:关于其采样率的音频驱动。在一些虚拟驱动中,它们报告48kHz但提供44.1kHz的缓冲区。在一个30分钟的会议中,这种漂移会变得可听。我们的修复是基于信心的纠正:我们测量实际缓冲的节奏,如果在三次连续的缓冲区中它始终与报告的格式不一致,我们便以正确的速度重新解释流,并进行交叉淡化以避免出现点击声。在Windows上,这种复杂性大部分被捕获引擎的内部混合器抽象化。权衡是控制:在macOS上,我们自行检测并修复像说谎驱动这种边缘案例;在Windows上,我们用简化代替了这种细致。 确保能够保存的录音 正常的MP4在文件结尾写入其元数据。在此之前崩溃,录音就消失了。在两个平台上,我们使用片段化的MP4代替。每个片段都是自包含的。第30分钟的崩溃最多只会丢失最后一秒。片段同时存储到本地和云端。如果网络断开,片段会保留在本地,并在连接恢复时自动继续上传。桌面录音曾经是我们最常见的支持票源之一。现在,它是一个只需正常工作的无聊部分。整个重写的过程
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡