Audio Bleed Between Breakout Rooms — Local Mute and Subsession Approaches Both Have Issues in Brave

summary

I’m building a breakout room feature using the Zoom Video SDK for Web. I’ve implemented two separate approaches to achieve participant audio isolation between rooms:

  1. Approach 1 (Legacy): Local mute / unmute via muteUserAudioLocally / unmuteUserAudioLocally

  2. Approach 2 (Current): Zoom native subsessions via

    SubsessionClient

In both approaches, participants in one breakout room can sometimes hear audio from another room. The issue is intermittent in Chrome/Edge, but consistently reproducible in Brave browser.


Environment

  • SDK: @zoom/videosdk (web) Version (2.3.14)

  • Framework: React (TypeScript)

  • Affected browsers: Brave (consistent), Chrome/Edge (intermittent)


Approach 1 — Local Mute / Unmute

How It Works

When breakout rooms start, I determine which participants belong to each room. Participants outside a user’s room are muted locally using muteUserAudioLocally, and participants inside the room are unmuted using unmuteUserAudioLocally.



// Muting a participant locally (from the listener's perspective)

const stream = client.getMediaStream()

await stream.muteUserAudioLocally(zoomUserId)

// Unmuting a participant locally

await stream.unmuteUserAudioLocally(zoomUserId)

The Problem

  • Works correctly most of the time in Chrome and Edge

  • In Brave, the local mute does not always take effect, or its effect is reversed when the WebRTC connection renegotiates

  • Occasionally in Chrome/Edge, a participant in Room A can hear audio from Room B — suggesting the mute state gets lost

  • The issue appears to be timing-related — if participants’ WebRTC streams connect or reconnect after the mute is applied, they arrive in an unmuted state

Questions:

  • Is muteUserAudioLocally guaranteed to persist across WebRTC renegotiations?

  • Is there a way to be notified when a participant’s audio stream is re-established, so we can re-apply local mute state?

  • Does Brave’s aggressive fingerprinting / WebRTC policies interfere with muteUserAudioLocally?


Approach 2 — Native Subsessions

How It Works

I use the

SubsessionClient with manual assignment mode to create subsessions and pre-assign users before opening.

typescript
// Step 1: Create subsession containers (manual assignment mode = 2)

const result = await subsessionClient.createSubsessions(

    ['Room A', 'Room B'],

2 // manual assignment mode

)

// Step 2: Get subsession list, pre-assign users to each room's userList

const subsessionList = subsessionClient.getSubsessionList()

subsessionList[0].userList = [{ userId: studentAZoomId }]

subsessionList[1].userList = [{ userId: studentBZoomId }]

// Add a 1.5s delay — without this, openSubsessions can throw OPERATION_TIMEOUT

await new Promise(resolve => setTimeout(resolve, 1500))

// Step 3: Open with pre-assigned users in one call

await subsessionClient.openSubsessions(subsessionList)
typescript

/

/ Teacher joining a room

await subsessionClient.joinSubsession(subsessionId)

// Teacher leaving (takes no arguments per SDK docs)

await subsessionClient.leaveSubsession()

The Problem

When I test with two students in separate subsessions on Brave browser, the student in Room A can still hear the student in Room B.

My theory is that Brave’s WebRTC privacy shields prevent the old WebRTC connection from being fully torn down when the user is moved to a new subsession. The “zombie” stream from the previous context continues to deliver audio.

As a mitigation, I’m additionally calling muteUserAudioLocally on all out-of-room participants even when subsessions are active — but this has the same Brave-specific reliability issues as Approach 1.

Questions:

  1. When openSubsessions() moves a participant to a subsession, does the SDK guarantee that the previous WebRTC connection is fully closed before the new subsession connection is established?

  2. Is there a specific Zoom SDK event I should listen to that fires after the subsession audio/video streams are fully re-established? I currently listen to user-joined-subsession but by that time, old streams may not yet be torn down.

  3. Is there a recommended way to force-close the audio track from the previous subsession context before the new one opens?

  4. Does the Zoom Video SDK have any built-in handling for browsers that implement restrictive WebRTC policies (like Brave’s --disable-webrtc-allow-legacy-tls-protocols or its fingerprinting protection)?

  5. Is isAutoJoinSubsession: true in openSubsessions() relevant only to the caller (teacher), or does it affect how student streams are initialized?


