Skip to content

feat: Detectors for broken microphone setup#2090

Merged
oliverlaz merged 20 commits intomainfrom
detectors
Feb 3, 2026
Merged

feat: Detectors for broken microphone setup#2090
oliverlaz merged 20 commits intomainfrom
detectors

Conversation

@oliverlaz
Copy link
Copy Markdown
Member

@oliverlaz oliverlaz commented Jan 21, 2026

💡 Overview

Adds a broken microphone setup integration. Upon unmuting or switching a device, we start polling for audio levels.
If we don't get any captured audio after a certain threshold (default is 5 seconds), we emit a mic.capture_report event with capturesAudio: false.
Integrators can use this event to show a warning message to the end users so they can adjust their setup (or switch to a different device).
While being silent, the SDK will keep publishing this event. Once sound is detected, this poller will emit mic.capture_report with capturesAudio: true right away, and will stop emitting further events until there is a device switch or unmute of the current one.

📝 Implementation notes

🎫 Ticket: https://linear.app/stream/issue/REACT-759/detectors-api-detection-of-broken-microphone-setup
📑 Docs: https://github.com/GetStream/docs-content/pull/968

Summary by CodeRabbit

  • New Features

    • Browser no-audio detector with configurable silence threshold and on-demand microphone test.
    • New mic.capture_report event and a mic-capture error notification surfaced in call controls and audio preview.
    • Per-microphone test buttons and inline test results in the device inspector UI.
  • Bug Fixes

    • Initialization guards to prevent duplicate subscriptions when reusing call/device instances.
  • Tests

    • Comprehensive no-audio detector tests and Web Audio API test mocks added.
  • Documentation

    • Added CLAUDE.md with project guidance.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Jan 21, 2026

⚠️ No Changeset found

Latest commit: bf07c9d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 21, 2026

📝 Walkthrough

Walkthrough

Adds a browser no-audio detector and integrates it into MicrophoneManager to emit mic.capture_report events; extends event typings; exposes a MicCaptureErrorNotification UI; makes device-manager setup idempotent; adds Web Audio test mocks and comprehensive no-audio detector tests.

Changes

Cohort / File(s) Summary
Event types
packages/client/src/coordinator/connection/types.ts
Add MicCaptureReportEvent, include it in StreamVideoEvent, and update EventTypes/per-type mappings to derive from StreamVideoEvent.
Device base
packages/client/src/devices/DeviceManager.ts
Changed areSubscriptionsSetUp and findDevice visibility from private to protected for subclass reuse.
Microphone manager
packages/client/src/devices/MicrophoneManager.ts
Integrate createNoAudioDetector, manage async detector lifecycle/cleanup, add silenceThresholdMs with setSilenceThreshold(), add performTest(), enforce setup idempotence, and emit mic.capture_report.
No-audio detector helper
packages/client/src/helpers/no-audio-detector.ts
New module: createNoAudioDetector and NoAudioDetectorOptions — samples audio via Web Audio API, implements a simple state machine, emits capture-status changes, and returns async cleanup.
Sound detector tweak
packages/client/src/helpers/sound-detector.ts
Inlined default constants into createSoundDetector options (no behavioral change).
RNSpeech detector
packages/client/src/helpers/RNSpeechDetector.ts
Minor refactors: import/variable renames, simplified stream selection, use native getStats type, and increased sampling interval to 250ms.
ScreenShare manager
packages/client/src/devices/ScreenShareManager.ts
Add early-return guard in setup() to avoid duplicate subscriptions.
Call note
packages/client/src/Call.ts
Add clarifying comment about re-initializing device managers when reusing recycled Call instances.
React notifications
packages/react-sdk/src/components/Notification/MicCaptureErrorNotification.tsx, .../Notification/index.ts, .../Notification/Notification.tsx
Add MicCaptureErrorNotification component and export; subscribe to mic.capture_report; minor className correction on Notification wrapper.
Call controls & samples
packages/react-sdk/src/components/CallControls/CallControls.tsx, sample-apps/react/react-dogfood/components/ActiveCall.tsx, .../Lobby.tsx, .../style/lobby.scss
Wrap audio UI with MicCaptureErrorNotification; sample app layout/styling tweaks and import updates.
Inspector UI
sample-apps/react/react-dogfood/components/Inspector/DevicesDash.tsx, .../style/Inspector.scss
Add per-device mic test UI and state; expose test-related props and simple styling for test controls.
Tests & mocks
packages/client/src/helpers/__tests__/no-audio-detector.test.ts, packages/client/src/devices/__tests__/web-audio.mocks.ts, packages/client/src/devices/__tests__/MicrophoneManager.test.ts, packages/client/src/devices/__tests__/mocks.ts
Add Web Audio test mocks, comprehensive no-audio detector tests, initialize audio-context mock in tests, and adjust mock track label / async cleanup usage.
Misc docs
sample-apps/react/react-dogfood/CLAUDE.md
Add repository guidance document for the react-dogfood sample app.

