From bc2915be897f29103c4384ccd58c75c91e0b5914 Mon Sep 17 00:00:00 2001 From: Michael Fairley <4374785+mfairley@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:31:17 -0600 Subject: [PATCH] fix: prevent unnecessary track restarts on unmute when using ideal device constraints When using `{ ideal: 'default' }` device constraints (the default), unmuting would trigger unnecessary track restarts because the comparison between the constraint value ('default') and actual device ID (e.g. 'audio') would always fail. This adds a `pendingDeviceChange` flag that is only set when `setDeviceId()` is explicitly called while the track is muted. The unmute logic now checks this flag instead of comparing constraint values against the actual device, ensuring restarts only happen when the user actually requested a device change. --- .changeset/funny-masks-strive.md | 5 +++++ src/room/track/LocalAudioTrack.ts | 11 ++++------- src/room/track/LocalTrack.ts | 4 ++++ 3 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 .changeset/funny-masks-strive.md diff --git a/.changeset/funny-masks-strive.md b/.changeset/funny-masks-strive.md new file mode 100644 index 0000000000..ec09d72cee --- /dev/null +++ b/.changeset/funny-masks-strive.md @@ -0,0 +1,5 @@ +--- +"livekit-client": patch +--- + +Fix unnecessary track restarts on unmute when using ideal device constraints diff --git a/src/room/track/LocalAudioTrack.ts b/src/room/track/LocalAudioTrack.ts index fb77e2e9ec..41918bcb57 100644 --- a/src/room/track/LocalAudioTrack.ts +++ b/src/room/track/LocalAudioTrack.ts @@ -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'; @@ -74,14 +74,11 @@ export default class LocalAudioTrack extends LocalTrack { 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); diff --git a/src/room/track/LocalTrack.ts b/src/room/track/LocalTrack.ts index 0306b7ace6..a1a86a4a21 100644 --- a/src/room/track/LocalTrack.ts +++ b/src/room/track/LocalTrack.ts @@ -65,6 +65,8 @@ export default abstract class LocalTrack< protected trackChangeLock: Mutex; + protected pendingDeviceChange: boolean = false; + /** * * @param mediaTrack @@ -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; } @@ -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(