| <!doctype html> |
| <meta charset=utf-8> |
| <title>RTCRtpParameters encodings</title> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script src="/webrtc/dictionary-helper.js"></script> |
| <script src="/webrtc/RTCRtpParameters-helper.js"></script> |
| <script src="/webrtc/third_party/sdp/sdp.js"></script> |
| <script> |
| 'use strict'; |
| |
| async function negotiate(pc1, pc2) { |
| await pc1.setLocalDescription(); |
| await pc2.setRemoteDescription(pc1.localDescription); |
| await pc2.setLocalDescription(); |
| await pc1.setRemoteDescription(pc2.localDescription); |
| } |
| |
| ['audio', 'video'].forEach(kind => { |
| test(t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const transceiver = pc.addTransceiver(kind); |
| const capabilities = transceiver.getHeaderExtensionsToNegotiate(); |
| const capability = capabilities.find((capability) => { |
| return capability.uri === 'urn:ietf:params:rtp-hdrext:sdes:mid'; |
| }); |
| assert_not_equals(capability, undefined); |
| assert_equals(capability.direction, 'sendrecv'); |
| }, `the ${kind} transceiver.getHeaderExtensionsToNegotiate() includes mandatory extensions`); |
| }); |
| |
| test(t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const transceiver = pc.addTransceiver('audio'); |
| const capabilities = transceiver.getHeaderExtensionsToNegotiate(); |
| capabilities[0].uri = ''; |
| assert_throws_js(TypeError, () => { |
| transceiver.setHeaderExtensionsToNegotiate(capabilities); |
| }, 'transceiver should throw TypeError when setting an empty URI'); |
| }, `setHeaderExtensionsToNegotiate throws TypeError on encountering missing URI`); |
| |
| test(t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const transceiver = pc.addTransceiver('audio'); |
| const capabilities = transceiver.getHeaderExtensionsToNegotiate(); |
| capabilities[0].direction = ''; |
| assert_throws_js(TypeError, () => { |
| transceiver.setHeaderExtensionsToNegotiate(capabilities); |
| }, 'transceiver should throw TypeError when setting an empty direction'); |
| }, `setHeaderExtensionsToNegotiate throws TypeError on encountering missing direction`); |
| |
| test(t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const transceiver = pc.addTransceiver('audio'); |
| const capabilities = transceiver.getHeaderExtensionsToNegotiate(); |
| capabilities[0].uri = '4711'; |
| assert_throws_dom('InvalidModificationError', () => { |
| transceiver.setHeaderExtensionsToNegotiate(capabilities); |
| }, 'transceiver should throw InvalidModificationError when setting an unknown URI'); |
| }, `setHeaderExtensionsToNegotiate throws InvalidModificationError on encountering unknown URI`); |
| |
| test(t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const transceiver = pc.addTransceiver('video'); |
| const capabilities = transceiver.getHeaderExtensionsToNegotiate().filter(capability => { |
| return capability.uri === 'urn:ietf:params:rtp-hdrext:sdes:mid'; |
| }); |
| assert_throws_dom('InvalidModificationError', () => { |
| transceiver.setHeaderExtensionsToNegotiate(capabilities); |
| }, 'transceiver should throw InvalidModificationError when removing elements from the list'); |
| }, `setHeaderExtensionsToNegotiate throws InvalidModificationError when removing elements from the list`); |
| |
| test(t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const transceiver = pc.addTransceiver('video'); |
| const capabilities = transceiver.getHeaderExtensionsToNegotiate(); |
| capabilities.push({ |
| uri: '4711', |
| direction: 'recvonly', |
| }); |
| assert_throws_dom('InvalidModificationError', () => { |
| transceiver.setHeaderExtensionsToNegotiate(capabilities); |
| }, 'transceiver should throw InvalidModificationError when adding elements to the list'); |
| }, `setHeaderExtensionsToNegotiate throws InvalidModificationError when adding elements to the list`); |
| |
| test(t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const transceiver = pc.addTransceiver('audio'); |
| const capabilities = transceiver.getHeaderExtensionsToNegotiate(); |
| const capability = capabilities.find((capability) => { |
| return capability.uri === 'urn:ietf:params:rtp-hdrext:sdes:mid'; |
| }); |
| ['sendonly', 'recvonly', 'inactive', 'stopped'].map(direction => { |
| capability.direction = direction; |
| assert_throws_dom('InvalidModificationError', () => { |
| transceiver.setHeaderExtensionsToNegotiate(capabilities); |
| }, `transceiver should throw InvalidModificationError when setting a mandatory header extension\'s direction to ${direction}`); |
| }); |
| }, `setHeaderExtensionsToNegotiate throws InvalidModificationError when setting a mandatory header extension\'s direction to something else than "sendrecv"`); |
| |
| test(t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const transceiver = pc.addTransceiver('audio'); |
| const capabilities = transceiver.getHeaderExtensionsToNegotiate(); |
| const selected_capability = capabilities.find((capability) => { |
| return capability.direction === 'sendrecv' && |
| capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid'; |
| }); |
| selected_capability.direction = 'stopped'; |
| const offered_capabilities = transceiver.getHeaderExtensionsToNegotiate(); |
| const altered_capability = capabilities.find((capability) => { |
| return capability.uri === selected_capability.uri && |
| capability.direction === 'stopped'; |
| }); |
| assert_not_equals(altered_capability, undefined); |
| }, `modified direction set by setHeaderExtensionsToNegotiate is visible in subsequent getHeaderExtensionsToNegotiate`); |
| |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const transceiver = pc.addTransceiver('video'); |
| const capabilities = transceiver.getHeaderExtensionsToNegotiate(); |
| const offer = await pc.createOffer(); |
| const extensions = SDPUtils.matchPrefix(SDPUtils.splitSections(offer.sdp)[1], 'a=extmap:') |
| .map(line => SDPUtils.parseExtmap(line)); |
| for (const capability of capabilities) { |
| if (capability.direction === 'stopped') { |
| assert_equals(undefined, extensions.find(e => e.uri === capability.uri)); |
| } else { |
| assert_not_equals(undefined, extensions.find(e => e.uri === capability.uri)); |
| } |
| } |
| }, `Unstopped extensions turn up in offer`); |
| |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const transceiver = pc.addTransceiver('video'); |
| const capabilities = transceiver.getHeaderExtensionsToNegotiate(); |
| const selected_capability = capabilities.find((capability) => { |
| return capability.direction === 'sendrecv' && |
| capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid'; |
| }); |
| selected_capability.direction = 'stopped'; |
| transceiver.setHeaderExtensionsToNegotiate(capabilities); |
| const offer = await pc.createOffer(); |
| const extensions = SDPUtils.matchPrefix(SDPUtils.splitSections(offer.sdp)[1], 'a=extmap:') |
| .map(line => SDPUtils.parseExtmap(line)); |
| for (const capability of capabilities) { |
| if (capability.direction === 'stopped') { |
| assert_equals(undefined, extensions.find(e => e.uri === capability.uri)); |
| } else { |
| assert_not_equals(undefined, extensions.find(e => e.uri === capability.uri)); |
| } |
| } |
| }, `Stopped extensions do not turn up in offers`); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| // Disable a non-mandatory extension before first negotiation. |
| const transceiver = pc1.addTransceiver('video'); |
| const capabilities = transceiver.getHeaderExtensionsToNegotiate(); |
| const selected_capability = capabilities.find((capability) => { |
| return capability.direction === 'sendrecv' && |
| capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid'; |
| }); |
| selected_capability.direction = 'stopped'; |
| transceiver.setHeaderExtensionsToNegotiate(capabilities); |
| |
| await negotiate(pc1, pc2); |
| const negotiated_capabilites = transceiver.getNegotiatedHeaderExtensions(); |
| |
| assert_equals(capabilities.length, negotiated_capabilites.length); |
| }, `The set of negotiated extensions has the same size as the set of extensions to negotiate`); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| // Disable a non-mandatory extension before first negotiation. |
| const transceiver = pc1.addTransceiver('video'); |
| const capabilities = transceiver.getHeaderExtensionsToNegotiate(); |
| const selected_capability = capabilities.find((capability) => { |
| return capability.direction === 'sendrecv' && |
| capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid'; |
| }); |
| selected_capability.direction = 'stopped'; |
| transceiver.setHeaderExtensionsToNegotiate(capabilities); |
| |
| await negotiate(pc1, pc2); |
| const negotiated_capabilites = transceiver.getNegotiatedHeaderExtensions(); |
| |
| // Attempt enabling the extension. |
| selected_capability.direction = 'sendrecv'; |
| |
| // The enabled extension should not be part of the negotiated set. |
| transceiver.setHeaderExtensionsToNegotiate(capabilities); |
| await negotiate(pc1, pc2); |
| assert_not_equals( |
| transceiver.getNegotiatedHeaderExtensions().find(capability => { |
| return capability.uri === selected_capability.uri && |
| capability.direction === 'sendrecv'; |
| }), undefined); |
| }, `Header extensions can be reactivated in subsequent offers`); |
| |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| |
| const t1 = pc.addTransceiver('video'); |
| const t2 = pc.addTransceiver('video'); |
| const extensionUri = 'urn:3gpp:video-orientation'; |
| |
| assert_true(!!t1.getHeaderExtensionsToNegotiate().find(ext => ext.uri === extensionUri)); |
| const ext1 = t1.getHeaderExtensionsToNegotiate(); |
| ext1.find(ext => ext.uri === extensionUri).direction = 'stopped'; |
| t1.setHeaderExtensionsToNegotiate(ext1); |
| |
| assert_true(!!t2.getHeaderExtensionsToNegotiate().find(ext => ext.uri === extensionUri)); |
| const ext2 = t2.getHeaderExtensionsToNegotiate(); |
| ext2.find(ext => ext.uri === extensionUri).direction = 'sendrecv'; |
| t2.setHeaderExtensionsToNegotiate(ext2); |
| |
| const offer = await pc.createOffer(); |
| const sections = SDPUtils.splitSections(offer.sdp); |
| sections.shift(); |
| const extensions = sections.map(section => { |
| return SDPUtils.matchPrefix(section, 'a=extmap:') |
| .map(SDPUtils.parseExtmap); |
| }); |
| assert_equals(extensions.length, 2); |
| assert_false(!!extensions[0].find(extension => extension.uri === extensionUri)); |
| assert_true(!!extensions[1].find(extension => extension.uri === extensionUri)); |
| }, 'Header extensions can be deactivated on a per-mline basis'); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| const t1 = pc1.addTransceiver('video'); |
| |
| await pc1.setLocalDescription(); |
| await pc2.setRemoteDescription(pc1.localDescription); |
| // Get the transceiver after it is created by SRD. |
| const t2 = pc2.getTransceivers()[0]; |
| const t2_capabilities = t2.getHeaderExtensionsToNegotiate(); |
| const t2_capability_to_stop = t2_capabilities |
| .find(capability => capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid'); |
| assert_not_equals(undefined, t2_capability_to_stop); |
| t2_capability_to_stop.direction = 'stopped'; |
| t2.setHeaderExtensionsToNegotiate(t2_capabilities); |
| |
| await pc2.setLocalDescription(); |
| await pc1.setRemoteDescription(pc2.localDescription); |
| |
| const t1_negotiated = t1.getNegotiatedHeaderExtensions() |
| .find(extension => extension.uri === t2_capability_to_stop.uri); |
| assert_not_equals(undefined, t1_negotiated); |
| assert_equals(t1_negotiated.direction, 'stopped'); |
| const t1_capability = t1.getHeaderExtensionsToNegotiate() |
| .find(extension => extension.uri === t2_capability_to_stop.uri); |
| assert_not_equals(undefined, t1_capability); |
| assert_equals(t1_capability.direction, 'sendrecv'); |
| }, 'Extensions not negotiated by the peer are `stopped` in getNegotiatedHeaderExtensions'); |
| |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| |
| const transceiver = pc.addTransceiver('video'); |
| const negotiated_capabilites = transceiver.getNegotiatedHeaderExtensions(); |
| assert_equals(negotiated_capabilites.length, |
| transceiver.getHeaderExtensionsToNegotiate().length); |
| for (const capability of negotiated_capabilites) { |
| assert_equals(capability.direction, 'stopped'); |
| } |
| }, 'Prior to negotiation, getNegotiatedHeaderExtensions() returns `stopped` for all extensions.'); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| // Disable a non-mandatory extension before first negotiation. |
| const transceiver = pc1.addTransceiver('video'); |
| const capabilities = transceiver.getHeaderExtensionsToNegotiate(); |
| const selected_capability = capabilities.find((capability) => { |
| return capability.direction === 'sendrecv' && |
| capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid'; |
| }); |
| selected_capability.direction = 'stopped'; |
| transceiver.setHeaderExtensionsToNegotiate(capabilities); |
| |
| await negotiate(pc1, pc2); |
| const negotiated_capabilites = transceiver.getNegotiatedHeaderExtensions(); |
| |
| const local_negotiated = transceiver.getNegotiatedHeaderExtensions().find(ext => { |
| return ext.uri === selected_capability.uri; |
| }); |
| assert_equals(local_negotiated.direction, 'stopped'); |
| const remote_negotiated = pc2.getTransceivers()[0].getNegotiatedHeaderExtensions().find(ext => { |
| return ext.uri === selected_capability.uri; |
| }); |
| assert_equals(remote_negotiated.direction, 'stopped'); |
| }, 'Answer header extensions are a subset of the offered header extensions'); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| // Disable a non-mandatory extension before first negotiation. |
| const transceiver = pc1.addTransceiver('video'); |
| const capabilities = transceiver.getHeaderExtensionsToNegotiate(); |
| const selected_capability = capabilities.find((capability) => { |
| return capability.direction === 'sendrecv' && |
| capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid'; |
| }); |
| selected_capability.direction = 'stopped'; |
| transceiver.setHeaderExtensionsToNegotiate(capabilities); |
| |
| await negotiate(pc1, pc2); |
| // Negotiate, switching sides. |
| await negotiate(pc2, pc1); |
| |
| // PC2 will re-offer the extension. |
| const remote_reoffered = pc2.getTransceivers()[0].getHeaderExtensionsToNegotiate().find(ext => { |
| return ext.uri === selected_capability.uri; |
| }); |
| assert_equals(remote_reoffered.direction, 'sendrecv'); |
| |
| // But PC1 will still reject the extension. |
| const negotiated_capabilites = transceiver.getNegotiatedHeaderExtensions(); |
| const local_negotiated = transceiver.getNegotiatedHeaderExtensions().find(ext => { |
| return ext.uri === selected_capability.uri; |
| }); |
| assert_equals(local_negotiated.direction, 'stopped'); |
| }, 'A subsequent offer from the other side will reoffer extensions not negotiated by the initial offerer'); |
| </script> |