Additional Context

  • I also tried applying local mute as a secondary layer on top of subsessions (muting all out-of-room participants even when subsessions are active). This still doesn’t fully resolve the issue in Brave

  • The OPERATION_TIMEOUT on openSubsessions() is fairly common — I’m currently working around it with a 1.5 second delay and 3 retries. Is this a known issue and is there a recommended fix?

  • The issue on page refresh (browser reload mid-session) is more pronounced — the WebRTC reconnection seems to re-establish streams in an unmuted state before our isolation logic re-fires


What I’m Looking For

  • Confirmation on whether muteUserAudioLocally persists across WebRTC renegotiations

  • Best practice for re-applying audio isolation after a subsession transition

  • Guidance on Brave / privacy-hardened browser compatibility with subsession audio routing

  • Whether there is a more reliable SDK event or hook to know when subsession audio streams are “settled”

Hi @puvivinocmv

Thanks for your feedback.

Could you share some problematic session IDs for both approach 1 and approach 2 so we can use them for troubleshooting?

Thanks
Vic

Hi @vic.yang , thanks for the reply. Please find the session ID for Approach 1 below:

4662447b-79c0-4e1f-b8e5-5e0a2c480f1e

I am currently testing the subsession approach locally and will share the session ID soon. In the subsession approach, I’m noticing a delay of a few seconds when joining and leaving.

I also tried an alternative approach using adjustUserAudioVolumeLocally to create a breakout room. With this approach, I’m able to reproduce the issue in the Brave browser as well.

Hi @puvivinocmv

4662447b-79c0-4e1f-b8e5-5e0a2c480f1e

It seems this is not a Zoom-format session ID. You can find the correct one in the Web Portal under Dashboard → Past Sessions.

Thanks
Vic

Hi @vic.yang , Can yo please check this 4662447b-79c0-4e1f-b8e5-5e0a2c480f1e,GOC,OSC,N3,Criz VUMIgJQ0sTB+cCTpQG+ABvA==

Hi @puvivinocmv

UMIgJQ0sTB+cCTpQG+ABvA==

After analyzing the logs, we found an older version of the Video SDK (2.1.0) is being used, we are currently unable to obtain detailed logs for this issue. Could you upgrade to a newer version of the Video SDK?

Additionally, regarding the issue you mentioned with the Brave browser, due to Brave’s privacy policies and our current technical limitations, WebRTC is not supported on Brave at this time.

  • Is muteUserAudioLocally guaranteed to persist across WebRTC renegotiations?

Yes.

  • Is there a way to be notified when a participant’s audio stream is re-established, so we can re-apply local mute state?

Listen for the user-added, user-updated and user-removed event to monitor the user changes.

  • Does Brave’s aggressive fingerprinting / WebRTC policies interfere with muteUserAudioLocally?

No. Even for the web assembly audio solution, this function works as expected.

When openSubsessions() moves a participant to a subsession, does the SDK guarantee that the previous WebRTC connection is fully closed before the new subsession connection is established?

After calling openSubsessions, if isAutoJoinSubsession is set, non-host users will automatically join the subsession. This subsession is isolated from the main session—you can think of it as a new session.

Is there a specific Zoom SDK event I should listen to that fires after the subsession audio/video streams are fully re-established?

For the current user, listen fot the current-audio-change event, for the remote users, listen for the user-updated event and care about the audio property.

Is there a recommended way to force-close the audio track from the previous subsession context before the new one opens?

You don’t need to handle this, as the SDK already does it.

Is isAutoJoinSubsession: true in openSubsessions() relevant only to the caller (teacher), or does it affect how student streams are initialized?

It depends on the role of the user,

  • The OPERATION_TIMEOUT on openSubsessions() is fairly common — I’m currently working around it with a 1.5 second delay and 3 retries. Is this a known issue and is there a recommended fix?

openSubsessions takes some time to complete. You can await the returned promise to be resolved; no additional retries are needed.

Thanks
Vic

Hi @vic.yang ,

