Skip to content

Fix AudioSwitchHandler leaving audio active when stopped during start - #1038

Open
davidliu wants to merge 1 commit into
mainfrom
dl/audioswitch-start-stop-guard
Open

davidliu wants to merge 1 commit into
mainfrom
dl/audioswitch-start-stop-guard

Conversation

@davidliu

Copy link
Copy Markdown
Contributor

Problem

start() builds the AudioSwitch in a job on the AudioSwitchHandlerThread. If stop() is called while that job is already running, stop() can't cancel it, and it finds no audioSwitch to tear down yet. The job then stores the switch and calls start()/activate(), leaving audio focus, audio mode and routing active with nothing to stop them.

Fix

start() records the handler it posted the creation job to. Before storing the switch, the job checks under the handler's lock (the same one start()/stop() use) that handler is still that one.

  • stop() got there first: handler has changed, so the job drops the switch before starting it. That's safe because the audioswitch constructors register nothing; scanning, focus and routing only begin in start()/activate().
  • The job got there first: the switch is stored, so a following stop() finds it and posts its teardown behind the running job.

This follows up on #967, which made stop() clear audioSwitch straight away.

Testing

  • :livekit-android-sdk:compileDebugKotlin, detektRelease, spotlessCheck

🤖 Generated with Claude Code

If stop() was called while the audio thread was still creating the
AudioSwitch in start(), stop() saw no switch to tear down, and the new
switch was then started and activated with nothing left to stop it.

start() now records the handler it posted to. The creation job checks
under the handler's lock that this start is still current before
publishing the switch, and drops it otherwise. Once published, a
subsequent stop() posts its teardown behind the running job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Oct 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 545cace

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
client-sdk-android Patch

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

@devin-ai-integration devin-ai-integration Bot 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.

Devin Review found 1 potential issue.

Devin Review

Comment on lines +262 to +266
synchronized(this@AudioSwitchHandler) {
if (handler !== startHandler) {
return@postAtFrontOfQueue
}
audioSwitch = switch

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.

🔴 Repeated starts leave audio routing active

If start() runs again during switch construction, both jobs pass the startHandler check and publish separate switches. The second replaces audioSwitch, so stop() cannot stop the first; audio routing remains active.

Learn more

A start job is queued on the handler thread, but the switch reference stays null until construction finishes. If another start() occurs while construction is already running, start observes null and queues a second job on the same handler. Both jobs pass the handler identity check. The second job replaces the first switch reference; stop only retains and stops the latest switch.

Example: Thread A begins constructing switch A. A second start() queues switch B before A publishes. Both publish and activate. A later stop() stops B; A stays active.

Recommended fix: Track a start generation or pending-start state, and reject jobs that no longer own the current generation. Ensure a running job is stopped if a newer start supersedes it; add a test that pauses construction across a repeated start().

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@github-actions

Copy link
Copy Markdown
Contributor

Diffuse output:

OLD: diffuse-source-file
NEW: livekit-android-sdk-release.aar

 AAR      │ old      │ new      │ diff   
──────────┼──────────┼──────────┼────────
      jar │    3 MiB │    3 MiB │ +188 B 
 manifest │  1.5 KiB │  1.5 KiB │    0 B 
 lint-jar │ 12.7 KiB │ 12.7 KiB │    0 B 
    other │  2.4 KiB │  2.4 KiB │    0 B 
──────────┼──────────┼──────────┼────────
    total │    3 MiB │    3 MiB │ +188 B 

 JAR     │ old   │ new   │ diff      
─────────┼───────┼───────┼───────────
 classes │  1706 │  1706 │ 0 (+0 -0) 
 methods │ 21983 │ 21983 │ 0 (+3 -3) 
  fields │  5703 │  5703 │ 0 (+0 -0)
AAR
 size  │ diff   │ path          
───────┼────────┼───────────────
 3 MiB │ +188 B │ ∆ classes.jar 
───────┼────────┼───────────────
 3 MiB │ +188 B │ (total)
JAR
METHODS:

   old   │ new   │ diff      
  ───────┼───────┼───────────
   21983 │ 21983 │ 0 (+3 -3) 
  
  + io.livekit.android.audio.AudioSwitchHandler selectDevice_lambda_6(AudioSwitchHandler, AudioDevice)
  + io.livekit.android.audio.AudioSwitchHandler start_lambda_4(AudioSwitchHandler, Handler)
  + io.livekit.android.audio.AudioSwitchHandler stop_lambda_5(AbstractAudioSwitch)
  
  - io.livekit.android.audio.AudioSwitchHandler selectDevice_lambda_5(AudioSwitchHandler, AudioDevice)
  - io.livekit.android.audio.AudioSwitchHandler start_lambda_3(AudioSwitchHandler)
  - io.livekit.android.audio.AudioSwitchHandler stop_lambda_4(AbstractAudioSwitch)

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.

1 participant