| <!doctype html> |
| <meta charset=utf-8> |
| <meta name="timeout" content="long"> |
| <title>RTCRtpParameters codecs</title> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script src="dictionary-helper.js"></script> |
| <script src="RTCRtpParameters-helper.js"></script> |
| <script src="RTCPeerConnection-helper.js"></script> |
| <script src="./third_party/sdp/sdp.js"></script> |
| <script> |
| 'use strict'; |
| |
| // Test is based on the following editor draft: |
| // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html |
| |
| // The following helper functions are called from RTCRtpParameters-helper.js: |
| // doOfferAnswerExchange |
| // validateSenderRtpParameters |
| |
| /* |
| 5.2. RTCRtpSender Interface |
| interface RTCRtpSender { |
| Promise<void> setParameters(optional RTCRtpParameters parameters); |
| RTCRtpParameters getParameters(); |
| }; |
| |
| dictionary RTCRtpParameters { |
| DOMString transactionId; |
| sequence<RTCRtpEncodingParameters> encodings; |
| sequence<RTCRtpHeaderExtensionParameters> headerExtensions; |
| RTCRtcpParameters rtcp; |
| sequence<RTCRtpCodecParameters> codecs; |
| }; |
| |
| dictionary RTCRtpCodecParameters { |
| [readonly] |
| unsigned short payloadType; |
| |
| [readonly] |
| DOMString mimeType; |
| |
| [readonly] |
| unsigned long clockRate; |
| |
| [readonly] |
| unsigned short channels; |
| |
| [readonly] |
| DOMString sdpFmtpLine; |
| }; |
| |
| getParameters |
| - The codecs sequence is populated based on the codecs that have been negotiated |
| for sending, and which the user agent is currently capable of sending. |
| |
| If setParameters has removed or reordered codecs, getParameters MUST return |
| the shortened/reordered list. However, every time codecs are renegotiated by |
| a new offer/answer exchange, the list of codecs MUST be restored to the full |
| negotiated set, in the priority order indicated by the remote description, |
| in effect discarding the effects of setParameters. |
| |
| codecs |
| - When using the setParameters method, the codecs sequence from the corresponding |
| call to getParameters can be reordered and entries can be removed, but entries |
| cannot be added, and the RTCRtpCodecParameters dictionary members cannot be modified. |
| */ |
| |
| // Get the first codec from param.codecs. |
| // Assert that param.codecs has at least one element |
| function getFirstCodec(param) { |
| const { codecs } = param; |
| assert_greater_than(codecs.length, 0); |
| return codecs[0]; |
| } |
| |
| function compareCodecParam(observed, expected) { |
| assert_equals(observed.payloadType, expected.payloadType); |
| assert_equals(observed.clockRate, expected.clockRate); |
| assert_equals(observed.channels, expected.channels); |
| // Comparisons of mime-type are case-insensitive (see |
| // https://datatracker.ietf.org/doc/html/rfc2045#section-5.1) |
| assert_equals(observed.mimeType.toLowerCase(), expected.mimeType.toLowerCase()); |
| // This is not ideal; Firefox does not store fmtp verbatim, it stores a |
| // reserialiaztion of the parsed form. We would like to be able to test |
| // the other fields without tripping over that. So, we only test |
| // sdpFmtpLine if it is a property of |expected|. |
| if (expected.hasOwnProperty('sdpFmtpLine')) { |
| assert_equals(observed.sdpFmtpLine, expected.sdpFmtpLine); |
| } |
| } |
| |
| function compareCodecParams(observed, expected) { |
| assert_equals(observed.length, expected.length); |
| for (let i = 0; i < observed.length; ++i) { |
| compareCodecParam(observed[i], expected[i]); |
| } |
| } |
| |
| // Does not support disregarding unsupported codecs in the SDP, so is not |
| // suitable for all test-cases. |
| function checkCodecsAgainstSDP(codecs, msection) { |
| const rtpParameters = SDPUtils.parseRtpParameters(msection); |
| const {kind} = SDPUtils.parseMLine(msection); |
| |
| assert_not_equals(codecs.length, 0); |
| assert_equals(codecs.length, rtpParameters.codecs.length); |
| for (let i = 0; i < codecs.length; ++i) { |
| const observed = codecs[i]; |
| const fromSdp = rtpParameters.codecs[i]; |
| const expected = { |
| payloadType: fromSdp.payloadType, |
| clockRate: fromSdp.clockRate, |
| mimeType: `${kind}/${fromSdp.name}`, |
| }; |
| if (kind == 'audio') { |
| expected.channels = fromSdp.channels; |
| } |
| const fmtps = SDPUtils.matchPrefixAndTrim(msection, `a=fmtp:${fromSdp.payloadType}`); |
| if (fmtps.length == 1) { |
| expected.sdpFmtpLine = fmtps[0]; |
| } else { |
| // compareCodecParam will check if observed.sdpFmtpLine is undefined if we |
| // set this, but will not perform any checks if we do not. |
| expected.sdpFmtpLine = undefined; |
| } |
| compareCodecParam(observed, expected); |
| } |
| } |
| |
| ['audio', 'video'].forEach(kind => { |
| /* |
| 5.2. setParameters |
| 7. If parameters.encodings.length is different from N, or if any parameter |
| in the parameters argument, marked as a Read-only parameter, has a value |
| that is different from the corresponding parameter value returned from |
| sender.getParameters(), abort these steps and return a promise rejected |
| with a newly created InvalidModificationError. Note that this also applies |
| to transactionId. |
| */ |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| |
| const { sender } = pc.addTransceiver(kind); |
| await doOfferAnswerExchange(t, pc); |
| |
| const param = sender.getParameters(); |
| validateSenderRtpParameters(param); |
| |
| const codec = getFirstCodec(param); |
| |
| if(codec.payloadType === undefined) { |
| codec.payloadType = 8; |
| } else { |
| codec.payloadType += 1; |
| } |
| |
| return promise_rejects_dom(t, 'InvalidModificationError', |
| sender.setParameters(param)); |
| }, `setParameters() with codec.payloadType modified should reject with InvalidModificationError (${kind})`); |
| |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const { sender } = pc.addTransceiver(kind); |
| await doOfferAnswerExchange(t, pc); |
| const param = sender.getParameters(); |
| validateSenderRtpParameters(param); |
| |
| const codec = getFirstCodec(param); |
| |
| if(codec.mimeType === undefined) { |
| codec.mimeType = `${kind}/piedpiper`; |
| } else { |
| codec.mimeType = `${codec.mimeType}-modified`; |
| } |
| |
| return promise_rejects_dom(t, 'InvalidModificationError', |
| sender.setParameters(param)); |
| }, `setParameters() with codec.mimeType modified should reject with InvalidModificationError (${kind})`); |
| |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const { sender } = pc.addTransceiver(kind); |
| await doOfferAnswerExchange(t, pc); |
| const param = sender.getParameters(); |
| validateSenderRtpParameters(param); |
| |
| const codec = getFirstCodec(param); |
| |
| if(codec.clockRate === undefined) { |
| codec.clockRate = 8000; |
| } else { |
| codec.clockRate += 1; |
| } |
| |
| return promise_rejects_dom(t, 'InvalidModificationError', |
| sender.setParameters(param)); |
| }, `setParameters() with codec.clockRate modified should reject with InvalidModificationError (${kind})`); |
| |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const { sender } = pc.addTransceiver(kind); |
| await doOfferAnswerExchange(t, pc); |
| const param = sender.getParameters(); |
| validateSenderRtpParameters(param); |
| |
| const codec = getFirstCodec(param); |
| |
| if(codec.channels === undefined) { |
| codec.channels = 6; |
| } else { |
| codec.channels += 1; |
| } |
| |
| return promise_rejects_dom(t, 'InvalidModificationError', |
| sender.setParameters(param)); |
| }, `setParameters() with codec.channels modified should reject with InvalidModificationError (${kind})`); |
| |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const { sender } = pc.addTransceiver(kind); |
| await doOfferAnswerExchange(t, pc); |
| const param = sender.getParameters(); |
| validateSenderRtpParameters(param); |
| |
| const codec = getFirstCodec(param); |
| |
| if(codec.sdpFmtpLine === undefined) { |
| codec.sdpFmtpLine = 'a=fmtp:98 0-15'; |
| } else { |
| codec.sdpFmtpLine = `${codec.sdpFmtpLine};foo=1`; |
| } |
| |
| return promise_rejects_dom(t, 'InvalidModificationError', |
| sender.setParameters(param)); |
| }, `setParameters() with codec.sdpFmtpLine modified should reject with InvalidModificationError (${kind})`); |
| |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const { sender } = pc.addTransceiver(kind); |
| await doOfferAnswerExchange(t, pc); |
| const param = sender.getParameters(); |
| validateSenderRtpParameters(param); |
| |
| const { codecs } = param; |
| |
| codecs.push({ |
| payloadType: 2, |
| mimeType: `${kind}/piedpiper`, |
| clockRate: 1000, |
| channels: 2 |
| }); |
| |
| return promise_rejects_dom(t, 'InvalidModificationError', |
| sender.setParameters(param)); |
| }, `setParameters() with new codecs inserted should reject with InvalidModificationError (${kind})`); |
| |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const { sender } = pc.addTransceiver(kind); |
| await doOfferAnswerExchange(t, pc); |
| const param = sender.getParameters(); |
| validateSenderRtpParameters(param); |
| |
| const { codecs } = param; |
| codecs.pop(); |
| |
| return promise_rejects_dom(t, 'InvalidModificationError', |
| sender.setParameters(param)); |
| }, `setParameters() with codecs removed should reject with InvalidModificationError (${kind})`); |
| |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const { sender } = pc.addTransceiver(kind); |
| await doOfferAnswerExchange(t, pc); |
| const param = sender.getParameters(); |
| validateSenderRtpParameters(param); |
| |
| const { codecs } = param; |
| codecs.reverse(); |
| |
| return promise_rejects_dom(t, 'InvalidModificationError', |
| sender.setParameters(param)); |
| }, `setParameters() with codecs reordered should reject with InvalidModificationError (${kind})`); |
| |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const { sender } = pc.addTransceiver(kind); |
| await doOfferAnswerExchange(t, pc); |
| const param = sender.getParameters(); |
| validateSenderRtpParameters(param); |
| |
| delete param.codecs; |
| |
| return promise_rejects_dom(t, 'InvalidModificationError', |
| sender.setParameters(param)); |
| }, `setParameters() with codecs undefined should reject with InvalidModificationError (${kind})`); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| const sender1 = pc1.addTransceiver(kind).sender; |
| let param = sender1.getParameters(); |
| assert_array_equals(param.codecs, [], 'No sender codecs in initial stable state'); |
| |
| const offer = await pc1.createOffer(); |
| param = sender1.getParameters(); |
| assert_array_equals(param.codecs, [], 'No sender codecs in initial stable state (after createOffer)'); |
| |
| await pc1.setLocalDescription(offer); |
| param = sender1.getParameters(); |
| assert_array_equals(param.codecs, [], 'No sender codecs in initial have-local-offer'); |
| |
| await pc2.setRemoteDescription(offer); |
| const [sender2] = pc2.getSenders(); |
| param = sender2.getParameters(); |
| assert_array_equals(param.codecs, [], 'No sender codecs in initial have-remote-offer'); |
| |
| const answer = await pc2.createAnswer(); |
| param = sender2.getParameters(); |
| assert_array_equals(param.codecs, [], 'No sender codecs in initial have-remote-offer (after createAnswer)'); |
| }, `RTCRtpSender.getParameters() should not have codecs before SDP negotiation completes (${kind})`); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| const receiver1 = pc1.addTransceiver(kind).receiver; |
| let param = receiver1.getParameters(); |
| assert_array_equals(param.codecs, [], 'No receiver codecs in initial stable state'); |
| |
| const offer = await pc1.createOffer(); |
| param = receiver1.getParameters(); |
| assert_array_equals(param.codecs, [], 'No receiver codecs in initial stable state (after createOffer)'); |
| |
| await pc1.setLocalDescription(offer); |
| param = receiver1.getParameters(); |
| assert_array_equals(param.codecs, [], 'No receiver codecs in initial have-local-offer'); |
| |
| await pc2.setRemoteDescription(offer); |
| const [receiver2] = pc2.getReceivers(); |
| param = receiver2.getParameters(); |
| assert_array_equals(param.codecs, [], 'No receiver codecs in initial have-remote-offer'); |
| |
| const answer = await pc2.createAnswer(); |
| param = receiver2.getParameters(); |
| assert_array_equals(param.codecs, [], 'No receiver codecs in initial have-remote-offer (after createAnswer)'); |
| }, `RTCRtpReceiver.getParameters() should not have codecs before SDP negotiation completes (${kind})`); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| const sender1 = pc1.addTransceiver(kind).sender; |
| await exchangeOfferAnswer(pc1, pc2); |
| const [sender2] = pc2.getSenders(); |
| |
| let param = sender1.getParameters(); |
| assert_array_field(param, 'codecs'); |
| assert_not_equals(param.codecs.length, 0); |
| for (const codec of param.codecs) { |
| validateCodecParameters(codec); |
| } |
| |
| param = sender2.getParameters(); |
| assert_array_field(param, 'codecs'); |
| assert_not_equals(param.codecs.length, 0); |
| for (const codec of param.codecs) { |
| validateCodecParameters(codec); |
| } |
| }, `RTCRtpSender.getParameters() should have codecs after negotiation (${kind})`); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| const receiver1 = pc1.addTransceiver(kind).receiver; |
| await exchangeOfferAnswer(pc1, pc2); |
| const [receiver2] = pc2.getReceivers(); |
| |
| let param = receiver1.getParameters(); |
| assert_array_field(param, 'codecs'); |
| assert_not_equals(param.codecs.length, 0); |
| for (const codec of param.codecs) { |
| validateCodecParameters(codec); |
| } |
| |
| param = receiver2.getParameters(); |
| assert_array_field(param, 'codecs'); |
| assert_not_equals(param.codecs.length, 0); |
| for (const codec of param.codecs) { |
| validateCodecParameters(codec); |
| } |
| }, `RTCRtpReceiver.getParameters() should have codecs after negotiation (${kind})`); |
| |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| |
| const { sender } = pc.addTransceiver(kind); |
| await doOfferAnswerExchange(t, pc); |
| |
| const {codecs} = pc.getReceivers()[0].getParameters(); |
| const sections = SDPUtils.splitSections(pc.localDescription.sdp); |
| checkCodecsAgainstSDP(codecs, sections[1]); |
| }, `RTCRtpReceiver.getParameters() codecs should match local SDP (${kind}, offerer)`); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| pc1.addTransceiver(kind); |
| await exchangeOfferAnswer(pc1, pc2); |
| |
| const {codecs} = pc2.getReceivers()[0].getParameters(); |
| const sections = SDPUtils.splitSections(pc2.localDescription.sdp); |
| checkCodecsAgainstSDP(codecs, sections[1]); |
| }, `RTCRtpReceiver.getParameters() codecs should match local SDP (${kind}, answerer)`); |
| }); |
| |
| // SDP with unusual payload types and fmtp, and an unknown codec |
| const audioSdp = `v=0 |
| o=- 166855176514521964 2 IN IP4 127.0.0.1 |
| s=- |
| t=0 0 |
| m=audio 9 UDP/TLS/RTP/SAVPF 121 111 0 101 |
| c=IN IP4 0.0.0.0 |
| a=rtcp:9 IN IP4 0.0.0.0 |
| a=ice-ufrag:foobarba |
| a=ice-pwd:foobarba |
| 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 |
| a=setup:passive |
| a=mid:mid1 |
| a=sendrecv |
| a=rtcp-rsize |
| a=rtpmap:101 telephone-event/8000/1 |
| a=rtpmap:121 flarglblurp/8000/2 |
| a=rtpmap:111 opus/48000/2 |
| a=fmtp:111 maxaveragebitrate=20001;unknownparam=foo |
| `; |
| |
| // SDP with unusual payload types and fmtp, and an unknown codec |
| const videoSdp = `v=0 |
| o=- 1878890426675213188 2 IN IP4 127.0.0.1 |
| s=- |
| t=0 0 |
| m=video 9 UDP/TLS/RTP/SAVPF 116 117 120 124 119 123 122 121 118 |
| c=IN IP4 0.0.0.0 |
| a=sendrecv |
| a=fmtp:117 apt=116 |
| a=fmtp:120 max-fs=12277;max-fr=50;unknownparam=foo |
| a=fmtp:124 apt=120 |
| a=fmtp:119 max-fs=12266;max-fr=40 |
| a=fmtp:123 apt=119 |
| a=fmtp:118 apt=121 |
| a=ice-pwd:60840251a559417c253d68478b0020fb |
| a=ice-ufrag:741347dd |
| 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 |
| a=setup:passive |
| a=mid:mid1 |
| a=rtcp-mux |
| a=rtcp-rsize |
| a=rtpmap:116 flarglblurp/90000 |
| a=rtpmap:117 rtx/90000 |
| a=rtpmap:120 VP9/90000 |
| a=rtpmap:124 rtx/90000 |
| a=rtpmap:119 VP8/90000 |
| a=rtpmap:123 rtx/90000 |
| a=rtpmap:122 ulpfec/90000 |
| a=rtpmap:121 red/90000 |
| a=rtpmap:118 rtx/90000 |
| `; |
| |
| const remoteSdpParamsTests = [ |
| { |
| description: 'audio, no fmtp checks', |
| kind: 'audio', |
| sdp: audioSdp, |
| expectedCodecs: [ |
| {payloadType: 111, clockRate: 48000, channels: 2, mimeType: 'audio/opus'}, |
| {payloadType: 0, clockRate: 8000, channels: 1, mimeType: 'audio/pcmu'}, |
| {payloadType: 101, clockRate: 8000, channels: 1, mimeType: 'audio/telephone-event'}, |
| ] |
| }, |
| |
| { |
| description: 'audio, with fmtp checks', |
| kind: 'audio', |
| sdp: audioSdp, |
| expectedCodecs: [ |
| {payloadType: 111, clockRate: 48000, channels: 2, mimeType: 'audio/opus', sdpFmtpLine: 'maxaveragebitrate=20001;unknownparam=foo'}, |
| {payloadType: 0, clockRate: 8000, channels: 1, mimeType: 'audio/pcmu', sdpFmtpLine: undefined}, |
| {payloadType: 101, clockRate: 8000, channels: 1, mimeType: 'audio/telephone-event', sdpFmtpLine: undefined}, |
| ] |
| }, |
| |
| { |
| description: 'video, minimal fmtp checks', |
| kind: 'video', |
| sdp: videoSdp, |
| expectedCodecs: [ |
| {payloadType: 120, clockRate: 90000, mimeType: 'video/vp9'}, |
| {payloadType: 124, clockRate: 90000, mimeType: 'video/rtx', sdpFmtpLine: 'apt=120'}, |
| {payloadType: 119, clockRate: 90000, mimeType: 'video/vp8'}, |
| {payloadType: 123, clockRate: 90000, mimeType: 'video/rtx', sdpFmtpLine: 'apt=119'}, |
| {payloadType: 122, clockRate: 90000, mimeType: 'video/ulpfec'}, |
| {payloadType: 121, clockRate: 90000, mimeType: 'video/red'}, |
| {payloadType: 118, clockRate: 90000, mimeType: 'video/rtx', sdpFmtpLine: 'apt=121'}, |
| ] |
| }, |
| |
| { |
| description: 'video, with fmtp checks', |
| kind: 'video', |
| sdp: videoSdp, |
| expectedCodecs: [ |
| {payloadType: 120, clockRate: 90000, mimeType: 'video/vp9', sdpFmtpLine: 'max-fs=12277;max-fr=50;unknownparam=foo'}, |
| {payloadType: 124, clockRate: 90000, mimeType: 'video/rtx', sdpFmtpLine: 'apt=120'}, |
| {payloadType: 119, clockRate: 90000, mimeType: 'video/vp8', sdpFmtpLine: 'max-fs=12266;max-fr=40'}, |
| {payloadType: 123, clockRate: 90000, mimeType: 'video/rtx', sdpFmtpLine: 'apt=119'}, |
| {payloadType: 122, clockRate: 90000, mimeType: 'video/ulpfec', sdpFmtpLine: undefined}, |
| {payloadType: 121, clockRate: 90000, mimeType: 'video/red', sdpFmtpLine: undefined}, |
| {payloadType: 118, clockRate: 90000, mimeType: 'video/rtx', sdpFmtpLine: 'apt=121'}, |
| ] |
| }, |
| ]; |
| |
| remoteSdpParamsTests.forEach(test => { |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| |
| pc.addTransceiver(test.kind, { direction: 'sendrecv'}); |
| await pc.setLocalDescription(); |
| const {sender, mid} = pc.getTransceivers()[0]; |
| await pc.setRemoteDescription({sdp: test.sdp.replace('mid1', mid), type: 'answer'}); |
| const {codecs} = sender.getParameters(); |
| compareCodecParams(codecs, test.expectedCodecs); |
| }, `RTCRtpSender.getParameters() codecs should match remote SDP (${test.description}, offerer)`); |
| |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| |
| await pc.setRemoteDescription({sdp: test.sdp, type: 'offer'}); |
| await pc.setLocalDescription(); |
| const {codecs} = pc.getSenders()[0].getParameters(); |
| compareCodecParams(codecs, test.expectedCodecs); |
| }, `RTCRtpSender.getParameters() codecs should match remote SDP (${test.description}, answerer)`); |
| }); |
| |
| </script> |