Sequence Diagram(s)

sequenceDiagram
    participant MM as MicrophoneManager
    participant NAD as NoAudioDetector
    participant TR as Tracer/StreamClient
    participant UI as MicCaptureErrorNotification

    MM->>NAD: createNoAudioDetector(mediaStream, options)
    NAD->>NAD: sample audio periodically
    alt audio present
        NAD->>TR: emit mic.capture_report (capturesAudio: true)
        TR->>UI: deliver event
        UI->>UI: hide notification
    else prolonged silence
        NAD->>TR: emit mic.capture_report (capturesAudio: false, noAudioDurationMs)
        TR->>UI: deliver event
        UI->>UI: show notification
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • santhoshvai
  • greenfrvr
  • jdimovska

Poem

🐰 I listen close where quiet skies unfurl,
I count the pauses, watch each silent curl,
If mics go still, I puff my little drum,
A hop, a ping to wake the quiet hum,
Hooray — the call can hear each laughing pearl.

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Detectors for broken microphone setup' clearly and concisely summarizes the main feature being added in this changeset.
Description check ✅ Passed The PR description follows the template structure with Overview and Implementation notes sections, clearly explaining the feature and linking to the relevant Linear ticket and documentation PR.
Linked Issues check ✅ Passed The code changes comprehensively address REACT-759 requirements: audio polling detection, mic.capture_report events with proper states, configurable thresholds, and continuous reporting during silence with immediate success detection.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing microphone break detection: core detector logic, device manager updates, event types, UI notifications, and supporting infrastructure. Minor documentation and styling updates are reasonable supporting changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch detectors

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@packages/client/src/devices/MicrophoneManager.ts`:
- Around line 118-147: The no-audio detector created by createNoAudioDetector
(stored in this.noAudioDetectorCleanup) can remain running because it's only
stopped in the status/mediaStream subscription; update MicrophoneManager.dispose
to explicitly call and await this.noAudioDetectorCleanup (if present) and handle
errors (e.g., try/catch or .catch logging via this.logger.warn) before clearing
this.noAudioDetectorCleanup so the detector's timers/audio context are always
cleaned up when the manager is disposed.

In `@packages/client/src/helpers/no-audio-detector.ts`:
- Around line 288-289: The RN detector is being constructed with new
RNSpeechDetector() which opens a new mic stream; change the initialization so
the detector uses the provided audioStream (e.g., new
RNSpeechDetector(audioStream) or call speechDetector.setStream(audioStream)
after creation) so detection reflects the active track and avoids extra
permission prompts; update any related logic that assumes internal stream
ownership (references: RNSpeechDetector, speechDetector, RNDetectionState,
audioStream).

In `@packages/react-sdk/src/components/Notification/Notification.tsx`:
- Line 46: Typo in the wrapper className: change the misspelled
"str-video__notiication-wrapper" to the correct BEM-style
"str-video__notification-wrapper" on the div in Notification component (the JSX
element using refs.setReference) so styles apply; update any
references/tests/SCSS that expect the corrected class name to keep them
consistent.
🧹 Nitpick comments (3)
packages/react-sdk/src/components/Notification/MicCaptureErrorNotification.tsx (1)

22-27: Ensure mic.capture_report subscriptions always clean up.

Line 22–27 assumes call.on(...) returns an unsubscribe. If it doesn’t, handlers will accumulate on unmount or call changes. Consider explicit off cleanup for safety.

♻️ Suggested defensive cleanup
  useEffect(() => {
    if (!call) return;
-    return call.on('mic.capture_report', (event) => {
-      setIsVisible(!event.capturesAudio);
-    });
+    const handler = (event: { capturesAudio: boolean }) => {
+      setIsVisible(!event.capturesAudio);
+    };
+    call.on('mic.capture_report', handler);
+    return () => {
+      call.off?.('mic.capture_report', handler);
+    };
  }, [call]);

As per coding guidelines, ensure event listeners are cleaned up in effects.

packages/client/src/helpers/no-audio-detector.ts (1)

195-213: Consider stopping the RN detector once audio is confirmed.
After capturesAudio: true, the RN path keeps the underlying speech detector running; consider clearing the interval and stopping detection to save battery/CPU and align with the browser path’s “stop on audio” behavior.

packages/client/src/devices/MicrophoneManager.ts (1)

262-272: Consider applying threshold changes immediately when the detector is active.
Right now the new threshold only takes effect after a status/stream change. If you expect runtime tuning, consider restarting the detector when the mic is already enabled.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@packages/client/detectors-api.md`:
- Around line 287-289: The frequency resolution and latency lines currently use
Nyquist (22050 Hz) instead of the actual sample rate; update the "Frequency
resolution" and "Latency" calculations to use Sample Rate / fftSize and fftSize
/ Sample Rate respectively (e.g., at 44100 Hz: 44100 / 256 = ~172.3 Hz per bin
and 256 / 44100 = ~5.8 ms; at 48000 Hz: 48000 / 256 = ~187.5 Hz per bin and 256
/ 48000 = ~5.3 ms), or explicitly state the assumed sample rate if the docs are
locked to 44100 Hz so readers know which formula/values apply.

