Skip to content

fix(webinar): panelist mute state after returning from breakout session#5007

Open
xinhyao wants to merge 1 commit into
webex:nextfrom
xinhyao:fix(webinar)-SPARK-814773-fix-panelist-mute-state-after-breakout
Open

fix(webinar): panelist mute state after returning from breakout session#5007
xinhyao wants to merge 1 commit into
webex:nextfrom
xinhyao:fix(webinar)-SPARK-814773-fix-panelist-mute-state-after-breakout

Conversation

@xinhyao
Copy link
Copy Markdown
Contributor

@xinhyao xinhyao commented May 27, 2026

This pull request addresses

https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-814773 — Panelists can hear each other after returning from breakout to main session.

Problem

In a webinar, after returning from a breakout session to the main session, two regression scenarios occur:

Case 1 — Panelist who was unmuted then re-muted by host:

  • Before BO: host unmutes panelist, then mutes them again
  • After BO returns to main: panelist's UI flips to unmuted and the microphone is actually open

Case 2 — Attendee promoted to panelist (no operations during the session):

  • After BO returns to main: panelist's UI correctly shows muted, but other participants can hear their audio

Root Cause

When returning from a breakout to the main session, LocusInfo replays the cached main-session locus via mainSessionLocusCache. Partial DTOs received during the breakout can pollute this cache, causing parseSelf to diff out a spurious remoteMuted: true → false transition. This fires a fake handleServerRemoteMuteUpdate(muted=false) event in muteState, even though the server never actually cleared the remote mute.

The old handleServerRemoteMuteUpdate blindly called muteLocalStream(false, 'remotelyMuted') whenever muted=false, which caused:

  • Case 1 (host unmuted then re-muted the panelist before BO): UI flips to unmute, the mic opens, and the mute icon disappears from other participants' tiles.

Background — isMuted() is the OR of three flags: client.localMute (local intent), server.localMute (Locus controls.audio.muted), and server.remoteMute (host-muted state). If any one is true, the user is considered muted.

Before BO, the host's unmute had already cleared client.localMute via the LOCAL_UNMUTE_REQUIRED path. The host's later re-mute set server.remoteMute back to true, so the user stayed muted going into BO. On BO→Main, the locus cache replays a stale SELF_REMOTE_MUTE_STATUS_UPDATED(muted=false) (entry point: meeting/index.ts:3714). The old handleServerRemoteMuteUpdate unconditionally:

Sets server.remoteMute = false.
Calls muteLocalStream(false), which clears systemMute on the stream; the resulting handleLocalStreamMuteStateChange reads the stream back and sets client.localMute = false too.
Never re-syncs the user's true intent back to Locus.
server.localMute was already false from the earlier unmute, so all three flags collapse to false → isMuted() returns false → mic opens locally, and because Locus controls.audio.muted was never corrected, other participants' tiles drop the mute icon.

  • Case 2 (attendee promoted to panelist, no operations): UI correctly stays muted (because client.localMute=true was never cleared), but other participants can hear the user. muteLocalStream(false) opened the stream the same way; only the client.localMute safety flag prevented the UI from also flipping.

by making the following changes

plugin-meetings/src/meeting/muteState.tshandleServerRemoteMuteUpdate:

  1. On muted=true: also set client.localMute=true. A later stale remoteMute=false can no longer flip isMuted() to false, because client.localMute holds the line.
  2. On muted=false: do not touch the stream. A bare remoteMute=false only means "hard-mute lock released"; the legitimate server-driven unmute path is LOCAL_UNMUTE_REQUIRED (handleServerLocalUnmuteRequired), which already calls muteLocalStream(false, 'localUnmuteRequired').
  3. Always call applyClientStateToServer: when the stale event makes the server's controls.audio.muted diverge from the client's intent, this pushes the client's true state back to the server so other participants' tiles render the mute icon correctly.

Change Type

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Tooling change
  • Internal code refactor

The following scenarios were tested

Unit tests

Added four new unit tests in test/unit/spec/meeting/muteState.js:

  • does not unmute the local stream when server clears remote mute while user is locally muted (breakout -> main regression) — verifies that when the user is already locally muted, a remoteMute=false event does not call setServerMuted on the stream.
  • does not touch the local stream when remote mute stays false (no transition) — verifies that a remoteMute=false event with no prior remoteMute=true has no stream side effects.
  • keeps isMuted() true when a stale remoteMute=false replays after remote mute — covers Case 1: after host unmutes then mutes the user, a subsequent stale remoteMute=false from the locus cache must not flip isMuted() to false or open the stream (validates the client.localMute=true lock + no-op on muted=false).
  • syncs client mute intent back to server on stale remoteMute=false — covers Case 2: when the client's intent and server's controls.audio.muted diverge, the stale event must trigger applyClientStateToServer to re-sync, so other participants' tiles continue to show the mute icon.

Existing tests around handleServerRemoteMuteUpdate continue to pass.

Risk

  • Low. Change is scoped to a single function in muteState.ts. Semantic change is asymmetric: muted=true keeps existing behavior plus a defensive flag lock; muted=false no longer touches the stream. The latter is a deliberate behavior change but matches the original design — server-forced unmute belongs to LOCAL_UNMUTE_REQUIRED, not to remoteMute=false.
  • The "release hard-mute lock without auto-unmuting" semantic matches native client behavior.

The GAI Coding Policy And Copyright Annotation Best Practices

  • GAI was not used (or, no additional notation is required)
  • Code was generated entirely by GAI
  • GAI was used to create a draft that was subsequently customized or modified
  • Coder created a draft manually that was non-substantively modified by GAI (e.g., refactoring was performed by GAI on manually written code)
  • Tool used for AI assistance (GitHub Copilot / Other - specify)
    • Github Copilot
    • Other - Please Specify
  • This PR is related to
    • Feature
    • Defect fix
    • Tech Debt
    • Automation

I certified that

  • I have read and followed contributing guidelines
  • I discussed changes with code owners prior to submitting this pull request
  • I have not skipped any automated checks
  • All existing and new tests passed
  • I have updated the documentation accordingly

Make sure to have followed the contributing guidelines before submitting.

@xinhyao xinhyao requested review from a team as code owners May 27, 2026 07:56
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb661a8dbf

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +383 to +386
if (muted) {
this.state.client.localMute = true;
this.muteLocalStream(meeting, true, 'remotelyMuted');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve server-driven video unmute handling

When a host clears a participant's remote video mute, Meeting delivers SELF_REMOTE_VIDEO_MUTE_STATUS_UPDATED with payload.muted === false directly to this method, and there is no video equivalent of LOCAL_UNMUTE_REQUIRED to clear the camera stream afterward. With the new if (muted) guard, that false update only changes state.server.remoteMute; it never calls setServerMuted(false, 'remotelyMuted'), so the LocalCameraStream remains user-muted from the earlier remote mute and the participant stays muted even though the server unmuted them.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this is fine, we don't have a feature for hosts to unmute video, only user can unmute their video

@aws-amplify-us-east-2
Copy link
Copy Markdown

This pull request is automatically being deployed by Amplify Hosting (learn more).

Access this pull request here: https://pr-5007.d3m3l2kee0btzx.amplifyapp.com

@marcin-bazyl marcin-bazyl added the validated If the pull request is validated for automation. label May 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

validated If the pull request is validated for automation.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants