RTCRtpTransceiver-headerExtensionControl.html (15789B)
1 <!doctype html> 2 <meta charset=utf-8> 3 <title>RTCRtpParameters encodings</title> 4 <script src="/resources/testharness.js"></script> 5 <script src="/resources/testharnessreport.js"></script> 6 <script src="/webrtc/dictionary-helper.js"></script> 7 <script src="/webrtc/RTCRtpParameters-helper.js"></script> 8 <script src="/webrtc/third_party/sdp/sdp.js"></script> 9 <script> 10 'use strict'; 11 12 async function negotiate(pc1, pc2) { 13 await pc1.setLocalDescription(); 14 await pc2.setRemoteDescription(pc1.localDescription); 15 await pc2.setLocalDescription(); 16 await pc1.setRemoteDescription(pc2.localDescription); 17 } 18 19 ['audio', 'video'].forEach(kind => { 20 test(t => { 21 const pc = new RTCPeerConnection(); 22 t.add_cleanup(() => pc.close()); 23 const transceiver = pc.addTransceiver(kind); 24 const capabilities = transceiver.getHeaderExtensionsToNegotiate(); 25 const capability = capabilities.find((capability) => { 26 return capability.uri === 'urn:ietf:params:rtp-hdrext:sdes:mid'; 27 }); 28 assert_not_equals(capability, undefined); 29 assert_equals(capability.direction, 'sendrecv'); 30 }, `the ${kind} transceiver.getHeaderExtensionsToNegotiate() includes mandatory extensions`); 31 }); 32 33 test(t => { 34 const pc = new RTCPeerConnection(); 35 t.add_cleanup(() => pc.close()); 36 const transceiver = pc.addTransceiver('audio'); 37 const capabilities = transceiver.getHeaderExtensionsToNegotiate(); 38 capabilities[0].uri = ''; 39 assert_throws_js(TypeError, () => { 40 transceiver.setHeaderExtensionsToNegotiate(capabilities); 41 }, 'transceiver should throw TypeError when setting an empty URI'); 42 }, `setHeaderExtensionsToNegotiate throws TypeError on encountering missing URI`); 43 44 test(t => { 45 const pc = new RTCPeerConnection(); 46 t.add_cleanup(() => pc.close()); 47 const transceiver = pc.addTransceiver('audio'); 48 const capabilities = transceiver.getHeaderExtensionsToNegotiate(); 49 capabilities[0].direction = ''; 50 assert_throws_js(TypeError, () => { 51 transceiver.setHeaderExtensionsToNegotiate(capabilities); 52 }, 'transceiver should throw TypeError when setting an empty direction'); 53 }, `setHeaderExtensionsToNegotiate throws TypeError on encountering missing direction`); 54 55 test(t => { 56 const pc = new RTCPeerConnection(); 57 t.add_cleanup(() => pc.close()); 58 const transceiver = pc.addTransceiver('audio'); 59 const capabilities = transceiver.getHeaderExtensionsToNegotiate(); 60 capabilities[0].uri = '4711'; 61 assert_throws_dom('InvalidModificationError', () => { 62 transceiver.setHeaderExtensionsToNegotiate(capabilities); 63 }, 'transceiver should throw InvalidModificationError when setting an unknown URI'); 64 }, `setHeaderExtensionsToNegotiate throws InvalidModificationError on encountering unknown URI`); 65 66 test(t => { 67 const pc = new RTCPeerConnection(); 68 t.add_cleanup(() => pc.close()); 69 const transceiver = pc.addTransceiver('video'); 70 const capabilities = transceiver.getHeaderExtensionsToNegotiate().filter(capability => { 71 return capability.uri === 'urn:ietf:params:rtp-hdrext:sdes:mid'; 72 }); 73 assert_throws_dom('InvalidModificationError', () => { 74 transceiver.setHeaderExtensionsToNegotiate(capabilities); 75 }, 'transceiver should throw InvalidModificationError when removing elements from the list'); 76 }, `setHeaderExtensionsToNegotiate throws InvalidModificationError when removing elements from the list`); 77 78 test(t => { 79 const pc = new RTCPeerConnection(); 80 t.add_cleanup(() => pc.close()); 81 const transceiver = pc.addTransceiver('video'); 82 const capabilities = transceiver.getHeaderExtensionsToNegotiate(); 83 capabilities.push({ 84 uri: '4711', 85 direction: 'recvonly', 86 }); 87 assert_throws_dom('InvalidModificationError', () => { 88 transceiver.setHeaderExtensionsToNegotiate(capabilities); 89 }, 'transceiver should throw InvalidModificationError when adding elements to the list'); 90 }, `setHeaderExtensionsToNegotiate throws InvalidModificationError when adding elements to the list`); 91 92 test(t => { 93 const pc = new RTCPeerConnection(); 94 t.add_cleanup(() => pc.close()); 95 const transceiver = pc.addTransceiver('audio'); 96 const capabilities = transceiver.getHeaderExtensionsToNegotiate(); 97 const capability = capabilities.find((capability) => { 98 return capability.uri === 'urn:ietf:params:rtp-hdrext:sdes:mid'; 99 }); 100 ['sendonly', 'recvonly', 'inactive', 'stopped'].map(direction => { 101 capability.direction = direction; 102 assert_throws_dom('InvalidModificationError', () => { 103 transceiver.setHeaderExtensionsToNegotiate(capabilities); 104 }, `transceiver should throw InvalidModificationError when setting a mandatory header extension\'s direction to ${direction}`); 105 }); 106 }, `setHeaderExtensionsToNegotiate throws InvalidModificationError when setting a mandatory header extension\'s direction to something else than "sendrecv"`); 107 108 test(t => { 109 const pc = new RTCPeerConnection(); 110 t.add_cleanup(() => pc.close()); 111 const transceiver = pc.addTransceiver('audio'); 112 const capabilities = transceiver.getHeaderExtensionsToNegotiate(); 113 const selected_capability = capabilities.find((capability) => { 114 return capability.direction === 'sendrecv' && 115 capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid'; 116 }); 117 selected_capability.direction = 'stopped'; 118 const offered_capabilities = transceiver.getHeaderExtensionsToNegotiate(); 119 const altered_capability = capabilities.find((capability) => { 120 return capability.uri === selected_capability.uri && 121 capability.direction === 'stopped'; 122 }); 123 assert_not_equals(altered_capability, undefined); 124 }, `modified direction set by setHeaderExtensionsToNegotiate is visible in subsequent getHeaderExtensionsToNegotiate`); 125 126 promise_test(async t => { 127 const pc = new RTCPeerConnection(); 128 t.add_cleanup(() => pc.close()); 129 const transceiver = pc.addTransceiver('video'); 130 const capabilities = transceiver.getHeaderExtensionsToNegotiate(); 131 const offer = await pc.createOffer(); 132 const extensions = SDPUtils.matchPrefix(SDPUtils.splitSections(offer.sdp)[1], 'a=extmap:') 133 .map(line => SDPUtils.parseExtmap(line)); 134 for (const capability of capabilities) { 135 if (capability.direction === 'stopped') { 136 assert_equals(undefined, extensions.find(e => e.uri === capability.uri)); 137 } else { 138 assert_not_equals(undefined, extensions.find(e => e.uri === capability.uri)); 139 } 140 } 141 }, `Unstopped extensions turn up in offer`); 142 143 promise_test(async t => { 144 const pc = new RTCPeerConnection(); 145 t.add_cleanup(() => pc.close()); 146 const transceiver = pc.addTransceiver('video'); 147 const capabilities = transceiver.getHeaderExtensionsToNegotiate(); 148 const selected_capability = capabilities.find((capability) => { 149 return capability.direction === 'sendrecv' && 150 capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid'; 151 }); 152 selected_capability.direction = 'stopped'; 153 transceiver.setHeaderExtensionsToNegotiate(capabilities); 154 const offer = await pc.createOffer(); 155 const extensions = SDPUtils.matchPrefix(SDPUtils.splitSections(offer.sdp)[1], 'a=extmap:') 156 .map(line => SDPUtils.parseExtmap(line)); 157 for (const capability of capabilities) { 158 if (capability.direction === 'stopped') { 159 assert_equals(undefined, extensions.find(e => e.uri === capability.uri)); 160 } else { 161 assert_not_equals(undefined, extensions.find(e => e.uri === capability.uri)); 162 } 163 } 164 }, `Stopped extensions do not turn up in offers`); 165 166 promise_test(async t => { 167 const pc1 = new RTCPeerConnection(); 168 t.add_cleanup(() => pc1.close()); 169 const pc2 = new RTCPeerConnection(); 170 t.add_cleanup(() => pc2.close()); 171 172 // Disable a non-mandatory extension before first negotiation. 173 const transceiver = pc1.addTransceiver('video'); 174 const capabilities = transceiver.getHeaderExtensionsToNegotiate(); 175 const selected_capability = capabilities.find((capability) => { 176 return capability.direction === 'sendrecv' && 177 capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid'; 178 }); 179 selected_capability.direction = 'stopped'; 180 transceiver.setHeaderExtensionsToNegotiate(capabilities); 181 182 await negotiate(pc1, pc2); 183 const negotiated_capabilites = transceiver.getNegotiatedHeaderExtensions(); 184 185 assert_equals(capabilities.length, negotiated_capabilites.length); 186 }, `The set of negotiated extensions has the same size as the set of extensions to negotiate`); 187 188 promise_test(async t => { 189 const pc1 = new RTCPeerConnection(); 190 t.add_cleanup(() => pc1.close()); 191 const pc2 = new RTCPeerConnection(); 192 t.add_cleanup(() => pc2.close()); 193 194 // Disable a non-mandatory extension before first negotiation. 195 const transceiver = pc1.addTransceiver('video'); 196 const capabilities = transceiver.getHeaderExtensionsToNegotiate(); 197 const selected_capability = capabilities.find((capability) => { 198 return capability.direction === 'sendrecv' && 199 capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid'; 200 }); 201 selected_capability.direction = 'stopped'; 202 transceiver.setHeaderExtensionsToNegotiate(capabilities); 203 204 await negotiate(pc1, pc2); 205 const negotiated_capabilities = transceiver.getNegotiatedHeaderExtensions(); 206 207 // Attempt enabling the extension. 208 const selected_capability_2 = negotiated_capabilities.find((capability) => { 209 return capability.uri === selected_capability.uri; 210 }); 211 selected_capability_2.direction = 'sendrecv'; 212 // The enabled extension should not be part of the negotiated set. 213 transceiver.setHeaderExtensionsToNegotiate(negotiated_capabilities); 214 await negotiate(pc1, pc2); 215 assert_equals( 216 transceiver.getNegotiatedHeaderExtensions().find(capability => { 217 return capability.uri === selected_capability.uri 218 }).direction, 'sendrecv'); 219 }, `Header extensions can be reactivated in subsequent offers`); 220 221 promise_test(async t => { 222 const pc = new RTCPeerConnection(); 223 t.add_cleanup(() => pc.close()); 224 225 const t1 = pc.addTransceiver('video'); 226 const t2 = pc.addTransceiver('video'); 227 const extensionUri = 'urn:3gpp:video-orientation'; 228 229 assert_true(!!t1.getHeaderExtensionsToNegotiate().find(ext => ext.uri === extensionUri)); 230 const ext1 = t1.getHeaderExtensionsToNegotiate(); 231 ext1.find(ext => ext.uri === extensionUri).direction = 'stopped'; 232 t1.setHeaderExtensionsToNegotiate(ext1); 233 234 assert_true(!!t2.getHeaderExtensionsToNegotiate().find(ext => ext.uri === extensionUri)); 235 const ext2 = t2.getHeaderExtensionsToNegotiate(); 236 ext2.find(ext => ext.uri === extensionUri).direction = 'sendrecv'; 237 t2.setHeaderExtensionsToNegotiate(ext2); 238 239 const offer = await pc.createOffer(); 240 const sections = SDPUtils.splitSections(offer.sdp); 241 sections.shift(); 242 const extensions = sections.map(section => { 243 return SDPUtils.matchPrefix(section, 'a=extmap:') 244 .map(SDPUtils.parseExtmap); 245 }); 246 assert_equals(extensions.length, 2); 247 assert_false(!!extensions[0].find(extension => extension.uri === extensionUri)); 248 assert_true(!!extensions[1].find(extension => extension.uri === extensionUri)); 249 }, 'Header extensions can be deactivated on a per-mline basis'); 250 251 promise_test(async t => { 252 const pc1 = new RTCPeerConnection(); 253 t.add_cleanup(() => pc1.close()); 254 const pc2 = new RTCPeerConnection(); 255 t.add_cleanup(() => pc2.close()); 256 257 const t1 = pc1.addTransceiver('video'); 258 259 await pc1.setLocalDescription(); 260 await pc2.setRemoteDescription(pc1.localDescription); 261 // Get the transceiver after it is created by SRD. 262 const t2 = pc2.getTransceivers()[0]; 263 const t2_capabilities = t2.getHeaderExtensionsToNegotiate(); 264 const t2_capability_to_stop = t2_capabilities 265 .find(capability => capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid'); 266 assert_not_equals(undefined, t2_capability_to_stop); 267 t2_capability_to_stop.direction = 'stopped'; 268 t2.setHeaderExtensionsToNegotiate(t2_capabilities); 269 270 await pc2.setLocalDescription(); 271 await pc1.setRemoteDescription(pc2.localDescription); 272 273 const t1_negotiated = t1.getNegotiatedHeaderExtensions() 274 .find(extension => extension.uri === t2_capability_to_stop.uri); 275 assert_not_equals(undefined, t1_negotiated); 276 assert_equals(t1_negotiated.direction, 'stopped', 'in negotiated'); 277 // The negotiated extension is reflected into HeaderExtensionsToNegotiate 278 const t1_capability = t1.getHeaderExtensionsToNegotiate() 279 .find(extension => extension.uri === t2_capability_to_stop.uri); 280 assert_not_equals(undefined, t1_capability); 281 assert_equals(t1_capability.direction, 'stopped', 'in ToNegotiate'); 282 }, 'Extensions not negotiated by the peer are `stopped` in getNegotiatedHeaderExtensions'); 283 284 promise_test(async t => { 285 const pc = new RTCPeerConnection(); 286 t.add_cleanup(() => pc.close()); 287 288 const transceiver = pc.addTransceiver('video'); 289 const negotiated_capabilites = transceiver.getNegotiatedHeaderExtensions(); 290 assert_equals(negotiated_capabilites.length, 291 transceiver.getHeaderExtensionsToNegotiate().length); 292 for (const capability of negotiated_capabilites) { 293 assert_equals(capability.direction, 'stopped'); 294 } 295 }, 'Prior to negotiation, getNegotiatedHeaderExtensions() returns `stopped` for all extensions.'); 296 297 promise_test(async t => { 298 const pc1 = new RTCPeerConnection(); 299 t.add_cleanup(() => pc1.close()); 300 const pc2 = new RTCPeerConnection(); 301 t.add_cleanup(() => pc2.close()); 302 303 // Disable a non-mandatory extension before first negotiation. 304 const transceiver = pc1.addTransceiver('video'); 305 const capabilities = transceiver.getHeaderExtensionsToNegotiate(); 306 const selected_capability = capabilities.find((capability) => { 307 return capability.direction === 'sendrecv' && 308 capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid'; 309 }); 310 selected_capability.direction = 'stopped'; 311 transceiver.setHeaderExtensionsToNegotiate(capabilities); 312 313 await negotiate(pc1, pc2); 314 const negotiated_capabilites = transceiver.getNegotiatedHeaderExtensions(); 315 316 const local_negotiated = transceiver.getNegotiatedHeaderExtensions().find(ext => { 317 return ext.uri === selected_capability.uri; 318 }); 319 assert_equals(local_negotiated.direction, 'stopped'); 320 const remote_negotiated = pc2.getTransceivers()[0].getNegotiatedHeaderExtensions().find(ext => { 321 return ext.uri === selected_capability.uri; 322 }); 323 assert_equals(remote_negotiated.direction, 'stopped'); 324 }, 'Answer header extensions are a subset of the offered header extensions'); 325 326 promise_test(async t => { 327 const pc1 = new RTCPeerConnection(); 328 t.add_cleanup(() => pc1.close()); 329 const pc2 = new RTCPeerConnection(); 330 t.add_cleanup(() => pc2.close()); 331 332 // Disable a non-mandatory extension before first negotiation. 333 const transceiver = pc1.addTransceiver('video'); 334 const capabilities = transceiver.getHeaderExtensionsToNegotiate(); 335 const selected_capability = capabilities.find((capability) => { 336 return capability.direction === 'sendrecv' && 337 capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid'; 338 }); 339 selected_capability.direction = 'stopped'; 340 transceiver.setHeaderExtensionsToNegotiate(capabilities); 341 342 await negotiate(pc1, pc2); 343 // Negotiate, switching sides. 344 await negotiate(pc2, pc1); 345 346 // PC2 will NOT re-offer the extension. 347 const remote_reoffered = pc2.getTransceivers()[0].getHeaderExtensionsToNegotiate().find(ext => { 348 return ext.uri === selected_capability.uri; 349 }); 350 assert_equals(remote_reoffered.direction, 'stopped', 'in pc2 ToNegotiate'); 351 352 const negotiated_capabilites = transceiver.getNegotiatedHeaderExtensions(); 353 const local_negotiated = transceiver.getNegotiatedHeaderExtensions().find(ext => { 354 return ext.uri === selected_capability.uri; 355 }); 356 assert_equals(local_negotiated.direction, 'stopped', 'in t1 Negotiated'); 357 }, 'A subsequent offer from the other side will not reoffer extensions not negotiated by the initial offerer'); 358 </script>