Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/funny-masks-strive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"livekit-client": patch
---

Fix unnecessary track restarts on unmute when using ideal device constraints
11 changes: 4 additions & 7 deletions src/room/track/LocalAudioTrack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TrackEvent } from '../events';
import { computeBitrate, monitorFrequency } from '../stats';
import type { AudioSenderStats } from '../stats';
import type { LoggerOptions } from '../types';
import { isReactNative, isWeb, unwrapConstraint } from '../utils';
import { isReactNative, isWeb } from '../utils';
import LocalTrack from './LocalTrack';
import { Track } from './Track';
import type { AudioCaptureOptions } from './options';
Expand Down Expand Up @@ -74,14 +74,11 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
return this;
}

const deviceHasChanged =
this._constraints.deviceId &&
this._mediaStreamTrack.getSettings().deviceId !==
unwrapConstraint(this._constraints.deviceId);

if (
this.source === Track.Source.Microphone &&
(this.stopOnMute || this._mediaStreamTrack.readyState === 'ended' || deviceHasChanged) &&
(this.stopOnMute ||
this._mediaStreamTrack.readyState === 'ended' ||
this.pendingDeviceChange) &&
!this.isUserProvided
) {
this.log.debug('reacquiring mic track', this.logContext);
Expand Down
4 changes: 4 additions & 0 deletions src/room/track/LocalTrack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ export default abstract class LocalTrack<

protected trackChangeLock: Mutex;

protected pendingDeviceChange: boolean = false;

/**
*
* @param mediaTrack
Expand Down Expand Up @@ -246,6 +248,7 @@ export default abstract class LocalTrack<
// when track is muted, underlying media stream track is stopped and
// will be restarted later
if (this.isMuted) {
this.pendingDeviceChange = true;
return true;
}
Comment on lines 250 to 253
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check LocalVideoTrack unmute logic for device change handling

# Search for unmute method in LocalVideoTrack
echo "=== Searching for unmute method in LocalVideoTrack ==="
ast-grep --pattern $'class LocalVideoTrack {
  $$$
  unmute() {
    $$$
  }
  $$$
}'

# Also check for any pendingDeviceChange references
echo -e "\n=== Searching for pendingDeviceChange references ==="
rg -n "pendingDeviceChange" --type ts

# Search for LocalVideoTrack class definition
echo -e "\n=== Searching for LocalVideoTrack class ==="
rg -n "class LocalVideoTrack" --type ts

# Search for unmute in LocalVideoTrack context
echo -e "\n=== Searching for unmute method ==="
rg -n "unmute\(\)" --type ts -A 5 -B 2

Repository: livekit/client-sdk-js

Length of output: 5452


🏁 Script executed:

#!/bin/bash
# Get full unmute implementations in both tracks

echo "=== LocalAudioTrack unmute method (full) ==="
sed -n '69,93p' src/room/track/LocalAudioTrack.ts

echo -e "\n=== LocalVideoTrack unmute method (full) ==="
sed -n '161,178p' src/room/track/LocalVideoTrack.ts

echo -e "\n=== LocalTrack device change handling context (lines 245-260) ==="
sed -n '245,260p' src/room/track/LocalTrack.ts

Repository: livekit/client-sdk-js

Length of output: 1829


LocalVideoTrack should also check and reset pendingDeviceChange on unmute for consistency.

The flag is set when a device change is deferred due to muting, but only LocalAudioTrack explicitly resets it in its unmute handler. LocalVideoTrack's unmute always restarts the track (if source is Camera), so it implicitly handles the pending change—but does not reset the flag. This can cause pendingDeviceChange to remain true on subsequent mute/unmute cycles, creating inconsistent state. Consider adding the same check and reset logic to LocalVideoTrack.unmute() as found in LocalAudioTrack.unmute() (lines 81–86).

🤖 Prompt for AI Agents
In `@src/room/track/LocalTrack.ts` around lines 250 - 253, LocalVideoTrack.unmute
should mirror LocalAudioTrack.unmute by clearing the pendingDeviceChange flag
when unmuting so it doesn't remain true across cycles; update the
LocalVideoTrack.unmute method to check this.pendingDeviceChange at the start (or
before any restart logic for camera source), set this.pendingDeviceChange =
false and return true (or otherwise short-circuit the deferred device-change
flow) so state is consistent with LocalAudioTrack.unmute.


Expand Down Expand Up @@ -365,6 +368,7 @@ export default abstract class LocalTrack<

await this.setMediaStreamTrack(newTrack);
this._constraints = constraints;
this.pendingDeviceChange = false;
this.emit(TrackEvent.Restarted, this);
if (this.manuallyStopped) {
this.log.warn(
Expand Down
Loading