RTCPeerConnection-setRemoteDescription-offer.html (16122B)
1 <!doctype html> 2 <meta charset=utf-8> 3 <title>RTCPeerConnection.prototype.setRemoteDescription - offer</title> 4 <script src="/resources/testharness.js"></script> 5 <script src="/resources/testharnessreport.js"></script> 6 <script src="RTCPeerConnection-helper.js"></script> 7 <script src="/webrtc/third_party/sdp/sdp.js"></script> 8 <script> 9 'use strict'; 10 11 // The following helper functions are called from RTCPeerConnection-helper.js: 12 // assert_session_desc_similar() 13 // generateAudioReceiveOnlyOffer 14 15 /* 16 4.3.2. Interface Definition 17 [Constructor(optional RTCConfiguration configuration)] 18 interface RTCPeerConnection : EventTarget { 19 Promise<void> setRemoteDescription( 20 RTCSessionDescriptionInit description); 21 22 readonly attribute RTCSessionDescription? remoteDescription; 23 readonly attribute RTCSessionDescription? currentRemoteDescription; 24 readonly attribute RTCSessionDescription? pendingRemoteDescription; 25 ... 26 }; 27 28 4.6.2. RTCSessionDescription Class 29 dictionary RTCSessionDescriptionInit { 30 required RTCSdpType type; 31 DOMString sdp = ""; 32 }; 33 34 4.6.1. RTCSdpType 35 enum RTCSdpType { 36 "offer", 37 "pranswer", 38 "answer", 39 "rollback" 40 }; 41 */ 42 43 /* 44 4.3.1.6. Set the RTCSessionSessionDescription 45 2.2.3. Otherwise, if description is set as a remote description, then run one of 46 the following steps: 47 - If description is of type "offer", set connection.pendingRemoteDescription 48 attribute to description and signaling state to have-remote-offer. 49 */ 50 51 promise_test(t => { 52 const pc1 = new RTCPeerConnection(); 53 t.add_cleanup(() => pc1.close()); 54 pc1.createDataChannel('datachannel'); 55 56 const pc2 = new RTCPeerConnection(); 57 t.add_cleanup(() => pc2.close()); 58 59 const states = []; 60 pc2.addEventListener('signalingstatechange', () => states.push(pc2.signalingState)); 61 62 return pc1.createOffer() 63 .then(offer => { 64 return pc2.setRemoteDescription(offer) 65 .then(() => { 66 assert_equals(pc2.signalingState, 'have-remote-offer'); 67 assert_session_desc_similar(pc2.remoteDescription, offer); 68 assert_session_desc_similar(pc2.pendingRemoteDescription, offer); 69 assert_equals(pc2.currentRemoteDescription, null); 70 71 assert_array_equals(states, ['have-remote-offer']); 72 }); 73 }); 74 }, 'setRemoteDescription with valid offer should succeed'); 75 76 promise_test(t => { 77 const pc1 = new RTCPeerConnection(); 78 t.add_cleanup(() => pc1.close()); 79 pc1.createDataChannel('datachannel'); 80 81 const pc2 = new RTCPeerConnection(); 82 t.add_cleanup(() => pc2.close()); 83 84 const states = []; 85 pc2.addEventListener('signalingstatechange', () => states.push(pc2.signalingState)); 86 87 return pc1.createOffer() 88 .then(offer => { 89 return pc2.setRemoteDescription(offer) 90 .then(() => pc2.setRemoteDescription(offer)) 91 .then(() => { 92 assert_equals(pc2.signalingState, 'have-remote-offer'); 93 assert_session_desc_similar(pc2.remoteDescription, offer); 94 assert_session_desc_similar(pc2.pendingRemoteDescription, offer); 95 assert_equals(pc2.currentRemoteDescription, null); 96 97 assert_array_equals(states, ['have-remote-offer']); 98 }); 99 }); 100 }, 'setRemoteDescription multiple times should succeed'); 101 102 promise_test(t => { 103 const pc1 = new RTCPeerConnection(); 104 t.add_cleanup(() => pc1.close()); 105 pc1.createDataChannel('datachannel'); 106 107 const pc2 = new RTCPeerConnection(); 108 t.add_cleanup(() => pc2.close()); 109 110 const states = []; 111 pc2.addEventListener('signalingstatechange', () => states.push(pc2.signalingState)); 112 113 return pc1.createOffer() 114 .then(offer1 => { 115 return pc1.setLocalDescription(offer1) 116 .then(()=> { 117 return generateAudioReceiveOnlyOffer(pc1) 118 .then(offer2 => { 119 assert_session_desc_not_similar(offer1, offer2); 120 121 return pc2.setRemoteDescription(offer1) 122 .then(() => pc2.setRemoteDescription(offer2)) 123 .then(() => { 124 assert_equals(pc2.signalingState, 'have-remote-offer'); 125 assert_session_desc_similar(pc2.remoteDescription, offer2); 126 assert_session_desc_similar(pc2.pendingRemoteDescription, offer2); 127 assert_equals(pc2.currentRemoteDescription, null); 128 129 assert_array_equals(states, ['have-remote-offer']); 130 }); 131 }); 132 }); 133 }); 134 }, 'setRemoteDescription multiple times with different offer should succeed'); 135 136 /* 137 4.3.1.6. Set the RTCSessionSessionDescription 138 2.1.4. If the content of description is not valid SDP syntax, then reject p with 139 an RTCError (with errorDetail set to "sdp-syntax-error" and the 140 sdpLineNumber attribute set to the line number in the SDP where the syntax 141 error was detected) and abort these steps. 142 */ 143 promise_test(t => { 144 const pc = new RTCPeerConnection(); 145 146 t.add_cleanup(() => pc.close()); 147 148 return pc.setRemoteDescription({ 149 type: 'offer', 150 sdp: 'Invalid SDP' 151 }) 152 .then(() => { 153 assert_unreached('Expect promise to be rejected'); 154 }, err => { 155 assert_equals(err.errorDetail, 'sdp-syntax-error', 156 'Expect error detail field to set to sdp-syntax-error'); 157 158 assert_true(err instanceof RTCError, 159 'Expect err to be instance of RTCError'); 160 }); 161 }, 'setRemoteDescription(offer) with invalid SDP should reject with RTCError'); 162 163 promise_test(async t => { 164 const pc1 = new RTCPeerConnection(); 165 const pc2 = new RTCPeerConnection(); 166 t.add_cleanup(() => pc1.close()); 167 t.add_cleanup(() => pc2.close()); 168 await pc1.setLocalDescription(await pc1.createOffer()); 169 await pc1.setRemoteDescription(await pc2.createOffer()); 170 assert_equals(pc1.signalingState, 'have-remote-offer'); 171 }, 'setRemoteDescription(offer) from have-local-offer should roll back and succeed'); 172 173 promise_test(async t => { 174 const pc1 = new RTCPeerConnection(); 175 const pc2 = new RTCPeerConnection(); 176 t.add_cleanup(() => pc1.close()); 177 t.add_cleanup(() => pc2.close()); 178 await pc1.setLocalDescription(await pc1.createOffer()); 179 const p = pc1.setRemoteDescription(await pc2.createOffer()); 180 await new Promise(r => pc1.onsignalingstatechange = r); 181 assert_equals(pc1.signalingState, 'stable'); 182 assert_equals(pc1.pendingLocalDescription, null); 183 assert_equals(pc1.pendingRemoteDescription, null); 184 await new Promise(r => pc1.onsignalingstatechange = r); 185 assert_equals(pc1.signalingState, 'have-remote-offer'); 186 assert_equals(pc1.pendingLocalDescription, null); 187 assert_equals(pc1.pendingRemoteDescription.type, 'offer'); 188 await p; 189 }, 'setRemoteDescription(offer) from have-local-offer fires signalingstatechange twice'); 190 191 promise_test(async t => { 192 const pc1 = new RTCPeerConnection(); 193 t.add_cleanup(() => pc1.close()); 194 const pc2 = new RTCPeerConnection(); 195 t.add_cleanup(() => pc2.close()); 196 197 pc1.addTransceiver('audio', { direction: 'recvonly' }); 198 const srdPromise = pc2.setRemoteDescription(await pc1.createOffer()); 199 200 assert_equals(pc2.signalingState, "stable", "signalingState should not be set synchronously after a call to sRD"); 201 202 assert_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should not be set synchronously after a call to sRD"); 203 assert_equals(pc2.currentRemoteDescription, null, "currentRemoteDescription should not be set synchronously after a call to sRD"); 204 205 const statePromise = new Promise(resolve => { 206 pc2.onsignalingstatechange = () => { 207 resolve(pc2.signalingState); 208 } 209 }); 210 211 const raceValue = await Promise.race([statePromise, srdPromise]); 212 assert_equals(raceValue, "have-remote-offer", "signalingstatechange event should fire before sRD resolves"); 213 assert_not_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should be updated before the signalingstatechange event"); 214 assert_equals(pc2.pendingRemoteDescription.type, "offer"); 215 assert_equals(pc2.pendingRemoteDescription, pc2.remoteDescription); 216 assert_equals(pc2.currentRemoteDescription, null, "currentRemoteDescription should not be set after a call to sRD(offer)"); 217 218 await srdPromise; 219 }, "setRemoteDescription(offer) in stable should update internal state with a queued task, in the right order"); 220 221 promise_test(async t => { 222 const pc1 = new RTCPeerConnection(); 223 t.add_cleanup(() => pc1.close()); 224 const pc2 = new RTCPeerConnection(); 225 t.add_cleanup(() => pc2.close()); 226 227 pc2.addTransceiver('audio', { direction: 'recvonly' }); 228 await pc2.setLocalDescription(await pc2.createOffer()); 229 230 // Implicit rollback! 231 pc1.addTransceiver('audio', { direction: 'recvonly' }); 232 const srdPromise = pc2.setRemoteDescription(await pc1.createOffer()); 233 234 assert_equals(pc2.signalingState, "have-local-offer", "signalingState should not be set synchronously after a call to sRD"); 235 236 assert_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should not be set synchronously after a call to sRD"); 237 assert_not_equals(pc2.pendingLocalDescription, null, "pendingLocalDescription should not be set synchronously after a call to sRD"); 238 assert_equals(pc2.pendingLocalDescription.type, "offer"); 239 assert_equals(pc2.pendingLocalDescription, pc2.localDescription); 240 241 // First, we should go through stable (the implicit rollback part) 242 const stablePromise = new Promise(resolve => { 243 pc2.onsignalingstatechange = () => { 244 resolve(pc2.signalingState); 245 } 246 }); 247 248 let raceValue = await Promise.race([stablePromise, srdPromise]); 249 assert_equals(raceValue, "stable", "signalingstatechange event should fire before sRD resolves"); 250 assert_equals(pc2.pendingLocalDescription, null, "pendingLocalDescription should be updated before the signalingstatechange event"); 251 assert_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should be updated before the signalingstatechange event"); 252 253 const haveRemoteOfferPromise = new Promise(resolve => { 254 pc2.onsignalingstatechange = () => { 255 resolve(pc2.signalingState); 256 } 257 }); 258 259 raceValue = await Promise.race([haveRemoteOfferPromise, srdPromise]); 260 assert_equals(raceValue, "have-remote-offer", "signalingstatechange event should fire before sRD resolves"); 261 assert_not_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should be updated before the signalingstatechange event"); 262 assert_equals(pc2.pendingRemoteDescription.type, "offer"); 263 assert_equals(pc2.pendingRemoteDescription, pc2.remoteDescription); 264 assert_equals(pc2.pendingRemoteDescription, pc2.pendingRemoteDescription); 265 assert_equals(pc2.pendingLocalDescription, null, "pendingLocalDescription should be updated before the signalingstatechange event"); 266 267 await srdPromise; 268 }, "setRemoteDescription(offer) in have-local-offer should update internal state with a queued task, in the right order"); 269 270 promise_test(async t => { 271 const pc1 = new RTCPeerConnection(); 272 const pc2 = new RTCPeerConnection(); 273 t.add_cleanup(() => pc1.close()); 274 t.add_cleanup(() => pc2.close()); 275 await pc1.setLocalDescription(await pc1.createOffer()); 276 const offer = await pc2.createOffer(); 277 const p1 = pc1.setLocalDescription({type: 'rollback'}); 278 await new Promise(r => pc1.onsignalingstatechange = r); 279 assert_equals(pc1.signalingState, 'stable'); 280 const p2 = pc1.addIceCandidate(); 281 const p3 = pc1.setRemoteDescription(offer); 282 await promise_rejects_dom(t, 'InvalidStateError', p2); 283 await p1; 284 await p3; 285 assert_equals(pc1.signalingState, 'have-remote-offer'); 286 }, 'Naive rollback approach is not glare-proof (control)'); 287 288 promise_test(async t => { 289 const pc1 = new RTCPeerConnection(); 290 const pc2 = new RTCPeerConnection(); 291 t.add_cleanup(() => pc1.close()); 292 t.add_cleanup(() => pc2.close()); 293 await pc1.setLocalDescription(await pc1.createOffer()); 294 const p = pc1.setRemoteDescription(await pc2.createOffer()); 295 await new Promise(r => pc1.onsignalingstatechange = r); 296 assert_equals(pc1.signalingState, 'stable'); 297 await pc1.addIceCandidate(); 298 await p; 299 assert_equals(pc1.signalingState, 'have-remote-offer'); 300 }, 'setRemoteDescription(offer) from have-local-offer is glare-proof'); 301 302 promise_test(async t => { 303 const pc1 = new RTCPeerConnection(); 304 const pc2 = new RTCPeerConnection(); 305 t.add_cleanup(() => pc1.close()); 306 t.add_cleanup(() => pc2.close()); 307 await pc1.setLocalDescription(await pc1.createOffer()); 308 const statePromise = new Promise(r => pc1.onsignalingstatechange = r); 309 // SRD with invalid SDP causes rollback. 310 const p = pc1.setRemoteDescription({type: 'offer', sdp: 'Invalid SDP'}); 311 // Ensure that p is eventually awaited for 312 t.add_cleanup(async () => Promise.allSettled([p])); 313 // Ensure that the test will fail rather than timing out if state 314 // does not change. 315 const timeoutPromise = new Promise( 316 (resolve, reject) => t.step_timeout(reject, 1000)); 317 try { 318 await Promise.any(statePromise, timeoutPromise); 319 } catch (error) { 320 assert_unreached('State should have changed'); 321 } 322 assert_equals(pc1.signalingState, 'stable'); 323 assert_equals(pc1.pendingLocalDescription, null); 324 assert_equals(pc1.pendingRemoteDescription, null); 325 await promise_rejects_dom(t, 'RTCError', p); 326 }, 'setRemoteDescription(invalidOffer) from have-local-offer does not undo rollback'); 327 328 promise_test(async t => { 329 const pc1 = new RTCPeerConnection(); 330 t.add_cleanup(() => pc1.close()); 331 const pc2 = new RTCPeerConnection(); 332 t.add_cleanup(() => pc2.close()); 333 pc1.addTransceiver('video'); 334 const offer = await pc1.createOffer(); 335 await pc2.setRemoteDescription(offer); 336 assert_equals(pc2.getTransceivers().length, 1); 337 await pc2.setRemoteDescription(offer); 338 assert_equals(pc2.getTransceivers().length, 1); 339 await pc1.setLocalDescription(offer); 340 const answer = await pc2.createAnswer(); 341 await pc2.setLocalDescription(answer); 342 await pc1.setRemoteDescription(answer); 343 }, 'repeated sRD(offer) works'); 344 345 promise_test(async t => { 346 const pc1 = new RTCPeerConnection(); 347 t.add_cleanup(() => pc1.close()); 348 const pc2 = new RTCPeerConnection(); 349 t.add_cleanup(() => pc2.close()); 350 pc1.addTransceiver('video'); 351 await exchangeOfferAnswer(pc1, pc2); 352 await waitForIceGatheringState(pc1, ['complete']); 353 await exchangeOfferAnswer(pc1, pc2); 354 await waitForIceStateChange(pc2, ['connected', 'completed']); 355 }, 'sRD(reoffer) with candidates and without trickle works'); 356 357 promise_test(async t => { 358 const pc1 = new RTCPeerConnection(); 359 t.add_cleanup(() => pc1.close()); 360 const pc2 = new RTCPeerConnection(); 361 t.add_cleanup(() => pc2.close()); 362 pc1.addTransceiver('video'); 363 const offer = await pc1.createOffer(); 364 const srdPromise = pc2.setRemoteDescription(offer); 365 assert_equals(pc2.getTransceivers().length, 0); 366 await srdPromise; 367 assert_equals(pc2.getTransceivers().length, 1); 368 }, 'Transceivers added by sRD(offer) should not show up until sRD resolves'); 369 370 promise_test(async t => { 371 const pc1 = new RTCPeerConnection(); 372 const pc2 = new RTCPeerConnection(); 373 t.add_cleanup(() => pc1.close()); 374 t.add_cleanup(() => pc2.close()); 375 pc1.addTransceiver('video'); 376 const description = await pc1.createOffer(); 377 const sections = SDPUtils.splitSections(description.sdp); 378 // Compose SDP with a duplicated media section (equal MSID lines) 379 // This is not permitted according to RFC 8830 section 2. 380 const sdp = sections[0] + sections[1] + sections[1].replace('a=mid:', 'a=mid:unique'); 381 const p = pc2.setRemoteDescription({type: 'offer', sdp: sdp}); 382 await promise_rejects_dom(t, 'OperationError', p); 383 }, 'setRemoteDescription(section with duplicate msid) rejects'); 384 385 </script>