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'});