mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 11:22:31 +00:00
fix(large-video)pin prev speaker on stage when local user is dominant speaker. (#16511)
* fix(large-video)pin prev speaker on stage when local user is dominant speaker. Also fix the case where the dominant speaker sometimes doesn't show up in the filmstrip when SS is pinned to stage. * fix(test) Add more tests for activeSpeaker * fix(test) Do not check for large-video if there is no remote video track. * fix(filmstrip) Get updated display name for calc speaker order
This commit is contained in:
@@ -62,65 +62,6 @@ const AVATAR_CHECKER_FUNCTIONS = [
|
||||
];
|
||||
/* eslint-enable arrow-body-style */
|
||||
|
||||
/**
|
||||
* Returns the list of active speakers that should be moved to the top of the sorted list of participants so that the
|
||||
* dominant speaker is visible always on the vertical filmstrip in stage layout.
|
||||
*
|
||||
* @param {Function | Object} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
|
||||
* retrieve the state.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
export function getActiveSpeakersToBeDisplayed(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const {
|
||||
dominantSpeaker,
|
||||
fakeParticipants,
|
||||
sortedRemoteVirtualScreenshareParticipants,
|
||||
speakersList
|
||||
} = state['features/base/participants'];
|
||||
const { visibleRemoteParticipants } = state['features/filmstrip'];
|
||||
let activeSpeakers = new Map(speakersList);
|
||||
|
||||
// Do not re-sort the active speakers if dominant speaker is currently visible.
|
||||
if (dominantSpeaker && visibleRemoteParticipants.has(dominantSpeaker)) {
|
||||
return activeSpeakers;
|
||||
}
|
||||
let availableSlotsForActiveSpeakers = visibleRemoteParticipants.size;
|
||||
|
||||
if (activeSpeakers.has(dominantSpeaker ?? '')) {
|
||||
activeSpeakers.delete(dominantSpeaker ?? '');
|
||||
}
|
||||
|
||||
// Add dominant speaker to the beginning of the list (not including self) since the active speaker list is always
|
||||
// alphabetically sorted.
|
||||
if (dominantSpeaker && dominantSpeaker !== getLocalParticipant(state)?.id) {
|
||||
const updatedSpeakers = Array.from(activeSpeakers);
|
||||
|
||||
updatedSpeakers.splice(0, 0, [ dominantSpeaker, getParticipantById(state, dominantSpeaker)?.name ?? '' ]);
|
||||
activeSpeakers = new Map(updatedSpeakers);
|
||||
}
|
||||
|
||||
// Remove screenshares from the count.
|
||||
if (sortedRemoteVirtualScreenshareParticipants) {
|
||||
availableSlotsForActiveSpeakers -= sortedRemoteVirtualScreenshareParticipants.size * 2;
|
||||
for (const screenshare of Array.from(sortedRemoteVirtualScreenshareParticipants.keys())) {
|
||||
const ownerId = getVirtualScreenshareParticipantOwnerId(screenshare as string);
|
||||
|
||||
activeSpeakers.delete(ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove fake participants from the count.
|
||||
if (fakeParticipants) {
|
||||
availableSlotsForActiveSpeakers -= fakeParticipants.size;
|
||||
}
|
||||
const truncatedSpeakersList = Array.from(activeSpeakers).slice(0, availableSlotsForActiveSpeakers);
|
||||
|
||||
truncatedSpeakersList.sort((a: any, b: any) => a[1].localeCompare(b[1]));
|
||||
|
||||
return new Map(truncatedSpeakersList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the first loadable avatar URL for a participant.
|
||||
*
|
||||
|
||||
@@ -76,12 +76,12 @@ const DEFAULT_STATE = {
|
||||
numberOfParticipantsNotSupportingE2EE: 0,
|
||||
overwrittenNameList: {},
|
||||
pinnedParticipant: undefined,
|
||||
previousSpeakers: new Set<string>(),
|
||||
raisedHandsQueue: [],
|
||||
remote: new Map(),
|
||||
remoteVideoSources: new Set<string>(),
|
||||
sortedRemoteVirtualScreenshareParticipants: new Map(),
|
||||
sortedRemoteParticipants: new Map(),
|
||||
speakersList: new Map()
|
||||
sortedRemoteParticipants: new Map()
|
||||
};
|
||||
|
||||
export interface IParticipantsState {
|
||||
@@ -94,12 +94,12 @@ export interface IParticipantsState {
|
||||
numberOfParticipantsNotSupportingE2EE: number;
|
||||
overwrittenNameList: { [id: string]: string; };
|
||||
pinnedParticipant?: string;
|
||||
previousSpeakers: Set<string>;
|
||||
raisedHandsQueue: Array<{ hasBeenNotified?: boolean; id: string; raisedHandTimestamp: number; }>;
|
||||
remote: Map<string, IParticipant>;
|
||||
remoteVideoSources: Set<string>;
|
||||
sortedRemoteParticipants: Map<string, string>;
|
||||
sortedRemoteVirtualScreenshareParticipants: Map<string, string>;
|
||||
speakersList: Map<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,22 +156,10 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
||||
const { participant } = action;
|
||||
const { id, previousSpeakers = [] } = participant;
|
||||
const { dominantSpeaker, local } = state;
|
||||
const newSpeakers = [ id, ...previousSpeakers ];
|
||||
const sortedSpeakersList: Array<Array<string>> = [];
|
||||
|
||||
for (const speaker of newSpeakers) {
|
||||
if (speaker !== local?.id) {
|
||||
const remoteParticipant = state.remote.get(speaker);
|
||||
|
||||
remoteParticipant
|
||||
&& sortedSpeakersList.push(
|
||||
[ speaker, _getDisplayName(state, remoteParticipant?.name) ]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the remote speaker list sorted alphabetically.
|
||||
sortedSpeakersList.sort((a, b) => a[1].localeCompare(b[1]));
|
||||
// Build chronologically ordered Set of remote speakers (excluding local)
|
||||
const previousSpeakersSet: Set<string>
|
||||
= new Set(previousSpeakers.filter((speaker: string) => speaker !== local?.id));
|
||||
|
||||
// Only one dominant speaker is allowed.
|
||||
if (dominantSpeaker) {
|
||||
@@ -181,8 +169,8 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
||||
if (_updateParticipantProperty(state, id, 'dominantSpeaker', true)) {
|
||||
return {
|
||||
...state,
|
||||
dominantSpeaker: id, // @ts-ignore
|
||||
speakersList: new Map(sortedSpeakersList)
|
||||
dominantSpeaker: id,
|
||||
previousSpeakers: previousSpeakersSet
|
||||
};
|
||||
}
|
||||
|
||||
@@ -435,7 +423,7 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
||||
}
|
||||
|
||||
// Remove the participant from the list of speakers.
|
||||
state.speakersList.has(id) && state.speakersList.delete(id);
|
||||
state.previousSpeakers.delete(id);
|
||||
|
||||
if (pinnedParticipant === id) {
|
||||
state.pinnedParticipant = undefined;
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
import { IReduxState, IStore } from '../app/types';
|
||||
import {
|
||||
getActiveSpeakersToBeDisplayed,
|
||||
getVirtualScreenshareParticipantOwnerId
|
||||
} from '../base/participants/functions';
|
||||
import { getParticipantById, getVirtualScreenshareParticipantOwnerId } from '../base/participants/functions';
|
||||
|
||||
import { setRemoteParticipants } from './actions';
|
||||
import { isFilmstripScrollVisible } from './functions';
|
||||
|
||||
/**
|
||||
* Returns an array containing the first `count` items from a set.
|
||||
*
|
||||
* @param {Set<T>} set - The set from which to take items.
|
||||
* @param {number} count - The number of items to take.
|
||||
* @returns {T[]} An array containing the taken items.
|
||||
* @private
|
||||
*/
|
||||
function _takeFirstN<T>(set: Set<T>, count: number): T[] {
|
||||
const result: T[] = [];
|
||||
|
||||
for (const item of set) {
|
||||
if (result.length >= count) break;
|
||||
result.push(item);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the reorderd list of the remote participants.
|
||||
*
|
||||
@@ -16,7 +32,7 @@ import { isFilmstripScrollVisible } from './functions';
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
export function updateRemoteParticipants(store: IStore, force?: boolean, participantId?: string) {
|
||||
export function updateRemoteParticipants(store: IStore, force?: boolean, participantId?: string): void {
|
||||
const state = store.getState();
|
||||
let reorderedParticipants = [];
|
||||
const { sortedRemoteVirtualScreenshareParticipants } = state['features/base/participants'];
|
||||
@@ -33,14 +49,26 @@ export function updateRemoteParticipants(store: IStore, force?: boolean, partici
|
||||
}
|
||||
|
||||
const {
|
||||
dominantSpeaker,
|
||||
fakeParticipants,
|
||||
previousSpeakers,
|
||||
sortedRemoteParticipants
|
||||
} = state['features/base/participants'];
|
||||
const { visibleRemoteParticipants } = state['features/filmstrip'];
|
||||
const remoteParticipants = new Map(sortedRemoteParticipants);
|
||||
const screenShareParticipants = sortedRemoteVirtualScreenshareParticipants
|
||||
? [ ...sortedRemoteVirtualScreenshareParticipants.keys() ] : [];
|
||||
const sharedVideos = fakeParticipants ? Array.from(fakeParticipants.keys()) : [];
|
||||
const speakers = getActiveSpeakersToBeDisplayed(state);
|
||||
const speakers: Set<string> = new Set();
|
||||
const dominant = dominantSpeaker ? getParticipantById(state, dominantSpeaker) : undefined;
|
||||
const config = state['features/base/config'];
|
||||
const defaultRemoteDisplayName = config?.defaultRemoteDisplayName ?? 'Fellow Jitster';
|
||||
|
||||
// Generate the remote active speakers list.
|
||||
if (dominant && !dominant.local) {
|
||||
speakers.add(dominant.id);
|
||||
}
|
||||
previousSpeakers.forEach(id => speakers.add(id));
|
||||
|
||||
for (const screenshare of screenShareParticipants) {
|
||||
const ownerId = getVirtualScreenshareParticipantOwnerId(screenshare);
|
||||
@@ -57,7 +85,16 @@ export function updateRemoteParticipants(store: IStore, force?: boolean, partici
|
||||
remoteParticipants.delete(speaker);
|
||||
}
|
||||
|
||||
// Always update the order of the thubmnails.
|
||||
// Calculate the number of slots available for active speakers and then sort them alphabetically to ensure
|
||||
// consistent order.
|
||||
const numberOfActiveSpeakerSlots
|
||||
= visibleRemoteParticipants.size - (screenShareParticipants.length * 2) - sharedVideos.length;
|
||||
const activeSpeakersDisplayed = _takeFirstN(speakers, numberOfActiveSpeakerSlots)
|
||||
.sort((a: string, b: string) => {
|
||||
return (getParticipantById(state, a)?.name ?? defaultRemoteDisplayName)
|
||||
.localeCompare(getParticipantById(state, b)?.name ?? defaultRemoteDisplayName);
|
||||
});
|
||||
|
||||
const participantsWithScreenShare = screenShareParticipants.reduce<string[]>((acc, screenshare) => {
|
||||
const ownerId = getVirtualScreenshareParticipantOwnerId(screenshare);
|
||||
|
||||
@@ -67,10 +104,11 @@ export function updateRemoteParticipants(store: IStore, force?: boolean, partici
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
// Always update the order of the thumbnails.
|
||||
reorderedParticipants = [
|
||||
...participantsWithScreenShare,
|
||||
...sharedVideos,
|
||||
...Array.from(speakers.keys()),
|
||||
...activeSpeakersDisplayed,
|
||||
...Array.from(remoteParticipants.keys())
|
||||
];
|
||||
|
||||
|
||||
@@ -161,18 +161,26 @@ function _electParticipantInLargeVideo(state: IReduxState) {
|
||||
}
|
||||
}
|
||||
|
||||
// Next, pick the dominant speaker (other than self).
|
||||
// Next, pick the dominant speaker or the last active speaker if the dominant speaker is local.
|
||||
participant = getDominantSpeakerParticipant(state);
|
||||
if (participant && !participant.local) {
|
||||
// Return the screensharing participant id associated with this endpoint if multi-stream is enabled and
|
||||
// auto_pin_latest_screen_share setting is disabled.
|
||||
const screenshareParticipant = getVirtualScreenshareParticipantByOwnerId(state, participant.id);
|
||||
let speakerId: string | undefined;
|
||||
|
||||
return screenshareParticipant?.id ?? participant.id;
|
||||
if (participant?.local) {
|
||||
const { previousSpeakers } = state['features/base/participants'];
|
||||
|
||||
if (previousSpeakers?.size) {
|
||||
speakerId = previousSpeakers.keys().next().value;
|
||||
}
|
||||
} else if (participant) {
|
||||
speakerId = participant.id;
|
||||
}
|
||||
|
||||
// In case this is the local participant.
|
||||
participant = undefined;
|
||||
// Return the screensharing participant id associated with this endpoint.
|
||||
if (speakerId) {
|
||||
const screenshareParticipant = getVirtualScreenshareParticipantByOwnerId(state, speakerId);
|
||||
|
||||
return screenshareParticipant?.id ?? speakerId;
|
||||
}
|
||||
|
||||
// Next, pick the most recent participant with video.
|
||||
const lastVisibleRemoteParticipant = _electLastVisibleRemoteParticipant(state);
|
||||
|
||||
@@ -579,6 +579,50 @@ export class Participant {
|
||||
return new LargeVideo(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a specific participant to be displayed on large video.
|
||||
*
|
||||
* @param {string} expectedEndpointId - The endpoint ID of the participant expected on large video.
|
||||
* @param {string} timeoutMsg - Optional custom timeout message.
|
||||
* @param {number} timeout - Optional timeout in milliseconds (default: 30000).
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async waitForParticipantOnLargeVideo(
|
||||
expectedEndpointId: string,
|
||||
timeoutMsg?: string,
|
||||
timeout: number = 30_000): Promise<void> {
|
||||
await this.driver.waitUntil(
|
||||
async () => await this.getLargeVideo().getResource() === expectedEndpointId,
|
||||
{
|
||||
timeout,
|
||||
timeoutMsg: timeoutMsg || `Expected ${expectedEndpointId} on large video for ${this.name}`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for any one of the specified participants to be displayed on large video.
|
||||
*
|
||||
* @param {string[]} expectedEndpointIds - Array of endpoint IDs, any one of which is expected on large video.
|
||||
* @param {string} timeoutMsg - Optional custom timeout message.
|
||||
* @param {number} timeout - Optional timeout in milliseconds (default: 30000).
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async waitForAnyParticipantOnLargeVideo(
|
||||
expectedEndpointIds: string[],
|
||||
timeoutMsg?: string,
|
||||
timeout: number = 30_000): Promise<void> {
|
||||
await this.driver.waitUntil(
|
||||
async () => {
|
||||
const largeVideoResource = await this.getLargeVideo().getResource();
|
||||
|
||||
return expectedEndpointIds.includes(largeVideoResource);
|
||||
},
|
||||
{
|
||||
timeout,
|
||||
timeoutMsg: timeoutMsg || `Expected one of [${expectedEndpointIds.join(', ')}] on large video for ${this.name}`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the videoQuality Dialog.
|
||||
*
|
||||
|
||||
@@ -13,6 +13,16 @@ describe('Active speaker', () => {
|
||||
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
// Exit tile view to enter stage view (default for 3 participants is tile view)
|
||||
await p1.getToolbar().clickExitTileViewButton();
|
||||
await p1.waitForTileViewDisplayed(true);
|
||||
|
||||
await p2.getToolbar().clickExitTileViewButton();
|
||||
await p2.waitForTileViewDisplayed(true);
|
||||
|
||||
await p3.getToolbar().clickExitTileViewButton();
|
||||
await p3.waitForTileViewDisplayed(true);
|
||||
|
||||
await muteAudioAndCheck(p1, p2);
|
||||
await muteAudioAndCheck(p2, p1);
|
||||
await muteAudioAndCheck(p3, p1);
|
||||
@@ -31,6 +41,110 @@ describe('Active speaker', () => {
|
||||
await assertOneDominantSpeaker(p2);
|
||||
await assertOneDominantSpeaker(p3);
|
||||
});
|
||||
|
||||
it('testLocalDominantSpeakerShowsLastRemote', async () => {
|
||||
await ensureThreeParticipants();
|
||||
|
||||
const { p1, p2, p3 } = ctx;
|
||||
const p2EndpointId = await p2.getEndpointId();
|
||||
const p3EndpointId = await p3.getEndpointId();
|
||||
|
||||
// p2 speaks first
|
||||
await p2.getToolbar().clickAudioUnmuteButton();
|
||||
await p1.waitForParticipantOnLargeVideo(p2EndpointId, 'P2 not displayed as dominant speaker on P1');
|
||||
await p2.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// p3 speaks second
|
||||
await p3.getToolbar().clickAudioUnmuteButton();
|
||||
await p1.waitForParticipantOnLargeVideo(p3EndpointId, 'P3 not displayed as dominant speaker on P1');
|
||||
await p3.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// Now p1 starts screenshare and becomes dominant speaker
|
||||
await p1.getToolbar().clickDesktopSharingButton();
|
||||
|
||||
// p1 should see p3 (last remote dominant speaker) on stage
|
||||
await p1.getToolbar().clickAudioUnmuteButton();
|
||||
await p1.waitForParticipantOnLargeVideo(p3EndpointId, 'P1 should see P3 (last remote speaker) on stage when local is dominant');
|
||||
|
||||
await p1.getToolbar().clickAudioMuteButton();
|
||||
await p1.getToolbar().clickStopDesktopSharingButton();
|
||||
});
|
||||
|
||||
it('testAlternatingLocalAndRemoteDominantSpeaker', async () => {
|
||||
await ensureThreeParticipants();
|
||||
|
||||
const { p1, p2, p3 } = ctx;
|
||||
const p2EndpointId = await p2.getEndpointId();
|
||||
const p3EndpointId = await p3.getEndpointId();
|
||||
|
||||
// Test alternating pattern: p2 -> p1 (local) -> p3 -> p1 (local) -> p2
|
||||
// This tests that local dominant speaker consistently shows last remote speaker
|
||||
|
||||
// p2 speaks
|
||||
await p2.getToolbar().clickAudioUnmuteButton();
|
||||
await p1.waitForParticipantOnLargeVideo(p2EndpointId, 'P2 not shown on P1');
|
||||
await p2.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// p1 (local) speaks - should still show p2
|
||||
await p1.getToolbar().clickAudioUnmuteButton();
|
||||
await p1.waitForParticipantOnLargeVideo(p2EndpointId, 'P1 local dominant should show P2 (last remote)');
|
||||
await p1.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// p3 speaks
|
||||
await p3.getToolbar().clickAudioUnmuteButton();
|
||||
await p1.waitForParticipantOnLargeVideo(p3EndpointId, 'P3 not shown on P1');
|
||||
await p3.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// p1 (local) speaks again - should now show p3
|
||||
await p1.getToolbar().clickAudioUnmuteButton();
|
||||
await p1.waitForParticipantOnLargeVideo(p3EndpointId, 'P1 local dominant should show P3 (last remote)');
|
||||
await p1.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// p2 speaks again
|
||||
await p2.getToolbar().clickAudioUnmuteButton();
|
||||
await p1.waitForParticipantOnLargeVideo(p2EndpointId, 'P2 not shown on P1 after second speak');
|
||||
await p2.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// p1 (local) speaks - should show p2 again
|
||||
await p1.getToolbar().clickAudioUnmuteButton();
|
||||
await p1.waitForParticipantOnLargeVideo(p2EndpointId, 'P1 local dominant should show P2 (last remote) again');
|
||||
await p1.getToolbar().clickAudioMuteButton();
|
||||
});
|
||||
|
||||
it('testDominantSpeakerWithSimultaneousSpeakers', async () => {
|
||||
await ensureThreeParticipants();
|
||||
|
||||
const { p1, p2, p3 } = ctx;
|
||||
const p2EndpointId = await p2.getEndpointId();
|
||||
const p3EndpointId = await p3.getEndpointId();
|
||||
|
||||
// Multiple participants unmute simultaneously (this is the scenario we're testing)
|
||||
await p2.getToolbar().clickAudioUnmuteButton();
|
||||
await p3.getToolbar().clickAudioUnmuteButton();
|
||||
|
||||
// Wait for one to become dominant (bridge will pick one)
|
||||
await p1.waitForAnyParticipantOnLargeVideo(
|
||||
[ p2EndpointId, p3EndpointId ],
|
||||
'No dominant speaker detected when multiple speak simultaneously');
|
||||
|
||||
// Get which one was selected as dominant
|
||||
const firstDominant = await p1.getLargeVideo().getResource();
|
||||
|
||||
// Mute both
|
||||
await Promise.all([
|
||||
p2.getToolbar().clickAudioMuteButton(),
|
||||
p3.getToolbar().clickAudioMuteButton()
|
||||
]);
|
||||
|
||||
// Now p1 becomes local dominant speaker
|
||||
// Should show the last remote dominant speaker (firstDominant)
|
||||
await p1.getToolbar().clickAudioUnmuteButton();
|
||||
await p1.waitForParticipantOnLargeVideo(
|
||||
firstDominant,
|
||||
'Local dominant should show last remote dominant after simultaneous speakers');
|
||||
|
||||
await p1.getToolbar().clickAudioMuteButton();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -63,14 +177,12 @@ async function testActiveSpeaker(
|
||||
await activeSpeaker.getFilmstrip().assertAudioMuteIconIsDisplayed(activeSpeaker, true);
|
||||
|
||||
// Verify that the user is now an active speaker from otherParticipant1's perspective
|
||||
const otherParticipant1Driver = otherParticipant1.driver;
|
||||
|
||||
await otherParticipant1Driver.waitUntil(
|
||||
async () => await otherParticipant1.getFilmstrip().isDominantSpeaker(speakerEndpoint),
|
||||
{
|
||||
timeout: 30_000, // 30 seconds
|
||||
timeoutMsg: `${activeSpeaker.name} is not selected as active speaker.`
|
||||
});
|
||||
await otherParticipant1.waitForParticipantOnLargeVideo(speakerEndpoint,
|
||||
`Active speaker ${activeSpeaker.name} not displayed on large video.`,
|
||||
30_000); // 30 seconds
|
||||
await otherParticipant2.waitForParticipantOnLargeVideo(speakerEndpoint,
|
||||
`Active speaker ${activeSpeaker.name} not displayed on large video.`,
|
||||
30_000); // 30 seconds
|
||||
|
||||
// just a debug print to go in logs
|
||||
activeSpeaker.log('Muting in testActiveSpeaker');
|
||||
|
||||
@@ -175,11 +175,6 @@ describe('Start muted', () => {
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
|
||||
|
||||
await Promise.all([
|
||||
p1.getLargeVideo().waitForSwitchTo(await p2.getEndpointId()),
|
||||
p2.getLargeVideo().waitForSwitchTo(await p1.getEndpointId())
|
||||
]);
|
||||
|
||||
await unmuteVideoAndCheck(p2, p1);
|
||||
await p1.getLargeVideo().assertPlaying();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user