Skip to content

fix: acquire microphone before creating the web call - #189

Merged
dhruva-vapi merged 1 commit into
mainfrom
devin/1792709258-mic-preflight
Oct 22, 2026
Merged

dhruva-vapi merged 1 commit into
mainfrom
devin/1792709258-mic-preflight

Conversation

@dhruva-vapi

Copy link
Copy Markdown
Contributor

Related: PRISM-1083 (Zeals)

Problem

About 40% of Zeals' web-widget calls end with assistant-did-not-receive-customer-audio.

vapi.start() sends POST /call/web before it creates the Daily call object, and the mic is only requested later, inside join(). As a result, a backend call is created and billed even when the user denies or ignores the mic prompt. The backend then times out after customerJoinTimeoutSeconds (15s by default).

Fix

start():
  track = await microphoneAcquire()   // getUserMedia, before POST /call/web
  webCall = POST /call/web
  Daily.createCallObject({ audioSource: track, ... })   // reuse the track, no second acquisition
  • On failure (denied, no device, empty stream, mediaDevices unavailable): no backend call is made. start() emits call-start-failed with stage: 'microphone-preflight' and error with type: 'microphone-preflight-<DOMException name>', then returns null.
  • Device selection: a device-ID audioSource becomes { deviceId: { exact } }. A caller-supplied MediaStreamTrack is reused as-is and never stopped by the SDK.
  • Track cleanup: the SDK stops its own track in cleanup(), in stop(), and on API or join failure.
  • Pending prompt: stop() cancels it (AbortError), and a track granted late is stopped. A concurrent second start() returns null. There's no timeout; stop() is the way out.
  • Opt-out: on by default. Disable it with vapi.start(assistant, ..., { microphonePreflightEnabled: false }) (StartCallOptions).
  • Cleanup of an existing call object now happens before preflight instead of after the API call.

Verification

Tested in real Chrome with a fake mic device and native permission prompts, comparing published @vapi-ai/web@3.0.0 against this branch. I checked each attempt via GET /call.

Scenario Backend call created? Result
3.0.0, mic denied Yes silence-timed-out
3.0.0, prompt ignored >20s Yes assistant-did-not-receive-customer-audio
This PR, mic denied No microphone-preflight-NotAllowedError
This PR, mic allowed Yes greeting played, stop() worked
This PR, prompt pending, then stop() No microphone-preflight-AbortError, no hang

before/after

test recording

New __tests__/microphone-preflight.test.ts. npm run build passes and npm test passes 73/73.

Not tested: a physical mic, iOS Safari, iframe mic policy, and the opt-out path in a browser (the opt-out has a unit test).

The widgets (client-sdk-html-script-tag, client-sdk-react) pin SDK 2.x, so they need a version bump to pick this up.

Link to Devin session: https://app.devin.ai/sessions/f0c8f585b41d4cbaa0459bfc8d22e211
Open in Devin Desktop: https://app.devin.ai/desktop/session/f0c8f585b41d4cbaa0459bfc8d22e211?variant=devin

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

I'll fix CI failures and address comments from users with write access that start with 'Devin'.

  • Disable automatic comment, CI, and merge conflict monitoring

@shubham-vapiai shubham-vapiai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

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