In `@packages/client/src/devices/__tests__/MicrophoneManager.test.ts`:
- Line 23: The tests call setupAudioContextMock() which stubs the global
AudioContext via vi.stubGlobal() and is not cleaned up; import
cleanupAudioContextMock from './web-audio.mocks' and add an afterEach hook that
calls cleanupAudioContextMock() to restore the global and prevent cross-test
leakage (ensure the afterEach runs in the same test file where
setupAudioContextMock() is used).

In `@packages/client/src/helpers/__tests__/no-audio-detector.test.ts`:
- Around line 513-538: The test fails because emitIntervalMs is hardcoded to
5000 instead of defaulting to noAudioThresholdMs per the JSDoc; update the
createNoAudioDetector implementation to set emitIntervalMs =
options.emitIntervalMs ?? options.noAudioThresholdMs (or equivalent using the
internal option names) so when emitIntervalMs is omitted it inherits
noAudioThresholdMs, ensuring behavior matches the JSDoc and the test
(references: createNoAudioDetector, emitIntervalMs, noAudioThresholdMs).
🧹 Nitpick comments (1)
packages/client/detectors-api.md (1)

472-503: Consider adding language specifiers to fenced code blocks.

The event flow diagrams at lines 472, 484, and 496 are missing language specifiers. While not critical, adding text or plaintext identifiers would satisfy linting rules and improve consistency.

📝 Suggested formatting improvement
-```
+```text
 1. User enables microphone
 2. Detection starts (500ms interval)
 ...

