fix: prevent mute cycling when restarting tracks during unmute#1793
fix: prevent mute cycling when restarting tracks during unmute#1793mfairley wants to merge 1 commit intolivekit:mainfrom
Conversation
🦋 Changeset detectedLatest commit: 83f3f62 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 |
📝 WalkthroughWalkthroughPropagates a new optional Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
📜 Recent review detailsConfiguration used: Organization UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (1)
🧰 Additional context used🧬 Code graph analysis (2)src/room/track/LocalVideoTrack.ts (3)
src/room/track/LocalAudioTrack.ts (3)
🔇 Additional comments (7)
✏️ Tip: You can disable this entire section by setting Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
src/room/track/LocalAudioTrack.tssrc/room/track/LocalTrack.tssrc/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
targetEnabledavoids transient mute cycling in the unmute-restart path.
324-367: Restart path cleanly propagates targetEnabled.The forwarding into
setMediaStreamTrackkeeps restart behavior consistent with the new unmute flow.src/room/track/LocalAudioTrack.ts (2)
77-89: Reacquire on unmute now forces enabled state.Passing
trueavoids the mute-cycle regression during track restart.
98-107: Restart propagation looks good.The new optional
targetEnabledcleanly 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.
6535184 to
f62a223
Compare
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.
f62a223 to
83f3f62
Compare
|
@xianshijing-lk could you please review this PR? |
| this.log.debug('re-acquired MediaStreamTrack', this.logContext); | ||
|
|
||
| await this.setMediaStreamTrack(newTrack); | ||
| await this.setMediaStreamTrack(newTrack, false, targetEnabled); |
There was a problem hiding this comment.
restarts would always cause a track to be enabled afterwards, so I believe we can get rid of passing the targetEnabled prop in the other methods and only keep it here
There was a problem hiding this comment.
Could you explain a bit more? We are triggering the restart from the unmute method so I think we need to pass the argument from unmute to where it's needed, which requires going through a few methods.
There was a problem hiding this comment.
my thinking was that for restartTrack it's implicit that targetEnabled = true.
But looking at it again that seems like a misconception on my side, sorry about that!
Just wanted to ensure the public API stays as lean as possible and ideally it would be preferable to not expose the targetEnabled param on restartTrack
There was a problem hiding this comment.
Yeah, I understand wanting to keep the public API as lean as possible. Some alternative approaches include introducing a class variable such as isPendingUnmute, but then that introduces additional state to manage and is less explicit. Another is using the current approach but having a better name e.g. targetEnabled -> isUnmuting since this is probably the only case when you don't want to immediately sync the mute state. Another is adding this parameter to VideoCaptureOptions/AudioCaptureOptions but it doesn't seem to quite fit with the intention of that. Let me know if you have any preferences.
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:
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:
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
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.