@webex/plugin-meetings 2.31.4 → 2.32.0

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.
@@ -125,6 +125,7 @@ export const MEDIA_UPDATE_TYPE = {
125
125
  * @property {String} audio.deviceId
126
126
  * @property {Object} video
127
127
  * @property {String} video.deviceId
128
+ * @property {String} video.localVideoQuality // [240p, 360p, 480p, 720p, 1080p]
128
129
  */
129
130
 
130
131
  /**
@@ -2742,6 +2743,15 @@ export default class Meeting extends StatelessWebexPlugin {
2742
2743
  aspectRatio, frameRate, height, width, deviceId
2743
2744
  } = videoTrack.getSettings();
2744
2745
 
2746
+ const {localQualityLevel} = this.mediaProperties;
2747
+
2748
+ if (Number(localQualityLevel.slice(0, -1)) > height) {
2749
+ LoggerProxy.logger.error(`Meeting:index#setLocalVideoTrack --> Local video quality of ${localQualityLevel} not supported,
2750
+ downscaling to highest possible resolution of ${height}p`);
2751
+
2752
+ this.mediaProperties.setLocalQualityLevel(`${height}p`);
2753
+ }
2754
+
2745
2755
  this.mediaProperties.setLocalVideoTrack(videoTrack);
2746
2756
  if (this.video) this.video.applyClientStateLocally(this);
2747
2757
 
@@ -4004,6 +4014,9 @@ export default class Meeting extends StatelessWebexPlugin {
4004
4014
  LoggerProxy.logger.warn('Meeting:index#getMediaStreams --> Please use `meeting.shareScreen()` to manually start the screen share after successfully joining the meeting');
4005
4015
  }
4006
4016
 
4017
+ if (!audioVideo.video) {
4018
+ audioVideo = {...audioVideo, video: {...audioVideo.video, ...VIDEO_RESOLUTIONS[this.mediaProperties.localQualityLevel].video}};
4019
+ }
4007
4020
  // extract deviceId if exists otherwise default to null.
4008
4021
  const {deviceId: preferredVideoDevice} = (audioVideo && audioVideo.video || {deviceId: null});
4009
4022
  const lastVideoDeviceId = this.mediaProperties.getVideoDeviceId();
@@ -5342,7 +5355,7 @@ export default class Meeting extends StatelessWebexPlugin {
5342
5355
  /**
5343
5356
  * Sets the quality of the local video stream
5344
5357
  * @param {String} level {LOW|MEDIUM|HIGH}
5345
- * @returns {Promise}
5358
+ * @returns {Promise<MediaStream>} localStream
5346
5359
  */
5347
5360
  setLocalVideoQuality(level) {
5348
5361
  LoggerProxy.logger.log(`Meeting:index#setLocalVideoQuality --> Setting quality to ${level}`);
@@ -5371,13 +5384,22 @@ export default class Meeting extends StatelessWebexPlugin {
5371
5384
  sendShare: this.mediaProperties.mediaDirection.sendShare
5372
5385
  };
5373
5386
 
5387
+ // When changing local video quality level
5388
+ // Need to stop current track first as chrome doesn't support resolution upscaling(for eg. changing 480p to 720p)
5389
+ // Without feeding it a new track
5390
+ // open bug link: https://bugs.chromium.org/p/chromium/issues/detail?id=943469
5391
+ if (isBrowser('chrome') && this.mediaProperties.videoTrack) Media.stopTracks(this.mediaProperties.videoTrack);
5392
+
5374
5393
  return this.getMediaStreams(mediaDirection, VIDEO_RESOLUTIONS[level])
5375
- .then(([localStream]) =>
5376
- this.updateVideo({
5394
+ .then(async ([localStream]) => {
5395
+ await this.updateVideo({
5377
5396
  sendVideo: true,
5378
5397
  receiveVideo: true,
5379
5398
  stream: localStream
5380
- }));
5399
+ });
5400
+
5401
+ return localStream;
5402
+ });
5381
5403
  }
5382
5404
 
5383
5405
  /**
@@ -5410,9 +5432,10 @@ export default class Meeting extends StatelessWebexPlugin {
5410
5432
  }
5411
5433
 
5412
5434
  /**
5413
- * Sets the quality level of all meeting media (incoming/outgoing)
5435
+ * This is deprecated, please use setLocalVideoQuality for setting local and setRemoteQualityLevel for remote
5414
5436
  * @param {String} level {LOW|MEDIUM|HIGH}
5415
5437
  * @returns {Promise}
5438
+ * @deprecated After FHD support
5416
5439
  */
5417
5440
  setMeetingQuality(level) {
5418
5441
  LoggerProxy.logger.log(`Meeting:index#setMeetingQuality --> Setting quality to ${level}`);
@@ -20,7 +20,7 @@ import {
20
20
  PEER_CONNECTION_STATE,
21
21
  OFFER,
22
22
  QUALITY_LEVELS,
23
- MAX_FRAMESIZES
23
+ REMOTE_VIDEO_CONSTRAINTS
24
24
  } from '../constants';
25
25
  import BEHAVIORAL_METRICS from '../metrics/constants';
26
26
  import {error, eventType} from '../metrics/config';
@@ -70,18 +70,16 @@ const insertBandwidthLimit = (sdpLines, index) => {
70
70
  * @param {String} [level=QUALITY_LEVELS.HIGH] quality level for max-fs
71
71
  * @returns {String}
72
72
  */
73
- const setMaxFs = (sdp, level = QUALITY_LEVELS.HIGH) => {
74
- if (!MAX_FRAMESIZES[level]) {
75
- throw new ParameterError(`setMaxFs: unable to set max framesize, value for level "${level}" is not defined`);
73
+ const setRemoteVideoConstraints = (sdp, level = QUALITY_LEVELS.HIGH) => {
74
+ const maxFs = REMOTE_VIDEO_CONSTRAINTS.MAX_FS[level];
75
+
76
+ if (!maxFs) {
77
+ throw new ParameterError(`setRemoteVideoConstraints: unable to set max framesize, value for level "${level}" is not defined`);
76
78
  }
77
- // eslint-disable-next-line no-warning-comments
78
- // TODO convert with sdp parser, no munging
79
- let replaceSdp = sdp;
80
- const maxFsLine = `${SDP.MAX_FS}${MAX_FRAMESIZES[level]}`;
81
79
 
82
- replaceSdp = replaceSdp.replace(/(\na=fmtp:(\d+).*profile-level-id=.*)/gi, `$1;${maxFsLine}`);
80
+ const modifiedSdp = PeerConnectionUtils.adjustH264Profile(sdp, maxFs);
83
81
 
84
- return replaceSdp;
82
+ return modifiedSdp;
85
83
  };
86
84
 
87
85
 
@@ -188,8 +186,8 @@ pc.iceCandidate = (peerConnection, {remoteQualityLevel}) =>
188
186
  const miliseconds = parseInt(Math.abs(Date.now() - now), 4);
189
187
 
190
188
  peerConnection.sdp = limitBandwidth(peerConnection.localDescription.sdp);
191
- peerConnection.sdp = setMaxFs(peerConnection.sdp, remoteQualityLevel);
192
189
  peerConnection.sdp = PeerConnectionUtils.convertCLineToIpv4(peerConnection.sdp);
190
+ peerConnection.sdp = setRemoteVideoConstraints(peerConnection.sdp, remoteQualityLevel);
193
191
 
194
192
  const invalidSdpPresent = isSdpInvalid(peerConnection.sdp);
195
193
 
@@ -540,8 +538,9 @@ pc.createAnswer = (params, {meetingId, remoteQualityLevel}) => {
540
538
  .then(() => pc.iceCandidate(peerConnection, {remoteQualityLevel}))
541
539
  .then(() => {
542
540
  peerConnection.sdp = limitBandwidth(peerConnection.localDescription.sdp);
543
- peerConnection.sdp = setMaxFs(peerConnection.sdp, remoteQualityLevel);
544
541
  peerConnection.sdp = PeerConnectionUtils.convertCLineToIpv4(peerConnection.sdp);
542
+ peerConnection.sdp = setRemoteVideoConstraints(peerConnection.sdp, remoteQualityLevel);
543
+
545
544
  if (!checkH264Support(peerConnection.sdp)) {
546
545
  throw new MediaError('openH264 is downloading please Wait. Upload logs if not working on second try');
547
546
  }
@@ -0,0 +1,117 @@
1
+ import { parse } from '@webex/ts-sdp';
2
+
3
+ interface IPeerConnectionUtils {
4
+ convertCLineToIpv4: (sdp: string) => string;
5
+ adjustH264Profile: (sdp: string, maxFsValue: number) => string;
6
+ }
7
+
8
+ const PeerConnectionUtils = {} as IPeerConnectionUtils;
9
+
10
+ // max-fs values for all H264 profile levels
11
+ const maxFsForProfileLevel = {
12
+ 10: 99,
13
+ 11: 396,
14
+ 12: 396,
15
+ 13: 396,
16
+ 20: 396,
17
+ 21: 792,
18
+ 22: 1620,
19
+ 30: 1620,
20
+ 31: 3600,
21
+ 32: 5120,
22
+ 40: 8192,
23
+ 41: 8192,
24
+ 42: 8704,
25
+ 50: 22080,
26
+ 51: 36864,
27
+ 52: 36864,
28
+ 60: 139264,
29
+ 61: 139264,
30
+ 62: 139264,
31
+ };
32
+
33
+ const framesPerSecond = 30;
34
+
35
+ /**
36
+ * Convert C line to IPv4
37
+ * @param {string} sdp
38
+ * @returns {string}
39
+ */
40
+ PeerConnectionUtils.convertCLineToIpv4 = (sdp: string) => {
41
+ let replaceSdp = sdp;
42
+
43
+ // TODO: remove this once linus supports Ipv6 c line.currently linus rejects SDP with c line having ipv6 candidates we are
44
+ // mocking ipv6 to ipv4 candidates
45
+ // https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-299232
46
+ replaceSdp = replaceSdp.replace(/c=IN IP6 .*/gi, 'c=IN IP4 0.0.0.0');
47
+
48
+ return replaceSdp;
49
+ };
50
+
51
+ /**
52
+ * estimate profile levels for max-fs & max-mbps values
53
+ * @param {string} sdp
54
+ * @param {number} maxFsValue
55
+ * @returns {string}
56
+ */
57
+ PeerConnectionUtils.adjustH264Profile = (sdp: string, maxFsValue: number) => {
58
+ // converting with ts-sdp parser, no munging
59
+ const parsedSdp = parse(sdp);
60
+
61
+ parsedSdp.avMedia.forEach((media) => {
62
+ if (media.type === 'video') {
63
+ media.codecs.forEach((codec) => {
64
+ if (codec.name?.toUpperCase() === 'H264') {
65
+ // there should really be just 1 fmtp line, but just in case, we process all of them
66
+ codec.fmtParams = codec.fmtParams.map((fmtp) => {
67
+ const parsedRegex = fmtp.match(/(.*)profile-level-id=(\w{4})(\w{2})(.*)/);
68
+
69
+ if (parsedRegex && parsedRegex.length === 5) {
70
+ const stuffBeforeProfileLevelId = parsedRegex[1];
71
+ const profile = parsedRegex[2].toLowerCase();
72
+ const levelId = parseInt(parsedRegex[3], 16);
73
+ const stuffAfterProfileLevelId = parsedRegex[4];
74
+
75
+ if (!maxFsForProfileLevel[levelId]) {
76
+ throw new Error(
77
+ `found unsupported h264 profile level id value in the SDP: ${levelId}`
78
+ );
79
+ }
80
+
81
+ if (maxFsForProfileLevel[levelId] === maxFsValue) {
82
+ // profile level already matches our desired max-fs value, so we don't need to do anything
83
+ return fmtp;
84
+ }
85
+ if (maxFsForProfileLevel[levelId] < maxFsValue) {
86
+ // profile level has too low max-fs, so we need to override it (this is upgrading)
87
+ return `${fmtp};max-fs=${maxFsValue};max-mbps=${maxFsValue * framesPerSecond}`;
88
+ }
89
+
90
+ // profile level has too high max-fs value, so we need to use a lower level
91
+
92
+ // find highest level that has the matching maxFs
93
+ const newLevelId = Object.keys(maxFsForProfileLevel)
94
+ .reverse()
95
+ .find((key) => maxFsForProfileLevel[key] === maxFsValue);
96
+
97
+ if (newLevelId) {
98
+ // Object.keys returns keys as strings, so we need to parse it to an int again and then convert to hex
99
+ const newLevelIdHex = parseInt(newLevelId, 10).toString(16);
100
+
101
+ return `${stuffBeforeProfileLevelId}profile-level-id=${profile}${newLevelIdHex};max-mbps=${maxFsValue * framesPerSecond}${stuffAfterProfileLevelId}`;
102
+ }
103
+
104
+ throw new Error(`unsupported maxFsValue: ${maxFsValue}`);
105
+ }
106
+
107
+ return fmtp;
108
+ });
109
+ }
110
+ });
111
+ }
112
+ });
113
+
114
+ return parsedSdp.toString();
115
+ };
116
+
117
+ export default PeerConnectionUtils;
@@ -671,7 +671,7 @@ describe('plugin-meetings', () => {
671
671
  const audioVideoSettings = {};
672
672
 
673
673
  sinon.stub(meeting.mediaProperties, 'videoDeviceId').value(videoDevice);
674
-
674
+ sinon.stub(meeting.mediaProperties, 'localQualityLevel').value('480p');
675
675
  await meeting.getMediaStreams(mediaDirection, audioVideoSettings);
676
676
 
677
677
  assert.calledWith(Media.getUserMedia, {
@@ -680,6 +680,8 @@ describe('plugin-meetings', () => {
680
680
  },
681
681
  {
682
682
  video: {
683
+ width: {max: 640, ideal: 640},
684
+ height: {max: 480, ideal: 480},
683
685
  deviceId: videoDevice
684
686
  }
685
687
  });
@@ -701,6 +703,30 @@ describe('plugin-meetings', () => {
701
703
  assert.calledWith(meeting.mediaProperties.setVideoDeviceId, newVideoDevice);
702
704
  });
703
705
 
706
+ it('uses the passed custom video resolution', async () => {
707
+ const mediaDirection = {sendAudio: true, sendVideo: true, sendShare: false};
708
+ const customAudioVideoSettings = {
709
+ video: {
710
+ width: {
711
+ max: 400,
712
+ ideal: 400
713
+ },
714
+ height: {
715
+ max: 200,
716
+ ideal: 200
717
+ }
718
+ }
719
+ };
720
+
721
+ sinon.stub(meeting.mediaProperties, 'localQualityLevel').value('200p');
722
+ await meeting.getMediaStreams(mediaDirection, customAudioVideoSettings);
723
+
724
+ assert.calledWith(Media.getUserMedia, {
725
+ ...mediaDirection,
726
+ isSharing: false
727
+ },
728
+ customAudioVideoSettings);
729
+ });
704
730
  it('should not access camera if sendVideo is false ', async () => {
705
731
  await meeting.getMediaStreams({sendAudio: true, sendVideo: false});
706
732
 
@@ -2155,11 +2181,19 @@ describe('plugin-meetings', () => {
2155
2181
  describe('#setLocalVideoQuality', () => {
2156
2182
  let mediaDirection;
2157
2183
 
2184
+ const fakeTrack = {getSettings: () => ({height: 720})};
2185
+ const USER_AGENT_CHROME_MAC =
2186
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' +
2187
+ 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36';
2188
+
2158
2189
  beforeEach(() => {
2159
2190
  mediaDirection = {sendAudio: true, sendVideo: true, sendShare: false};
2160
2191
  meeting.getMediaStreams = sinon.stub().returns(Promise.resolve([]));
2161
- meeting.updateVideo = sinon.stub().returns(Promise.resolve());
2162
2192
  meeting.mediaProperties.mediaDirection = mediaDirection;
2193
+ meeting.canUpdateMedia = sinon.stub().returns(true);
2194
+ MeetingUtil.validateOptions = sinon.stub().returns(Promise.resolve());
2195
+ MeetingUtil.updateTransceiver = sinon.stub().returns(Promise.resolve());
2196
+ sinon.stub(MeetingUtil, 'getTrack').returns({videoTrack: fakeTrack});
2163
2197
  });
2164
2198
 
2165
2199
  it('should have #setLocalVideoQuality', () => {
@@ -2167,15 +2201,35 @@ describe('plugin-meetings', () => {
2167
2201
  });
2168
2202
 
2169
2203
  it('should call getMediaStreams with the proper level', () => meeting.setLocalVideoQuality(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
2204
+ delete mediaDirection.receiveVideo;
2170
2205
  assert.calledWith(meeting.getMediaStreams,
2171
2206
  mediaDirection,
2172
2207
  CONSTANTS.VIDEO_RESOLUTIONS[CONSTANTS.QUALITY_LEVELS.LOW]);
2173
2208
  }));
2174
2209
 
2210
+ it('when browser is chrome then it should stop previous video track', () => {
2211
+ meeting.mediaProperties.videoTrack = fakeTrack;
2212
+ assert.equal(
2213
+ BrowserDetection(USER_AGENT_CHROME_MAC).getBrowserName(),
2214
+ 'Chrome'
2215
+ );
2216
+ meeting.setLocalVideoQuality(CONSTANTS.QUALITY_LEVELS.LOW)
2217
+ .then(() => {
2218
+ assert.calledWith(Media.stopTracks, fakeTrack);
2219
+ });
2220
+ });
2221
+
2175
2222
  it('should set mediaProperty with the proper level', () => meeting.setLocalVideoQuality(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
2176
2223
  assert.equal(meeting.mediaProperties.localQualityLevel, CONSTANTS.QUALITY_LEVELS.LOW);
2177
2224
  }));
2178
2225
 
2226
+ it('when device does not support 1080p then it should set localQualityLevel with highest possible resolution', () => {
2227
+ meeting.setLocalVideoQuality(CONSTANTS.QUALITY_LEVELS['1080p'])
2228
+ .then(() => {
2229
+ assert.equal(meeting.mediaProperties.localQualityLevel, CONSTANTS.QUALITY_LEVELS['720p']);
2230
+ });
2231
+ });
2232
+
2179
2233
  it('should error if set to a invalid level', () => {
2180
2234
  assert.isRejected(meeting.setLocalVideoQuality('invalid'));
2181
2235
  });
@@ -2217,54 +2271,6 @@ describe('plugin-meetings', () => {
2217
2271
  });
2218
2272
  });
2219
2273
 
2220
- describe('#setMeetingQuality', () => {
2221
- let mediaDirection;
2222
-
2223
- beforeEach(() => {
2224
- mediaDirection = {
2225
- receiveAudio: true, receiveVideo: true, receiveShare: false, sendVideo: true
2226
- };
2227
- meeting.setRemoteQualityLevel = sinon.stub().returns(Promise.resolve());
2228
- meeting.setLocalVideoQuality = sinon.stub().returns(Promise.resolve());
2229
- meeting.mediaProperties.mediaDirection = mediaDirection;
2230
- });
2231
-
2232
- it('should have #setMeetingQuality', () => {
2233
- assert.exists(meeting.setMeetingQuality);
2234
- });
2235
-
2236
- it('should call setRemoteQualityLevel', () => meeting.setMeetingQuality(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
2237
- assert.calledOnce(meeting.setRemoteQualityLevel);
2238
- }));
2239
-
2240
- it('should not call setRemoteQualityLevel when receiveVideo and receiveAudio are false', () => {
2241
- mediaDirection.receiveAudio = false;
2242
- mediaDirection.receiveVideo = false;
2243
- meeting.mediaProperties.mediaDirection = mediaDirection;
2244
-
2245
- return meeting.setMeetingQuality(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
2246
- assert.notCalled(meeting.setRemoteQualityLevel);
2247
- });
2248
- });
2249
-
2250
- it('should call setLocalVideoQuality', () => meeting.setMeetingQuality(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
2251
- assert.calledOnce(meeting.setLocalVideoQuality);
2252
- }));
2253
-
2254
- it('should not call setLocalVideoQuality when sendVideo is false', () => {
2255
- mediaDirection.sendVideo = false;
2256
- meeting.mediaProperties.mediaDirection = mediaDirection;
2257
-
2258
- return meeting.setMeetingQuality(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
2259
- assert.notCalled(meeting.setLocalVideoQuality);
2260
- });
2261
- });
2262
-
2263
- it('should error if set to a invalid level', () => {
2264
- assert.isRejected(meeting.setMeetingQuality('invalid'));
2265
- });
2266
- });
2267
-
2268
2274
  describe('#usePhoneAudio', () => {
2269
2275
  beforeEach(() => {
2270
2276
  meeting.meetingRequest.dialIn = sinon.stub().returns(Promise.resolve({body: {locus: 'testData'}}));
@@ -1,19 +1,24 @@
1
-
2
1
  import {assert} from '@webex/test-helper-chai';
3
2
  import PeerConnectionUtils from '@webex/plugin-meetings/src/peer-connection-manager/util';
4
3
 
4
+ import {
5
+ SDP_MULTIPLE_VIDEO_CODECS,
6
+ SDP_MULTIPLE_VIDEO_CODECS_WITH_LOWERED_H264_PROFILE_LEVEL,
7
+ SDP_MULTIPLE_VIDEO_CODECS_WITH_MAX_FS
8
+ } from './utils.test-fixtures';
9
+
5
10
  describe('Peerconnection Manager', () => {
6
11
  describe('Utils', () => {
7
12
  describe('convertCLineToIpv4', () => {
8
13
  it('changes ipv6 to ipv4 default', () => {
9
14
  const localSdp = 'v=0\r\n' +
10
- 'm=video 5004 UDP/TLS/RTP/SAVPF 102 127 97 99\r\n' +
11
- 'c=IN IP6 2607:fb90:d27c:b314:211a:32dd:c47f:ffe\r\n' +
12
- 'a=rtpmap:127 H264/90000\r\n';
15
+ 'm=video 5004 UDP/TLS/RTP/SAVPF 102 127 97 99\r\n' +
16
+ 'c=IN IP6 2607:fb90:d27c:b314:211a:32dd:c47f:ffe\r\n' +
17
+ 'a=rtpmap:127 H264/90000\r\n';
13
18
  const resultSdp = 'v=0\r\n' +
14
- 'm=video 5004 UDP/TLS/RTP/SAVPF 102 127 97 99\r\n' +
15
- 'c=IN IP4 0.0.0.0\r\n' +
16
- 'a=rtpmap:127 H264/90000\r\n';
19
+ 'm=video 5004 UDP/TLS/RTP/SAVPF 102 127 97 99\r\n' +
20
+ 'c=IN IP4 0.0.0.0\r\n' +
21
+ 'a=rtpmap:127 H264/90000\r\n';
17
22
 
18
23
 
19
24
  const temp = PeerConnectionUtils.convertCLineToIpv4(localSdp);
@@ -21,5 +26,23 @@ describe('Peerconnection Manager', () => {
21
26
  assert.equal(temp, resultSdp);
22
27
  });
23
28
  });
29
+
30
+ describe('adjustH264Profile', () => {
31
+ it('appends max-fs and max-mbps to h264 fmtp lines when h264MaxFs value is higher than the value from the profile', () => {
32
+ const modifiedSdp = PeerConnectionUtils.adjustH264Profile(SDP_MULTIPLE_VIDEO_CODECS, 8192);
33
+
34
+ assert.equal(modifiedSdp, SDP_MULTIPLE_VIDEO_CODECS_WITH_MAX_FS);
35
+ });
36
+ it('keeps fmtp lines the same when h264MaxFs value matches the value from the profile', () => {
37
+ const modifiedSdp = PeerConnectionUtils.adjustH264Profile(SDP_MULTIPLE_VIDEO_CODECS, 3600);
38
+
39
+ assert.equal(modifiedSdp, SDP_MULTIPLE_VIDEO_CODECS);
40
+ });
41
+ it('changes the profile level in h264 fmtp lines when h264MaxFs value is lower than the value from the profile', () => {
42
+ const modifiedSdp = PeerConnectionUtils.adjustH264Profile(SDP_MULTIPLE_VIDEO_CODECS, 1620);
43
+
44
+ assert.equal(modifiedSdp, SDP_MULTIPLE_VIDEO_CODECS_WITH_LOWERED_H264_PROFILE_LEVEL);
45
+ });
46
+ });
24
47
  });
25
48
  });