Apply the same pattern to the code blocks at lines 484 and 496.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/client/src/helpers/no-audio-detector.ts`:
- Around line 5-45: The JSDoc defaults for NoAudioDetectorOptions
(detectionFrequencyInMs = 500, audioLevelThreshold = 5) don't match the actual
defaults used in createBrowserDetector; update createBrowserDetector so its
default values for detectionFrequencyInMs and audioLevelThreshold match the
documented values (set detectionFrequencyInMs to 500 and audioLevelThreshold to
5) so the implementation and the NoAudioDetectorOptions docs are consistent.
🧹 Nitpick comments (3)
packages/client/src/helpers/RNSpeechDetector.ts (1)

179-210: Consider using a small threshold instead of zero for noise detection.

Using normalizedAudioLevel > 0 may be overly sensitive—even minimal background noise or sensor artifacts can produce non-zero values. Consider aligning with a small threshold (similar to the browser implementation's approach) for more reliable detection.

💡 Suggested improvement
+const NOISE_THRESHOLD = 0.001; // Minimal threshold to filter sensor noise
+
 const normalizedAudioLevel =
   typeof audioLevel === 'number' ? audioLevel : 0;
 onSoundDetectedStateChanged({
-  isSoundDetected: normalizedAudioLevel > 0,
+  isSoundDetected: normalizedAudioLevel > NOISE_THRESHOLD,
   audioLevel: normalizedAudioLevel,
 });
packages/client/src/helpers/no-audio-detector.ts (2)

178-191: State reset is unreachable when audio is detected.

When wasInNoAudioState is true, the function returns early without resetting state (lines 186-188 are unreachable). While the detector stops afterward, resetting state before returning would be cleaner and safer if the control flow ever changes.

♻️ Suggested improvement
 const handleAudioDetected = (
   state: DetectionState,
 ): CaptureStatusEvent | undefined => {
   const wasInNoAudioState = state.noAudioStartTime !== null;
+
+  // Reset timing state
+  state.noAudioStartTime = null;
+  state.lastEmitTime = null;
+
   if (wasInNoAudioState) {
     return { capturesAudio: true };
   }
-
-  // Reset timing state
-  state.noAudioStartTime = null;
-  state.lastEmitTime = null;
-
   return undefined;
 };

196-214: Interval not cleared directly when audio is detected.

When wasInNoAudioState is true, shouldStop is set but checkIntervalId is not cleared (lines 200-203 return early). The interval continues firing (returning early due to shouldStop) until stopDetector is called asynchronously. Consider clearing the interval directly for immediate cleanup.

♻️ Suggested improvement
 const handleAudioDetectedRN = (
   state: RNDetectionState,
 ): CaptureStatusEvent | undefined => {
   const wasInNoAudioState = state.noAudioStartTime !== null;
+
+  // Reset timing state and clear interval
+  state.noAudioStartTime = null;
+  state.lastEmitTime = null;
+  if (state.checkIntervalId) {
+    clearInterval(state.checkIntervalId);
+    state.checkIntervalId = undefined;
+  }
+
   if (wasInNoAudioState) {
     state.shouldStop = true;
     return { capturesAudio: true };
   }
-
-  // Reset timing state
-  state.noAudioStartTime = null;
-  state.lastEmitTime = null;
-  if (state.checkIntervalId) {
-    clearInterval(state.checkIntervalId);
-    state.checkIntervalId = undefined;
-  }
-
   return undefined;
 };

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@packages/client/src/devices/MicrophoneManager.ts`:
- Around line 264-274: The setSilenceThreshold method only assigns
silenceThresholdMs but doesn’t stop an already-running detector, so when
thresholdMs <= 0 the detector must be stopped; update
setSilenceThreshold(thresholdMs: number) in MicrophoneManager to set
this.silenceThresholdMs = thresholdMs and if thresholdMs <= 0 call the existing
detector stop/cleanup method (e.g.,
stopSilenceDetector/clearSilenceTimeout/stopDetector — whichever function is
responsible for tearing down the active detector) to disable detection
immediately; additionally, if you want to support changing to a positive
threshold, ensure you (re)start the detector when thresholdMs > 0 by invoking
the detector initialization routine used elsewhere in the class.

In `@packages/client/src/helpers/no-audio-detector.ts`:
- Around line 3-42: The NoAudioDetectorOptions type currently requires
noAudioThresholdMs and emitIntervalMs but callers omit them and
createNoAudioDetector doesn't apply defaults; make noAudioThresholdMs and
emitIntervalMs optional in NoAudioDetectorOptions, set defaults in
createNoAudioDetector (e.g., default noAudioThresholdMs = 5000 and
emitIntervalMs = noAudioThresholdMs when not provided), normalize the options
into a single object and pass that normalized options object into
transitionState (and any other internal callers) so comparisons use defined
numbers and TypeScript reflects the optional defaults.
- Around line 110-119: transitionState currently only emits when audioDetected
and state.kind is 'IDLE' or 'EMITTING', which leaves 'DETECTING' stuck; change
the logic in transitionState so that any audioDetected returns emit(true, state)
immediately (use the existing emit(...) helper) regardless of the current
state.kind (including 'DETECTING'), otherwise fall back to existing
noEmit(state) behavior to stop/continue intervals.

