Repository navigation
Conversation
Bring AudioSwitchManager in line with client-sdk-android's AudioSwitchHandler: - Bump audioswitch to 039a35ae and use CommDeviceAudioSwitch on Android 12+. - Run AudioSwitch on a dedicated HandlerThread instead of the main looper. - Clear audioSwitch synchronously in stop() so a quick stop/start re-creates the switch instead of reusing a stopped one (client-sdk-android #967). - Guard against stop() racing with switch creation on the audio thread, which could leave a switch activated and never stopped. - Apply setting changes (including preferredDeviceList) to a running switch on the audio thread. - Stop the audio session in the module's invalidate() so React reloads release audio focus and the handler thread. - Drop unused listener fields, enableSpeakerphone and setMicrophoneMute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: b7223ca The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
| HandlerThread currentThread = thread; | ||
| if (currentThread != null) { | ||
| currentThread.quitSafely(); | ||
| } | ||
|
|
||
| public void setMicrophoneMute(boolean mute) { | ||
| audioManager.setMicrophoneMute(mute); | ||
| handler = null; | ||
| thread = null; |
There was a problem hiding this comment.
🔴 Old teardown disrupts reconnected audio
On a rapid stop-to-start transition, start() can activate a new switch before the old switch stops. The old teardown then changes shared Android audio state, disrupting the new session's routing.
Learn more
Each start creates a new HandlerThread after stop clears thread. The old switch's stop() is only queued on its old thread, so it can run after the new switch activates on a different thread. Both switches manage the same Android audio focus, mode, and routing; a late teardown can undo the new session's setup.
Example: A call stops, then reconnects immediately. The new thread activates the new switch while the old thread is busy. The old thread subsequently stops its switch and changes the device's audio mode or route, even though the new call is active.
Recommended fix: Serialize old-switch teardown and new-switch activation across session generations, for example by retaining one worker until teardown completes or queueing activation behind the previous teardown. Keep stop() safe when invoked from the worker thread, and test rapid stop/start with a delayed teardown.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if (audioSwitch == null) { | ||
| handler.removeCallbacksAndMessages(null); | ||
| handler.postAtFrontOfQueue(() -> { | ||
| audioSwitch = new AudioSwitch( | ||
| context, | ||
| loggingEnabled, | ||
| audioFocusChangeListener, | ||
| preferredDeviceList | ||
| ); | ||
| audioSwitch.setManageAudioFocus(manageAudioFocus); | ||
| audioSwitch.setFocusMode(focusMode); | ||
| audioSwitch.setAudioMode(audioMode); | ||
| audioSwitch.setAudioStreamType(audioStreamType); | ||
| audioSwitch.setAudioAttributeContentType(audioAttributeContentType); | ||
| audioSwitch.setAudioAttributeUsageType(audioAttributeUsageType); | ||
| audioSwitch.setForceHandleAudioRouting(forceHandleAudioRouting); | ||
| audioSwitch.start(audioDeviceChangeListener); | ||
| audioSwitch.activate(); | ||
| final Handler startHandler = currentHandler; | ||
| startHandler.removeCallbacksAndMessages(null); | ||
| startHandler.postAtFrontOfQueue(() -> { |
There was a problem hiding this comment.
🔴 Concurrent starts leave an active switch
If start() runs twice during switch construction, both start tasks can activate separate switches. audioSwitch retains only the second, so stopping the session leaves the first switch active.
Learn more
audioSwitch stays null while the first start runnable constructs the switch. A second start() can therefore schedule another runnable; removing queued messages does not cancel one already running. Both runnables pass the handler identity check and activate, but the second overwrites the only reference to the first.
Example: Call A enters new CommDeviceAudioSwitch(...). Another startAudioSession() arrives before A publishes. Call B queues another start. A and B each activate; a later stopAudioSession() stops only B, leaving A registered and active.
Recommended fix: Track a pending or in-progress start under the manager lock, and refuse duplicate start requests until that start is finished. Preserve generation checks so stop() can cancel an in-progress start, and test concurrent/repeated starts during slow construction.
Was this helpful? React with 👍 or 👎 to provide feedback.
Brings the Android
AudioSwitchManagerin line with client-sdk-android'sAudioSwitchHandler, and updates audioswitch to the same commit (039a35ae).Changes
CommDeviceAudioSwitch: uses the communication device API on Android 12+ andAudioSwitchbelow that.HandlerThreadinstead of the main looper.stop()race fix:audioSwitchis cleared synchronously, so a quick stop→start creates a new switch instead of reusing a stopped one, which could leave audio stuck on the earpiece. Ported from fix(audio): clear audioSwitch synchronously in AudioSwitchHandler.stop() client-sdk-android#967.stop()during switch creation: ifstop()runs while the audio thread is still building the switch, the new switch is dropped before it starts. Previously it could end up activated and never stopped. client-sdk-android still has this race.preferredDeviceList, now take effect on a running switch.invalidate()now stops the audio session, so a reload releases audio focus and the handler thread.enableSpeakerphoneandsetMicrophoneMute. Nothing in the SDK called them, and they changed routing directly onAudioManager, which would conflict withCommDeviceAudioSwitch.No JS API changes.
Testing
example/androidCommDeviceAudioSwitch): speaker/earpiece/Bluetooth/wired switching,selectAudioOutputAudioSwitch)🤖 Generated with Claude Code