Skip to content

Conversation

@mfairley
Copy link

@mfairley mfairley commented Jan 24, 2026

When unmuting audio/video tracks that require reacquisition (e.g., due to
stopOnMute, ended track state, or device changes), the track restart would
cause a mute cycle (enabled=false then immediately enabled=true).

This happened because:

  1. restartTrack() creates a new MediaStreamTrack
  2. setMediaStreamTrack() syncs enabled state with !this.isMuted
  3. But isMuted is still true at this point (super.unmute() hasn't run yet)
  4. So the new track starts with enabled=false
  5. Then super.unmute() sets enabled=true

On iOS, this causes audible mute/unmute sounds for the microphone and can
trigger system callbacks multiple times as the system detects the rapid
enabled state changes.

The fix adds a targetEnabled parameter that flows through:

  • unmute() -> restartTrack(undefined, true)
  • restartTrack() -> restart(constraints, targetEnabled)
  • restart() -> setMediaStreamTrack(newTrack, false, targetEnabled)

When targetEnabled is provided, setMediaStreamTrack uses it instead of
deriving the enabled state from isMuted, ensuring the new track starts
with the correct enabled state.

Related issues

This partially addresses the above issues but does not fix the possibly unnecessary restarts caused by device ID mismatches.

Test plan

  • Tested muting and unmuting microphone on iOS
  • Tested muting and unmuting video on iOS
  • Verified no mute cycling occurs during unmute with track restart

Summary by CodeRabbit

  • Bug Fixes
    • Prevented mute–unmute cycling when re-acquiring microphones by preserving the intended enabled state during restarts.
    • Ensured camera restarts refresh simulcast encodings and rebind sender tracks so video continuity and quality are maintained.
    • Improved unmute behavior so devices come back enabled predictably after restart, avoiding unexpected muted states.

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot
Copy link

changeset-bot bot commented Jan 24, 2026

🦋 Changeset detected

Latest commit: 83f3f62

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

This PR includes changesets to release 1 package
Name Type
livekit-client 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

@CLAassistant
Copy link

CLAassistant commented Jan 24, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link

coderabbitai bot commented Jan 24, 2026

📝 Walkthrough

Walkthrough

Propagates a new optional targetEnabled flag through restart paths in LocalTrack, LocalAudioTrack, and LocalVideoTrack so reacquired MediaStreamTracks can be set to the intended enabled state; LocalVideoTrack also rebinds simulcast encodings after restart.

Changes

Cohort / File(s) Summary
Core restart propagation
src/room/track/LocalTrack.ts
Added targetEnabled?: boolean to restart(...); added private async setMediaStreamTrack(newTrack, force?, targetEnabled?); propagate targetEnabled to control MediaStreamTrack.enabled during track swaps to avoid mute cycling.
Audio restart and unmute flow
src/room/track/LocalAudioTrack.ts
restartTrack(options?: AudioCaptureOptions, targetEnabled?: boolean) and restart(..., targetEnabled?) signatures updated; unmute path calls restartTrack(undefined, true) so the reacquired mic honors enabled state.
Video restart, unmute, and simulcast refresh
src/room/track/LocalVideoTrack.ts
restartTrack(options?: VideoCaptureOptions, targetEnabled?: boolean) added and forwarded; unmute passes targetEnabled=true. After restart, iterates simulcast encodings and clones/replaces each simulcast MediaStreamTrack on active RTCRtpSenders to rebind streams.
Changelog
.changeset/gold-pens-fly.md
Adds a changeset noting a patch: prevents unmute→mute→unmute cycling for track restarts occurring during unmute.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I nudged the stream and gave a twitch,

Tracks restart without the glitch,
Enabled flags now hop in line,
Simulcast hums, rebinding fine,
A tiny hop — the code's just rich!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: prevent mute cycling when restarting tracks during unmute' directly and clearly describes the main change: preventing mute cycling during unmute with track restarts by adding a targetEnabled parameter.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f62a223 and 83f3f62.

📒 Files selected for processing (4)
  • .changeset/gold-pens-fly.md
  • src/room/track/LocalAudioTrack.ts
  • src/room/track/LocalTrack.ts
  • src/room/track/LocalVideoTrack.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • .changeset/gold-pens-fly.md
🧰 Additional context used
🧬 Code graph analysis (2)
src/room/track/LocalVideoTrack.ts (3)
src/room/track/options.ts (1)
  • VideoCaptureOptions (169-192)
src/room/track/LocalTrack.ts (1)
  • constraints (38-40)
src/room/track/utils.ts (1)
  • constraintsForOptions (64-103)
src/room/track/LocalAudioTrack.ts (3)
src/room/track/options.ts (1)
  • AudioCaptureOptions (244-298)
src/room/track/LocalTrack.ts (1)
  • constraints (38-40)
src/room/track/utils.ts (1)
  • constraintsForOptions (64-103)
🔇 Additional comments (7)
src/room/track/LocalTrack.ts (2)

148-210: Good: targetEnabled prevents transient mute cycling on restart.
Clear, minimal change, and the fallback to !this.isMuted preserves existing behavior when the flag isn’t provided.


328-372: LGTM: restart now propagates targetEnabled correctly.
Forwarding the flag to setMediaStreamTrack keeps the enabled state consistent through reacquisition.

src/room/track/LocalVideoTrack.ts (2)

161-172: Nice: unmute reacquisition explicitly targets enabled state.
This matches the PR goal and avoids the mute/unmute oscillation on iOS.


249-268: LGTM: restartTrack forwards targetEnabled and keeps post-restart logic intact.
The propagation is clean, and the restart flow remains coherent.

src/room/track/LocalAudioTrack.ts (3)

69-90: Good: mic reacquisition on unmute sets explicit enabled target.
Aligns with the new enabled-state contract and prevents cycling.


98-107: LGTM: restartTrack signature and forwarding are consistent.
Optional targetEnabled keeps existing call sites compatible.


109-114: LGTM: restart override forwards targetEnabled and preserves silence check.
No regression risk spotted here.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/room/track/LocalAudioTrack.ts`:
- Around line 109-110: The method signature for LocalAudioTrack.restart is not
formatted to match Prettier; re-run the project's Prettier auto-fixer or
manually reformat the signature of the restart method (protected async
restart(constraints?: MediaTrackConstraints, targetEnabled?: boolean):
Promise<typeof this>) to match the repository's style (e.g., adjust spacing or
break parameters onto separate lines) and ensure the body (including the
super.restart call) remains unchanged so the CI Prettier check passes.

In `@src/room/track/LocalTrack.ts`:
- Line 148: Reformat the LocalTrack class method signature setMediaStreamTrack
so it matches the project's Prettier rules: break the parameter list onto
multiple lines (one parameter per line) and ensure consistent spacing around the
commas and optional parameter markers (e.g., force?: boolean, targetEnabled?:
boolean) and the opening brace; update the signature of private async
setMediaStreamTrack(newTrack: MediaStreamTrack, force?: boolean, targetEnabled?:
boolean) accordingly so the line no longer violates Prettier.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1744ee4 and c447e8d.

📒 Files selected for processing (3)
  • src/room/track/LocalAudioTrack.ts
  • src/room/track/LocalTrack.ts
  • src/room/track/LocalVideoTrack.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/room/track/LocalVideoTrack.ts (3)
src/room/track/options.ts (1)
  • VideoCaptureOptions (169-192)
src/room/track/LocalTrack.ts (1)
  • constraints (38-40)
src/room/track/utils.ts (1)
  • constraintsForOptions (64-103)
🪛 ESLint
src/room/track/LocalAudioTrack.ts

[error] 109-109: Replace constraints?:·MediaTrackConstraints,·targetEnabled?:·boolean with ⏎····constraints?:·MediaTrackConstraints,⏎····targetEnabled?:·boolean,⏎··

(prettier/prettier)

src/room/track/LocalTrack.ts

[error] 148-148: Replace newTrack:·MediaStreamTrack,·force?:·boolean,·targetEnabled?:·boolean with ⏎····newTrack:·MediaStreamTrack,⏎····force?:·boolean,⏎····targetEnabled?:·boolean,⏎··

(prettier/prettier)

🪛 GitHub Actions: Test
src/room/track/LocalAudioTrack.ts

[error] 109-109: prettier/prettier formatting issue in LocalAudioTrack.ts: Replace 'constraints?: MediaTrackConstraints, targetEnabled?: boolean' with proper formatting.

🪛 GitHub Check: test
src/room/track/LocalAudioTrack.ts

[failure] 109-109:
Replace constraints?:·MediaTrackConstraints,·targetEnabled?:·boolean with ⏎····constraints?:·MediaTrackConstraints,⏎····targetEnabled?:·boolean,⏎··

src/room/track/LocalTrack.ts

[failure] 148-148:
Replace newTrack:·MediaStreamTrack,·force?:·boolean,·targetEnabled?:·boolean with ⏎····newTrack:·MediaStreamTrack,⏎····force?:·boolean,⏎····targetEnabled?:·boolean,⏎··

🔇 Additional comments (6)
src/room/track/LocalTrack.ts (2)

204-206: Enabled state now deterministic during restart.

Using targetEnabled avoids transient mute cycling in the unmute-restart path.


324-367: Restart path cleanly propagates targetEnabled.

The forwarding into setMediaStreamTrack keeps restart behavior consistent with the new unmute flow.

src/room/track/LocalAudioTrack.ts (2)

77-89: Reacquire on unmute now forces enabled state.

Passing true avoids the mute-cycle regression during track restart.


98-107: Restart propagation looks good.

The new optional targetEnabled cleanly flows into the base restart.

src/room/track/LocalVideoTrack.ts (2)

161-172: Unmute now restarts with explicit enabled state.

This mirrors the audio fix and should avoid the mute-cycle during camera reacquisition.


249-268: Restart now refreshes simulcast tracks appropriately.

Cloning and replacing simulcast tracks after restart keeps encodings in sync with the new MediaStreamTrack.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@mfairley mfairley force-pushed the fix/unmute-restart-mute-cycling branch 2 times, most recently from 6535184 to f62a223 Compare January 24, 2026 19:14
When unmuting audio/video tracks that require reacquisition (e.g., due to
stopOnMute, ended track state, or device changes), the track restart would
cause a mute cycle (enabled=false then immediately enabled=true).

This happened because:
1. restartTrack() creates a new MediaStreamTrack
2. setMediaStreamTrack() syncs enabled state with !this.isMuted
3. But isMuted is still true at this point (super.unmute() hasn't run yet)
4. So the new track starts with enabled=false
5. Then super.unmute() sets enabled=true

On iOS, this causes audible mute/unmute sounds for the microphone and can
trigger system callbacks multiple times as the system detects the rapid
enabled state changes.

The fix adds a targetEnabled parameter that flows through:
- unmute() -> restartTrack(undefined, true)
- restartTrack() -> restart(constraints, targetEnabled)
- restart() -> setMediaStreamTrack(newTrack, false, targetEnabled)

When targetEnabled is provided, setMediaStreamTrack uses it instead of
deriving the enabled state from isMuted, ensuring the new track starts
with the correct enabled state.
@mfairley
Copy link
Author

@xianshijing-lk could you please review this PR?

@davidliu davidliu requested review from davidliu and lukasIO January 27, 2026 11:32
Copy link
Contributor

@davidliu davidliu left a comment

Choose a reason for hiding this comment

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

LGTM, @lukasIO is there anything here that could affect web? Should be an isolated change.

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.

3 participants