RTCRtpParameters-codecs.html (21103B)
1 <!doctype html> 2 <meta charset=utf-8> 3 <meta name="timeout" content="long"> 4 <title>RTCRtpParameters codecs</title> 5 <script src="/resources/testharness.js"></script> 6 <script src="/resources/testharnessreport.js"></script> 7 <script src="dictionary-helper.js"></script> 8 <script src="RTCRtpParameters-helper.js"></script> 9 <script src="RTCPeerConnection-helper.js"></script> 10 <script src="./third_party/sdp/sdp.js"></script> 11 <script> 12 'use strict'; 13 14 // Test is based on the following editor draft: 15 // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html 16 17 // The following helper functions are called from RTCRtpParameters-helper.js: 18 // doOfferAnswerExchange 19 // validateSenderRtpParameters 20 21 /* 22 5.2. RTCRtpSender Interface 23 interface RTCRtpSender { 24 Promise<void> setParameters(optional RTCRtpParameters parameters); 25 RTCRtpParameters getParameters(); 26 }; 27 28 dictionary RTCRtpParameters { 29 DOMString transactionId; 30 sequence<RTCRtpEncodingParameters> encodings; 31 sequence<RTCRtpHeaderExtensionParameters> headerExtensions; 32 RTCRtcpParameters rtcp; 33 sequence<RTCRtpCodecParameters> codecs; 34 }; 35 36 dictionary RTCRtpCodecParameters { 37 [readonly] 38 unsigned short payloadType; 39 40 [readonly] 41 DOMString mimeType; 42 43 [readonly] 44 unsigned long clockRate; 45 46 [readonly] 47 unsigned short channels; 48 49 [readonly] 50 DOMString sdpFmtpLine; 51 }; 52 53 getParameters 54 - The codecs sequence is populated based on the codecs that have been negotiated 55 for sending, and which the user agent is currently capable of sending. 56 57 If setParameters has removed or reordered codecs, getParameters MUST return 58 the shortened/reordered list. However, every time codecs are renegotiated by 59 a new offer/answer exchange, the list of codecs MUST be restored to the full 60 negotiated set, in the priority order indicated by the remote description, 61 in effect discarding the effects of setParameters. 62 63 codecs 64 - When using the setParameters method, the codecs sequence from the corresponding 65 call to getParameters can be reordered and entries can be removed, but entries 66 cannot be added, and the RTCRtpCodecParameters dictionary members cannot be modified. 67 */ 68 69 // Get the first codec from param.codecs. 70 // Assert that param.codecs has at least one element 71 function getFirstCodec(param) { 72 const { codecs } = param; 73 assert_greater_than(codecs.length, 0); 74 return codecs[0]; 75 } 76 77 function compareCodecParam(observed, expected) { 78 assert_equals(observed.payloadType, expected.payloadType); 79 assert_equals(observed.clockRate, expected.clockRate); 80 assert_equals(observed.channels, expected.channels); 81 // Comparisons of mime-type are case-insensitive (see 82 // https://datatracker.ietf.org/doc/html/rfc2045#section-5.1) 83 assert_equals(observed.mimeType.toLowerCase(), expected.mimeType.toLowerCase()); 84 // This is not ideal; Firefox does not store fmtp verbatim, it stores a 85 // reserialiaztion of the parsed form. We would like to be able to test 86 // the other fields without tripping over that. So, we only test 87 // sdpFmtpLine if it is a property of |expected|. 88 if (expected.hasOwnProperty('sdpFmtpLine')) { 89 assert_equals(observed.sdpFmtpLine, expected.sdpFmtpLine); 90 } 91 } 92 93 function compareCodecParams(observed, expected) { 94 assert_equals(observed.length, expected.length); 95 for (let i = 0; i < observed.length; ++i) { 96 compareCodecParam(observed[i], expected[i]); 97 } 98 } 99 100 // Does not support disregarding unsupported codecs in the SDP, so is not 101 // suitable for all test-cases. 102 function checkCodecsAgainstSDP(codecs, msection) { 103 const rtpParameters = SDPUtils.parseRtpParameters(msection); 104 const {kind} = SDPUtils.parseMLine(msection); 105 106 assert_not_equals(codecs.length, 0); 107 assert_equals(codecs.length, rtpParameters.codecs.length); 108 for (let i = 0; i < codecs.length; ++i) { 109 const observed = codecs[i]; 110 const fromSdp = rtpParameters.codecs[i]; 111 const expected = { 112 payloadType: fromSdp.payloadType, 113 clockRate: fromSdp.clockRate, 114 mimeType: `${kind}/${fromSdp.name}`, 115 }; 116 if (kind == 'audio') { 117 expected.channels = fromSdp.channels; 118 } 119 const fmtps = SDPUtils.matchPrefixAndTrim(msection, `a=fmtp:${fromSdp.payloadType}`); 120 if (fmtps.length == 1) { 121 expected.sdpFmtpLine = fmtps[0]; 122 } else { 123 // compareCodecParam will check if observed.sdpFmtpLine is undefined if we 124 // set this, but will not perform any checks if we do not. 125 expected.sdpFmtpLine = undefined; 126 } 127 compareCodecParam(observed, expected); 128 } 129 } 130 131 ['audio', 'video'].forEach(kind => { 132 /* 133 5.2. setParameters 134 7. If parameters.encodings.length is different from N, or if any parameter 135 in the parameters argument, marked as a Read-only parameter, has a value 136 that is different from the corresponding parameter value returned from 137 sender.getParameters(), abort these steps and return a promise rejected 138 with a newly created InvalidModificationError. Note that this also applies 139 to transactionId. 140 */ 141 promise_test(async t => { 142 const pc = new RTCPeerConnection(); 143 t.add_cleanup(() => pc.close()); 144 145 const { sender } = pc.addTransceiver(kind); 146 await doOfferAnswerExchange(t, pc); 147 148 const param = sender.getParameters(); 149 validateSenderRtpParameters(param); 150 151 const codec = getFirstCodec(param); 152 153 if(codec.payloadType === undefined) { 154 codec.payloadType = 8; 155 } else { 156 codec.payloadType += 1; 157 } 158 159 return promise_rejects_dom(t, 'InvalidModificationError', 160 sender.setParameters(param)); 161 }, `setParameters() with codec.payloadType modified should reject with InvalidModificationError (${kind})`); 162 163 promise_test(async t => { 164 const pc = new RTCPeerConnection(); 165 t.add_cleanup(() => pc.close()); 166 const { sender } = pc.addTransceiver(kind); 167 await doOfferAnswerExchange(t, pc); 168 const param = sender.getParameters(); 169 validateSenderRtpParameters(param); 170 171 const codec = getFirstCodec(param); 172 173 if(codec.mimeType === undefined) { 174 codec.mimeType = `${kind}/piedpiper`; 175 } else { 176 codec.mimeType = `${codec.mimeType}-modified`; 177 } 178 179 return promise_rejects_dom(t, 'InvalidModificationError', 180 sender.setParameters(param)); 181 }, `setParameters() with codec.mimeType modified should reject with InvalidModificationError (${kind})`); 182 183 promise_test(async t => { 184 const pc = new RTCPeerConnection(); 185 t.add_cleanup(() => pc.close()); 186 const { sender } = pc.addTransceiver(kind); 187 await doOfferAnswerExchange(t, pc); 188 const param = sender.getParameters(); 189 validateSenderRtpParameters(param); 190 191 const codec = getFirstCodec(param); 192 193 if(codec.clockRate === undefined) { 194 codec.clockRate = 8000; 195 } else { 196 codec.clockRate += 1; 197 } 198 199 return promise_rejects_dom(t, 'InvalidModificationError', 200 sender.setParameters(param)); 201 }, `setParameters() with codec.clockRate modified should reject with InvalidModificationError (${kind})`); 202 203 promise_test(async t => { 204 const pc = new RTCPeerConnection(); 205 t.add_cleanup(() => pc.close()); 206 const { sender } = pc.addTransceiver(kind); 207 await doOfferAnswerExchange(t, pc); 208 const param = sender.getParameters(); 209 validateSenderRtpParameters(param); 210 211 const codec = getFirstCodec(param); 212 213 if(codec.channels === undefined) { 214 codec.channels = 6; 215 } else { 216 codec.channels += 1; 217 } 218 219 return promise_rejects_dom(t, 'InvalidModificationError', 220 sender.setParameters(param)); 221 }, `setParameters() with codec.channels modified should reject with InvalidModificationError (${kind})`); 222 223 promise_test(async t => { 224 const pc = new RTCPeerConnection(); 225 t.add_cleanup(() => pc.close()); 226 const { sender } = pc.addTransceiver(kind); 227 await doOfferAnswerExchange(t, pc); 228 const param = sender.getParameters(); 229 validateSenderRtpParameters(param); 230 231 const codec = getFirstCodec(param); 232 233 if(codec.sdpFmtpLine === undefined) { 234 codec.sdpFmtpLine = 'a=fmtp:98 0-15'; 235 } else { 236 codec.sdpFmtpLine = `${codec.sdpFmtpLine};foo=1`; 237 } 238 239 return promise_rejects_dom(t, 'InvalidModificationError', 240 sender.setParameters(param)); 241 }, `setParameters() with codec.sdpFmtpLine modified should reject with InvalidModificationError (${kind})`); 242 243 promise_test(async t => { 244 const pc = new RTCPeerConnection(); 245 t.add_cleanup(() => pc.close()); 246 const { sender } = pc.addTransceiver(kind); 247 await doOfferAnswerExchange(t, pc); 248 const param = sender.getParameters(); 249 validateSenderRtpParameters(param); 250 251 const { codecs } = param; 252 253 codecs.push({ 254 payloadType: 2, 255 mimeType: `${kind}/piedpiper`, 256 clockRate: 1000, 257 channels: 2 258 }); 259 260 return promise_rejects_dom(t, 'InvalidModificationError', 261 sender.setParameters(param)); 262 }, `setParameters() with new codecs inserted should reject with InvalidModificationError (${kind})`); 263 264 promise_test(async t => { 265 const pc = new RTCPeerConnection(); 266 t.add_cleanup(() => pc.close()); 267 const { sender } = pc.addTransceiver(kind); 268 await doOfferAnswerExchange(t, pc); 269 const param = sender.getParameters(); 270 validateSenderRtpParameters(param); 271 272 const { codecs } = param; 273 codecs.pop(); 274 275 return promise_rejects_dom(t, 'InvalidModificationError', 276 sender.setParameters(param)); 277 }, `setParameters() with codecs removed should reject with InvalidModificationError (${kind})`); 278 279 promise_test(async t => { 280 const pc = new RTCPeerConnection(); 281 t.add_cleanup(() => pc.close()); 282 const { sender } = pc.addTransceiver(kind); 283 await doOfferAnswerExchange(t, pc); 284 const param = sender.getParameters(); 285 validateSenderRtpParameters(param); 286 287 const { codecs } = param; 288 codecs.reverse(); 289 290 return promise_rejects_dom(t, 'InvalidModificationError', 291 sender.setParameters(param)); 292 }, `setParameters() with codecs reordered should reject with InvalidModificationError (${kind})`); 293 294 promise_test(async t => { 295 const pc = new RTCPeerConnection(); 296 t.add_cleanup(() => pc.close()); 297 const { sender } = pc.addTransceiver(kind); 298 await doOfferAnswerExchange(t, pc); 299 const param = sender.getParameters(); 300 validateSenderRtpParameters(param); 301 302 delete param.codecs; 303 304 return promise_rejects_dom(t, 'InvalidModificationError', 305 sender.setParameters(param)); 306 }, `setParameters() with codecs undefined should reject with InvalidModificationError (${kind})`); 307 308 promise_test(async t => { 309 const pc1 = new RTCPeerConnection(); 310 t.add_cleanup(() => pc1.close()); 311 const pc2 = new RTCPeerConnection(); 312 t.add_cleanup(() => pc2.close()); 313 314 const sender1 = pc1.addTransceiver(kind).sender; 315 let param = sender1.getParameters(); 316 assert_array_equals(param.codecs, [], 'No sender codecs in initial stable state'); 317 318 const offer = await pc1.createOffer(); 319 param = sender1.getParameters(); 320 assert_array_equals(param.codecs, [], 'No sender codecs in initial stable state (after createOffer)'); 321 322 await pc1.setLocalDescription(offer); 323 param = sender1.getParameters(); 324 assert_array_equals(param.codecs, [], 'No sender codecs in initial have-local-offer'); 325 326 await pc2.setRemoteDescription(offer); 327 const [sender2] = pc2.getSenders(); 328 param = sender2.getParameters(); 329 assert_array_equals(param.codecs, [], 'No sender codecs in initial have-remote-offer'); 330 331 const answer = await pc2.createAnswer(); 332 param = sender2.getParameters(); 333 assert_array_equals(param.codecs, [], 'No sender codecs in initial have-remote-offer (after createAnswer)'); 334 }, `RTCRtpSender.getParameters() should not have codecs before SDP negotiation completes (${kind})`); 335 336 promise_test(async t => { 337 const pc1 = new RTCPeerConnection(); 338 t.add_cleanup(() => pc1.close()); 339 const pc2 = new RTCPeerConnection(); 340 t.add_cleanup(() => pc2.close()); 341 342 const receiver1 = pc1.addTransceiver(kind).receiver; 343 let param = receiver1.getParameters(); 344 assert_array_equals(param.codecs, [], 'No receiver codecs in initial stable state'); 345 346 const offer = await pc1.createOffer(); 347 param = receiver1.getParameters(); 348 assert_array_equals(param.codecs, [], 'No receiver codecs in initial stable state (after createOffer)'); 349 350 await pc1.setLocalDescription(offer); 351 param = receiver1.getParameters(); 352 assert_array_equals(param.codecs, [], 'No receiver codecs in initial have-local-offer'); 353 354 await pc2.setRemoteDescription(offer); 355 const [receiver2] = pc2.getReceivers(); 356 param = receiver2.getParameters(); 357 assert_array_equals(param.codecs, [], 'No receiver codecs in initial have-remote-offer'); 358 359 const answer = await pc2.createAnswer(); 360 param = receiver2.getParameters(); 361 assert_array_equals(param.codecs, [], 'No receiver codecs in initial have-remote-offer (after createAnswer)'); 362 }, `RTCRtpReceiver.getParameters() should not have codecs before SDP negotiation completes (${kind})`); 363 364 promise_test(async t => { 365 const pc1 = new RTCPeerConnection(); 366 t.add_cleanup(() => pc1.close()); 367 const pc2 = new RTCPeerConnection(); 368 t.add_cleanup(() => pc2.close()); 369 370 const sender1 = pc1.addTransceiver(kind).sender; 371 await exchangeOfferAnswer(pc1, pc2); 372 const [sender2] = pc2.getSenders(); 373 374 let param = sender1.getParameters(); 375 assert_array_field(param, 'codecs'); 376 assert_not_equals(param.codecs.length, 0); 377 for (const codec of param.codecs) { 378 validateCodecParameters(codec); 379 } 380 381 param = sender2.getParameters(); 382 assert_array_field(param, 'codecs'); 383 assert_not_equals(param.codecs.length, 0); 384 for (const codec of param.codecs) { 385 validateCodecParameters(codec); 386 } 387 }, `RTCRtpSender.getParameters() should have codecs after negotiation (${kind})`); 388 389 promise_test(async t => { 390 const pc1 = new RTCPeerConnection(); 391 t.add_cleanup(() => pc1.close()); 392 const pc2 = new RTCPeerConnection(); 393 t.add_cleanup(() => pc2.close()); 394 395 const receiver1 = pc1.addTransceiver(kind).receiver; 396 await exchangeOfferAnswer(pc1, pc2); 397 const [receiver2] = pc2.getReceivers(); 398 399 let param = receiver1.getParameters(); 400 assert_array_field(param, 'codecs'); 401 assert_not_equals(param.codecs.length, 0); 402 for (const codec of param.codecs) { 403 validateCodecParameters(codec); 404 } 405 406 param = receiver2.getParameters(); 407 assert_array_field(param, 'codecs'); 408 assert_not_equals(param.codecs.length, 0); 409 for (const codec of param.codecs) { 410 validateCodecParameters(codec); 411 } 412 }, `RTCRtpReceiver.getParameters() should have codecs after negotiation (${kind})`); 413 414 promise_test(async t => { 415 const pc = new RTCPeerConnection(); 416 t.add_cleanup(() => pc.close()); 417 418 const { sender } = pc.addTransceiver(kind); 419 await doOfferAnswerExchange(t, pc); 420 421 const {codecs} = pc.getReceivers()[0].getParameters(); 422 const sections = SDPUtils.splitSections(pc.localDescription.sdp); 423 checkCodecsAgainstSDP(codecs, sections[1]); 424 }, `RTCRtpReceiver.getParameters() codecs should match local SDP (${kind}, offerer)`); 425 426 promise_test(async t => { 427 const pc1 = new RTCPeerConnection(); 428 t.add_cleanup(() => pc1.close()); 429 const pc2 = new RTCPeerConnection(); 430 t.add_cleanup(() => pc2.close()); 431 432 pc1.addTransceiver(kind); 433 await exchangeOfferAnswer(pc1, pc2); 434 435 const {codecs} = pc2.getReceivers()[0].getParameters(); 436 const sections = SDPUtils.splitSections(pc2.localDescription.sdp); 437 checkCodecsAgainstSDP(codecs, sections[1]); 438 }, `RTCRtpReceiver.getParameters() codecs should match local SDP (${kind}, answerer)`); 439 }); 440 441 // SDP with unusual payload types and fmtp, and an unknown codec 442 const audioSdp = `v=0 443 o=- 166855176514521964 2 IN IP4 127.0.0.1 444 s=- 445 t=0 0 446 m=audio 9 UDP/TLS/RTP/SAVPF 121 111 0 101 447 c=IN IP4 0.0.0.0 448 a=rtcp:9 IN IP4 0.0.0.0 449 a=ice-ufrag:foobarba 450 a=ice-pwd:foobarba 451 a=fingerprint:sha-256 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 452 a=setup:passive 453 a=mid:mid1 454 a=sendrecv 455 a=rtcp-rsize 456 a=rtpmap:101 telephone-event/8000/1 457 a=rtpmap:121 flarglblurp/8000/2 458 a=rtpmap:111 opus/48000/2 459 a=fmtp:111 maxaveragebitrate=20001;unknownparam=foo 460 `; 461 462 // SDP with unusual payload types and fmtp, and an unknown codec 463 const videoSdp = `v=0 464 o=- 1878890426675213188 2 IN IP4 127.0.0.1 465 s=- 466 t=0 0 467 m=video 9 UDP/TLS/RTP/SAVPF 116 117 120 124 119 123 122 121 118 468 c=IN IP4 0.0.0.0 469 a=sendrecv 470 a=fmtp:117 apt=116 471 a=fmtp:120 max-fs=12277;max-fr=50;unknownparam=foo 472 a=fmtp:124 apt=120 473 a=fmtp:119 max-fs=12266;max-fr=40 474 a=fmtp:123 apt=119 475 a=fmtp:118 apt=121 476 a=ice-pwd:60840251a559417c253d68478b0020fb 477 a=ice-ufrag:741347dd 478 a=fingerprint:sha-256 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 479 a=setup:passive 480 a=mid:mid1 481 a=rtcp-mux 482 a=rtcp-rsize 483 a=rtpmap:116 flarglblurp/90000 484 a=rtpmap:117 rtx/90000 485 a=rtpmap:120 VP9/90000 486 a=rtpmap:124 rtx/90000 487 a=rtpmap:119 VP8/90000 488 a=rtpmap:123 rtx/90000 489 a=rtpmap:122 ulpfec/90000 490 a=rtpmap:121 red/90000 491 a=rtpmap:118 rtx/90000 492 `; 493 494 const remoteSdpParamsTests = [ 495 { 496 description: 'audio, no fmtp checks', 497 kind: 'audio', 498 sdp: audioSdp, 499 expectedCodecs: [ 500 {payloadType: 111, clockRate: 48000, channels: 2, mimeType: 'audio/opus'}, 501 {payloadType: 0, clockRate: 8000, channels: 1, mimeType: 'audio/pcmu'}, 502 {payloadType: 101, clockRate: 8000, channels: 1, mimeType: 'audio/telephone-event'}, 503 ] 504 }, 505 506 { 507 description: 'audio, with fmtp checks', 508 kind: 'audio', 509 sdp: audioSdp, 510 expectedCodecs: [ 511 {payloadType: 111, clockRate: 48000, channels: 2, mimeType: 'audio/opus', sdpFmtpLine: 'maxaveragebitrate=20001;unknownparam=foo'}, 512 {payloadType: 0, clockRate: 8000, channels: 1, mimeType: 'audio/pcmu', sdpFmtpLine: undefined}, 513 {payloadType: 101, clockRate: 8000, channels: 1, mimeType: 'audio/telephone-event', sdpFmtpLine: undefined}, 514 ] 515 }, 516 517 { 518 description: 'video, minimal fmtp checks', 519 kind: 'video', 520 sdp: videoSdp, 521 expectedCodecs: [ 522 {payloadType: 120, clockRate: 90000, mimeType: 'video/vp9'}, 523 {payloadType: 124, clockRate: 90000, mimeType: 'video/rtx', sdpFmtpLine: 'apt=120'}, 524 {payloadType: 119, clockRate: 90000, mimeType: 'video/vp8'}, 525 {payloadType: 123, clockRate: 90000, mimeType: 'video/rtx', sdpFmtpLine: 'apt=119'}, 526 {payloadType: 122, clockRate: 90000, mimeType: 'video/ulpfec'}, 527 {payloadType: 121, clockRate: 90000, mimeType: 'video/red'}, 528 {payloadType: 118, clockRate: 90000, mimeType: 'video/rtx', sdpFmtpLine: 'apt=121'}, 529 ] 530 }, 531 532 { 533 description: 'video, with fmtp checks', 534 kind: 'video', 535 sdp: videoSdp, 536 expectedCodecs: [ 537 {payloadType: 120, clockRate: 90000, mimeType: 'video/vp9', sdpFmtpLine: 'max-fs=12277;max-fr=50;unknownparam=foo'}, 538 {payloadType: 124, clockRate: 90000, mimeType: 'video/rtx', sdpFmtpLine: 'apt=120'}, 539 {payloadType: 119, clockRate: 90000, mimeType: 'video/vp8', sdpFmtpLine: 'max-fs=12266;max-fr=40'}, 540 {payloadType: 123, clockRate: 90000, mimeType: 'video/rtx', sdpFmtpLine: 'apt=119'}, 541 {payloadType: 122, clockRate: 90000, mimeType: 'video/ulpfec', sdpFmtpLine: undefined}, 542 {payloadType: 121, clockRate: 90000, mimeType: 'video/red', sdpFmtpLine: undefined}, 543 {payloadType: 118, clockRate: 90000, mimeType: 'video/rtx', sdpFmtpLine: 'apt=121'}, 544 ] 545 }, 546 ]; 547 548 remoteSdpParamsTests.forEach(test => { 549 promise_test(async t => { 550 const pc = new RTCPeerConnection(); 551 t.add_cleanup(() => pc.close()); 552 553 pc.addTransceiver(test.kind, { direction: 'sendrecv'}); 554 await pc.setLocalDescription(); 555 const {sender, mid} = pc.getTransceivers()[0]; 556 await pc.setRemoteDescription({sdp: test.sdp.replace('mid1', mid), type: 'answer'}); 557 const {codecs} = sender.getParameters(); 558 compareCodecParams(codecs, test.expectedCodecs); 559 }, `RTCRtpSender.getParameters() codecs should match remote SDP (${test.description}, offerer)`); 560 561 promise_test(async t => { 562 const pc = new RTCPeerConnection(); 563 t.add_cleanup(() => pc.close()); 564 565 await pc.setRemoteDescription({sdp: test.sdp, type: 'offer'}); 566 await pc.setLocalDescription(); 567 const {codecs} = pc.getSenders()[0].getParameters(); 568 compareCodecParams(codecs, test.expectedCodecs); 569 }, `RTCRtpSender.getParameters() codecs should match remote SDP (${test.description}, answerer)`); 570 }); 571 572 </script>