Skip to content

Fix call volume stuck with Bluetooth headphones - #96

Open
Magmusacy wants to merge 3 commits into
masterfrom
fce-3790
Open

Magmusacy wants to merge 3 commits into
masterfrom
fce-3790

Conversation

@Magmusacy

@Magmusacy Magmusacy commented Sep 18, 2026

Copy link
Copy Markdown

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 (AudioTrackStateCallback on the audio device module):

  • set MODE_IN_COMMUNICATION, so the volume keys control the call
  • route to a connected Bluetooth headset. Without this the call falls back to the earpiece in that mode.
  • switch to a headset that connects mid-call, like iOS and Telecom calls do
  • when playout stops, undo only what we set

Telecom (VoIP) calls manage mode and routing themselves and are left alone.

Checked

Pixel 7 (Android 16), Sony WH-1000XM5, Fishjam chat example:

  • before: MODE_NORMAL, volume keys don't change the headset call volume
  • after: MODE_IN_COMMUNICATION, routed to bt_sco_hs, volume up moves the headset call volume 1 → 3, and audio plays in the headphones

Not yet checked on a device: switching to headphones connected mid-call.

https://claude.ai/code/session_01Ec4FfZfNYtQMY5HSyNnKJq

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
Copilot AI lite review requested due to automatic review settings September 18, 2026 15:39
@linear

linear Bot commented Sep 18, 2026

Copy link
Copy Markdown

FCE-3790

@Magmusacy Magmusacy changed the title fix(android): call volume stuck with Bluetooth headphones fix call volume stuck with Bluetooth headphones Sep 18, 2026
@Magmusacy Magmusacy changed the title fix call volume stuck with Bluetooth headphones Fix call volume stuck with Bluetooth headphones Sep 18, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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

  • setCommunicationDevice returns 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 existing dispatchSelectApi31 path 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 setMode below; 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. Check telecomOwnsRouting before 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

  • ownsCommunicationMode is set unconditionally without saving the previous AudioManager mode. If the mode was already MODE_IN_COMMUNICATION, or another audio user changes it while this playout is active, stopping this track forces the process-wide mode to MODE_NORMAL and 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_A2DP until SCO is started; this manager already treats that type as a valid legacy Bluetooth target in dispatchSelectLegacy. Filtering it out here means the automatic path can leave the device in communication mode without ever starting SCO or routing to the headset. Include TYPE_BLUETOOTH_A2DP in 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 == null branch. Consumers that provide the documented WebRTCModuleOptions.audioDeviceModule—including a custom JavaAudioDeviceModule that still uses voice-communication audio—never call setInCommunication, 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.

Comment thread android/src/main/java/com/oney/WebRTCModule/AudioOutputManager.java
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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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

Comment on lines +597 to +602
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
audioManager.clearCommunicationDevice();
} else {
audioManager.setBluetoothScoOn(false);
audioManager.stopBluetoothSco();
}
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