Thank you for your clarification, I really appreciate your effort.

Please find the latest session ID along with the updated SDK version -
1lZEruDqTJ+IAYoDMngOzQ==

Apart from subsessions, I would like to understand whether breakout rooms can be achieved using local audio controls such as mute/unmute or adjustUserAudioVolumeLocally. Will this approach work effectively, or are there any potential limitations or issues I should be aware of?

Additionally, could you please confirm the recommended browsers for using the Video SDK?

Thank you.

Hi @puvivinocmv

1lZEruDqTJ+IAYoDMngOzQ==

After analyzing the logs, we found that muteUserAudioLocally and unmuteUserAudioLocally were being called at a high frequency within a short period (about more than 10 times per second for each method). Could you check whether this is a usage issue?

Regarding the implementation of breakout rooms, we recommend using the subsession feature provided by the Video SDK. This is because mute/unmuteUserAudioLocally only affects audio, while other functions such as the user list and video cannot achieve the same level of isolation as subsessions.

Thanks
Vic

Thanks @vic.yang , I shall investigate the usage discrepancy; however, not all users are encountering audio beeping issues, as these occurrences appear to be sporadic.

Hi @vic.yang

I’m looking for some architectural advice regarding Breakout Rooms (Subsessions) in the Video SDK for Web.

The Problem: > When a participant refreshes their browser, they rejoin the meeting with a new userId and drop into the main session. They cannot independently call joinSubsession() to return to their room because the SDK throws Error 7502 (Invalid/Stale Subsession ID), presumably because their local guest list hasn’t synced yet.

Our Current Architecture: Because the participant is blocked from joining, we have had to build a strict “Host Pull” routing system:

  1. The Host listens for the user-added event.

  2. The Host matches the new Zoom userId to our backend database to find their assigned room.

  3. The Host calls assignUserToSubsession() to pull the participant into the correct room.

The Edge Case (Simultaneous Refresh): We discovered a critical race condition with this approach. If the Host and the Participant experience a network drop or refresh at the exact same time, the Participant lands in the main room before the Host’s user-added listener has finished mounting. The event fires into the void, and the participant is permanently stranded.

To mitigate this, we had to add a “Sweep” mechanism where the Host calls getUnassignedUserList() a few seconds after initializing to catch anyone who slipped through during the Host’s downtime.

My Question: While our Host-driven routing works, it is highly complex and relies entirely on the Host’s connection stability. Is there any mechanism, setting, or token persistence in the Web SDK that allows a participant to securely rejoin a subsession themselves after a browser refresh without hitting Error 7502? Or is this Host-driven routing (Listeners + Sweeps) the officially intended design pattern?

Thanks in advance for the clarification!

Hi @puvivinocmv

If you need users assigned to subsessions to automatically join their designated subsession, you can set isAutoJoinSubsession to true in the options when calling openSubsessions().

In addition, if a user is already in a subsession and rejoins due to a network issue, the Video SDK Web will automatically put them back into the corresponding subsession without requiring any additional host pull action.

If subsessions will be opened and closed multiple times during the session, it is recommended to reuse the existing subsession list. Alternatively, when the session status is closed, it is recommended to call the getSubsessionList() method to retrieve the latest subsession list.

Thanks
Vic

Hi @vic.yang , This is not working for me

In addition, if a user is already in a subsession and rejoins due to a network issue, the Video SDK Web will automatically put them back into the corresponding subsession without requiring any additional host pull action.

Please find the details below

I am passing the option isAutoJoinSubsession: true. I am also passing user_identity in the JWT so users have a persistent ID.

The Issue: When a user is currently in a subsession and refreshes their browser screen, they land back in the main session instead of automatically rejoining their designated subsession.

Verification: I can confirm they are being dropped into the main room. On the Host side, the user-added event is triggered the moment the user refreshes, and when I check getUnassignedUserList(), that user is listed there as unassigned.

Hi @puvivinocmv

We checked the sample app and found that when a user refreshes while in a subsession, they can automatically rejoin that subsession.

On the host side, you can use subsession.getSubsessionList() and check the userList field. You will notice that the userId changes after a refresh, but the userGuid remains the same, which can be used to track the same user.

