diff --git a/docs/samples/browser-plugin-meetings/app.js b/docs/samples/browser-plugin-meetings/app.js
index e0aa78e1158..b3d3a9c29f2 100644
--- a/docs/samples/browser-plugin-meetings/app.js
+++ b/docs/samples/browser-plugin-meetings/app.js
@@ -38,7 +38,7 @@ const breakoutsList = document.getElementById('breakouts-list');
const breakoutTable = document.getElementById('breakout-table');
const breakoutHostOperation = document.getElementById('breakout-host-operation');
const getStatsButton = document.getElementById('get-stats');
-const tcpReachabilityConfigElm = document.getElementById('enable-tcp-reachability');
+const tcpReachabilityConfigElm = document.getElementById('enable-tcp-reachability');
const tlsReachabilityConfigElm = document.getElementById('enable-tls-reachability');
const guestName = document.querySelector('#guest-name');
@@ -388,7 +388,7 @@ createMeetingSelectElm.addEventListener('change', (event) => {
}
else {
notes.classList.add('hidden');
-
+
}
});
@@ -950,7 +950,7 @@ function cleanUpMedia() {
elem.srcObject.getTracks().forEach((track) => track.stop());
// eslint-disable-next-line no-param-reassign
elem.srcObject = null;
-
+
if(elem.id === "local-video") {
clearVideoResolutionCheckInterval(localVideoResElm, localVideoResolutionInterval);
}
@@ -1566,7 +1566,7 @@ async function stopStartVideo() {
console.error(error);
}
}
-
+
}
async function stopStartAudio() {
@@ -1608,7 +1608,7 @@ async function stopStartAudio() {
console.error(error);
}
}
-
+
}
function populateSourceDevices(mediaDevice) {
@@ -3052,6 +3052,11 @@ function moveFromDevice() {
});
}
+function isUserSelf(member) {
+ const meeting = getCurrentMeeting();
+ return meeting.selfId === member.id
+}
+
function claimPersonalMeetingRoom() {
console.log('DevicesControls#claimPersonalMeetingRoom()');
@@ -3094,7 +3099,7 @@ participantTable.addEventListener('click', (event) => {
}
const muteButton = document.getElementById('mute-participant-btn')
if (selectedParticipant.isAudioMuted) {
- muteButton.innerText = meeting.selfId === selectedParticipant.id ? 'Unmute' : 'Request to unmute';
+ muteButton.innerText = isUserSelf(selectedParticipant) ? 'Unmute' : 'Request to unmute';
} else {
muteButton.innerText = 'Mute';
}
@@ -3330,6 +3335,25 @@ function toggleBreakout() {
}
}
+async function toggleBrb() {
+ const meeting = getCurrentMeeting();
+
+ if (meeting) {
+ const brbButton = document.getElementById('brb-btn');
+ const isBrbEnabled = brbButton.innerText === 'Step away';
+
+ try {
+ const result = await meeting.beRightBack(isBrbEnabled);
+ console.log(`meeting.beRightBack(${isBrbEnabled}): success. Result: ${result}`);
+ } catch (error) {
+ console.error(`meeting.beRightBack({${isBrbEnabled}): error: `, error);
+ } finally {
+ localMedia?.microphoneStream?.setUserMuted(isBrbEnabled);
+ localMedia?.cameraStream?.setUserMuted(isBrbEnabled);
+ }
+ }
+}
+
const createAdmitDiv = () => {
const containerDiv = document.createElement('div');
@@ -3712,18 +3736,21 @@ function createMembersTable(members) {
const th3 = document.createElement('th');
const th4 = document.createElement('th');
const th5 = document.createElement('th');
+ const th6 = document.createElement('th');
th1.innerText = 'NAME';
th2.innerText = 'VIDEO';
th3.innerText = 'AUDIO';
th4.innerText = 'STATUS';
th5.innerText = 'SUPPORTS BREAKOUTS';
+ th6.innerText = 'AWAY';
tr.appendChild(th1);
tr.appendChild(th2);
tr.appendChild(th3);
tr.appendChild(th4);
tr.appendChild(th5);
+ tr.appendChild(th6);
return tr;
}
@@ -3735,6 +3762,7 @@ function createMembersTable(members) {
const td3 = document.createElement('td');
const td4 = document.createElement('td');
const td5 = document.createElement('td');
+ const td6 = document.createElement('td');
const label1 = createLabel(member.id);
const label2 = createLabel(member.id, member.isVideoMuted ? 'NO' : 'YES');
const label3 = createLabel(member.id, member.isAudioMuted ? 'NO' : 'YES');
@@ -3763,11 +3791,19 @@ function createMembersTable(members) {
td5.appendChild(label5);
+
+ if (isUserSelf(member) && member.isInMeeting) {
+ td6.appendChild(createButton(member.isBrb ? 'Back to meeting' : 'Step away', toggleBrb, {id: 'brb-btn'}));
+ } else {
+ td6.appendChild(createLabel(member.id, member.isBrb ? 'YES' : 'NO'));
+ }
+
tr.appendChild(td1);
tr.appendChild(td2);
tr.appendChild(td3);
tr.appendChild(td4);
tr.appendChild(td5);
+ tr.appendChild(td6);
return tr;
}
@@ -3778,7 +3814,7 @@ function createMembersTable(members) {
thead.appendChild(createHeadRow());
- Object.entries(members).forEach(([key, value]) => {
+ Object.entries(members).forEach(([_, value]) => {
if (value.status !== 'NOT_IN_MEETING') {
const row = createRow(value);
diff --git a/docs/samples/browser-plugin-meetings/index.html b/docs/samples/browser-plugin-meetings/index.html
index a994bf11ed5..ed4e4fd341e 100644
--- a/docs/samples/browser-plugin-meetings/index.html
+++ b/docs/samples/browser-plugin-meetings/index.html
@@ -216,7 +216,6 @@
-
diff --git a/docs/samples/browser-plugin-meetings/style.css b/docs/samples/browser-plugin-meetings/style.css
index adb172f8cb3..8246de3e5ea 100644
--- a/docs/samples/browser-plugin-meetings/style.css
+++ b/docs/samples/browser-plugin-meetings/style.css
@@ -54,7 +54,7 @@ button.btn-code {
}
.box {
- max-width: 70rem;
+ max-width: 70rem;
margin-inline: auto;
margin-bottom: 1rem;
}
@@ -93,7 +93,7 @@ button.btn-code {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
/* border: 10px solid black */
-}
+}
.video-section {
display: flex;
@@ -445,4 +445,4 @@ legend {
background-color:lightgrey;
border-radius:5px;
padding: 0 2px;
-}
\ No newline at end of file
+}
diff --git a/packages/@webex/plugin-meetings/src/constants.ts b/packages/@webex/plugin-meetings/src/constants.ts
index 5d5b493835e..757d0a99687 100644
--- a/packages/@webex/plugin-meetings/src/constants.ts
+++ b/packages/@webex/plugin-meetings/src/constants.ts
@@ -303,6 +303,7 @@ export const EVENT_TRIGGERS = {
MEETING_SELF_CANNOT_VIEW_PARTICIPANT_LIST: 'meeting:self:cannotViewParticipantList',
MEETING_SELF_IS_SHARING_BLOCKED: 'meeting:self:isSharingBlocked',
MEETING_SELF_ROLES_CHANGED: 'meeting:self:rolesChanged',
+ MEETING_SELF_BRB_UPDATE: 'meeting:self:brbUpdate',
MEETING_CONTROLS_LAYOUT_UPDATE: 'meeting:layout:update',
MEETING_ENTRY_EXIT_TONE_UPDATE: 'meeting:entryExitTone:update',
MEETING_BREAKOUTS_UPDATE: 'meeting:breakouts:update',
@@ -709,6 +710,7 @@ export const LOCUSINFO = {
SELF_IS_SHARING_BLOCKED_CHANGE: 'SELF_IS_SHARING_BLOCKED_CHANGE',
SELF_MEETING_BREAKOUTS_CHANGED: 'SELF_MEETING_BREAKOUTS_CHANGED',
SELF_MEETING_INTERPRETATION_CHANGED: 'SELF_MEETING_INTERPRETATION_CHANGED',
+ SELF_MEETING_BRB_CHANGED: 'SELF_MEETING_BRB_CHANGED',
MEDIA_INACTIVITY: 'MEDIA_INACTIVITY',
LINKS_SERVICES: 'LINKS_SERVICES',
LINKS_RESOURCES: 'LINKS_RESOURCES',
diff --git a/packages/@webex/plugin-meetings/src/locus-info/index.ts b/packages/@webex/plugin-meetings/src/locus-info/index.ts
index 93df547fb20..0ebd24dfa8c 100644
--- a/packages/@webex/plugin-meetings/src/locus-info/index.ts
+++ b/packages/@webex/plugin-meetings/src/locus-info/index.ts
@@ -1393,6 +1393,19 @@ export default class LocusInfo extends EventsScope {
);
}
+ if (parsedSelves.updates.brbChanged) {
+ this.emitScoped(
+ {
+ file: 'locus-info',
+ function: 'updateSelf',
+ },
+ LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED,
+ {
+ brb: parsedSelves.current.brb,
+ }
+ );
+ }
+
if (parsedSelves.updates.interpretationChanged) {
this.emitScoped(
{
diff --git a/packages/@webex/plugin-meetings/src/locus-info/selfUtils.ts b/packages/@webex/plugin-meetings/src/locus-info/selfUtils.ts
index 0ba3e4d8178..d27f32e47d5 100644
--- a/packages/@webex/plugin-meetings/src/locus-info/selfUtils.ts
+++ b/packages/@webex/plugin-meetings/src/locus-info/selfUtils.ts
@@ -66,6 +66,7 @@ SelfUtils.parse = (self: any, deviceId: string) => {
breakoutSessions: SelfUtils.getBreakoutSessions(self),
breakout: SelfUtils.getBreakout(self),
interpretation: SelfUtils.getInterpretation(self),
+ brb: SelfUtils.getBrb(self),
};
}
@@ -75,6 +76,7 @@ SelfUtils.parse = (self: any, deviceId: string) => {
SelfUtils.getBreakoutSessions = (self) => self?.controls?.breakout?.sessions;
SelfUtils.getBreakout = (self) => self?.controls?.breakout;
SelfUtils.getInterpretation = (self) => self?.controls?.interpretation;
+SelfUtils.getBrb = (self) => self?.controls?.brb;
SelfUtils.getLayout = (self) =>
Array.isArray(self?.controls?.layouts) ? self.controls.layouts[0].type : undefined;
@@ -128,6 +130,7 @@ SelfUtils.getSelves = (oldSelf, newSelf, deviceId) => {
updates.isSharingBlockedChanged = previous?.isSharingBlocked !== current.isSharingBlocked;
updates.breakoutsChanged = SelfUtils.breakoutsChanged(previous, current);
updates.interpretationChanged = SelfUtils.interpretationChanged(previous, current);
+ updates.brbChanged = SelfUtils.brbChanged(previous, current);
return {
previous,
@@ -159,6 +162,9 @@ SelfUtils.breakoutsChanged = (previous, current) =>
SelfUtils.interpretationChanged = (previous, current) =>
!isEqual(previous?.interpretation, current?.interpretation) && !!current?.interpretation;
+SelfUtils.brbChanged = (previous, current) =>
+ !isEqual(previous?.brb, current?.brb) && current?.brb !== undefined;
+
SelfUtils.isMediaInactive = (previous, current) => {
if (
previous &&
diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts
index f5311e002e3..59f76657951 100644
--- a/packages/@webex/plugin-meetings/src/meeting/index.ts
+++ b/packages/@webex/plugin-meetings/src/meeting/index.ts
@@ -3362,6 +3362,20 @@ export default class Meeting extends StatelessWebexPlugin {
}
});
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED, (payload) => {
+ Trigger.trigger(
+ this,
+ {
+ file: 'meeting/index',
+ function: 'setUpLocusInfoSelfListener',
+ },
+ EVENT_TRIGGERS.MEETING_SELF_BRB_UPDATE,
+ {
+ payload,
+ }
+ );
+ });
+
this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ROLES_CHANGED, (payload) => {
const isModeratorOrCohost =
payload.newRoles?.includes(SELF_ROLES.MODERATOR) ||
@@ -3565,6 +3579,50 @@ export default class Meeting extends StatelessWebexPlugin {
return this.members.admitMembers(memberIds, locusUrls);
}
+ /**
+ * Manages be right back status updates for the current participant.
+ *
+ * @param {boolean} enabled - Indicates whether the user enabled brb or not.
+ * @returns {Promise} resolves when the brb status is updated or does nothing if not in a multistream meeting.
+ * @throws {Error} - Throws an error if the request fails.
+ */
+ public async beRightBack(enabled: boolean): Promise {
+ if (!this.isMultistream) {
+ const errorMessage = 'Meeting:index#beRightBack --> Not a multistream meeting';
+ const error = new Error(errorMessage);
+
+ LoggerProxy.logger.error(error);
+
+ return Promise.reject(error);
+ }
+
+ if (!this.mediaProperties.webrtcMediaConnection) {
+ const errorMessage = 'Meeting:index#beRightBack --> WebRTC media connection is not defined';
+ const error = new Error(errorMessage);
+
+ LoggerProxy.logger.error(error);
+
+ return Promise.reject(error);
+ }
+
+ // this logic should be applied only to multistream meetings
+ return this.meetingRequest
+ .sendBrb({
+ enabled,
+ locusUrl: this.locusUrl,
+ deviceUrl: this.deviceUrl,
+ selfId: this.selfId,
+ })
+ .then(() => {
+ this.sendSlotManager.setSourceStateOverride(MediaType.VideoMain, enabled ? 'away' : null);
+ })
+ .catch((error) => {
+ LoggerProxy.logger.error('Meeting:index#beRightBack --> Error ', error);
+
+ return Promise.reject(error);
+ });
+ }
+
/**
* Remove the member from the meeting, boot them
* @param {String} memberId
diff --git a/packages/@webex/plugin-meetings/src/meeting/request.ts b/packages/@webex/plugin-meetings/src/meeting/request.ts
index c8b34564d65..1e907f9b992 100644
--- a/packages/@webex/plugin-meetings/src/meeting/request.ts
+++ b/packages/@webex/plugin-meetings/src/meeting/request.ts
@@ -27,7 +27,7 @@ import {
_SLIDES_,
ANNOTATION,
} from '../constants';
-import {SendReactionOptions, ToggleReactionsOptions} from './request.type';
+import {SendReactionOptions, BrbOptions, ToggleReactionsOptions} from './request.type';
import MeetingUtil from './util';
import {AnnotationInfo} from '../annotation/annotation.types';
import {ClientMediaPreferences} from '../reachability/reachability.types';
@@ -909,4 +909,29 @@ export default class MeetingRequest extends StatelessWebexPlugin {
uri: locusUrl,
});
}
+
+ /**
+ * Sends a request to set be right back status.
+ *
+ * @param {Object} options - The options for brb request.
+ * @param {boolean} options.enabled - Whether brb status is enabled.
+ * @param {string} options.locusUrl - The URL of the locus.
+ * @param {string} options.deviceUrl - The URL of the device.
+ * @param {string} options.selfId - The ID of the participant.
+ * @returns {Promise}
+ */
+ sendBrb({enabled, locusUrl, deviceUrl, selfId}: BrbOptions) {
+ const uri = `${locusUrl}/${PARTICIPANT}/${selfId}/${CONTROLS}`;
+
+ return this.locusDeltaRequest({
+ method: HTTP_VERBS.PATCH,
+ uri,
+ body: {
+ brb: {
+ enabled,
+ deviceUrl,
+ },
+ },
+ });
+ }
}
diff --git a/packages/@webex/plugin-meetings/src/meeting/request.type.ts b/packages/@webex/plugin-meetings/src/meeting/request.type.ts
index bc191f97499..f6bee002cbb 100644
--- a/packages/@webex/plugin-meetings/src/meeting/request.type.ts
+++ b/packages/@webex/plugin-meetings/src/meeting/request.type.ts
@@ -11,3 +11,10 @@ export type ToggleReactionsOptions = {
locusUrl: string;
requestingParticipantId: string;
};
+
+export type BrbOptions = {
+ enabled: boolean;
+ locusUrl: string;
+ deviceUrl: string;
+ selfId: string;
+};
diff --git a/packages/@webex/plugin-meetings/src/member/index.ts b/packages/@webex/plugin-meetings/src/member/index.ts
index bff33659887..78bf2595cea 100644
--- a/packages/@webex/plugin-meetings/src/member/index.ts
+++ b/packages/@webex/plugin-meetings/src/member/index.ts
@@ -28,6 +28,7 @@ export default class Member {
isRecording: any;
isRemovable: any;
isSelf: any;
+ isBrb: boolean;
isUser: any;
isVideoMuted: any;
roles: IExternalRoles;
@@ -227,6 +228,13 @@ export default class Member {
* @memberof Member
*/
this.isRemovable = null;
+ /**
+ * @instance
+ * @type {Boolean}
+ * @public
+ * @memberof Member
+ */
+ this.isBrb = false;
/**
* @instance
* @type {String}
@@ -295,6 +303,7 @@ export default class Member {
this.supportsInterpretation = MemberUtil.isInterpretationSupported(participant);
this.supportLiveAnnotation = MemberUtil.isLiveAnnotationSupported(participant);
this.isGuest = MemberUtil.isGuest(participant);
+ this.isBrb = MemberUtil.isBrb(participant);
this.isUser = MemberUtil.isUser(participant);
this.isDevice = MemberUtil.isDevice(participant);
this.isModerator = MemberUtil.isModerator(participant);
diff --git a/packages/@webex/plugin-meetings/src/member/types.ts b/packages/@webex/plugin-meetings/src/member/types.ts
index 8889d625e08..591c2d74881 100644
--- a/packages/@webex/plugin-meetings/src/member/types.ts
+++ b/packages/@webex/plugin-meetings/src/member/types.ts
@@ -23,6 +23,14 @@ export type ParticipantWithRoles = {
};
};
+export type ParticipantWithBrb = {
+ controls?: {
+ brb?: {
+ enabled: boolean;
+ };
+ };
+};
+
// values are inherited from locus so don't update these
export enum MediaStatus {
RECVONLY = 'RECVONLY', // participant only receiving and not sending
diff --git a/packages/@webex/plugin-meetings/src/member/util.ts b/packages/@webex/plugin-meetings/src/member/util.ts
index b9b3e74ad5e..b53c5d204ca 100644
--- a/packages/@webex/plugin-meetings/src/member/util.ts
+++ b/packages/@webex/plugin-meetings/src/member/util.ts
@@ -4,6 +4,7 @@ import {
ServerRoles,
ServerRoleShape,
IMediaStatus,
+ ParticipantWithBrb,
} from './types';
import {
_USER_,
@@ -29,7 +30,7 @@ import ParameterError from '../common/errors/parameter';
const MemberUtil: any = {};
/**
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @returns {Boolean}
*/
MemberUtil.canReclaimHost = (participant) => {
@@ -43,14 +44,23 @@ MemberUtil.canReclaimHost = (participant) => {
};
/**
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @returns {[ServerRoleShape]}
*/
MemberUtil.getControlsRoles = (participant: ParticipantWithRoles): Array =>
participant?.controls?.role?.roles;
/**
- * @param {Object} participant the locus participant
+ * Checks if the participant has the brb status enabled.
+ *
+ * @param {ParticipantWithBrb} participant - The locus participant object.
+ * @returns {boolean} - True if the participant has brb enabled, false otherwise.
+ */
+MemberUtil.isBrb = (participant: ParticipantWithBrb): boolean =>
+ participant.controls?.brb?.enabled || false;
+
+/**
+ * @param {Object} participant - The locus participant object.
* @param {ServerRoles} controlRole the search role
* @returns {Boolean}
*/
@@ -60,28 +70,28 @@ MemberUtil.hasRole = (participant: any, controlRole: ServerRoles): boolean =>
);
/**
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @returns {Boolean}
*/
MemberUtil.hasCohost = (participant: ParticipantWithRoles): boolean =>
MemberUtil.hasRole(participant, ServerRoles.Cohost) || false;
/**
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @returns {Boolean}
*/
MemberUtil.hasModerator = (participant: ParticipantWithRoles): boolean =>
MemberUtil.hasRole(participant, ServerRoles.Moderator) || false;
/**
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @returns {Boolean}
*/
MemberUtil.hasPresenter = (participant: ParticipantWithRoles): boolean =>
MemberUtil.hasRole(participant, ServerRoles.Presenter) || false;
/**
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @returns {IExternalRoles}
*/
MemberUtil.extractControlRoles = (participant: ParticipantWithRoles): IExternalRoles => {
@@ -95,7 +105,7 @@ MemberUtil.extractControlRoles = (participant: ParticipantWithRoles): IExternalR
};
/**
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @returns {Boolean}
*/
MemberUtil.isUser = (participant: any) => participant && participant.type === _USER_;
@@ -103,13 +113,13 @@ MemberUtil.isUser = (participant: any) => participant && participant.type === _U
MemberUtil.isModerator = (participant) => participant && participant.moderator;
/**
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @returns {Boolean}
*/
MemberUtil.isGuest = (participant: any) => participant && participant.guest;
/**
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @returns {Boolean}
*/
MemberUtil.isDevice = (participant: any) => participant && participant.type === _RESOURCE_ROOM_;
@@ -120,7 +130,7 @@ MemberUtil.isModeratorAssignmentProhibited = (participant) =>
/**
* checks to see if the participant id is the same as the passed id
* there are multiple ids that can be used
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @param {String} id
* @returns {Boolean}
*/
@@ -130,7 +140,7 @@ MemberUtil.isSame = (participant: any, id: string) =>
/**
* checks to see if the participant id is the same as the passed id for associated devices
* there are multiple ids that can be used
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @param {String} id
* @returns {Boolean}
*/
@@ -142,7 +152,7 @@ MemberUtil.isAssociatedSame = (participant: any, id: string) =>
);
/**
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @param {Boolean} isGuest
* @param {String} status
* @returns {Boolean}
@@ -161,7 +171,7 @@ MemberUtil.isNotAdmitted = (participant: any, isGuest: boolean, status: string):
!status === _IN_MEETING_);
/**
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @returns {Boolean}
*/
MemberUtil.isAudioMuted = (participant: any) => {
@@ -173,7 +183,7 @@ MemberUtil.isAudioMuted = (participant: any) => {
};
/**
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @returns {Boolean}
*/
MemberUtil.isVideoMuted = (participant: any): boolean => {
@@ -185,7 +195,7 @@ MemberUtil.isVideoMuted = (participant: any): boolean => {
};
/**
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @returns {Boolean}
*/
MemberUtil.isHandRaised = (participant: any) => {
@@ -197,7 +207,7 @@ MemberUtil.isHandRaised = (participant: any) => {
};
/**
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @returns {Boolean}
*/
MemberUtil.isBreakoutsSupported = (participant) => {
@@ -209,7 +219,7 @@ MemberUtil.isBreakoutsSupported = (participant) => {
};
/**
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @returns {Boolean}
*/
MemberUtil.isInterpretationSupported = (participant) => {
@@ -223,7 +233,7 @@ MemberUtil.isInterpretationSupported = (participant) => {
};
/**
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @returns {Boolean}
*/
MemberUtil.isLiveAnnotationSupported = (participant) => {
@@ -279,7 +289,7 @@ MemberUtil.getRecordingMember = (controls: any) => {
};
/**
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @returns {Boolean}
*/
MemberUtil.isRecording = (participant: any) => {
@@ -325,7 +335,7 @@ MemberUtil.isMutable = (isSelf, isDevice, isInMeeting, isMuted, type) => {
};
/**
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @returns {String}
*/
MemberUtil.extractStatus = (participant: any) => {
@@ -355,7 +365,7 @@ MemberUtil.extractStatus = (participant: any) => {
};
/**
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @returns {String}
*/
MemberUtil.extractId = (participant: any) => {
@@ -368,7 +378,7 @@ MemberUtil.extractId = (participant: any) => {
/**
* extracts the media status from nested participant object
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @returns {Object}
*/
MemberUtil.extractMediaStatus = (participant: any): IMediaStatus => {
@@ -383,7 +393,7 @@ MemberUtil.extractMediaStatus = (participant: any): IMediaStatus => {
};
/**
- * @param {Object} participant the locus participant
+ * @param {Object} participant - The locus participant object.
* @returns {String}
*/
MemberUtil.extractName = (participant: any) => {
diff --git a/packages/@webex/plugin-meetings/src/multistream/sendSlotManager.ts b/packages/@webex/plugin-meetings/src/multistream/sendSlotManager.ts
index 2ed5eacf941..8bc0e3ee7e7 100644
--- a/packages/@webex/plugin-meetings/src/multistream/sendSlotManager.ts
+++ b/packages/@webex/plugin-meetings/src/multistream/sendSlotManager.ts
@@ -4,6 +4,7 @@ import {
LocalStream,
MultistreamRoapMediaConnection,
NamedMediaGroup,
+ StreamState,
} from '@webex/internal-media-core';
export default class SendSlotManager {
@@ -83,6 +84,36 @@ export default class SendSlotManager {
);
}
+ /**
+ * Sets the source state override for the given media type.
+ * @param {MediaType} mediaType - The type of media (must be MediaType.VideoMain to apply source state changes).
+ * @param {StreamState | null} state - The state to set or null to clear the override value.
+ * @returns {void}
+ */
+ public setSourceStateOverride(mediaType: MediaType, state: StreamState | null) {
+ if (mediaType !== MediaType.VideoMain) {
+ throw new Error(
+ `sendSlotManager cannot set source state override which media type is ${mediaType}`
+ );
+ }
+
+ const slot = this.slots.get(mediaType);
+
+ if (!slot) {
+ throw new Error(`Slot for ${mediaType} does not exist`);
+ }
+
+ if (state) {
+ slot.setSourceStateOverride(state);
+ } else {
+ slot.clearSourceStateOverride();
+ }
+
+ this.LoggerProxy.logger.info(
+ `SendSlotsManager->setSourceStateOverride#set source state override for ${mediaType} to ${state}`
+ );
+ }
+
/**
* This method publishes the given stream to the sendSlot for the given mediaType
* @param {MediaType} mediaType MediaType of the sendSlot to which a stream needs to be published (AUDIO_MAIN/VIDEO_MAIN/AUDIO_SLIDES/VIDEO_SLIDES)
diff --git a/packages/@webex/plugin-meetings/test/unit/spec/locus-info/index.js b/packages/@webex/plugin-meetings/test/unit/spec/locus-info/index.js
index e9c012ba2b4..4b34ae51d30 100644
--- a/packages/@webex/plugin-meetings/test/unit/spec/locus-info/index.js
+++ b/packages/@webex/plugin-meetings/test/unit/spec/locus-info/index.js
@@ -793,6 +793,75 @@ describe('plugin-meetings', () => {
});
describe('#updateSelf', () => {
+ it('should trigger SELF_MEETING_BRB_CHANGED when brb state changed', () => {
+ locusInfo.self = undefined;
+
+ const assertBrb = (enabled) => {
+ const selfWithBrbChanged = cloneDeep(self);
+ selfWithBrbChanged.controls.brb = enabled;
+
+ locusInfo.emitScoped = sinon.stub();
+ locusInfo.updateSelf(selfWithBrbChanged, []);
+
+ assert.calledWith(
+ locusInfo.emitScoped,
+ {file: 'locus-info', function: 'updateSelf'},
+ LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED,
+ {brb: enabled}
+ );
+ };
+
+ assertBrb(true);
+ assertBrb(false);
+ });
+
+ it('should not trigger SELF_MEETING_BRB_CHANGED when brb state did not change', () => {
+ const assertBrbUnchanged = (value) => {
+ locusInfo.self = undefined;
+
+ const selfWithBrbChanged = cloneDeep(self);
+ selfWithBrbChanged.controls.brb = value;
+ locusInfo.self = selfWithBrbChanged;
+
+ locusInfo.emitScoped = sinon.stub();
+
+ const newSelf = cloneDeep(self);
+ newSelf.controls.brb = value;
+
+ locusInfo.updateSelf(newSelf, []);
+
+ assert.neverCalledWith(
+ locusInfo.emitScoped,
+ {file: 'locus-info', function: 'updateSelf'},
+ LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED,
+ {brb: value}
+ );
+ };
+
+ assertBrbUnchanged(true);
+ assertBrbUnchanged(false);
+ });
+
+ it('should not trigger SELF_MEETING_BRB_CHANGED when brb state is undefined', () => {
+ const selfWithBrbChanged = cloneDeep(self);
+ selfWithBrbChanged.controls.brb = false;
+ locusInfo.self = selfWithBrbChanged;
+
+ locusInfo.emitScoped = sinon.stub();
+
+ const newSelf = cloneDeep(self);
+ newSelf.controls.brb = undefined;
+
+ locusInfo.updateSelf(newSelf, []);
+
+ assert.neverCalledWith(
+ locusInfo.emitScoped,
+ {file: 'locus-info', function: 'updateSelf'},
+ LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED,
+ {brb: undefined}
+ );
+ });
+
it('should trigger CONTROLS_MEETING_LAYOUT_UPDATED when the meeting layout controls change', () => {
const layoutType = 'EXAMPLE TYPE';
diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js
index 71a930a9253..8c933d8050b 100644
--- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js
+++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js
@@ -32,8 +32,8 @@ import {
NETWORK_STATUS,
ONLINE,
OFFLINE,
- ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT,
-} from '@webex/plugin-meetings/src/constants';
+ ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT, HTTP_VERBS, PARTICIPANT, CONTROLS,
+} from "@webex/plugin-meetings/src/constants";
import {
ConnectionState,
MediaConnectionEventNames,
@@ -3701,6 +3701,80 @@ describe('plugin-meetings', () => {
});
});
+ describe(`#beRightBack`, () => {
+ const fakeMultistreamRoapMediaConnection = {
+ createSendSlot: sinon.stub().returns({
+ setSourceStateOverride: sinon.stub().resolves(),
+ clearSourceStateOverride: sinon.stub().resolves(),
+ }),
+ };
+
+ beforeEach(() => {
+ meeting.meetingRequest.sendBrb = sinon.stub().resolves({body: 'test'});
+ meeting.mediaProperties.webrtcMediaConnection = {createSendSlot: sinon.stub()};
+ meeting.sendSlotManager.createSlot(
+ fakeMultistreamRoapMediaConnection,
+ MediaType.VideoMain
+ );
+
+ meeting.locusUrl = 'locus url';
+ meeting.deviceUrl = 'device url';
+ meeting.selfId = 'self id';
+ });
+
+ afterEach(() => {
+ sinon.restore();
+ });
+
+ it('should have #beRightBack', () => {
+ assert.exists(meeting.beRightBack);
+ });
+
+ describe('when in a multistream meeting', () => {
+
+ beforeEach(() => {
+ meeting.isMultistream = true;
+ });
+
+ it('should enable #beRightBack and return a promise', async () => {
+ const brbResult = meeting.beRightBack(true);
+
+ await brbResult;
+ assert.exists(brbResult.then);
+ assert.calledOnce(meeting.meetingRequest.sendBrb);
+ })
+
+ it('should disable #beRightBack and return a promise', async () => {
+ const brbResult = meeting.beRightBack(false);
+
+ await brbResult;
+ assert.exists(brbResult.then);
+ assert.calledOnce(meeting.meetingRequest.sendBrb);
+ })
+ });
+
+ describe('when in a transcoded meeting', () => {
+
+ beforeEach(() => {
+ meeting.isMultistream = false;
+ });
+
+ it('should ignore enabling #beRightBack', async () => {
+ meeting.beRightBack(true);
+
+ assert.isRejected((Promise.reject()));
+ assert.notCalled(meeting.meetingRequest.sendBrb);
+ })
+
+ it('should ignore disabling #beRightBack', async () => {
+ meeting.beRightBack(false);
+
+ assert.isRejected((Promise.reject()));
+ assert.notCalled(meeting.meetingRequest.sendBrb);
+ })
+ });
+ });
+
/* This set of tests are like semi-integration tests, they use real MuteState, Media, LocusMediaRequest and Roap classes.
They mock the @webex/internal-media-core and sending of /media http requests to Locus.
Their main purpose is to test that we send the right http requests to Locus and make right calls
@@ -4796,6 +4870,7 @@ describe('plugin-meetings', () => {
});
});
+
[
{mute: true, title: 'user muting a track before confluence is created'},
{mute: false, title: 'user unmuting a track before confluence is created'},
@@ -8662,6 +8737,7 @@ describe('plugin-meetings', () => {
});
});
});
+
describe('#setUpLocusInfoSelfListener', () => {
it('listens to the self unadmitted guest event', (done) => {
meeting.startKeepAlive = sinon.stub();
@@ -8756,6 +8832,26 @@ describe('plugin-meetings', () => {
);
});
+ it('listens to the brb state changed event', () => {
+ const assertBrb = (enabled) => {
+ meeting.locusInfo.emit(
+ { function: 'test', file: 'test' },
+ LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED,
+ { brb: { enabled } },
+ )
+ assert.calledWithExactly(
+ TriggerProxy.trigger,
+ meeting,
+ {file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
+ EVENT_TRIGGERS.MEETING_SELF_BRB_UPDATE,
+ { payload: { brb: { enabled } } },
+ );
+ }
+
+ assertBrb(true);
+ assertBrb(false);
+ })
+
it('listens to the interpretation changed event', () => {
meeting.simultaneousInterpretation.updateSelfInterpretation = sinon.stub();
diff --git a/packages/@webex/plugin-meetings/test/unit/spec/member/util.js b/packages/@webex/plugin-meetings/test/unit/spec/member/util.js
index 69d5869262e..71b4713232e 100644
--- a/packages/@webex/plugin-meetings/test/unit/spec/member/util.js
+++ b/packages/@webex/plugin-meetings/test/unit/spec/member/util.js
@@ -5,13 +5,13 @@ import {_SEND_RECEIVE_, _RECEIVE_ONLY_} from '../../../../src/constants';
describe('plugin-meetings', () => {
describe('isHandRaised', () => {
- it('throws error when there is no participant', () => {
+ it('throws an error when there is no participant', () => {
assert.throws(() => {
MemberUtil.isHandRaised();
}, 'Raise hand could not be processed, participant is undefined.');
});
- it('returns false when controls is not there', () => {
+ it('returns false when controls are not present', () => {
const participant = {};
assert.isFalse(MemberUtil.isHandRaised(participant));
@@ -51,7 +51,7 @@ describe('plugin-meetings', () => {
});
describe('MemberUtil.canReclaimHost', () => {
- it('throws error when there is no participant', () => {
+ it('throws an error when there is no participant', () => {
assert.throws(() => {
MemberUtil.canReclaimHost();
}, 'canReclaimHostRole could not be processed, participant is undefined.');
@@ -352,8 +352,49 @@ describe('plugin-meetings', () => {
});
});
+ describe('MemberUtil.isBrb', () => {
+ it('returns true when brb is enabled', () => {
+ const participant = {
+ controls: {
+ brb: {
+ enabled: true,
+ },
+ },
+ };
+
+ assert.isTrue(MemberUtil.isBrb(participant));
+ });
+
+ it('returns false when brb is disabled', () => {
+ const participant = {
+ controls: {
+ brb: {
+ enabled: false,
+ },
+ },
+ };
+
+ assert.isFalse(MemberUtil.isBrb(participant));
+ });
+
+
+ it('returns false when brb is not present', () => {
+ const participant = {
+ controls: {},
+ };
+
+ assert.isFalse(MemberUtil.isBrb(participant));
+ });
+
+ it('returns false when controls are not present', () => {
+ const participant = {};
+
+ assert.isFalse(MemberUtil.isBrb(participant));
+ });
+ });
+
describe('MemberUtil.isBreakoutsSupported', () => {
- it('throws error when there is no participant', () => {
+ it('throws an error when there is no participant', () => {
assert.throws(() => {
MemberUtil.isBreakoutsSupported();
}, 'Breakout support could not be processed, participant is undefined.');
@@ -377,7 +418,7 @@ describe('plugin-meetings', () => {
});
describe('MemberUtil.isLiveAnnotationSupported', () => {
- it('throws error when there is no participant', () => {
+ it('throws an error when there is no participant', () => {
assert.throws(() => {
MemberUtil.isLiveAnnotationSupported();
}, 'LiveAnnotation support could not be processed, participant is undefined.');
@@ -401,7 +442,7 @@ describe('plugin-meetings', () => {
});
describe('MemberUtil.isInterpretationSupported', () => {
- it('throws error when there is no participant', () => {
+ it('throws an error when there is no participant', () => {
assert.throws(() => {
MemberUtil.isInterpretationSupported();
}, 'Interpretation support could not be processed, participant is undefined.');
@@ -432,7 +473,7 @@ describe('plugin-meetings', () => {
};
describe('MemberUtil.isAudioMuted', () => {
- it('throws error when there is no participant', () => {
+ it('throws an error when there is no participant', () => {
assert.throws(() => {
MemberUtil.isAudioMuted();
}, 'Audio could not be processed, participant is undefined.');
@@ -475,7 +516,7 @@ describe('plugin-meetings', () => {
});
describe('MemberUtil.isVideoMuted', () => {
- it('throws error when there is no participant', () => {
+ it('throws an error when there is no participant', () => {
assert.throws(() => {
MemberUtil.isVideoMuted();
}, 'Video could not be processed, participant is undefined.');
@@ -519,7 +560,7 @@ describe('plugin-meetings', () => {
});
describe('extractMediaStatus', () => {
- it('throws error when there is no participant', () => {
+ it('throws an error when there is no participant', () => {
assert.throws(() => {
MemberUtil.extractMediaStatus()
}, 'Media status could not be extracted, participant is undefined.');
@@ -529,7 +570,7 @@ describe('extractMediaStatus', () => {
const participant = {
status: {}
};
-
+
const mediaStatus = MemberUtil.extractMediaStatus(participant)
assert.deepEqual(mediaStatus, {audio: undefined, video: undefined});
@@ -542,7 +583,7 @@ describe('extractMediaStatus', () => {
videoStatus: 'SENDRECV'
}
};
-
+
const mediaStatus = MemberUtil.extractMediaStatus(participant)
assert.deepEqual(mediaStatus, {audio: 'RECVONLY', video: 'SENDRECV'});