<!doctype html> <meta charset=utf-8> <title>RTCPeerConnection.prototype.setRemoteDescription - offer</title> <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> <script src="RTCPeerConnection-helper.js"></script> <script> 'use strict'; // The following helper functions are called from RTCPeerConnection-helper.js: // assert_session_desc_similar() // generateAudioReceiveOnlyOffer /* 4.3.2. Interface Definition [Constructor(optional RTCConfiguration configuration)] interface RTCPeerConnection : EventTarget { Promise<void> setRemoteDescription( RTCSessionDescriptionInit description); readonly attribute RTCSessionDescription? remoteDescription; readonly attribute RTCSessionDescription? currentRemoteDescription; readonly attribute RTCSessionDescription? pendingRemoteDescription; ... }; 4.6.2. RTCSessionDescription Class dictionary RTCSessionDescriptionInit { required RTCSdpType type; DOMString sdp = ""; }; 4.6.1. RTCSdpType enum RTCSdpType { "offer", "pranswer", "answer", "rollback" }; */ /* 4.3.1.6. Set the RTCSessionSessionDescription 2.2.3. Otherwise, if description is set as a remote description, then run one of the following steps: - If description is of type "offer", set connection.pendingRemoteDescription attribute to description and signaling state to have-remote-offer. */ promise_test(t => { const pc1 = new RTCPeerConnection(); t.add_cleanup(() => pc1.close()); pc1.createDataChannel('datachannel'); const pc2 = new RTCPeerConnection(); t.add_cleanup(() => pc2.close()); const states = []; pc2.addEventListener('signalingstatechange', () => states.push(pc2.signalingState)); return pc1.createOffer() .then(offer => { return pc2.setRemoteDescription(offer) .then(() => { assert_equals(pc2.signalingState, 'have-remote-offer'); assert_session_desc_similar(pc2.remoteDescription, offer); assert_session_desc_similar(pc2.pendingRemoteDescription, offer); assert_equals(pc2.currentRemoteDescription, null); assert_array_equals(states, ['have-remote-offer']); }); }); }, 'setRemoteDescription with valid offer should succeed'); promise_test(t => { const pc1 = new RTCPeerConnection(); t.add_cleanup(() => pc1.close()); pc1.createDataChannel('datachannel'); const pc2 = new RTCPeerConnection(); t.add_cleanup(() => pc2.close()); const states = []; pc2.addEventListener('signalingstatechange', () => states.push(pc2.signalingState)); return pc1.createOffer() .then(offer => { return pc2.setRemoteDescription(offer) .then(() => pc2.setRemoteDescription(offer)) .then(() => { assert_equals(pc2.signalingState, 'have-remote-offer'); assert_session_desc_similar(pc2.remoteDescription, offer); assert_session_desc_similar(pc2.pendingRemoteDescription, offer); assert_equals(pc2.currentRemoteDescription, null); assert_array_equals(states, ['have-remote-offer']); }); }); }, 'setRemoteDescription multiple times should succeed'); promise_test(t => { const pc1 = new RTCPeerConnection(); t.add_cleanup(() => pc1.close()); pc1.createDataChannel('datachannel'); const pc2 = new RTCPeerConnection(); t.add_cleanup(() => pc2.close()); const states = []; pc2.addEventListener('signalingstatechange', () => states.push(pc2.signalingState)); return pc1.createOffer() .then(offer1 => { return pc1.setLocalDescription(offer1) .then(()=> { return generateAudioReceiveOnlyOffer(pc1) .then(offer2 => { assert_session_desc_not_similar(offer1, offer2); return pc2.setRemoteDescription(offer1) .then(() => pc2.setRemoteDescription(offer2)) .then(() => { assert_equals(pc2.signalingState, 'have-remote-offer'); assert_session_desc_similar(pc2.remoteDescription, offer2); assert_session_desc_similar(pc2.pendingRemoteDescription, offer2); assert_equals(pc2.currentRemoteDescription, null); assert_array_equals(states, ['have-remote-offer']); }); }); }); }); }, 'setRemoteDescription multiple times with different offer should succeed'); /* 4.3.1.6. Set the RTCSessionSessionDescription 2.1.4. If the content of description is not valid SDP syntax, then reject p with an RTCError (with errorDetail set to "sdp-syntax-error" and the sdpLineNumber attribute set to the line number in the SDP where the syntax error was detected) and abort these steps. */ promise_test(t => { const pc = new RTCPeerConnection(); t.add_cleanup(() => pc.close()); return pc.setRemoteDescription({ type: 'offer', sdp: 'Invalid SDP' }) .then(() => { assert_unreached('Expect promise to be rejected'); }, err => { assert_equals(err.errorDetail, 'sdp-syntax-error', 'Expect error detail field to set to sdp-syntax-error'); assert_true(err instanceof RTCError, 'Expect err to be instance of RTCError'); }); }, 'setRemoteDescription(offer) with invalid SDP should reject with RTCError'); promise_test(async t => { const pc1 = new RTCPeerConnection(); const pc2 = new RTCPeerConnection(); t.add_cleanup(() => pc1.close()); t.add_cleanup(() => pc2.close()); await pc1.setLocalDescription(await pc1.createOffer()); await pc1.setRemoteDescription(await pc2.createOffer()); assert_equals(pc1.signalingState, 'have-remote-offer'); }, 'setRemoteDescription(offer) from have-local-offer should roll back and succeed'); promise_test(async t => { const pc1 = new RTCPeerConnection(); const pc2 = new RTCPeerConnection(); t.add_cleanup(() => pc1.close()); t.add_cleanup(() => pc2.close()); await pc1.setLocalDescription(await pc1.createOffer()); const p = pc1.setRemoteDescription(await pc2.createOffer()); await new Promise(r => pc1.onsignalingstatechange = r); assert_equals(pc1.signalingState, 'stable'); assert_equals(pc1.pendingLocalDescription, null); assert_equals(pc1.pendingRemoteDescription, null); await new Promise(r => pc1.onsignalingstatechange = r); assert_equals(pc1.signalingState, 'have-remote-offer'); assert_equals(pc1.pendingLocalDescription, null); assert_equals(pc1.pendingRemoteDescription.type, 'offer'); await p; }, 'setRemoteDescription(offer) from have-local-offer fires signalingstatechange twice'); promise_test(async t => { const pc1 = new RTCPeerConnection(); t.add_cleanup(() => pc1.close()); const pc2 = new RTCPeerConnection(); t.add_cleanup(() => pc2.close()); pc1.addTransceiver('audio', { direction: 'recvonly' }); const srdPromise = pc2.setRemoteDescription(await pc1.createOffer()); assert_equals(pc2.signalingState, "stable", "signalingState should not be set synchronously after a call to sRD"); assert_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should not be set synchronously after a call to sRD"); assert_equals(pc2.currentRemoteDescription, null, "currentRemoteDescription should not be set synchronously after a call to sRD"); const statePromise = new Promise(resolve => { pc2.onsignalingstatechange = () => { resolve(pc2.signalingState); } }); const raceValue = await Promise.race([statePromise, srdPromise]); assert_equals(raceValue, "have-remote-offer", "signalingstatechange event should fire before sRD resolves"); assert_not_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should be updated before the signalingstatechange event"); assert_equals(pc2.pendingRemoteDescription.type, "offer"); assert_equals(pc2.pendingRemoteDescription.sdp, pc2.remoteDescription.sdp); assert_equals(pc2.currentRemoteDescription, null, "currentRemoteDescription should not be set after a call to sRD(offer)"); await srdPromise; }, "setRemoteDescription(offer) in stable should update internal state with a queued task, in the right order"); promise_test(async t => { const pc1 = new RTCPeerConnection(); t.add_cleanup(() => pc1.close()); const pc2 = new RTCPeerConnection(); t.add_cleanup(() => pc2.close()); pc2.addTransceiver('audio', { direction: 'recvonly' }); await pc2.setLocalDescription(await pc2.createOffer()); // Implicit rollback! pc1.addTransceiver('audio', { direction: 'recvonly' }); const srdPromise = pc2.setRemoteDescription(await pc1.createOffer()); assert_equals(pc2.signalingState, "have-local-offer", "signalingState should not be set synchronously after a call to sRD"); assert_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should not be set synchronously after a call to sRD"); assert_not_equals(pc2.pendingLocalDescription, null, "pendingLocalDescription should not be set synchronously after a call to sRD"); assert_equals(pc2.pendingLocalDescription.type, "offer"); assert_equals(pc2.pendingLocalDescription.sdp, pc2.localDescription.sdp); // First, we should go through stable (the implicit rollback part) const stablePromise = new Promise(resolve => { pc2.onsignalingstatechange = () => { resolve(pc2.signalingState); } }); let raceValue = await Promise.race([stablePromise, srdPromise]); assert_equals(raceValue, "stable", "signalingstatechange event should fire before sRD resolves"); assert_equals(pc2.pendingLocalDescription, null, "pendingLocalDescription should be updated before the signalingstatechange event"); assert_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should be updated before the signalingstatechange event"); const haveRemoteOfferPromise = new Promise(resolve => { pc2.onsignalingstatechange = () => { resolve(pc2.signalingState); } }); raceValue = await Promise.race([haveRemoteOfferPromise, srdPromise]); assert_equals(raceValue, "have-remote-offer", "signalingstatechange event should fire before sRD resolves"); assert_not_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should be updated before the signalingstatechange event"); assert_equals(pc2.pendingRemoteDescription.type, "offer"); assert_equals(pc2.pendingRemoteDescription.sdp, pc2.remoteDescription.sdp); assert_equals(pc2.pendingLocalDescription, null, "pendingLocalDescription should be updated before the signalingstatechange event"); await srdPromise; }, "setRemoteDescription(offer) in have-local-offer should update internal state with a queued task, in the right order"); promise_test(async t => { const pc1 = new RTCPeerConnection(); const pc2 = new RTCPeerConnection(); t.add_cleanup(() => pc1.close()); t.add_cleanup(() => pc2.close()); await pc1.setLocalDescription(await pc1.createOffer()); const offer = await pc2.createOffer(); const p1 = pc1.setLocalDescription({type: 'rollback'}); await new Promise(r => pc1.onsignalingstatechange = r); assert_equals(pc1.signalingState, 'stable'); const p2 = pc1.addIceCandidate(); const p3 = pc1.setRemoteDescription(offer); await promise_rejects_dom(t, 'InvalidStateError', p2); await p1; await p3; assert_equals(pc1.signalingState, 'have-remote-offer'); }, 'Naive rollback approach is not glare-proof (control)'); promise_test(async t => { const pc1 = new RTCPeerConnection(); const pc2 = new RTCPeerConnection(); t.add_cleanup(() => pc1.close()); t.add_cleanup(() => pc2.close()); await pc1.setLocalDescription(await pc1.createOffer()); const p = pc1.setRemoteDescription(await pc2.createOffer()); await new Promise(r => pc1.onsignalingstatechange = r); assert_equals(pc1.signalingState, 'stable'); await pc1.addIceCandidate(); await p; assert_equals(pc1.signalingState, 'have-remote-offer'); }, 'setRemoteDescription(offer) from have-local-offer is glare-proof'); promise_test(async t => { const pc1 = new RTCPeerConnection(); const pc2 = new RTCPeerConnection(); t.add_cleanup(() => pc1.close()); t.add_cleanup(() => pc2.close()); await pc1.setLocalDescription(await pc1.createOffer()); const p = pc1.setRemoteDescription({type: 'offer', sdp: 'Invalid SDP'}); await new Promise(r => pc1.onsignalingstatechange = r); assert_equals(pc1.signalingState, 'stable'); assert_equals(pc1.pendingLocalDescription, null); assert_equals(pc1.pendingRemoteDescription, null); await promise_rejects_dom(t, 'RTCError', p); }, 'setRemoteDescription(invalidOffer) from have-local-offer does not undo rollback'); promise_test(async t => { const pc1 = new RTCPeerConnection(); t.add_cleanup(() => pc1.close()); const pc2 = new RTCPeerConnection(); t.add_cleanup(() => pc2.close()); pc1.addTransceiver('video'); const offer = await pc1.createOffer(); await pc2.setRemoteDescription(offer); assert_equals(pc2.getTransceivers().length, 1); await pc2.setRemoteDescription(offer); assert_equals(pc2.getTransceivers().length, 1); await pc1.setLocalDescription(offer); const answer = await pc2.createAnswer(); await pc2.setLocalDescription(answer); await pc1.setRemoteDescription(answer); }, 'repeated sRD(offer) works'); promise_test(async t => { const pc1 = new RTCPeerConnection(); t.add_cleanup(() => pc1.close()); const pc2 = new RTCPeerConnection(); t.add_cleanup(() => pc2.close()); pc1.addTransceiver('video'); await exchangeOfferAnswer(pc1, pc2); await waitForIceGatheringState(pc1, ['complete']); await exchangeOfferAnswer(pc1, pc2); await waitForIceStateChange(pc2, ['connected', 'completed']); }, 'sRD(reoffer) with candidates and without trickle works'); promise_test(async t => { const pc1 = new RTCPeerConnection(); t.add_cleanup(() => pc1.close()); const pc2 = new RTCPeerConnection(); t.add_cleanup(() => pc2.close()); pc1.addTransceiver('video'); const offer = await pc1.createOffer(); const srdPromise = pc2.setRemoteDescription(offer); assert_equals(pc2.getTransceivers().length, 0); await srdPromise; assert_equals(pc2.getTransceivers().length, 1); }, 'Transceivers added by sRD(offer) should not show up until sRD resolves'); </script>