Why I built MeetingDouble (after my fifth ‘sorry, you froze’ joke).
May 15, 2026 · 8 min read
The fifth time it happened, I was on a Thursday client review at 3:40pm. I had two minutes between calls. I’d been sitting at the same chair since 9am. I stood up to stretch and check whether my partner was home yet, the meeting went one minute over, and when I sat back down a colleague said: “sorry, you froze for a sec, can you repeat that?”
I had not, in fact, frozen. I had been in the kitchen. The meeting had moved on without me and someone had filled the gap with the friendly little joke we all make when somebody else’s connection looks bad. I laughed it off and the call went on. It was the fifth time that week.
That night I opened a new note in Obsidian and wrote three lines: a video loop, a virtual camera, an away detector. I’ve been building MeetingDouble in the gaps between contract work ever since.
The setup nobody warned me about
I’d shipped two macOS apps before this one — a meeting reminder and a small Apple Notes companion. So my baseline expectation was a couple of weekends to scaffold, a couple to polish, a Sparkle auto-update pipeline copied from the previous app, a Stripe lifetime license, and out the door. The number that mattered was “time to first virtual camera frame visible in Zoom.”
That number ended up being six months.
CMIOExtensions are a different planet
The old way to ship a virtual camera on macOS was a DAL plug-in: a bundle you dropped into /Library/CoreMediaIO/Plug-Ins/DAL/ and the system picked it up on the next process launch. That mechanism is deprecated. Apple replaced it with CMIOExtensions: a System Extension, signed and notarized, that runs in its own process with its own entitlements, registered with the kernel, exposing a virtual camera device to anyone who asks CoreMediaIO for the camera list.
On paper this is cleaner. In practice it’s a stack of small surprises:
- The extension is a separate executable. Your main app cannot talk to it via shared memory or a normal IPC. You go through XPC, or you go through a shared
IOSurfacehanded across a Mach port. - The extension has no UI, no Dock icon, and limited entitlements. It can produce camera frames and almost nothing else.
- Installing the extension requires the user to approve a System Extension in Privacy & Security. If they miss the prompt or click Don’t Allow, the extension is permanently disabled and there’s no clear path to recovery from inside the app.
- Debugging is awful. You can’t attach lldb to the extension the way you would to a normal target. You read its
os_logoutput in Console, which is shared with every other process on the system. - The App Store will not accept any of this. So you are also signing for Developer ID, notarizing, building a DMG, hosting it yourself, and writing your own Sparkle feed.
The IOSurface week
The thing that broke me, for about a week, was getting a frame from the main app to the extension efficiently.
The naive version is to copy each frame. Capture from AVCaptureSession in the main app, serialize the pixel buffer, push it over XPC to the extension, deserialize on the other side, hand it to CMIOExtensionStream. This works for about thirty seconds. Then your fans spin up and your colleague says you sound like a plane.
The right version is to share an IOSurface: a GPU-resident pixel buffer that two processes can both read with no copy. The main app draws into the surface, sends the extension a small notification with the surface ID, and the extension publishes the surface’s contents as the next frame. Zero copies, no encoding, fans at idle.
This is theoretically a four-line change. In practice it required understanding that:
IOSurfacehandles can be sent across XPC but only if you wrap them correctly withIOSurfaceCreateXPCObject. Pass a raw integer ID and the receiving process sees nothing.- The extension’s sandbox has to be opened to accept the surface. The entitlement is undocumented in the obvious places; you find it in WWDC session notes from 2022.
- The surface format has to match the
CMIOExtensionStreamFormatDescriptionthe extension is publishing, down to the pixel format and byte stride. A subtle mismatch produces a feed of magenta frames with no error. IOSurfaceis reference-counted but the reference counts on each side are independent. Forget to release on the extension side and the surface leaks. Release too early and the main app crashes.
I rewrote that bridge four times. The version in shipping MeetingDouble is the fourth.
The away detector
With frames moving, the next problem was the actual product: knowing when to swap the live feed for the loop. The naive version is a single signal: idle time on mouse and keyboard. The first user (me) walked away to read on the couch with the laptop still in arm’s reach, and the app missed it.
The shipping version is a four-signal AND:
IOHIDIdleTime> 20 seconds.- Vision framework reports no face in the raw frame for > 5 seconds.
AVAudioEngineVAD reports the user silent.- Manual hotkey override (⌘⇧A) wins always.
Any one signal alone gives false positives — bathroom breaks, phone glances, deep listening. Requiring all four agreed cut the false-swap rate to essentially zero in my own use. I wrote about the recording side of this in how to record a loop that doesn’t look fake.
The thing I almost cut
For about a month, MeetingDouble had no microphone mute on return-to-live. You walked back to the desk, the loop crossfaded to live, your audio came back too. This is great if you walk back in silence. It is a disaster if you walk back chewing.
I cut it after a beta tester told me, with the specific weariness of someone who had just been caught, that he had returned to a call mid-bite. Now the mic stays muted until you click I’m back. It’s a single keystroke. It has saved me at least three times since I built it.
What this product is, and what it isn’t
MeetingDouble is small. It does one thing: it lets you step away from the desk during a meeting without the awkward apology. It is not a streaming setup, not a beauty filter, not a virtual background, not a presentation tool. The comparison post on OBS and Mac alternatives spells out what it isn’t in more detail.
What it is: a $129 one-time license, two Macs per buyer, lifetime updates, no subscription, no analytics in the app, and a code path that I personally have stepped through in lldb roughly a thousand times.
Why I’m writing this
Indie macOS apps live and die on word of mouth. The most reliable way to get word of mouth is to write honestly about what the thing is, what it isn’t, and what it cost to make. So: this is what it cost. If you’ve ever frozen in a meeting that you didn’t actually freeze in, this might be your tool.
MeetingDouble ships today. One payment, two Macs, lifetime updates. Buy a license — and if you want the tactical guide first, start with the loop-recording note.
More from MeetingDouble · All notes · Buy · $129