← reports

#4382 · Mobile microphone source unverified

BugPriority: MediumEffort: Mediumremote · mobile · connectopen on GitHub2026-09-26 · base 9c9bae7f36a237c7e1b96de3d4c2186d13967686

Verdict: NOT REPRODUCED · Root-cause confidence: low

1. TL;DR

A reporter observed that voice input started on a phone appeared to transcribe sound near a laptop running the same bb server. The exact phone client and audio source were not isolated. At the trusted main commit, the composer requests audio from its own browser and sends the resulting file to the server. A two-client hook test passed twice and did not call the laptop media API when recording started in the phone context. Without a physical phone and laptop, this does not settle the reported observation or identify a root cause.

2. Claims vs findings

ClaimStatusEvidence
Voice started on a phone used a separate laptop microphone.UnverifiedNo physical-device run, audio recording, track label, or isolated sound-source comparison is available.
The composer requests audio in the initiating browser.VerifieduseVoiceInput.ts lines 263–281; the focused probe calls only the phone-context media API.
The server selects a microphone on the laptop.Refuted for this routeapi.ts lines 176–187 uploads a file; system.ts lines 633–647 receives that file and invokes transcription.
Speech near a TV by the laptop appeared in the result.UnverifiedThe issue reports this observation, but no recording or controlled distance measurement permits source attribution.

3. Environment

4. Closest repeatable reproduction

  1. In a clean checkout of the commit above, run pnpm install --frozen-lockfile --prefer-offline and pnpm exec turbo run build.
  2. Copy voice-client-context.test.tsx into apps/app/src/hooks/.
  3. Run pnpm exec turbo run test --filter=@bb/app -- voice-client-context.test.tsx. The test mounts a laptop composer, switches to a phone browser context, and starts voice input there.
Expected if the reported client-routing defect exists: laptop getUserMedia is called.
Actual, first clean checkout: 1 test passed; phone getUserMedia called with { audio: true }; laptop getUserMedia not called.
Actual, second clean checkout: 1 test passed; same assertions.

This probes the app capture path, not physical microphone selection. Existing related suites also passed: 12 device-preference tests and 7 voice-hook tests.

// @vitest-environment jsdom

import { act, cleanup, renderHook } from "@testing-library/react";
import { afterEach, expect, it, vi } from "vitest";
import { useVoiceInput } from "./useVoiceInput";

vi.mock("@/components/ui/app-toast", () => ({ appToast: { error: vi.fn() } }));

class Recorder {
  static isTypeSupported = () => false;
  state = "inactive";
  onstart = () => {};
  onstop = () => {};
  ondataavailable = () => {};
  onerror = () => {};
  constructor(readonly stream: MediaStream) {}
  start() {
    this.state = "recording";
    this.onstart();
  }
  stop() {
    this.state = "inactive";
    this.onstop();
  }
}

afterEach(() => {
  cleanup();
  window.localStorage.clear();
  vi.unstubAllGlobals();
  vi.clearAllMocks();
});

it("captures through the browser that starts voice input", async () => {
  window.localStorage.clear();
  const phoneStream = { getTracks: () => [{ stop: vi.fn() }] };
  const phoneGetUserMedia = vi.fn().mockResolvedValue(phoneStream);
  const laptopGetUserMedia = vi.fn();
  vi.stubGlobal("MediaRecorder", Recorder);
  vi.stubGlobal("navigator", {
    mediaDevices: { getUserMedia: laptopGetUserMedia },
  });
  const laptop = renderHook(() =>
    useVoiceInput({ onTranscribe: vi.fn(), onTranscript: vi.fn() }),
  );
  vi.stubGlobal("navigator", {
    mediaDevices: { getUserMedia: phoneGetUserMedia },
  });
  const { result, unmount } = renderHook(() =>
    useVoiceInput({ onTranscribe: vi.fn(), onTranscript: vi.fn() }),
  );
  await act(async () => result.current.start());

  expect(phoneGetUserMedia).toHaveBeenCalledExactlyOnceWith({ audio: true });
  expect(laptopGetUserMedia).not.toHaveBeenCalled();
  expect(result.current.stream).toBe(phoneStream);
  unmount();
  laptop.unmount();
});

5. Root-cause assessment

No repository root cause is established. The initiating page passes its navigator.mediaDevices into requestAudioInputStream and constructs MediaRecorder from that stream (useVoiceInput.ts lines 263–281). The preference helper uses either the local default device or a local exact device ID, with a fallback to the local default when the exact device is absent (audio-input-device-preference.ts lines 43–77). The server receives an audio file after recording (system.ts lines 633–647). The mobile WebView diagnostic probe likewise calls its own navigator.mediaDevices.getUserMedia (mobile WebView voice probe lines 189–203). These paths do not show a call that captures the laptop microphone for a phone session. The browser or operating system could choose a device unexpectedly, but the current evidence cannot establish that as the cause.

6. Next experiment

On the reporter's exact phone client and laptop build, record two distinct spoken phrases in acoustically separated locations while the phone starts voice input. Log the phone page's selected MediaStreamTrack.label and getSettings().deviceId locally, plus the laptop's active recording indicator. Save a short local recording before transcription and compare it with the uploaded result. Those observations would distinguish capture-device selection from ambient pickup or transcription confusion before changing code.

7. Related issues

#4236 concerns transcription loss after leaving a composer; it does not address microphone source. No linked open pull request was found for #4382 when this report was prepared.

8. Verification

The same agent repeated the focused test in a second clean temporary checkout at 9c9bae7f36a237c7e1b96de3d4c2186d13967686, after a frozen install and Turbo build. The command above passed again (one test). No report correction was needed. Both runs are simulations; neither used physical phones, microphones, or bb Connect. The issue content was treated as untrusted claims; no issue-provided command, link, or code was executed.

9. Appendix

Commands run for each checkout: pnpm install --frozen-lockfile --prefer-offline; pnpm exec turbo run build; pnpm exec turbo run test --filter=@bb/app -- voice-client-context.test.tsx. In the first checkout, pnpm exec turbo run test --filter=@bb/app -- useVoiceInput.test.tsx audio-input-device-preference.test.ts passed 19 tests. The first checkout needed local PWA icon regeneration because the generated assets failed their check; this did not affect the microphone test. Code permalinks above were checked against the recorded commit. No image is included because this is an audio-source claim, not a visual defect.