Skip to content
Tech News
← Back to articles

Rebuilding our Electron meeting-recording engine in Swift

read original more articles
Why This Matters

This article highlights the importance of native integration for real-time media capture in desktop applications, demonstrating how moving from Electron to native Swift layers significantly improves reliability and performance. The development of the Atomic tool simplifies bridging native Swift properties with React, enabling more maintainable and reactive code, which benefits both developers and end-users by ensuring more consistent meeting recordings.

Key Takeaways

Our desktop app captures meetings without a bot and streams them to the cloud. For months, the recording engine was the hardest part of the product to make reliable. We'd fix one class of edge case, ship it, and a new one would surface the next week. Different root causes, same pattern.

The engine ran in the render process of our Electron app. We tried the obvious fixes: tighter lifecycle management, moving work off the main thread, isolating it from React's render cycle. Each change helped at the margin, but none addressed the real issue. A render process is the wrong place to do realtime audio and video capture. A capture engine can't tolerate GC pauses, throttling, or any of the other things a browser runtime does to stay responsive.

So we went native: ScreenCaptureKit on macOS, libobs on Windows, and a shared Swift layer tying it together.

Atomic: our Combine-to-Jotai bridge

Bridging a native runtime to React usually means writing native addon bindings by hand. You serialize every value that crosses the boundary, route events through stringly-typed names, and update three files whenever you add a property: the Swift class, the C++ binding, and the TypeScript wrapper. It works, but it's out of sync the moment anyone forgets a step.

What if every @Published property in Swift automatically became a Jotai atom in React? Fully reactive, type-safe, no glue code. That's what our internal tool Atomic does.

@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); // Flows into Swift. player.play(); // Updates flow back into React.

From React's perspective, these atoms are indistinguishable from any other Jotai atom. The fact that the data lives in a Swift runtime on a different thread is invisible.

The @NodeExport macro generates the entire bridge at compile time. Types map automatically ( Int → number , String? → string | null ). Value changes in Swift schedule callbacks on Node's event loop. Every new property we add on the Swift side is instantly available in React. And because Atomic is built on Swift, not Apple frameworks (we use OpenCombine on Windows), the same bridge runs on both platforms.

... continue reading