Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat(webinar): Panelist Join new data channel in practice session & Attendee receive whiteboard as stream #4041

Merged
merged 20 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
688b771
feat(ps): panelist should connect new data channel in PS
mickelr Dec 16, 2024
6534796
feat(ps): panelist should connect new data channel in PS
mickelr Dec 17, 2024
2f22660
Merge branch 'next' of https://github.com/mickelr/webex-js-sdk into f…
mickelr Dec 17, 2024
29ac7ff
Merge branch 'next' of https://github.com/mickelr/webex-js-sdk into f…
mickelr Dec 23, 2024
7aba5e4
feat(ps): breakout start displayhint
mickelr Dec 26, 2024
1da591f
Merge branch 'next' of https://github.com/mickelr/webex-js-sdk into f…
mickelr Dec 26, 2024
2767b73
feat(ps): switch data channel for promote/demote case
mickelr Jan 3, 2025
8d40710
feat(ps): notify Cantina already get webcast instance url
mickelr Jan 8, 2025
d4a691e
Merge branch 'next' of https://github.com/mickelr/webex-js-sdk into f…
mickelr Jan 10, 2025
c160f2a
feat(ps): attendees should receive whiteboard streaming instead of na…
mickelr Jan 13, 2025
5c896d9
Merge branch 'next' of https://github.com/mickelr/webex-js-sdk into f…
mickelr Jan 13, 2025
6695b43
feat(ps): attendees should receive whiteboard streaming instead of na…
mickelr Jan 13, 2025
3851072
feat(ps): remove function into webinar class
mickelr Jan 13, 2025
8829ed4
feat(ps): refactor
mickelr Jan 14, 2025
1adad62
feat(ps): add ut
mickelr Jan 15, 2025
a8f0126
feat(ps): add ut
mickelr Jan 15, 2025
34f9904
Merge branch 'next' of https://github.com/mickelr/webex-js-sdk into f…
mickelr Jan 16, 2025
527fb2b
feat(ps): add ut
mickelr Jan 16, 2025
756d9bc
feat(ps): add ut
mickelr Jan 16, 2025
dbe0399
Merge branch 'next' into feat/dataChannel4PS
mickelr Jan 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/@webex/plugin-meetings/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ export const EVENT_TRIGGERS = {
MEETING_RECONNECTION_FAILURE: 'meeting:reconnectionFailure',
MEETING_UNLOCKED: 'meeting:unlocked',
MEETING_LOCKED: 'meeting:locked',
MEETING_RESOURCE_LINKS_UPDATE: 'meeting:resourceLinks:update',
MEETING_INFO_AVAILABLE: 'meeting:meetingInfoAvailable',
MEETING_INFO_UPDATED: 'meeting:meetingInfoUpdated',
MEETING_LOG_UPLOAD_SUCCESS: 'meeting:logUpload:success',
Expand Down Expand Up @@ -963,6 +964,7 @@ export const DISPLAY_HINTS = {
DISABLE_ASK_FOR_HELP: 'DISABLE_ASK_FOR_HELP',
DISABLE_BREAKOUT_PREASSIGNMENTS: 'DISABLE_BREAKOUT_PREASSIGNMENTS',
DISABLE_LOBBY_TO_BREAKOUT: 'DISABLE_LOBBY_TO_BREAKOUT',
DISABLE_BREAKOUT_START: 'DISABLE_BREAKOUT_START',

// participants list
DISABLE_VIEW_THE_PARTICIPANT_LIST: 'DISABLE_VIEW_THE_PARTICIPANT_LIST',
Expand Down
6 changes: 4 additions & 2 deletions packages/@webex/plugin-meetings/src/locus-info/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1283,12 +1283,13 @@ export default class LocusInfo extends EventsScope {
/**
* handles when the locus.mediaShares is updated
* @param {Object} mediaShares the locus.mediaShares property
* @param {boolean} forceUpdate force to update the mediaShares
* @returns {undefined}
* @memberof LocusInfo
* emits internal event locus_info_update_media_shares
*/
updateMediaShares(mediaShares: object) {
if (mediaShares && !isEqual(this.mediaShares, mediaShares)) {
updateMediaShares(mediaShares: object, forceUpdate = false) {
if (mediaShares && (!isEqual(this.mediaShares, mediaShares) || forceUpdate)) {
const parsedMediaShares = MediaSharesUtils.getMediaShares(this.mediaShares, mediaShares);

this.updateMeeting(parsedMediaShares.current);
Expand All @@ -1303,6 +1304,7 @@ export default class LocusInfo extends EventsScope {
{
current: parsedMediaShares.current,
previous: parsedMediaShares.previous,
forceUpdate,
}
);
}
Expand Down
38 changes: 32 additions & 6 deletions packages/@webex/plugin-meetings/src/meeting/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,7 @@ export default class Meeting extends StatelessWebexPlugin {
* @memberof Meeting
*/
// @ts-ignore
this.webinar = new Webinar({}, {parent: this.webex});
this.webinar = new Webinar({meetingId: this.id}, {parent: this.webex});
/**
* helper class for managing receive slots (for multistream media connections)
*/
Expand Down Expand Up @@ -2740,6 +2740,7 @@ export default class Meeting extends StatelessWebexPlugin {
this.triggerAnnotationInfoEvent(contentShare, previousContentShare);

if (
!payload.forceUpdate &&
contentShare.beneficiaryId === previousContentShare?.beneficiaryId &&
contentShare.disposition === previousContentShare?.disposition &&
contentShare.deviceUrlSharing === previousContentShare.deviceUrlSharing &&
Expand Down Expand Up @@ -2786,7 +2787,11 @@ export default class Meeting extends StatelessWebexPlugin {
// It does not matter who requested to share the whiteboard, everyone gets the same view
else if (whiteboardShare.disposition === FLOOR_ACTION.GRANTED) {
// WHITEBOARD - sharing whiteboard
newShareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
// Webinar attendee should receive whiteboard as remote share
newShareStatus =
this.locusInfo?.info?.isWebinar && this.webinar?.selfIsAttendee
? SHARE_STATUS.REMOTE_SHARE_ACTIVE
: SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
}
// or if content share is either released or null and whiteboard share is either released or null, no one is sharing
else if (
Expand All @@ -2801,6 +2806,7 @@ export default class Meeting extends StatelessWebexPlugin {
LoggerProxy.logger.info(
`Meeting:index#setUpLocusInfoMediaInactiveListener --> this.shareStatus=${this.shareStatus} newShareStatus=${newShareStatus}`
);

if (newShareStatus !== this.shareStatus) {
const oldShareStatus = this.shareStatus;

Expand Down Expand Up @@ -3058,7 +3064,20 @@ export default class Meeting extends StatelessWebexPlugin {
*/
private setUpLocusResourcesListener() {
this.locusInfo.on(LOCUSINFO.EVENTS.LINKS_RESOURCES, (payload) => {
this.webinar.updateWebcastUrl(payload);
if (payload) {
this.webinar.updateWebcastUrl(payload);
Trigger.trigger(
this,
{
file: 'meeting/index',
function: 'setUpLocusInfoMeetingInfoListener',
},
EVENT_TRIGGERS.MEETING_RESOURCE_LINKS_UPDATE,
{
payload,
}
);
}
});
}

Expand Down Expand Up @@ -3377,6 +3396,7 @@ export default class Meeting extends StatelessWebexPlugin {
payload.newRoles?.includes(SELF_ROLES.MODERATOR)
);
this.webinar.updateRoleChanged(payload);

Trigger.trigger(
this,
{
Expand Down Expand Up @@ -5580,17 +5600,23 @@ export default class Meeting extends StatelessWebexPlugin {
*/
async updateLLMConnection() {
// @ts-ignore - Fix type
const {url, info: {datachannelUrl} = {}} = this.locusInfo;
const {url, info: {datachannelUrl, practiceSessionDatachannelUrl} = {}} = this.locusInfo;

const isJoined = this.isJoined();

// webinar panelist should use new data channel in practice session
const dataChannelUrl =
this.webinar.isJoinPracticeSessionDataChannel() && practiceSessionDatachannelUrl
? practiceSessionDatachannelUrl
: datachannelUrl;

// @ts-ignore - Fix type
if (this.webex.internal.llm.isConnected()) {
if (
// @ts-ignore - Fix type
url === this.webex.internal.llm.getLocusUrl() &&
// @ts-ignore - Fix type
datachannelUrl === this.webex.internal.llm.getDatachannelUrl() &&
dataChannelUrl === this.webex.internal.llm.getDatachannelUrl() &&
isJoined
) {
return undefined;
Expand All @@ -5607,7 +5633,7 @@ export default class Meeting extends StatelessWebexPlugin {

// @ts-ignore - Fix type
return this.webex.internal.llm
.registerAndConnect(url, datachannelUrl)
.registerAndConnect(url, dataChannelUrl)
.then((registerAndConnectResult) => {
// @ts-ignore - Fix type
this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
Expand Down
3 changes: 3 additions & 0 deletions packages/@webex/plugin-meetings/src/meeting/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,9 @@ const MeetingUtil = {
displayHints.includes(DISPLAY_HINTS.LEAVE_END_MEETING),

canManageBreakout: (displayHints) => displayHints.includes(DISPLAY_HINTS.BREAKOUT_MANAGEMENT),

canStartBreakout: (displayHints) => !displayHints.includes(DISPLAY_HINTS.DISABLE_BREAKOUT_START),

canBroadcastMessageToBreakout: (displayHints, policies = {}) =>
displayHints.includes(DISPLAY_HINTS.BROADCAST_MESSAGE_TO_BREAKOUT) &&
!!policies[SELF_POLICY.SUPPORT_BROADCAST_MESSAGE],
Expand Down
38 changes: 36 additions & 2 deletions packages/@webex/plugin-meetings/src/webinar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import {WebexPlugin, config} from '@webex/webex-core';
import uuid from 'uuid';
import {get} from 'lodash';
import {HEADERS, HTTP_VERBS, MEETINGS, SELF_ROLES} from '../constants';
import {_ID_, HEADERS, HTTP_VERBS, MEETINGS, SELF_ROLES, SHARE_STATUS} from '../constants';

import WebinarCollection from './collection';
import LoggerProxy from '../common/logs/logger-proxy';
Expand All @@ -25,6 +25,7 @@ const Webinar = WebexPlugin.extend({
selfIsPanelist: 'boolean', // self is panelist
selfIsAttendee: 'boolean', // self is attendee
practiceSessionEnabled: 'boolean', // practice session enabled
meetingId: 'string',
},

/**
Expand Down Expand Up @@ -68,14 +69,47 @@ const Webinar = WebexPlugin.extend({
const isPromoted =
oldRoles.includes(SELF_ROLES.ATTENDEE) && newRoles.includes(SELF_ROLES.PANELIST);
const isDemoted =
oldRoles.includes(SELF_ROLES.PANELIST) && newRoles.includes(SELF_ROLES.ATTENDEE);
(oldRoles.includes(SELF_ROLES.PANELIST) && newRoles.includes(SELF_ROLES.ATTENDEE)) ||
(!oldRoles.includes(SELF_ROLES.ATTENDEE) && newRoles.includes(SELF_ROLES.ATTENDEE)); // for attendee just join meeting case
this.set('selfIsPanelist', newRoles.includes(SELF_ROLES.PANELIST));
this.set('selfIsAttendee', newRoles.includes(SELF_ROLES.ATTENDEE));
this.updateCanManageWebcast(newRoles.includes(SELF_ROLES.MODERATOR));
this.updateStatusByRole({isPromoted, isDemoted});

return {isPromoted, isDemoted};
},

/**
* should join practice session data channel or not
* @param {Object} {isPromoted: boolean, isDemoted: boolean}} Role transition states
* @returns {void}
*/
updateStatusByRole({isPromoted, isDemoted}) {
const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);

if (
(isDemoted && meeting?.shareStatus === SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE) ||
isPromoted
) {
// attendees in webinar should subscribe streaming for whiteboard sharing
// while panelist still need subscribe native mode so trigger force update here
meeting?.locusInfo?.updateMediaShares(meeting?.locusInfo?.mediaShares, true);
}

if (this.practiceSessionEnabled) {
// may need change data channel in practice session
meeting?.updateLLMConnection();
}
},

/**
* should join practice session data channel or not
* @returns {boolean}
*/
isJoinPracticeSessionDataChannel() {
return this.selfIsPanelist && this.practiceSessionEnabled;
},

/**
* start or stop practice session for webinar
* @param {boolean} enabled
Expand Down
129 changes: 129 additions & 0 deletions packages/@webex/plugin-meetings/test/unit/spec/locus-info/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import LocusInfo from '@webex/plugin-meetings/src/locus-info';
import SelfUtils from '@webex/plugin-meetings/src/locus-info/selfUtils';
import InfoUtils from '@webex/plugin-meetings/src/locus-info/infoUtils';
import EmbeddedAppsUtils from '@webex/plugin-meetings/src/locus-info/embeddedAppsUtils';
import MediaSharesUtils from '@webex/plugin-meetings/src/locus-info//mediaSharesUtils';
import LocusDeltaParser from '@webex/plugin-meetings/src/locus-info/parser';
import Metrics from '@webex/plugin-meetings/src/metrics';

Expand Down Expand Up @@ -1637,6 +1638,134 @@ describe('plugin-meetings', () => {
});
});

describe('#updateMediaShares', () => {
let getMediaSharesSpy;

beforeEach(() => {
// Spy on MediaSharesUtils.getMediaShares
getMediaSharesSpy = sinon.stub(MediaSharesUtils, 'getMediaShares');

// Stub the emitScoped method to monitor its calls
sinon.stub(locusInfo, 'emitScoped');
});

afterEach(() => {
getMediaSharesSpy.restore();
locusInfo.emitScoped.restore();
});

it('should update media shares and emit LOCUS_INFO_UPDATE_MEDIA_SHARES when mediaShares change', () => {
const initialMediaShares = { audio: true, video: false };
const newMediaShares = { audio: false, video: true };

locusInfo.mediaShares = initialMediaShares;
locusInfo.parsedLocus = { mediaShares: null };

const parsedMediaShares = {
current: newMediaShares,
previous: initialMediaShares,
};

// Stub MediaSharesUtils.getMediaShares to return the expected parsedMediaShares
getMediaSharesSpy.returns(parsedMediaShares);

// Call the function
locusInfo.updateMediaShares(newMediaShares);

// Assert that MediaSharesUtils.getMediaShares was called with correct arguments
assert.calledWith(getMediaSharesSpy, initialMediaShares, newMediaShares);

// Assert that updateMeeting was called with the parsed current media shares
assert.deepEqual(locusInfo.parsedLocus.mediaShares, newMediaShares);
assert.deepEqual(locusInfo.mediaShares, newMediaShares);

// Assert that emitScoped was called with the correct event
assert.calledWith(
locusInfo.emitScoped,
{
file: 'locus-info',
function: 'updateMediaShares',
},
EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES,
{
current: newMediaShares,
previous: initialMediaShares,
forceUpdate: false,
}
);
});

it('should force update media shares and emit LOCUS_INFO_UPDATE_MEDIA_SHARES even if shares are the same', () => {
const initialMediaShares = { audio: true, video: false };
locusInfo.mediaShares = initialMediaShares;
locusInfo.parsedLocus = { mediaShares: null };

const parsedMediaShares = {
current: initialMediaShares,
previous: initialMediaShares,
};

getMediaSharesSpy.returns(parsedMediaShares);

// Call the function with forceUpdate = true
locusInfo.updateMediaShares(initialMediaShares, true);

// Assert that MediaSharesUtils.getMediaShares was called
assert.calledWith(getMediaSharesSpy, initialMediaShares, initialMediaShares);

// Assert that emitScoped was called with the correct event
assert.calledWith(
locusInfo.emitScoped,
{
file: 'locus-info',
function: 'updateMediaShares',
},
EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES,
{
current: initialMediaShares,
previous: initialMediaShares,
forceUpdate: true,
}
);
});

it('should not emit LOCUS_INFO_UPDATE_MEDIA_SHARES if mediaShares do not change and forceUpdate is false', () => {
const initialMediaShares = { audio: true, video: false };
locusInfo.mediaShares = initialMediaShares;

// Call the function with the same mediaShares and forceUpdate = false
locusInfo.updateMediaShares(initialMediaShares);

// Assert that MediaSharesUtils.getMediaShares was not called
assert.notCalled(getMediaSharesSpy);

// Assert that emitScoped was not called
assert.notCalled(locusInfo.emitScoped);
});

it('should update internal state correctly when mediaShares are updated', () => {
const initialMediaShares = { audio: true, video: false };
const newMediaShares = { audio: false, video: true };

locusInfo.mediaShares = initialMediaShares;
locusInfo.parsedLocus = { mediaShares: null };

const parsedMediaShares = {
current: newMediaShares,
previous: initialMediaShares,
};

getMediaSharesSpy.returns(parsedMediaShares);

// Call the function
locusInfo.updateMediaShares(newMediaShares);

// Assert that the internal state was updated correctly
assert.deepEqual(locusInfo.parsedLocus.mediaShares, newMediaShares);
assert.deepEqual(locusInfo.mediaShares, newMediaShares);
});
});

describe('#updateEmbeddedApps()', () => {
const newEmbeddedApps = [
{
Expand Down
Loading
Loading