--- title: 'Telling a Call From an Open App' excerpt: "GeekBye can notice you've joined a video meeting and offer to record it. The detection turns out to be the easy half — a Swift binary reading window titles every ten seconds. The hard half is precision: not firing when Zoom is merely open, not prompting for a meeting you're already recording, and not muting the microphone in the call you're actually in. Three releases, and each one is a guard that had to learn to not defeat itself." date: '2026-07-23' author: 'Steven' authorAvatar: '/images/blog/authors/steven.jpg' coverImage: '/images/blog/covers/telling-a-call-from-an-open-app.svg' tags: ['Engineering', 'macOS', 'Desktop', 'GeekBye Releases'] keywords: - 'detect active video meeting macos window title' - 'CGWindowListCopyWindowInfo window title matching swift' - 'know if user is in zoom teams meet call' - 'mute microphone app level not system wide electron' - 'suppress notification while recording already active' - 'avoid false positive meeting detection app open' lastModified: '2026-07-23' tldr: 'GeekBye''s automatic meeting detection is a native macOS Swift binary (`MeetingMonitor`) that polls `CGWindowListCopyWindowInfo` every ten seconds, reads each on-screen window''s title (`kCGWindowName`), and substring-matches it against a list of active-call patterns — window titles, deliberately not running processes, because ''Zoom is open'' is not ''you''re in a Zoom call.'' For browser meetings it also pulls the active tab''s URL via AppleScript, on a background queue with a 3-second timeout so a hung browser can''t stall the poll. When it matches, a small always-on-top banner offers ''Start Recording.'' The engineering is almost entirely about precision: the pattern list is full of carve-outs (`"Meet - "` not `"Google Meet"`; `"Zoom Meeting"` not `"Zoom Workplace"`), and an anti-nag state machine (stable per-platform `meetingId`, a dismissed-set, a `notificationActive` flag) keeps it from repeating itself. Two follow-up releases fix guards that defeated themselves: v1.8.18 changes mic mute from a system-wide input-volume zero (which muted your microphone in the actual meeting) to an app-level mute of only GeekBye''s own audio pipeline; and v1.8.19 replaces the self-resetting `notificationActive` flag with a callback that checks the real transcription state, so the app stops offering to record a meeting you''re already recording.' keyTakeaways: - "Detection uses window titles, not process lists: a Swift binary reads `kCGWindowName` from `CGWindowListCopyWindowInfo` because 'the app is running' (`us.zoom.xos` is open) is a different fact from 'you are in a call' — the whole design distinguishes the two" - 'The hard part of detection is not detecting: the pattern list is mostly carve-outs — match `"Meet - "` but not the bare `"Google Meet"` landing page, `"Zoom Meeting"` but not `"Zoom Workplace"` — because a false positive that prompts you when you''re not in a call is worse than a slow true positive' - "A convenience feature needs an anti-nag state machine: a stable per-platform `meetingId` (so joining/leaving doesn't re-trigger as the title changes), a `dismissedMeetings` set cleared only when the meeting ends, and a single-banner `notificationActive` flag — the feature is 20% detection and 80% not being annoying" - "v1.8.18's real bug: muting the mic 'to be safe' was implemented as `set volume input volume 0`, a system-wide mute that silenced your microphone in the actual Zoom/Meet call too — the fix mutes only GeekBye's own pipeline (zeroed samples in Listen mode, a `GainNode` at 0 in video mode), leaving the OS mic alone" - "v1.8.19's real bug: a guard that resets itself is not a guard — the `notificationActive` flag got cleared during the detector's own ended/detected cycling, so notifications reappeared mid-recording; the fix injects a `recordingActiveChecker` that reads the true transcription state instead of a local flag" --- GeekBye has a feature that sounds trivial and isn't: it notices when you've joined a video meeting and offers, with one click, to start recording it. The pitch is a single sentence. The implementation is three releases of learning that the interesting problem was never "detect a meeting" — it was "be right about it." Right about whether you're actually in a call versus just having the app open. Right about not asking twice. Right about not muting the wrong microphone. Each of the three releases is, at heart, a guard that had to be taught not to trip over itself. ## Window titles, not processes The first and most consequential decision is written into a commit message as a design statement, not a footnote: > Detection uses window title matching (not process detection) to distinguish "app is open" from "in an active call." That distinction is the whole feature. If you detected meetings by looking at which apps are _running_, you'd fire the instant Zoom launched — and Zoom is open on a lot of desktops all day without a single call. So GeekBye doesn't watch processes. On macOS it runs a small native Swift binary, `MeetingMonitor`, that every ten seconds calls `CGWindowListCopyWindowInfo` for the on-screen windows and reads each one's title via `kCGWindowName`. A window titled `"Zoom Meeting"` means you're in a call; the `"Zoom Workplace"` window means the app is merely open. The signal isn't "is Zoom alive," it's "does a window on your screen look like a live call." The poll interval is a hard constant — `pollIntervalSeconds: Double = 10.0` — plus one immediate check at startup, so a meeting is noticed within about ten seconds of its window appearing. (That's a code value, not a measured latency; treat "up to ~10 s" as arithmetic, not a benchmark.) Browser meetings need one more step, because a Chrome window titled `"Meet - Weekly Sync"` is promising but the certainty is in the URL. So when the frontmost app is a known browser — Chrome, Safari, Edge, Brave, and a handful more identified by bundle id — the binary extracts the active tab's URL through AppleScript and checks it against patterns like `meet.google.com/`, `zoom.us/j/`, and `teams.microsoft.com/`. AppleScript into another app can hang, so that call runs on a background `DispatchQueue` guarded by a `DispatchSemaphore` with a **3-second timeout**; a wedged browser times out instead of freezing the whole detector. ## The hard part is not firing Here's the part that surprised me when I read the pattern list. Detecting a call is a few string matches. _Not_ detecting one — not firing when you don't want it to — is where all the care went. The list of window-title patterns reads like a set of deliberate exclusions: - Google Meet matches `"Meet - "` and its en-dash variant `"Meet – "`, but **not** the bare string `"Google Meet"` — because that's the landing page you open before a call. - Zoom matches `"Zoom Meeting"` and `"Zoom Webinar"`, but **not** `"Zoom Workplace"` or a bare `"Zoom"` — the app sitting open. - Teams matches `"Meeting with"`, `"Meeting in"`, `" | Call"`, `" | Meeting"` — the in-call title shapes, not the idle client. - Slack huddles, Webex rooms, and WhatsApp calls each get their own in-call-only fragments. Every one of those is a decision to stay quiet in the common case where an app is open but you're not in a meeting. The design goal isn't maximal recall; it's not being the tool that pops a "want to record?" banner while you're reading Slack. A false positive here is expensive in a way a false negative isn't: miss a call and the user starts recording manually; misfire on an open app and the user learns to ignore your banner. The whole heuristic is tuned toward the second failure being rare. It's honest to name the limits of a heuristic this simple. The patterns are English-only — a localized OS or a non-English meeting app won't match, an acknowledged v1 limitation. And because window titles for other apps are only visible with Screen Recording permission granted, detection quietly degrades to nothing without it. Simple, legible, and imperfect in ways the team wrote down rather than hid. ## The offer, and the machinery that keeps it from nagging When a pattern matches, GeekBye shows a small frameless always-on-top banner in the corner — its own window, not an OS notification — with a **Start Recording** button. Click it and an IPC message (`meeting:start-recording`) hides the banner, brings up the main window, and tells the renderer to start a session. Dismiss it and the banner goes away. But a naive version of this would be maddening. Titles change constantly during a call — a participant joins, you rename the meeting, a screen-share swaps the window name — and each change would re-fire a detector that keyed on the exact string. So the detector, in `MeetingDetector.ts`, carries a small state machine whose entire job is restraint: - **A stable `meetingId` per platform.** The Swift side emits an id like `google-meet-active`, deliberately _not_ derived from the volatile window title, so joins, leaves, and renames within one call don't read as a new meeting. - **A `dismissedMeetings: Set`.** Dismiss the banner and that meeting's id is ignored — until the meeting actually ends, at which point the id clears so the _next_ real meeting can prompt fresh. - **A `notificationActive` boolean.** Only one banner at a time, so a Slack huddle popping up while you're already in a Meet doesn't stack a second prompt. That's the feature, really: a little detection and a lot of "don't be annoying." Which is exactly why the next two releases exist — because two of those guards turned out to defeat themselves. ## The mute that muted your actual meeting (v1.8.18) Recording has a mute button, and the first implementation took "mute" at its most literal. Shipped in v1.8.15, it muted the microphone by setting the operating system's input volume to zero: ``` osascript -e "set volume input volume 0" ``` with the previous volume saved to the database so it could be restored, a Windows equivalent through `waveInSetVolume`, and a `before-quit` safety net to make sure it un-muted. The code even described it as a feature — muting "at TWO levels," system and app, so the mic was blocked for _all_ apps. That last phrase is the bug stated as a boast. Blocking the mic for all apps means blocking it for the Zoom or Meet call you're actually sitting in. You press mute in GeekBye to stop _it_ from hearing you, and you've gone silent in your real meeting. The v1.8.18 fix is a demotion: stop touching the OS input volume entirely, and mute only GeekBye's own audio pipeline. In Listen mode, `RealtimeAudioCaptureService` replaces muted mic frames with a zero buffer so silence flows to the mixer; in video mode, `ScreenRecordingService` sets a Web Audio `GainNode` to zero. The system-wide mute, the saved-volume recovery, the whole cross-platform apparatus — all deleted. Mute should mean "this app stops listening," never "your microphone turns off for everyone." ## The guard that reset itself (v1.8.19) The other self-defeating guard was `notificationActive`. On paper it already answered "don't show a banner if one's up" — and it looked like it should also cover "don't offer to record a meeting I'm already recording." It didn't, and the commit that fixes it explains why precisely: > The notificationActive flag resets when the meeting detector cycles through ended/detected states, causing notifications to reappear mid-recording. The detector is a live poll; over a long call it can briefly read "ended" and then "detected" again — a title flicker, a window momentarily off-screen — and each cycle reset the flag. So the guard kept quietly re-arming itself while you were recording, and the banner would reappear offering to record the call you were already recording. The fix stops trusting a local flag and asks the source of truth instead: a `recordingActiveChecker` callback, injected from `main.ts` and wired to the real `isRealtimeTranscriptionActive` state, checked before any notification shows. ```ts // Don't show notification if recording/transcription is already active if (this.recordingActiveChecker?.()) { return } ``` The lesson is small and general: a guard whose state you own can be reset out from under you by your own control flow. A guard that reads the actual state it's protecting can't. `notificationActive` was tracking a _proxy_ for "are we recording"; the fix asks whether we're recording. ## Three things these releases taught us 1. **Detect the state you mean, not the one that's easy to see.** "The app is running" is trivial to check and wrong. "A window that looks like a live call exists" is a little harder and correct. The gap between those two is the entire feature. 2. **For a proactive feature, precision is the product.** Nobody remembers the meeting you detected correctly; they remember the three times you interrupted them when they weren't in one. The pattern carve-outs and the anti-nag state machine are not polish — they're the difference between a helpful banner and one you train yourself to ignore. 3. **A guard that can reset itself isn't a guard.** Both follow-up releases are the same shape: a safety mechanism that referenced a proxy it controlled (a saved volume, a local boolean) instead of the real state (the OS mic, the transcription status). Point guards at the truth, not at a copy of it. For the previous chapter in the v1 story, [taking the backend out of the upload path](/blog/taking-the-backend-out-of-the-upload-path) (v1.8.11–v1.8.13); for the next, [the three verbs that keep web audio alive](/blog/the-three-verbs-that-keep-web-audio-alive) (v1.8.16 + v2.0.2); and for the whole arc, [the anatomy of shipping software to perfection](/blog/anatomy-of-perfection).