In `@packages/client/src/helpers/RNSpeechDetector.ts`:
- Around line 26-31: In RNSpeechDetector, guard against null ICE candidates in
the 'icecandidate' event handlers for this.pc1 and this.pc2 before calling
addIceCandidate: check that e?.candidate is non-null (or truthy) and only then
call this.pc2.addIceCandidate(e.candidate) /
this.pc1.addIceCandidate(e.candidate); optionally wrap the addIceCandidate call
in a try/catch to quietly handle implementations that still throw. This mirrors
the null-candidate protection used in BasePeerConnection.ts.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@packages/client/src/devices/MicrophoneManager.ts`:
- Around line 296-317: In performTest, if createNoAudioDetector throws the
acquired MediaStream is leaked; wrap the call to createNoAudioDetector in a
try/catch, and on catch call disposeOfMediaStream(stream), log the error via
this.logger.warn/error, and reject or resolve the promiseWithResolvers (e.g.,
resolve(false) or reject(err)) to ensure the returned promise always settles.
Reference performTest, promiseWithResolvers, createNoAudioDetector,
disposeOfMediaStream and the local cleanup variable so the stream is always
disposed when detector setup fails.

In `@packages/client/src/helpers/no-audio-detector.ts`:
- Around line 1-2: The code in no-audio-detector.ts uses the web-only
AudioContext unguarded; wrap any creation or reference to AudioContext (and
related web-audio calls within the module, including the block around lines
116-146) with platform checks and throw a clear error when running in
non-browser environments: use the existing platform helpers (isReactNative(),
isMobileSafari(), isSafari(), isFirefox()) to detect React Native or non-browser
contexts and avoid calling new AudioContext(); if AudioContext is required only
in browsers, guard creation behind if (!isReactNative() && typeof AudioContext
!== "undefined") { /* create */ } else { throw new Error("AudioContext
unavailable: running in non-browser environment (React Native)"); } and add a
videoLoggerSystem.error with the same descriptive message before throwing.

In `@sample-apps/react/react-dogfood/CLAUDE.md`:
- Around line 195-197: The fenced code block in CLAUDE.md currently lacks a
language identifier; update the triple-backtick fence that wraps the lines
"?coordinator_url=..." and "?use_local_coordinator=true" to include a language
(e.g., bash) so syntax highlighting works. Locate the code block in the
CLAUDE.md content and change the opening ``` to ```bash, leaving the block
contents unchanged.

In `@sample-apps/react/react-dogfood/components/Inspector/DevicesDash.tsx`:
- Around line 188-268: The device label span in the DevicesDash list is not
keyboard-accessible and the test control lacks an accessible name; replace the
clickable <span> that calls props.manager?.select(device.deviceId) with a native
<button type="button"> (preserve the inline styles or CSS class) so it can be
focused and activated by keyboard, and add an appropriate ARIA state (e.g.,
aria-pressed or aria-current) that reflects device.deviceId ===
props.selectedDevice; also give the test control (props.onTestDevice handler /
the current test button) a clear accessible name (aria-label like "Test
microphone for {device.label}" or visually hidden text) and ensure the button
remains disabled/announced correctly when isTesting/props.testingDeviceId is
set.
🧹 Nitpick comments (1)
sample-apps/react/react-dogfood/CLAUDE.md (1)

73-84: Consider documenting the microphone capture notification feature.

The AI summary indicates that this PR adds MicCaptureErrorNotification component and mic capture detection features to the dogfood app. While this is general guidance documentation, mentioning this new notification/debugging feature in the component organization section could help developers understand the full scope of available components.

Optional addition to component list
 - **Debug/** - Debug panels and inspection tools
 - **Inspector/** - Deep SDK state inspection
 - **Settings/** - Audio/video/effects configuration
+- **Notifications/** - Error notifications (e.g., MicCaptureErrorNotification for broken mic detection)

@oliverlaz oliverlaz merged commit 552b3f4 into main Feb 3, 2026
20 checks passed
@oliverlaz oliverlaz deleted the detectors branch February 3, 2026 16:26
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.

2 participants