logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·06.11.26
← Back to news

Rebuilding our Electron meeting-recording engine in Swift

Original article
circleback.ai|9 points|1 comments|by arguiot|Aug 21, 2026

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:

  1. Serialize values across the boundary.
  2. Route events via string-based naming.
  3. Update three separate files (Swift class →\rightarrow→ C++ binding →\rightarrow→ 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:

FeaturemacOS ImplementationWindows Implementation
Primary APIScreenCaptureKitlibobs (via our OBSKit wrapper)
ProcessingRaw sample buffers →\rightarrow→ Manual assemblyIntegrated graph (Capture →\rightarrow→ Mix →\rightarrow→ Encode →\rightarrow→ Mux)
Video MethodNative content pickerWindows Graphics Capture (WGC)
FallbackN/ABitBlt (if WGC frames lag)
Special LogicManual clock synchronizationBlack-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:

  1. Timestamp all audio sources.
  2. Convert them to a global frame index.
  3. 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 500ms500\text{ms}500ms 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 48kHz48\text{kHz}48kHz while delivering 44.1kHz44.1\text{kHz}44.1kHz). We implemented a confidence-based correction system:

If (Actual Cadence≠Reported Rate) for 3 consecutive buffers→Reinterpret Stream\text{If } (\text{Actual Cadence} \neq \text{Reported Rate}) \text{ for } 3 \text{ consecutive buffers} \rightarrow \text{Reinterpret Stream}If (Actual Cadence=Reported Rate) for 3 consecutive buffers→Reinterpret Stream

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].