@webex/plugin-meetings 3.7.0-next.4 → 3.7.0-next.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/annotation/index.js +17 -0
- package/dist/annotation/index.js.map +1 -1
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/common/errors/{webinar-registration-error.js → join-webinar-error.js} +12 -12
- package/dist/common/errors/join-webinar-error.js.map +1 -0
- package/dist/common/errors/multistream-not-supported-error.js +53 -0
- package/dist/common/errors/multistream-not-supported-error.js.map +1 -0
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/constants.js +36 -6
- package/dist/constants.js.map +1 -1
- package/dist/index.js +16 -11
- package/dist/index.js.map +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/index.js +13 -2
- package/dist/locus-info/index.js.map +1 -1
- package/dist/locus-info/selfUtils.js +30 -17
- package/dist/locus-info/selfUtils.js.map +1 -1
- package/dist/meeting/in-meeting-actions.js +13 -1
- package/dist/meeting/in-meeting-actions.js.map +1 -1
- package/dist/meeting/index.js +922 -800
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/locusMediaRequest.js +9 -0
- package/dist/meeting/locusMediaRequest.js.map +1 -1
- package/dist/meeting/request.js +30 -0
- package/dist/meeting/request.js.map +1 -1
- package/dist/meeting/request.type.js.map +1 -1
- package/dist/meeting/util.js +16 -16
- package/dist/meeting/util.js.map +1 -1
- package/dist/meeting-info/meeting-info-v2.js +29 -17
- package/dist/meeting-info/meeting-info-v2.js.map +1 -1
- package/dist/meetings/index.js +6 -3
- package/dist/meetings/index.js.map +1 -1
- package/dist/meetings/util.js +1 -1
- package/dist/meetings/util.js.map +1 -1
- package/dist/member/index.js +9 -0
- package/dist/member/index.js.map +1 -1
- package/dist/member/types.js.map +1 -1
- package/dist/member/util.js +39 -28
- package/dist/member/util.js.map +1 -1
- package/dist/members/util.js +4 -2
- package/dist/members/util.js.map +1 -1
- package/dist/metrics/constants.js +1 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/multistream/remoteMedia.js +30 -15
- package/dist/multistream/remoteMedia.js.map +1 -1
- package/dist/multistream/sendSlotManager.js +24 -0
- package/dist/multistream/sendSlotManager.js.map +1 -1
- package/dist/recording-controller/enums.js +8 -4
- package/dist/recording-controller/enums.js.map +1 -1
- package/dist/recording-controller/index.js +18 -9
- package/dist/recording-controller/index.js.map +1 -1
- package/dist/recording-controller/util.js +13 -9
- package/dist/recording-controller/util.js.map +1 -1
- package/dist/roap/index.js +10 -8
- package/dist/roap/index.js.map +1 -1
- package/dist/types/annotation/index.d.ts +5 -0
- package/dist/types/common/errors/{webinar-registration-error.d.ts → join-webinar-error.d.ts} +2 -2
- package/dist/types/common/errors/multistream-not-supported-error.d.ts +17 -0
- package/dist/types/constants.d.ts +28 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/locus-info/index.d.ts +2 -1
- package/dist/types/meeting/in-meeting-actions.d.ts +12 -0
- package/dist/types/meeting/index.d.ts +20 -12
- package/dist/types/meeting/locusMediaRequest.d.ts +4 -0
- package/dist/types/meeting/request.d.ts +12 -1
- package/dist/types/meeting/request.type.d.ts +6 -0
- package/dist/types/meeting/util.d.ts +1 -1
- package/dist/types/meeting-info/meeting-info-v2.d.ts +4 -4
- package/dist/types/meetings/index.d.ts +3 -0
- package/dist/types/member/index.d.ts +1 -0
- package/dist/types/member/types.d.ts +7 -0
- package/dist/types/members/util.d.ts +2 -0
- package/dist/types/metrics/constants.d.ts +1 -1
- package/dist/types/multistream/sendSlotManager.d.ts +8 -1
- package/dist/types/recording-controller/enums.d.ts +5 -2
- package/dist/types/recording-controller/index.d.ts +1 -0
- package/dist/types/recording-controller/util.d.ts +2 -1
- package/dist/webinar/index.js +390 -7
- package/dist/webinar/index.js.map +1 -1
- package/package.json +23 -22
- package/src/annotation/index.ts +16 -0
- package/src/common/errors/join-webinar-error.ts +24 -0
- package/src/common/errors/multistream-not-supported-error.ts +30 -0
- package/src/config.ts +1 -1
- package/src/constants.ts +33 -3
- package/src/index.ts +5 -3
- package/src/locus-info/index.ts +17 -2
- package/src/locus-info/selfUtils.ts +19 -6
- package/src/meeting/in-meeting-actions.ts +25 -0
- package/src/meeting/index.ts +257 -80
- package/src/meeting/locusMediaRequest.ts +7 -0
- package/src/meeting/request.ts +26 -1
- package/src/meeting/request.type.ts +7 -0
- package/src/meeting/util.ts +8 -10
- package/src/meeting-info/meeting-info-v2.ts +23 -11
- package/src/meetings/index.ts +8 -2
- package/src/meetings/util.ts +2 -1
- package/src/member/index.ts +9 -0
- package/src/member/types.ts +8 -0
- package/src/member/util.ts +34 -24
- package/src/members/util.ts +1 -0
- package/src/metrics/constants.ts +1 -1
- package/src/multistream/remoteMedia.ts +28 -15
- package/src/multistream/sendSlotManager.ts +31 -0
- package/src/recording-controller/enums.ts +5 -2
- package/src/recording-controller/index.ts +17 -4
- package/src/recording-controller/util.ts +20 -5
- package/src/roap/index.ts +10 -8
- package/src/webinar/index.ts +235 -9
- package/test/unit/spec/annotation/index.ts +46 -1
- package/test/unit/spec/locus-info/index.js +222 -0
- package/test/unit/spec/locus-info/selfConstant.js +7 -0
- package/test/unit/spec/locus-info/selfUtils.js +91 -1
- package/test/unit/spec/meeting/in-meeting-actions.ts +15 -1
- package/test/unit/spec/meeting/index.js +685 -102
- package/test/unit/spec/meeting/utils.js +22 -19
- package/test/unit/spec/meeting-info/meetinginfov2.js +9 -4
- package/test/unit/spec/meetings/index.js +9 -5
- package/test/unit/spec/meetings/utils.js +10 -0
- package/test/unit/spec/member/util.js +52 -11
- package/test/unit/spec/members/utils.js +95 -0
- package/test/unit/spec/multistream/remoteMedia.ts +11 -7
- package/test/unit/spec/recording-controller/index.js +61 -5
- package/test/unit/spec/recording-controller/util.js +39 -3
- package/test/unit/spec/roap/index.ts +47 -0
- package/test/unit/spec/webinar/index.ts +504 -0
- package/dist/common/errors/webinar-registration-error.js.map +0 -1
- package/src/common/errors/webinar-registration-error.ts +0 -27
package/src/meeting/index.ts
CHANGED
|
@@ -31,7 +31,6 @@ import {
|
|
|
31
31
|
} from '@webex/internal-media-core';
|
|
32
32
|
|
|
33
33
|
import {
|
|
34
|
-
getDevices,
|
|
35
34
|
LocalStream,
|
|
36
35
|
LocalCameraStream,
|
|
37
36
|
LocalDisplayStream,
|
|
@@ -122,6 +121,8 @@ import {
|
|
|
122
121
|
MEETING_PERMISSION_TOKEN_REFRESH_REASON,
|
|
123
122
|
ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT,
|
|
124
123
|
NAMED_MEDIA_GROUP_TYPE_AUDIO,
|
|
124
|
+
WEBINAR_ERROR_WEBCAST,
|
|
125
|
+
WEBINAR_ERROR_REGISTRATIONID,
|
|
125
126
|
} from '../constants';
|
|
126
127
|
import BEHAVIORAL_METRICS from '../metrics/constants';
|
|
127
128
|
import ParameterError from '../common/errors/parameter';
|
|
@@ -129,7 +130,7 @@ import {
|
|
|
129
130
|
MeetingInfoV2PasswordError,
|
|
130
131
|
MeetingInfoV2CaptchaError,
|
|
131
132
|
MeetingInfoV2PolicyError,
|
|
132
|
-
|
|
133
|
+
MeetingInfoV2JoinWebinarError,
|
|
133
134
|
} from '../meeting-info/meeting-info-v2';
|
|
134
135
|
import {CSI, ReceiveSlotManager} from '../multistream/receiveSlotManager';
|
|
135
136
|
import SendSlotManager from '../multistream/sendSlotManager';
|
|
@@ -158,7 +159,9 @@ import ControlsOptionsManager from '../controls-options-manager';
|
|
|
158
159
|
import PermissionError from '../common/errors/permission';
|
|
159
160
|
import {LocusMediaRequest} from './locusMediaRequest';
|
|
160
161
|
import {ConnectionStateHandler, ConnectionStateEvent} from './connectionStateHandler';
|
|
161
|
-
import
|
|
162
|
+
import JoinWebinarError from '../common/errors/join-webinar-error';
|
|
163
|
+
import Member from '../member';
|
|
164
|
+
import MultistreamNotSupportedError from '../common/errors/multistream-not-supported-error';
|
|
162
165
|
|
|
163
166
|
// default callback so we don't call an undefined function, but in practice it should never be used
|
|
164
167
|
const DEFAULT_ICE_PHASE_CALLBACK = () => 'JOIN_MEETING_FINAL';
|
|
@@ -848,7 +851,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
848
851
|
* @memberof Meeting
|
|
849
852
|
*/
|
|
850
853
|
// @ts-ignore
|
|
851
|
-
this.webinar = new Webinar({}, {parent: this.webex});
|
|
854
|
+
this.webinar = new Webinar({meetingId: this.id}, {parent: this.webex});
|
|
852
855
|
/**
|
|
853
856
|
* helper class for managing receive slots (for multistream media connections)
|
|
854
857
|
*/
|
|
@@ -1767,15 +1770,20 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1767
1770
|
this.meetingInfo = err.meetingInfo;
|
|
1768
1771
|
}
|
|
1769
1772
|
throw new PermissionError();
|
|
1770
|
-
} else if (err instanceof
|
|
1773
|
+
} else if (err instanceof MeetingInfoV2JoinWebinarError) {
|
|
1771
1774
|
this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WEBINAR_REGISTRATION;
|
|
1775
|
+
if (WEBINAR_ERROR_WEBCAST.includes(err.wbxAppApiCode)) {
|
|
1776
|
+
this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NEED_JOIN_WITH_WEBCAST;
|
|
1777
|
+
} else if (WEBINAR_ERROR_REGISTRATIONID.includes(err.wbxAppApiCode)) {
|
|
1778
|
+
this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WEBINAR_NEED_REGISTRATIONID;
|
|
1779
|
+
}
|
|
1772
1780
|
this.meetingInfoFailureCode = err.wbxAppApiCode;
|
|
1773
1781
|
|
|
1774
1782
|
if (err.meetingInfo) {
|
|
1775
1783
|
this.meetingInfo = err.meetingInfo;
|
|
1776
1784
|
}
|
|
1777
1785
|
|
|
1778
|
-
throw new
|
|
1786
|
+
throw new JoinWebinarError();
|
|
1779
1787
|
} else if (err instanceof MeetingInfoV2PasswordError) {
|
|
1780
1788
|
LoggerProxy.logger.info(
|
|
1781
1789
|
// @ts-ignore
|
|
@@ -2660,6 +2668,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2660
2668
|
});
|
|
2661
2669
|
|
|
2662
2670
|
this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_PRACTICE_SESSION_STATUS_UPDATED, ({state}) => {
|
|
2671
|
+
this.webinar.updatePracticeSessionStatus(state);
|
|
2663
2672
|
Trigger.trigger(
|
|
2664
2673
|
this,
|
|
2665
2674
|
{file: 'meeting/index', function: 'setupLocusControlsListener'},
|
|
@@ -2733,6 +2742,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2733
2742
|
this.triggerAnnotationInfoEvent(contentShare, previousContentShare);
|
|
2734
2743
|
|
|
2735
2744
|
if (
|
|
2745
|
+
!payload.forceUpdate &&
|
|
2736
2746
|
contentShare.beneficiaryId === previousContentShare?.beneficiaryId &&
|
|
2737
2747
|
contentShare.disposition === previousContentShare?.disposition &&
|
|
2738
2748
|
contentShare.deviceUrlSharing === previousContentShare.deviceUrlSharing &&
|
|
@@ -2779,7 +2789,11 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2779
2789
|
// It does not matter who requested to share the whiteboard, everyone gets the same view
|
|
2780
2790
|
else if (whiteboardShare.disposition === FLOOR_ACTION.GRANTED) {
|
|
2781
2791
|
// WHITEBOARD - sharing whiteboard
|
|
2782
|
-
|
|
2792
|
+
// Webinar attendee should receive whiteboard as remote share
|
|
2793
|
+
newShareStatus =
|
|
2794
|
+
this.locusInfo?.info?.isWebinar && this.webinar?.selfIsAttendee
|
|
2795
|
+
? SHARE_STATUS.REMOTE_SHARE_ACTIVE
|
|
2796
|
+
: SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
|
|
2783
2797
|
}
|
|
2784
2798
|
// or if content share is either released or null and whiteboard share is either released or null, no one is sharing
|
|
2785
2799
|
else if (
|
|
@@ -2794,6 +2808,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2794
2808
|
LoggerProxy.logger.info(
|
|
2795
2809
|
`Meeting:index#setUpLocusInfoMediaInactiveListener --> this.shareStatus=${this.shareStatus} newShareStatus=${newShareStatus}`
|
|
2796
2810
|
);
|
|
2811
|
+
|
|
2797
2812
|
if (newShareStatus !== this.shareStatus) {
|
|
2798
2813
|
const oldShareStatus = this.shareStatus;
|
|
2799
2814
|
|
|
@@ -3051,7 +3066,20 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3051
3066
|
*/
|
|
3052
3067
|
private setUpLocusResourcesListener() {
|
|
3053
3068
|
this.locusInfo.on(LOCUSINFO.EVENTS.LINKS_RESOURCES, (payload) => {
|
|
3054
|
-
|
|
3069
|
+
if (payload) {
|
|
3070
|
+
this.webinar.updateWebcastUrl(payload);
|
|
3071
|
+
Trigger.trigger(
|
|
3072
|
+
this,
|
|
3073
|
+
{
|
|
3074
|
+
file: 'meeting/index',
|
|
3075
|
+
function: 'setUpLocusInfoMeetingInfoListener',
|
|
3076
|
+
},
|
|
3077
|
+
EVENT_TRIGGERS.MEETING_RESOURCE_LINKS_UPDATE,
|
|
3078
|
+
{
|
|
3079
|
+
payload,
|
|
3080
|
+
}
|
|
3081
|
+
);
|
|
3082
|
+
}
|
|
3055
3083
|
});
|
|
3056
3084
|
}
|
|
3057
3085
|
|
|
@@ -3361,6 +3389,20 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3361
3389
|
}
|
|
3362
3390
|
});
|
|
3363
3391
|
|
|
3392
|
+
this.locusInfo.on(LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED, (payload) => {
|
|
3393
|
+
Trigger.trigger(
|
|
3394
|
+
this,
|
|
3395
|
+
{
|
|
3396
|
+
file: 'meeting/index',
|
|
3397
|
+
function: 'setUpLocusInfoSelfListener',
|
|
3398
|
+
},
|
|
3399
|
+
EVENT_TRIGGERS.MEETING_SELF_BRB_UPDATE,
|
|
3400
|
+
{
|
|
3401
|
+
payload,
|
|
3402
|
+
}
|
|
3403
|
+
);
|
|
3404
|
+
});
|
|
3405
|
+
|
|
3364
3406
|
this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ROLES_CHANGED, (payload) => {
|
|
3365
3407
|
const isModeratorOrCohost =
|
|
3366
3408
|
payload.newRoles?.includes(SELF_ROLES.MODERATOR) ||
|
|
@@ -3370,6 +3412,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3370
3412
|
payload.newRoles?.includes(SELF_ROLES.MODERATOR)
|
|
3371
3413
|
);
|
|
3372
3414
|
this.webinar.updateRoleChanged(payload);
|
|
3415
|
+
|
|
3373
3416
|
Trigger.trigger(
|
|
3374
3417
|
this,
|
|
3375
3418
|
{
|
|
@@ -3516,6 +3559,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3516
3559
|
emailAddress: string;
|
|
3517
3560
|
email: string;
|
|
3518
3561
|
phoneNumber: string;
|
|
3562
|
+
roles: Array<string>;
|
|
3519
3563
|
},
|
|
3520
3564
|
alertIfActive = true
|
|
3521
3565
|
) {
|
|
@@ -3563,6 +3607,50 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3563
3607
|
return this.members.admitMembers(memberIds, locusUrls);
|
|
3564
3608
|
}
|
|
3565
3609
|
|
|
3610
|
+
/**
|
|
3611
|
+
* Manages be right back status updates for the current participant.
|
|
3612
|
+
*
|
|
3613
|
+
* @param {boolean} enabled - Indicates whether the user enabled brb or not.
|
|
3614
|
+
* @returns {Promise<void>} resolves when the brb status is updated or does nothing if not in a multistream meeting.
|
|
3615
|
+
* @throws {Error} - Throws an error if the request fails.
|
|
3616
|
+
*/
|
|
3617
|
+
public async beRightBack(enabled: boolean): Promise<void> {
|
|
3618
|
+
if (!this.isMultistream) {
|
|
3619
|
+
const errorMessage = 'Meeting:index#beRightBack --> Not a multistream meeting';
|
|
3620
|
+
const error = new Error(errorMessage);
|
|
3621
|
+
|
|
3622
|
+
LoggerProxy.logger.error(error);
|
|
3623
|
+
|
|
3624
|
+
return Promise.reject(error);
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3627
|
+
if (!this.mediaProperties.webrtcMediaConnection) {
|
|
3628
|
+
const errorMessage = 'Meeting:index#beRightBack --> WebRTC media connection is not defined';
|
|
3629
|
+
const error = new Error(errorMessage);
|
|
3630
|
+
|
|
3631
|
+
LoggerProxy.logger.error(error);
|
|
3632
|
+
|
|
3633
|
+
return Promise.reject(error);
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
// this logic should be applied only to multistream meetings
|
|
3637
|
+
return this.meetingRequest
|
|
3638
|
+
.setBrb({
|
|
3639
|
+
enabled,
|
|
3640
|
+
locusUrl: this.locusUrl,
|
|
3641
|
+
deviceUrl: this.deviceUrl,
|
|
3642
|
+
selfId: this.selfId,
|
|
3643
|
+
})
|
|
3644
|
+
.then(() => {
|
|
3645
|
+
this.sendSlotManager.setSourceStateOverride(MediaType.VideoMain, enabled ? 'away' : null);
|
|
3646
|
+
})
|
|
3647
|
+
.catch((error) => {
|
|
3648
|
+
LoggerProxy.logger.error('Meeting:index#beRightBack --> Error ', error);
|
|
3649
|
+
|
|
3650
|
+
return Promise.reject(error);
|
|
3651
|
+
});
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3566
3654
|
/**
|
|
3567
3655
|
* Remove the member from the meeting, boot them
|
|
3568
3656
|
* @param {String} memberId
|
|
@@ -3772,6 +3860,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3772
3860
|
this.userDisplayHints,
|
|
3773
3861
|
this.selfUserPolicies
|
|
3774
3862
|
),
|
|
3863
|
+
isPremiseRecordingEnabled: RecordingUtil.isPremiseRecordingEnabled(
|
|
3864
|
+
this.userDisplayHints,
|
|
3865
|
+
this.selfUserPolicies
|
|
3866
|
+
),
|
|
3775
3867
|
canRaiseHand: MeetingUtil.canUserRaiseHand(this.userDisplayHints),
|
|
3776
3868
|
canLowerAllHands: MeetingUtil.canUserLowerAllHands(this.userDisplayHints),
|
|
3777
3869
|
canLowerSomeoneElsesHand: MeetingUtil.canUserLowerSomeoneElsesHand(this.userDisplayHints),
|
|
@@ -3798,6 +3890,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3798
3890
|
this.userDisplayHints
|
|
3799
3891
|
),
|
|
3800
3892
|
canManageBreakout: MeetingUtil.canManageBreakout(this.userDisplayHints),
|
|
3893
|
+
canStartBreakout: MeetingUtil.canStartBreakout(this.userDisplayHints),
|
|
3801
3894
|
canBroadcastMessageToBreakout: MeetingUtil.canBroadcastMessageToBreakout(
|
|
3802
3895
|
this.userDisplayHints,
|
|
3803
3896
|
this.selfUserPolicies
|
|
@@ -3915,6 +4008,22 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
3915
4008
|
requiredHints: [DISPLAY_HINTS.DISABLE_STAGE_VIEW],
|
|
3916
4009
|
displayHints: this.userDisplayHints,
|
|
3917
4010
|
}),
|
|
4011
|
+
isPracticeSessionOn: ControlsOptionsUtil.hasHints({
|
|
4012
|
+
requiredHints: [DISPLAY_HINTS.PRACTICE_SESSION_ON],
|
|
4013
|
+
displayHints: this.userDisplayHints,
|
|
4014
|
+
}),
|
|
4015
|
+
isPracticeSessionOff: ControlsOptionsUtil.hasHints({
|
|
4016
|
+
requiredHints: [DISPLAY_HINTS.PRACTICE_SESSION_OFF],
|
|
4017
|
+
displayHints: this.userDisplayHints,
|
|
4018
|
+
}),
|
|
4019
|
+
canStartPracticeSession: ControlsOptionsUtil.hasHints({
|
|
4020
|
+
requiredHints: [DISPLAY_HINTS.SHOW_PRACTICE_SESSION_START],
|
|
4021
|
+
displayHints: this.userDisplayHints,
|
|
4022
|
+
}),
|
|
4023
|
+
canStopPracticeSession: ControlsOptionsUtil.hasHints({
|
|
4024
|
+
requiredHints: [DISPLAY_HINTS.SHOW_PRACTICE_SESSION_STOP],
|
|
4025
|
+
displayHints: this.userDisplayHints,
|
|
4026
|
+
}),
|
|
3918
4027
|
canShareFile:
|
|
3919
4028
|
(ControlsOptionsUtil.hasHints({
|
|
3920
4029
|
requiredHints: [DISPLAY_HINTS.SHARE_FILE],
|
|
@@ -4077,10 +4186,11 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4077
4186
|
*/
|
|
4078
4187
|
private setLogUploadTimer() {
|
|
4079
4188
|
// start with short timeouts and increase them later on so in case users have very long multi-hour meetings we don't get too fragmented logs
|
|
4080
|
-
const LOG_UPLOAD_INTERVALS = [0.1,
|
|
4189
|
+
const LOG_UPLOAD_INTERVALS = [0.1, 15, 30, 60]; // in minutes
|
|
4081
4190
|
|
|
4082
4191
|
const delay =
|
|
4083
4192
|
1000 *
|
|
4193
|
+
60 *
|
|
4084
4194
|
// @ts-ignore - config coming from registerPlugin
|
|
4085
4195
|
this.config.logUploadIntervalMultiplicationFactor *
|
|
4086
4196
|
LOG_UPLOAD_INTERVALS[this.logUploadIntervalIndex];
|
|
@@ -4519,11 +4629,12 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4519
4629
|
* Close the peer connections and remove them from the class.
|
|
4520
4630
|
* Cleanup any media connection related things.
|
|
4521
4631
|
*
|
|
4632
|
+
* @param {boolean} resetMuteStates whether to also reset the audio/video mute state information
|
|
4522
4633
|
* @returns {Promise}
|
|
4523
4634
|
* @public
|
|
4524
4635
|
* @memberof Meeting
|
|
4525
4636
|
*/
|
|
4526
|
-
public closePeerConnections() {
|
|
4637
|
+
public closePeerConnections(resetMuteStates = true) {
|
|
4527
4638
|
if (this.mediaProperties.webrtcMediaConnection) {
|
|
4528
4639
|
if (this.remoteMediaManager) {
|
|
4529
4640
|
this.remoteMediaManager.stop();
|
|
@@ -4540,8 +4651,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4540
4651
|
this.setNetworkStatus(undefined);
|
|
4541
4652
|
}
|
|
4542
4653
|
|
|
4543
|
-
|
|
4544
|
-
|
|
4654
|
+
if (resetMuteStates) {
|
|
4655
|
+
this.audio = null;
|
|
4656
|
+
this.video = null;
|
|
4657
|
+
}
|
|
4545
4658
|
|
|
4546
4659
|
return Promise.resolve();
|
|
4547
4660
|
}
|
|
@@ -4801,7 +4914,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4801
4914
|
* @param {Object} options - options to join with media
|
|
4802
4915
|
* @param {JoinOptions} [options.joinOptions] - see #join()
|
|
4803
4916
|
* @param {AddMediaOptions} [options.mediaOptions] - see #addMedia()
|
|
4804
|
-
* @returns {Promise} -- {join: see join(), media: see addMedia()}
|
|
4917
|
+
* @returns {Promise} -- {join: see join(), media: see addMedia(), multistreamEnabled: flag to indicate if we managed to join in multistream mode}
|
|
4805
4918
|
* @public
|
|
4806
4919
|
* @memberof Meeting
|
|
4807
4920
|
* @example
|
|
@@ -4891,6 +5004,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4891
5004
|
return {
|
|
4892
5005
|
join: joinResponse,
|
|
4893
5006
|
media: mediaResponse,
|
|
5007
|
+
multistreamEnabled: this.isMultistream,
|
|
4894
5008
|
};
|
|
4895
5009
|
} catch (error) {
|
|
4896
5010
|
LoggerProxy.logger.error('Meeting:index#joinWithMedia --> ', error);
|
|
@@ -4899,7 +5013,17 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4899
5013
|
|
|
4900
5014
|
this.roap.abortTurnDiscovery();
|
|
4901
5015
|
|
|
4902
|
-
if
|
|
5016
|
+
// if this was the first attempt, let's do a retry
|
|
5017
|
+
let shouldRetry = !isRetry;
|
|
5018
|
+
|
|
5019
|
+
if (CallDiagnosticUtils.isSdpOfferCreationError(error)) {
|
|
5020
|
+
// errors related to offer creation (for example missing H264 codec) will happen again no matter how many times we try,
|
|
5021
|
+
// so there is no point doing a retry
|
|
5022
|
+
shouldRetry = false;
|
|
5023
|
+
}
|
|
5024
|
+
|
|
5025
|
+
// we only want to call leave if join was successful and this was a retry or we won't be doing any more retries
|
|
5026
|
+
if (joined && (isRetry || !shouldRetry)) {
|
|
4903
5027
|
try {
|
|
4904
5028
|
await this.leave({resourceId: joinOptions?.resourceId, reason: 'joinWithMedia failure'});
|
|
4905
5029
|
} catch (e) {
|
|
@@ -4923,15 +5047,6 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4923
5047
|
}
|
|
4924
5048
|
);
|
|
4925
5049
|
|
|
4926
|
-
// if this was the first attempt, let's do a retry
|
|
4927
|
-
let shouldRetry = !isRetry;
|
|
4928
|
-
|
|
4929
|
-
if (CallDiagnosticUtils.isSdpOfferCreationError(error)) {
|
|
4930
|
-
// errors related to offer creation (for example missing H264 codec) will happen again no matter how many times we try,
|
|
4931
|
-
// so there is no point doing a retry
|
|
4932
|
-
shouldRetry = false;
|
|
4933
|
-
}
|
|
4934
|
-
|
|
4935
5050
|
if (shouldRetry) {
|
|
4936
5051
|
LoggerProxy.logger.warn('Meeting:index#joinWithMedia --> retrying call to joinWithMedia');
|
|
4937
5052
|
this.joinWithMediaRetryInfo.isRetry = true;
|
|
@@ -5187,7 +5302,16 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5187
5302
|
(this.config.receiveReactions || options.receiveReactions) &&
|
|
5188
5303
|
this.isReactionsSupported()
|
|
5189
5304
|
) {
|
|
5190
|
-
const
|
|
5305
|
+
const member = this.members.membersCollection.get(e.data.sender.participantId);
|
|
5306
|
+
if (!member) {
|
|
5307
|
+
// @ts-ignore -- fix type
|
|
5308
|
+
LoggerProxy.logger.warn(
|
|
5309
|
+
`Meeting:index#processRelayEvent --> Skipping handling of ${REACTION_RELAY_TYPES.REACTION} for ${this.id}. participantId ${e.data.sender.participantId} does not exist in membersCollection.`
|
|
5310
|
+
);
|
|
5311
|
+
break;
|
|
5312
|
+
}
|
|
5313
|
+
|
|
5314
|
+
const {name} = member;
|
|
5191
5315
|
const processedReaction: ProcessedReaction = {
|
|
5192
5316
|
reaction: e.data.reaction,
|
|
5193
5317
|
sender: {
|
|
@@ -5241,6 +5365,9 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5241
5365
|
this.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]
|
|
5242
5366
|
);
|
|
5243
5367
|
|
|
5368
|
+
// @ts-ignore
|
|
5369
|
+
this.webex.internal.voicea.deregisterEvents();
|
|
5370
|
+
|
|
5244
5371
|
this.areVoiceaEventsSetup = false;
|
|
5245
5372
|
this.triggerStopReceivingTranscriptionEvent();
|
|
5246
5373
|
}
|
|
@@ -5351,16 +5478,19 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5351
5478
|
this.meetingFiniteStateMachine.reset();
|
|
5352
5479
|
}
|
|
5353
5480
|
|
|
5354
|
-
//
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5481
|
+
// send client.call.initiated unless told not to
|
|
5482
|
+
if (options.sendCallInitiated !== false) {
|
|
5483
|
+
// @ts-ignore
|
|
5484
|
+
this.webex.internal.newMetrics.submitClientEvent({
|
|
5485
|
+
name: 'client.call.initiated',
|
|
5486
|
+
payload: {
|
|
5487
|
+
trigger: this.callStateForMetrics.joinTrigger || 'user-interaction',
|
|
5488
|
+
isRoapCallEnabled: true,
|
|
5489
|
+
pstnAudioType: options?.pstnAudioType,
|
|
5490
|
+
},
|
|
5491
|
+
options: {meetingId: this.id},
|
|
5492
|
+
});
|
|
5493
|
+
}
|
|
5364
5494
|
|
|
5365
5495
|
LoggerProxy.logger.log('Meeting:index#join --> Joining a meeting');
|
|
5366
5496
|
|
|
@@ -5548,17 +5678,23 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5548
5678
|
*/
|
|
5549
5679
|
async updateLLMConnection() {
|
|
5550
5680
|
// @ts-ignore - Fix type
|
|
5551
|
-
const {url, info: {datachannelUrl} = {}} = this.locusInfo;
|
|
5681
|
+
const {url, info: {datachannelUrl, practiceSessionDatachannelUrl} = {}} = this.locusInfo;
|
|
5552
5682
|
|
|
5553
5683
|
const isJoined = this.isJoined();
|
|
5554
5684
|
|
|
5685
|
+
// webinar panelist should use new data channel in practice session
|
|
5686
|
+
const dataChannelUrl =
|
|
5687
|
+
this.webinar.isJoinPracticeSessionDataChannel() && practiceSessionDatachannelUrl
|
|
5688
|
+
? practiceSessionDatachannelUrl
|
|
5689
|
+
: datachannelUrl;
|
|
5690
|
+
|
|
5555
5691
|
// @ts-ignore - Fix type
|
|
5556
5692
|
if (this.webex.internal.llm.isConnected()) {
|
|
5557
5693
|
if (
|
|
5558
5694
|
// @ts-ignore - Fix type
|
|
5559
5695
|
url === this.webex.internal.llm.getLocusUrl() &&
|
|
5560
5696
|
// @ts-ignore - Fix type
|
|
5561
|
-
|
|
5697
|
+
dataChannelUrl === this.webex.internal.llm.getDatachannelUrl() &&
|
|
5562
5698
|
isJoined
|
|
5563
5699
|
) {
|
|
5564
5700
|
return undefined;
|
|
@@ -5575,7 +5711,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5575
5711
|
|
|
5576
5712
|
// @ts-ignore - Fix type
|
|
5577
5713
|
return this.webex.internal.llm
|
|
5578
|
-
.registerAndConnect(url,
|
|
5714
|
+
.registerAndConnect(url, dataChannelUrl)
|
|
5579
5715
|
.then((registerAndConnectResult) => {
|
|
5580
5716
|
// @ts-ignore - Fix type
|
|
5581
5717
|
this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
|
|
@@ -5947,6 +6083,11 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5947
6083
|
public roapMessageReceived = (roapMessage: RoapMessage) => {
|
|
5948
6084
|
const mediaServer = MeetingsUtil.getMediaServer(roapMessage.sdp);
|
|
5949
6085
|
|
|
6086
|
+
if (this.isMultistream && mediaServer !== 'homer') {
|
|
6087
|
+
throw new MultistreamNotSupportedError(
|
|
6088
|
+
`Client asked for multistream backend (Homer), but got ${mediaServer} instead`
|
|
6089
|
+
);
|
|
6090
|
+
}
|
|
5950
6091
|
this.mediaProperties.webrtcMediaConnection.roapMessageReceived(roapMessage);
|
|
5951
6092
|
|
|
5952
6093
|
if (mediaServer) {
|
|
@@ -6069,16 +6210,20 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6069
6210
|
logText: `${LOG_HEADER} Roap Offer`,
|
|
6070
6211
|
}
|
|
6071
6212
|
).catch((error) => {
|
|
6213
|
+
const multistreamNotSupported = error instanceof MultistreamNotSupportedError;
|
|
6214
|
+
|
|
6072
6215
|
// @ts-ignore
|
|
6073
6216
|
this.webex.internal.newMetrics.submitClientEvent({
|
|
6074
6217
|
name: 'client.media-engine.remote-sdp-received',
|
|
6075
6218
|
payload: {
|
|
6076
|
-
canProceed:
|
|
6219
|
+
canProceed: multistreamNotSupported,
|
|
6077
6220
|
errors: [
|
|
6078
6221
|
// @ts-ignore
|
|
6079
6222
|
this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
|
|
6080
6223
|
{
|
|
6081
|
-
clientErrorCode:
|
|
6224
|
+
clientErrorCode: multistreamNotSupported
|
|
6225
|
+
? CALL_DIAGNOSTIC_CONFIG.MULTISTREAM_NOT_AVAILABLE_CLIENT_CODE
|
|
6226
|
+
: CALL_DIAGNOSTIC_CONFIG.MISSING_ROAP_ANSWER_CLIENT_CODE,
|
|
6082
6227
|
}
|
|
6083
6228
|
),
|
|
6084
6229
|
],
|
|
@@ -6086,7 +6231,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6086
6231
|
options: {meetingId: this.id, rawError: error},
|
|
6087
6232
|
});
|
|
6088
6233
|
|
|
6089
|
-
this.deferSDPAnswer.reject(
|
|
6234
|
+
this.deferSDPAnswer.reject(error);
|
|
6090
6235
|
clearTimeout(this.sdpResponseTimer);
|
|
6091
6236
|
this.sdpResponseTimer = undefined;
|
|
6092
6237
|
});
|
|
@@ -6414,6 +6559,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6414
6559
|
this.webex.meetings.geoHintInfo?.clientAddress ||
|
|
6415
6560
|
options.data.intervalMetadata.peerReflexiveIP ||
|
|
6416
6561
|
MQA_STATS.DEFAULT_IP;
|
|
6562
|
+
|
|
6563
|
+
const {members} = this.getMembers().membersCollection;
|
|
6564
|
+
|
|
6565
|
+
// Count members that are in the meeting
|
|
6566
|
+
options.data.intervalMetadata.meetingUserCount = Object.values(members).filter(
|
|
6567
|
+
(member: Member) => member.isInMeeting
|
|
6568
|
+
).length;
|
|
6569
|
+
|
|
6417
6570
|
// @ts-ignore
|
|
6418
6571
|
this.webex.internal.newMetrics.submitMQE({
|
|
6419
6572
|
name: 'client.mediaquality.event',
|
|
@@ -6745,32 +6898,6 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6745
6898
|
}
|
|
6746
6899
|
}
|
|
6747
6900
|
|
|
6748
|
-
/**
|
|
6749
|
-
* Handles device logging
|
|
6750
|
-
*
|
|
6751
|
-
* @private
|
|
6752
|
-
* @static
|
|
6753
|
-
* @param {boolean} isAudioEnabled
|
|
6754
|
-
* @param {boolean} isVideoEnabled
|
|
6755
|
-
* @returns {Promise<void>}
|
|
6756
|
-
*/
|
|
6757
|
-
|
|
6758
|
-
private static async handleDeviceLogging(isAudioEnabled, isVideoEnabled): Promise<void> {
|
|
6759
|
-
try {
|
|
6760
|
-
let devices = [];
|
|
6761
|
-
if (isVideoEnabled && isAudioEnabled) {
|
|
6762
|
-
devices = await getDevices();
|
|
6763
|
-
} else if (isVideoEnabled) {
|
|
6764
|
-
devices = await getDevices(Media.DeviceKind.VIDEO_INPUT);
|
|
6765
|
-
} else if (isAudioEnabled) {
|
|
6766
|
-
devices = await getDevices(Media.DeviceKind.AUDIO_INPUT);
|
|
6767
|
-
}
|
|
6768
|
-
MeetingUtil.handleDeviceLogging(devices);
|
|
6769
|
-
} catch {
|
|
6770
|
-
// getDevices may fail if we don't have browser permissions, that's ok, we still can have a media connection
|
|
6771
|
-
}
|
|
6772
|
-
}
|
|
6773
|
-
|
|
6774
6901
|
/**
|
|
6775
6902
|
* Returns a promise. This promise is created once the local sdp offer has been successfully created and is resolved
|
|
6776
6903
|
* once the remote sdp answer has been received.
|
|
@@ -6994,7 +7121,9 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6994
7121
|
|
|
6995
7122
|
const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
|
|
6996
7123
|
|
|
6997
|
-
LoggerProxy.logger.info(
|
|
7124
|
+
LoggerProxy.logger.info(
|
|
7125
|
+
`${LOG_HEADER} media connection created this.isMultistream=${this.isMultistream}`
|
|
7126
|
+
);
|
|
6998
7127
|
|
|
6999
7128
|
if (this.isMultistream) {
|
|
7000
7129
|
this.remoteMediaManager = new RemoteMediaManager(
|
|
@@ -7072,6 +7201,33 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7072
7201
|
}
|
|
7073
7202
|
}
|
|
7074
7203
|
|
|
7204
|
+
/**
|
|
7205
|
+
* Cleans up stats analyzer, peer connection and other things before
|
|
7206
|
+
* we can create a new transcoded media connection
|
|
7207
|
+
*
|
|
7208
|
+
* @private
|
|
7209
|
+
* @returns {Promise<void>}
|
|
7210
|
+
*/
|
|
7211
|
+
private async downgradeFromMultistreamToTranscoded(): Promise<void> {
|
|
7212
|
+
if (this.statsAnalyzer) {
|
|
7213
|
+
await this.statsAnalyzer.stopAnalyzer();
|
|
7214
|
+
}
|
|
7215
|
+
this.statsAnalyzer = null;
|
|
7216
|
+
|
|
7217
|
+
this.isMultistream = false;
|
|
7218
|
+
|
|
7219
|
+
if (this.mediaProperties.webrtcMediaConnection) {
|
|
7220
|
+
// close peer connection, but don't reset mute state information, because we will want to use it on the retry
|
|
7221
|
+
this.closePeerConnections(false);
|
|
7222
|
+
|
|
7223
|
+
this.mediaProperties.unsetPeerConnection();
|
|
7224
|
+
}
|
|
7225
|
+
|
|
7226
|
+
this.locusMediaRequest?.downgradeFromMultistreamToTranscoded();
|
|
7227
|
+
|
|
7228
|
+
this.createStatsAnalyzer();
|
|
7229
|
+
}
|
|
7230
|
+
|
|
7075
7231
|
/**
|
|
7076
7232
|
* Sends stats report, closes peer connection and cleans up any media connection
|
|
7077
7233
|
* related things before trying to establish media connection again with turn server
|
|
@@ -7266,19 +7422,33 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7266
7422
|
|
|
7267
7423
|
this.createStatsAnalyzer();
|
|
7268
7424
|
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7425
|
+
try {
|
|
7426
|
+
await this.establishMediaConnection(
|
|
7427
|
+
remoteMediaManagerConfig,
|
|
7428
|
+
bundlePolicy,
|
|
7429
|
+
forceTurnDiscovery,
|
|
7430
|
+
turnServerInfo
|
|
7431
|
+
);
|
|
7432
|
+
} catch (error) {
|
|
7433
|
+
if (error instanceof MultistreamNotSupportedError) {
|
|
7434
|
+
LoggerProxy.logger.warn(
|
|
7435
|
+
`${LOG_HEADER} we asked for multistream backend (Homer), but got transcoded backend, recreating media connection...`
|
|
7436
|
+
);
|
|
7275
7437
|
|
|
7276
|
-
|
|
7277
|
-
await Meeting.handleDeviceLogging(audioEnabled, videoEnabled);
|
|
7278
|
-
} else {
|
|
7279
|
-
LoggerProxy.logger.info(`${LOG_HEADER} device logging not required`);
|
|
7280
|
-
}
|
|
7438
|
+
await this.downgradeFromMultistreamToTranscoded();
|
|
7281
7439
|
|
|
7440
|
+
// Establish new media connection with forced TURN discovery
|
|
7441
|
+
// We need to do TURN discovery again, because backend will be creating a new confluence, so it might land on a different node or cluster
|
|
7442
|
+
await this.establishMediaConnection(
|
|
7443
|
+
remoteMediaManagerConfig,
|
|
7444
|
+
bundlePolicy,
|
|
7445
|
+
true,
|
|
7446
|
+
undefined
|
|
7447
|
+
);
|
|
7448
|
+
} else {
|
|
7449
|
+
throw error;
|
|
7450
|
+
}
|
|
7451
|
+
}
|
|
7282
7452
|
if (this.mediaProperties.hasLocalShareStream()) {
|
|
7283
7453
|
await this.enqueueScreenShareFloorRequest();
|
|
7284
7454
|
}
|
|
@@ -8248,7 +8418,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8248
8418
|
if (layoutType) {
|
|
8249
8419
|
if (!LAYOUT_TYPES.includes(layoutType)) {
|
|
8250
8420
|
return this.rejectWithErrorLog(
|
|
8251
|
-
|
|
8421
|
+
`Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType "${layoutType}" received.`
|
|
8252
8422
|
);
|
|
8253
8423
|
}
|
|
8254
8424
|
|
|
@@ -8606,6 +8776,11 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8606
8776
|
this.stopTranscription();
|
|
8607
8777
|
this.transcription = undefined;
|
|
8608
8778
|
}
|
|
8779
|
+
|
|
8780
|
+
this.annotation.deregisterEvents();
|
|
8781
|
+
|
|
8782
|
+
// @ts-ignore - fix types
|
|
8783
|
+
this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
|
|
8609
8784
|
};
|
|
8610
8785
|
|
|
8611
8786
|
/**
|
|
@@ -8643,10 +8818,12 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8643
8818
|
|
|
8644
8819
|
return;
|
|
8645
8820
|
}
|
|
8646
|
-
|
|
8821
|
+
|
|
8647
8822
|
const keepAliveInterval = (this.joinedWith.keepAliveSecs - 1) * 750; // taken from UCF
|
|
8648
8823
|
|
|
8649
8824
|
this.keepAliveTimerId = setInterval(() => {
|
|
8825
|
+
const {keepAliveUrl} = this.joinedWith;
|
|
8826
|
+
|
|
8650
8827
|
this.meetingRequest.keepAlive({keepAliveUrl}).catch((error) => {
|
|
8651
8828
|
LoggerProxy.logger.warn(
|
|
8652
8829
|
`Meeting:index#startKeepAlive --> Stopping sending keepAlives to ${keepAliveUrl} after error ${error}`
|
|
@@ -342,4 +342,11 @@ export class LocusMediaRequest extends WebexPlugin {
|
|
|
342
342
|
public isConfluenceCreated() {
|
|
343
343
|
return this.confluenceState === 'created';
|
|
344
344
|
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* This method needs to be called when we downgrade from multistream to transcoded connection.
|
|
348
|
+
*/
|
|
349
|
+
public downgradeFromMultistreamToTranscoded() {
|
|
350
|
+
this.config.preferTranscoding = true;
|
|
351
|
+
}
|
|
345
352
|
}
|