Thanks
Vic

Hi @vic.yang ,

Title: Video SDK Web: Late joiners/reconnecting users stuck in “invited” status and not auto-joining subsessions

Description: We are experiencing an issue with Zoom Video SDK (Web) Subsessions where participants who join the session late (after breakout rooms have started) or those who reconnect due to internet instability are not consistently auto-joining their assigned subsession.

Environment:

  • SDK: @zoom/videosdk

  • Platform: Web (Chrome/Edge)

The Issue: When breakout rooms are active, we attempt to assign late-joining students to a subsession using assignUserToSubsession. However:

  1. The user’s status (from getUserStatus) remains invited instead of transitioning to joining or in room.

  2. Even with isAutoJoinSubsession: true set during the initial openSubsessions call, the user is not physically moved.

  3. Our diagnostic checks show the user is present in the main session (visible via client.getAllUser()), but they remain stuck there.

How we create and start subsessions: We use the following workflow to initialize and open the rooms:

typescript

// From subsessionHelpers.ts

export const startSubsessionWorkflow = async (params: {

roomCount: number,

ssClient: any,

// … other params

}) => {

const { ssClient } = params;

// 1. Creating the rooms

const labels = [‘Room A’, ‘Room B’, ‘Room C’, ‘Room D’];

const subsessionList = await ssClient.createSubsessions(labels, SubsessionAllocationPattern.Manually);

// 2. Assigning users (Manual assignment before opening)

for (const { userId, roomId } of usersRooms) {

const subsession = subsessionList[roomId];

subsession.userList.push({ userId: zoomUserId });

}

// 3. Opening subsessions with Auto-Join enabled

await ssClient.openSubsessions(subsessionList, {

isAutoJoinSubsession: true,

isBackToMainSessionEnabled: true,

isAutoMoveBackToMainSession: true,

waitSeconds: 5

});

}
How we handle late joiners: When a student joins late, we detect them in the unassigned list and try to move them:

typescript



// Logic used for late joiners / reconnecting users

export const handleLateJoinersWorkflow = async (params: {

unassignedUsers: any[],

ssClient: any

}) => {

const { unassignedUsers, ssClient } = params;

const subsessionList = ssClient.getSubsessionList();

for (const user of unassignedUsers) {

const targetSubsession = subsessionList[targetRoomIndex];

// Attempting to move the late joiner

await ssClient.assignUserToSubsession(user.userId, targetSubsession.subsessionId);

}

}

Observations:

  • If a user is already in the main session when openSubsessions is called, auto-join works perfectly.

  • The failure happens specifically for users who arrive after the rooms are already InProgress.

  • We’ve verified that assignUserToSubsession is called successfully (no errors thrown), but the subsession-user-update event shows the user stuck as invited.

Hi @puvivinocmv

We tested the same setting, and users who joined the session later were able to correctly enter the specified subsession.

Could you share some problematic session IDs for further investigation?

Thanks
Vic

Hi @vic.yang Sorry for the delayed reply. Please find the details below

This is the status while in the sub session

{
“sessionId”: “N0bXFaPYQimQZyA9m4Gc4Q==”,
“isBreakoutActive”: true,
“roomId”: 1,
“status”: “session-joined”,
“subsessionStatus”: 2,
“teacherRoom”: null,
“unassignedUsers”: ,
“userCountInRoom”: 2,
“userId”: “USTUDENT4”,
“userRole”: “student”,
“userStatus”: “in room”,
“usersInRoom”: [
“USTUDENT4”,
“USTUDENT3”
]
}

if the user refresh the screen
{
“isBreakoutActive”: true,
“roomId”: 1,
“sessionId”: “N0bXFaPYQimQZyA9m4Gc4Q==”,
“status”: “session-joined”,
“subsessionStatus”: 1,
“teacherRoom”: null,
“unassignedUsers”: ,
“userCountInRoom”: 2,
“userId”: “USTUDENT4”,
“userRole”: “student”,
“userStatus”: “initial”,
“usersInRoom”: [
“USTUDENT3”,
“USTUDENT4”
]
}

No user in the main session but user should join the sub session automatically Can you please help me on this

Thanks