Skip to content

bug: speaker device alignment silently fails — users can't hear audio (Bluetooth) #1227

Description

@JamesPHoughton

Summary

Users report "can't hear" during video calls. Auto-diagnostics correctly identify the cause as speakerNotSet, attempt to re-align the speaker device, and report success — but post-fix validation shows the speaker is still null. The setSpeaker() call completes without error but the device never actually activates.

Sentry issues:

Investigation Findings

What the Sentry data shows

Diagnostic data from PE events consistently shows all three device types with currentId: null — not just the speaker:

{
  "deviceAlignment": {
    "camera": { "currentId": null, "currentLabel": null },
    "microphone": { "currentId": null, "currentLabel": null },
    "speaker": { "currentId": null, "currentLabel": null }
  }
}

Breadcrumb sequence from PE event 3348c86c:

  1. 14:14:04 — "Speaker aligned via id match" — alignment logs success
  2. 14:14:04TypeError: Cannot read properties of undefined (reading 'then')bug: startRecording() returns undefined during stage transitions, crashing VideoCall #1226 crash
  3. 14:14:21 — Diagnosed causes: speakerNotSet — speaker is null despite "successful" alignment
  4. 14:14:21 — Re-aligned speaker device — fix attempt
  5. 14:14:22 — Validation: stillPresent: speakerNotSetfix failed

Key observation: diagnostics vs alignment use different APIs

There is a disconnect between how devices are set and how they are read back:

  • Alignment uses devices.setSpeaker() and reads devices.currentSpeaker (from Daily's useDevices() React hook)
  • Diagnostics read from callObject.getInputDevices() / callObject.getOutputDevices() (synchronous call object methods)

These are two different APIs to the same underlying state. If the call object is in a bad state (e.g., after the #1226 crash/remount), getInputDevices()/getOutputDevices() may return nulls even though useDevices() shows valid devices.

Hypothesis: many PE events are caused by the #1226 crash

The same TypeError: Cannot read properties of undefined (reading 'then') from #1226 appears in the PE breadcrumbs. The sequence is:

  1. Stage transition triggers spurious VideoCall mount (the stale data race from bug: startRecording() returns undefined during stage transitions, crashing VideoCall #1226)
  2. VideoCall crashes during initialization (startRecording TypeError)
  3. ErrorBoundary remounts VideoCall, but the call object's internal state is corrupted
  4. getInputDevices()/getOutputDevices() return nulls for all device types
  5. User can't hear → reports "can't hear" → diagnostics find speakerNotSet → fix fails

If this hypothesis is correct, deploying the #1226 fix should significantly reduce PE/PG events.

Remaining concerns (independent of #1226)

Even if #1226 accounts for many events, there are likely real speaker issues too:

  1. setSpeaker() can silently fail: The Promise resolves without error even when the underlying HTMLAudioElement.setSinkId() doesn't actually change the output device (e.g., stale device ID from a disconnected Bluetooth device).

  2. No post-set verification: After setSpeaker() succeeds, neither the alignment code nor the FixAV recovery verifies that currentSpeaker actually reflects the change.

  3. Safari gesture requirement: Safari requires a user gesture for setSinkId(). When alignment runs from a React effect (no gesture context), setSinkId throws NotAllowedError. The gesture prompt system handles this, but QD shows 7 consecutive failures in one session.

  4. Bluetooth device churn: Bluetooth devices (AirPods, "sairpods", WH-1000XM4) can disconnect between alignment and use, leaving the speaker unset with no recovery.

Plan

  1. Deploy bug: startRecording() returns undefined during stage transitions, crashing VideoCall #1226 fix first (PR fix(call): prevent stale-data VideoCall mount and guard startRecording #1230) and observe whether PE/PG events drop
  2. If events persist, investigate:
    • Add post-set verification to alignSpeaker() — re-query devices.currentSpeaker after setSpeaker() and retry with fallback if unchanged
    • Align the diagnostics API to use devices.currentSpeaker (the hook) instead of callObject.getOutputDevices() (the snapshot), or verify they return the same data
    • Add Bluetooth disconnect detection and proactive fallback

User Impact

High — 22 distinct users unable to hear audio during calls. The FixAV panel diagnoses the problem but can't fix it, leaving users stuck.

Affected Code

  • client/src/call/hooks/useDeviceAlignment.jsalignSpeaker() lacks post-set verification
  • client/src/call/FixAV.jsxcollectAVDiagnostics() reads from callObject.getOutputDevices() which may differ from devices.currentSpeaker
  • client/src/call/utils/avRecovery.js — soft fix for speakerNotSet reports false success

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions