Skip to content

feat(discovery): capture virtual-meeting audio alongside the mic - #43

Merged
jhosepmyr merged 7 commits into
developfrom
feature/system-audio-capture
Oct 7, 2026
Merged

jhosepmyr merged 7 commits into
developfrom
feature/system-audio-capture

Conversation

@jhosepmyr

Copy link
Copy Markdown
Contributor

Description

Lets the analyst run a live discovery session during a Zoom, Google Meet or Teams call, not only in a face-to-face meeting. A small audio-source picker next to the record button chooses In person (microphone) or Virtual meeting (microphone + meeting audio), remembered per user.

The virtual source opens the browser's screen-share picker straight from the record click (getDisplayMedia with tab/window/system audio, voice processing off), drops the unused video track and mixes the shared audio with the mic in one mono Web Audio mixer. The backend keeps receiving the same 16 kHz Int16 PCM on /ws/stt, so no API change is needed, and the level meter shows the mix.

Edge cases handled with translated messages (es/en):

  • shared without ticking "Share tab audio" → warning and one-click retry, no silent capture;
  • picker dismissed → stays idle, no error;
  • "Stop sharing" mid-session → keeps recording the mic, shows a notice and a Share again action;
  • pause/resume keeps the shared tab, so resuming never reopens the picker;
  • Firefox, Safari and mobile → option disabled with "Available in desktop Chrome or Edge";
  • headphones hint to avoid a duplicated transcript.

Also fixes small issues in the recorder: the mic was requested twice on start, a session that failed to start kept the mic open, and a late close event from an old socket could kill the next one.

Feature module / area: discovery / core

Related issue / US: US56 — capture virtual-meeting audio (usability finding: the product only worked in person)


Type of Change

  • feat — new feature or UI component
  • fix — bug fix
  • refactor — code change without behavior change
  • test — tests only
  • docs — documentation only
  • build / ci — build, dependencies, or CI/CD
  • chore — maintenance

Checklist

  • The PR targets develop (not main)
  • Branch name follows feature/*, bugfix/*, or hotfix/*
  • Commits follow Conventional Commits
  • bun run lint passes locally (ESLint + angular-eslint)
  • bun run test passes locally (Vitest) — 305 passed
  • bun run build passes locally (no type errors, no budget exceeded)
  • New components use ChangeDetectionStrategy.OnPush and Angular signals
  • No localStorage/sessionStorage access for JWT tokens (use the auth store)
  • No bypassSecurityTrust* calls without explicit review
  • No secrets, credentials, or .env content committed
  • CHANGELOG.md updated under [Unreleased]

How to Test

Desktop Chrome or Edge, with headphones:

  1. Join a Meet in tab A. In ReqsAI (tab B) pick Virtual meeting; the headphones hint appears. Reload: the choice is remembered.
  2. Record, choose tab A without "Share tab audio" → amber warning, no session. Retry with it ticked → the meter moves for your voice and the meeting.
  3. Click Chrome's "Stop sharing" → recording continues with a notice and Share again.
  4. Pause, wait, resume → no picker; the transcript includes both sides.
  5. Stop → the sharing bar and mic indicator disappear.
  6. Open in Firefox → the virtual option is disabled with its reason.

Notes (optional)

  • Tab audio works on every desktop OS. Whole-screen system audio works on Windows/ChromeOS; on macOS it needs Chrome 141+ on macOS 14.2+, so the reliable path there is Meet/Zoom/Teams in a browser tab.
  • Both sides end up in one mono stream; speaker separation stays on the backend.

Adds a `meeting` audio source for virtual calls (Zoom / Meet / Teams): the
recorder opens the screen-share picker first (it needs the click's transient
activation), keeps the shared surface's audio with voice processing off,
stops the unused video track and then takes the mic. Both sources feed one
mono GainNode mixer (explicit 1 channel, speakers down-mix) that drives the
level meter and the PCM worklet, so `/ws/stt` still gets 16 kHz Int16 mono.

- Sharing without "Share tab audio" stops the tracks and sets a notice; a
  dismissed picker stays idle without messages.
- "Stop sharing" (audio track `ended`) disconnects the meeting audio and
  keeps streaming the mic with a notice; `shareMeetingAudio()` re-shares.
- `pauseStreaming()` keeps the shared surface so a resume never re-opens
  the picker; `stopStreaming()` releases everything.
- Socket handlers are detached before closing so a late close event can't
  tear down the next socket, and a session that fails to start releases
  the capture.
…ecorder

Mocks navigator.mediaDevices to cover the picker-before-mic ordering (no
await before getDisplayMedia), the display constraints, the dropped video
track, the no-audio-track warning, silent picker dismissal, real capture
failures, a missing getDisplayMedia, mic denial releasing the shared tab,
the "Stop sharing" mic-only fallback and re-share, and pause vs. stop
release. A real Web Audio graph with a fake socket checks that both
sources feed the mono mixer, which drives the meter and the worklet.
Also covers the desktop-Chromium support check and dismissal detection.
A compact segmented control next to the record button picks where the
next recording takes its audio from: "In person (microphone)" or "Virtual
meeting (microphone + meeting audio)". The choice is remembered per user
in localStorage (try/catch) and locked while starting or once a session
is live; the record click passes it to the recorder before any await so
the screen-share picker keeps the click's activation, and a second click
can't stack another picker.

The virtual option stays visible but inert outside desktop Chromium, with
the reason as tooltip and, on tap, as the composer hint. With the virtual
source selected the hint explains to tick "Share tab audio" and to wear
headphones; recorder notices (amber) render in the same line.
Covers the pressed state, switching to the virtual source, the inert
virtual option (tooltip + unavailable event) where meeting audio is
unsupported, the lock during a live session, the per-user storage key
and the fallback to the microphone when a stored virtual choice can't
be honoured.
…e session bar

Virtual-meeting recordings show a meeting-audio badge next to the timer
while the shared audio is mixed in. Once the user clicks "Stop sharing"
(the recording carries on with the mic), the bar shows an amber "Share
again" action that re-opens the picker straight from the click and mixes
the new surface into the live (or paused) recording.
…hare

Covers that in-person recordings show no meeting controls, that the badge
shows while the meeting audio is captured, and that after "Stop sharing"
the bar offers an accessible "Share again" button that calls the recorder.
@jhosepmyr
jhosepmyr requested a review from Eric396 as a code owner October 7, 2026 08:13
@jhosepmyr
jhosepmyr merged commit ab4ab54 into develop Oct 7, 2026
5 checks passed
@jhosepmyr jhosepmyr mentioned this pull request Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant