Rebuilding our Electron meeting-recording engine in Swift
Reengineering our Electron Recording Engine with Swift
By Arthur Guiot | April 16, 2026
Our desktop application is designed to capture meetings without the need for a bot, streaming the data directly to the cloud. However, for a long time, the recording engine was the most volatile part of our stack. We found ourselves in a cycle of "whack-a-mole": fixing one edge case only to have another emerge a week later.
We attempted several standard optimizations:
- Tightening lifecycle management
- Offloading heavy lifting from the main thread
- Decoupling the engine from React's render cycles
While these provided marginal gains, they didn't solve the root cause.
The Realization: A browser render process is fundamentally the wrong environment for real-time audio and video capture.
A high-performance capture engine cannot tolerate the non-deterministic nature of a browser runtime, such as garbage collection (GC) pauses or aggressive CPU throttling. To solve this, we migrated to a native architecture: ScreenCaptureKit for macOS and libobs for Windows, unified by a shared Swift layer.
Atomic: Bridging Swift to Jotai
Typically, connecting a native runtime to React requires tedious manual bindings. You usually have to:
- Serialize values across the boundary.
- Route events via string-based naming.
- Update three separate files (Swift class C++ binding TypeScript wrapper) for every single property change.
This process is brittle and prone to synchronization errors. We envisioned a better way: What if every @Published property in Swift automatically functioned as a Jotai atom in React?
The Implementation
Using a custom @NodeExport macro, we can define a native class that is instantly accessible in the frontend:
@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);
// Updating the atom flows directly back into the Swift runtime
store.set(volumeAtom, 0.5);
From the React perspective, these are standard Jotai atoms. The fact that the data resides in a Swift runtime on a separate thread is entirely abstracted. The @NodeExport macro handles the bridge at compile time, scheduling callbacks on the Node.js event loop. Because we utilize OpenCombine on Windows rather than Apple-specific frameworks, this bridge is fully cross-platform.
Dual Engines, Unified Interface
While the interface is shared, the underlying capture mechanisms differ by OS:
| Feature | macOS Implementation | Windows Implementation |
|---|---|---|
| Primary API | ScreenCaptureKit | libobs (via our OBSKit wrapper) |
| Processing | Raw sample buffers Manual assembly | Integrated graph (Capture Mix Encode Mux) |
| Video Method | Native content picker | Windows Graphics Capture (WGC) |
| Fallback | N/A | BitBlt (if WGC frames lag) |
| Special Logic | Manual clock synchronization | Black-frame detection & mid-stream switching |
The Challenge of Clock Drift
The macOS engine is significantly more complex because it deals with three independent hardware clocks across three capture sources. To synchronize them, we:
- Timestamp all audio sources.
- Convert them to a global frame index.
- Use a mixer that drains queues in lockstep.
If a source stalls (e.g., a frozen virtual device or muted mic), the mixer waits for before switching to single-source mode.
Handling "Lying" Drivers
We encountered a subtle issue where virtual audio drivers report an incorrect sample rate (e.g., reporting while delivering ). We implemented a confidence-based correction system:
To prevent audible pops or clicks during this transition, we apply a smooth crossfade. On Windows, libobs abstracts most of this, trading granular control for simplicity.
Ensuring Recording Persistence
Standard MP4 files are risky because they write critical metadata (the "moov atom") at the end of the file; if the app crashes before the file closes, the recording is lost.
To prevent this, we use a segmented approach:
- Parallel Streaming: Segments are written to local storage and the cloud simultaneously.
- Auto-Recovery: If the network connection drops, segments are cached locally and uploaded automatically once connectivity is restored.
Results
By building the Atomic bridge first, we were able to ship the entire rewrite in just two months. Adding new features became a streamlined process of writing Swift code and seeing the UI update in real-time. Most importantly, desktop recording—once a primary source of support tickets—is now stable.
Interested in solving these kinds of problems? [Join our team].