Conversation
WebRTC plays call audio as USAGE_VOICE_COMMUNICATION, but nothing put the device in MODE_IN_COMMUNICATION outside Telecom calls. The volume keys and slider then changed media volume, so the call volume could not be changed. Enter MODE_IN_COMMUNICATION while WebRTC playout runs, route to a connected Bluetooth headset (otherwise the call falls back to the earpiece), and undo both when playout stops. Telecom calls are left alone. Claude-Session: https://claude.ai/code/session_01Ec4FfZfNYtQMY5HSyNnKJq
There was a problem hiding this comment.
🟡 Changes recommended
Three moderate issues remain unresolved, including Telecom routing races, legacy Bluetooth discovery, and custom audio-device support.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR fixes Android WebRTC call-volume handling by enabling communication mode and routing audio to connected Bluetooth headsets.
Changes:
- Adds playout lifecycle callbacks.
- Manages Bluetooth routing and communication mode.
- Restores only WebRTC-owned audio state.
File summaries
| File | Summary | Findings |
|---|---|---|
android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java |
Registers WebRTC playout callbacks. | Supplied custom audio device modules do not receive the callback. Moderate, 1 vote. |
android/src/main/java/com/oney/WebRTCModule/AudioOutputManager.java |
Manages communication mode and Bluetooth routing. | Telecom ownership transition is racy. Moderate, 2 votes. API <31 discovery excludes TYPE_BLUETOOTH_A2DP. Moderate, 1 vote. |
Review details
Suppressed comments (5)
android/src/main/java/com/oney/WebRTCModule/AudioOutputManager.java:593
setCommunicationDevicereturns a boolean and can reject a device; ignoring it lets this callback continue as if Bluetooth was routed while audio remains on the earpiece. The existingdispatchSelectApi31path already checks this result, so use the same checked path here or handle the failure explicitly.
audioManager.setCommunicationDevice(headset);
android/src/main/java/com/oney/WebRTCModule/AudioOutputManager.java:605
- The Telecom guard only surrounds
setModebelow; if Telecom takes ownership while this playout is active, this stop callback still clears the communication device or stops SCO and can tear down Telecom’s active route. ChecktelecomOwnsRoutingbefore any route cleanup (after clearing this local ownership flag) and leave platform routing untouched while Telecom owns it.
audioManager.clearCommunicationDevice();
} else {
audioManager.setBluetoothScoOn(false);
audioManager.stopBluetoothSco();
android/src/main/java/com/oney/WebRTCModule/AudioOutputManager.java:608
ownsCommunicationModeis set unconditionally without saving the previousAudioManagermode. If the mode was alreadyMODE_IN_COMMUNICATION, or another audio user changes it while this playout is active, stopping this track forces the process-wide mode toMODE_NORMALand can interrupt that audio. Preserve or reference-count the prior owner and restore the mode only while this operation still owns it.
audioManager.setMode(AudioManager.MODE_NORMAL);
android/src/main/java/com/oney/WebRTCModule/AudioOutputManager.java:623
- On API < 31, a connected classic headset can be exposed as
TYPE_BLUETOOTH_A2DPuntil SCO is started; this manager already treats that type as a valid legacy Bluetooth target indispatchSelectLegacy. Filtering it out here means the automatic path can leave the device in communication mode without ever starting SCO or routing to the headset. IncludeTYPE_BLUETOOTH_A2DPin this discovery branch.
for (AudioDeviceInfo d : audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) {
if (d.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) return d;
android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java:133
- The callback is installed only in the
adm == nullbranch. Consumers that provide the documentedWebRTCModuleOptions.audioDeviceModule—including a customJavaAudioDeviceModulethat still uses voice-communication audio—never callsetInCommunication, so this Bluetooth volume fix silently does not apply. Provide an opt-in callback path for supplied Java ADMs or document this as an explicit limitation.
.setAudioTrackStateCallback(new JavaAudioDeviceModule.AudioTrackStateCallback() {
@Override
public void onWebRtcAudioTrackStart() {
audioOutputManager.setInCommunication(true);
}
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Route the call to a headset that connects while we own the communication mode, matching iOS and Telecom calls. Start/stop and the device callback run on different threads, so both are synchronized. Claude-Session: https://claude.ai/code/session_01Ec4FfZfNYtQMY5HSyNnKJq
setTelecomOwnsRouting now takes the same lock as setInCommunication and the device callback, so Telecom taking over can't interleave with our check-then-route. Claude-Session: https://claude.ai/code/session_01Ec4FfZfNYtQMY5HSyNnKJq
There was a problem hiding this comment.
🟡 Changes recommended
Cleanup can overwrite prior audio state or disrupt an active Telecom route.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
android/src/main/java/com/oney/WebRTCModule/AudioOutputManager.java:591
- This does not actually “undo only what we set”: it unconditionally claims ownership and later forces
MODE_NORMAL, even when the process was already in another audio mode before playout began. That can terminate another non-Telecom audio session's mode. Capture the prior mode when ownership is acquired and restore that value on stop (and only claim ownership when this manager changes it).
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Balanced
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | ||
| audioManager.clearCommunicationDevice(); | ||
| } else { | ||
| audioManager.setBluetoothScoOn(false); | ||
| audioManager.stopBluetoothSco(); | ||
| } |
Problem
On Android, with Bluetooth headphones in a call, the volume buttons and the system slider do nothing. The call stays at one level.
WebRTC plays call audio on the voice-call stream, but outside Telecom calls nothing switched the device to
MODE_IN_COMMUNICATION. So Android pointed the volume keys at media volume, not the call.Fix
While WebRTC playout runs (
AudioTrackStateCallbackon the audio device module):MODE_IN_COMMUNICATION, so the volume keys control the callTelecom (VoIP) calls manage mode and routing themselves and are left alone.
Checked
Pixel 7 (Android 16), Sony WH-1000XM5, Fishjam chat example:
MODE_NORMAL, volume keys don't change the headset call volumeMODE_IN_COMMUNICATION, routed tobt_sco_hs, volume up moves the headset call volume 1 → 3, and audio plays in the headphonesNot yet checked on a device: switching to headphones connected mid-call.
https://claude.ai/code/session_01Ec4FfZfNYtQMY5HSyNnKJq