| <!doctype html> | |
| <meta charset=utf-8> | |
| <meta name="variant" content="?interop-2026"> | |
| <meta name="variant" content="?rest"> | |
| <title>RTCPeerConnection.prototype.setRemoteDescription rollback</title> | |
| <script src="/resources/testharness.js"></script> | |
| <script src="/resources/testharnessreport.js"></script> | |
| <script src="RTCPeerConnection-helper.js"></script> | |
| <script> | |
| 'use strict'; | |
| const rest = !new URLSearchParams(location.search).has("interop-2026"); | |
| const interop2026 = !new URLSearchParams(location.search).has("rest"); | |
| // 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 RTCPeerConnection-helper.js: | |
| // assert_session_desc_similar | |
| // generateAudioReceiveOnlyOffer | |
| // generateDataChannelOffer | |
| /* | |
| 4.3.2. Interface Definition | |
| [Constructor(optional RTCConfiguration configuration)] | |
| interface RTCPeerConnection : EventTarget { | |
| Promise<void> setLocalDescription( | |
| RTCSessionDescriptionInit description); | |
| readonly attribute RTCSessionDescription? localDescription; | |
| readonly attribute RTCSessionDescription? currentLocalDescription; | |
| readonly attribute RTCSessionDescription? pendingLocalDescription; | |
| 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" | |
| }; | |
| */ | |
| if (rest) { | |
| /* | |
| 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 "rollback", then this is a rollback. | |
| Set connection.pendingRemoteDescription to null and signaling state to stable. | |
| */ | |
| promise_test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const states = []; | |
| pc.addEventListener('signalingstatechange', () => states.push(pc.signalingState)); | |
| return generateDataChannelOffer(pc) | |
| .then(offer => pc.setRemoteDescription(offer)) | |
| .then(() => { | |
| assert_equals(pc.signalingState, 'have-remote-offer'); | |
| assert_not_equals(pc.remoteDescription, null); | |
| assert_equals(pc.pendingRemoteDescription, pc.remoteDescription); | |
| assert_equals(pc.currentRemoteDescription, null); | |
| return pc.setRemoteDescription({type: 'rollback'}); | |
| }) | |
| .then(() => { | |
| assert_equals(pc.signalingState, 'stable'); | |
| assert_equals(pc.remoteDescription, null); | |
| assert_equals(pc.pendingRemoteDescription, null); | |
| assert_equals(pc.currentRemoteDescription, null); | |
| assert_array_equals(states, ['have-remote-offer', 'stable']); | |
| }); | |
| }, 'setRemoteDescription(rollback) in have-remote-offer state should revert to stable state'); | |
| /* | |
| 4.3.1.6. Set the RTCSessionSessionDescription | |
| 2.3. If the description's type is invalid for the current signaling state of | |
| connection, then reject p with a newly created InvalidStateError and abort | |
| these steps. | |
| [jsep] | |
| 4.1.8.2. Rollback | |
| - Rollback can only be used to cancel proposed changes; | |
| there is no support for rolling back from a stable state to a | |
| previous stable state | |
| */ | |
| promise_test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| return promise_rejects_dom(t, 'InvalidStateError', | |
| pc.setRemoteDescription({type: 'rollback'})); | |
| }, `setRemoteDescription(rollback) from stable state should reject with InvalidStateError`); | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| await pc.setLocalDescription(); | |
| await promise_rejects_dom(t, 'InvalidStateError', pc.setRemoteDescription({ type: 'rollback' })); | |
| }, `setRemoteDescription(rollback) after setting a local offer should reject with InvalidStateError`); | |
| promise_test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| return generateAudioReceiveOnlyOffer(pc) | |
| .then(offer => pc.setRemoteDescription(offer)) | |
| .then(() => pc.setRemoteDescription({ | |
| type: 'rollback', | |
| sdp: '!<Invalid SDP Content>;' | |
| })); | |
| }, `setRemoteDescription(rollback) should ignore invalid sdp content and succeed`); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| // We don't use this right away | |
| pc1.addTransceiver('audio', { direction: 'recvonly' }); | |
| const offer1 = await pc1.createOffer(); | |
| // Create offer from pc2, apply and rollback on pc1 | |
| pc2.addTransceiver('audio', { direction: 'recvonly' }); | |
| const offer2 = await pc2.createOffer(); | |
| await pc1.setRemoteDescription(offer2); | |
| await pc1.setRemoteDescription({type: "rollback"}); | |
| // Then try applying pc1's old offer | |
| await pc1.setLocalDescription(offer1); | |
| }, `local offer created before setRemoteDescription(remote offer) then rollback should still be usable`); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| const stream = await getNoiseStream({video: true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| pc1.addTrack(stream.getTracks()[0], stream); | |
| // We don't use this right away. pc1 has provisionally decided that the | |
| // (only) transceiver is bound to level 0. | |
| const offer1 = await pc1.createOffer(); | |
| // Create offer from pc2, apply and rollback on pc1 | |
| pc2.addTransceiver('audio', { direction: 'recvonly' }); | |
| pc2.addTransceiver('video', { direction: 'recvonly' }); | |
| const offer2 = await pc2.createOffer(); | |
| // pc1 now should change its mind about what level its video transceiver is | |
| // bound to. It was 0, now it is 1. | |
| await pc1.setRemoteDescription(offer2); | |
| // Rolling back should put things back the way they were. | |
| await pc1.setRemoteDescription({type: "rollback"}); | |
| // Then try applying pc1's old offer | |
| await pc1.setLocalDescription(offer1); | |
| }, "local offer created before setRemoteDescription(remote offer) with different transceiver level assignments then rollback should still be usable"); | |
| } | |
| if (interop2026) { | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| const stream = await getNoiseStream({video: true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| pc1.addTrack(stream.getTracks()[0], stream); | |
| await pc2.setRemoteDescription(await pc1.createOffer()); | |
| assert_equals(pc2.getTransceivers().length, 1); | |
| await pc2.setRemoteDescription({type: "rollback"}); | |
| assert_equals(pc2.getTransceivers().length, 0); | |
| }, "rollback of a remote offer should remove a transceiver"); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| const stream = await getNoiseStream({video: true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| pc1.addTrack(stream.getTracks()[0], stream); | |
| await pc2.setRemoteDescription(await pc1.createOffer()); | |
| assert_equals(pc2.getTransceivers().length, 1); | |
| const stream2 = await getNoiseStream({video: true}); | |
| t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); | |
| const track = stream2.getVideoTracks()[0]; | |
| await pc2.getTransceivers()[0].sender.replaceTrack(track); | |
| await pc2.setRemoteDescription({type: "rollback"}); | |
| assert_equals(pc2.getTransceivers().length, 0); | |
| }, "rollback of a remote offer should remove touched transceiver"); | |
| } | |
| if (rest) { | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| const stream = await getNoiseStream({video: true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| pc1.addTrack(stream.getTracks()[0], stream); | |
| await pc2.setRemoteDescription(await pc1.createOffer()); | |
| assert_equals(pc2.getTransceivers().length, 1); | |
| const stream2 = await getNoiseStream({video: true}); | |
| t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); | |
| pc2.addTrack(stream2.getTracks()[0], stream2); | |
| await pc2.setRemoteDescription({type: "rollback"}); | |
| assert_equals(pc2.getTransceivers().length, 1); | |
| assert_equals(pc2.getTransceivers()[0].mid, null); | |
| assert_equals(pc2.getTransceivers()[0].receiver.transport, null); | |
| }, "rollback of a remote offer should keep a transceiver"); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| const stream = await getNoiseStream({video: true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| pc1.addTrack(stream.getTracks()[0], stream); | |
| const stream2 = await getNoiseStream({video: true}); | |
| t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); | |
| pc2.addTrack(stream2.getTracks()[0], stream2); | |
| await pc2.setRemoteDescription(await pc1.createOffer()); | |
| assert_equals(pc2.getTransceivers().length, 1); | |
| await pc2.setRemoteDescription({type: "rollback"}); | |
| assert_equals(pc2.getTransceivers().length, 1); | |
| assert_equals(pc2.getTransceivers()[0].mid, null); | |
| assert_equals(pc2.getTransceivers()[0].receiver.transport, null); | |
| }, "rollback of a remote offer should keep a transceiver created by addtrack"); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| const stream = await getNoiseStream({video: true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| pc1.addTrack(stream.getTracks()[0], stream); | |
| await pc2.setRemoteDescription(await pc1.createOffer()); | |
| assert_equals(pc2.getTransceivers().length, 1); | |
| const stream2 = await getNoiseStream({video: true}); | |
| t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); | |
| pc2.addTrack(stream2.getTracks()[0], stream2); | |
| await pc2.getTransceivers()[0].sender.replaceTrack(null); | |
| await pc2.setRemoteDescription({type: "rollback"}); | |
| assert_equals(pc2.getTransceivers().length, 1); | |
| }, "rollback of a remote offer should keep a transceiver without tracks"); | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const stream = await getNoiseStream({video: true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| pc.addTrack(stream.getTracks()[0], stream); | |
| const states = []; | |
| const signalingstatechangeResolver = new Resolver(); | |
| pc.onsignalingstatechange = () => { | |
| states.push(pc.signalingState); | |
| signalingstatechangeResolver.resolve(); | |
| }; | |
| const offer = await pc.createOffer(); | |
| await pc.setLocalDescription(offer); | |
| assert_not_equals(pc.getTransceivers()[0].sender.transport, null); | |
| await pc.setLocalDescription({type: "rollback"}); | |
| assert_equals(pc.getTransceivers().length, 1); | |
| assert_equals(pc.getTransceivers()[0].mid, null) | |
| assert_equals(pc.getTransceivers()[0].sender.transport, null); | |
| await pc.setLocalDescription(offer); | |
| assert_equals(pc.getTransceivers().length, 1); | |
| await signalingstatechangeResolver.promise; | |
| assert_array_equals(states, ['have-local-offer', 'stable', 'have-local-offer']); | |
| }, "explicit rollback of local offer should remove transceivers and transport"); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| const states = []; | |
| const signalingstatechangeResolver = new Resolver(); | |
| pc1.onsignalingstatechange = () => { | |
| states.push(pc1.signalingState); | |
| signalingstatechangeResolver.resolve(); | |
| }; | |
| const stream1 = await getNoiseStream({audio: true}); | |
| t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop())); | |
| pc1.addTransceiver(stream1.getTracks()[0], stream1); | |
| const stream2 = await getNoiseStream({audio: true}); | |
| t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); | |
| pc2.addTransceiver(stream2.getTracks()[0], stream2); | |
| await pc1.setLocalDescription(await pc1.createOffer()); | |
| pc1.onnegotiationneeded = t.step_func(() => assert_true(false, "There should be no negotiationneeded event right now")); | |
| await pc1.setRemoteDescription(await pc2.createOffer()); | |
| await pc1.setLocalDescription(await pc1.createAnswer()); | |
| await signalingstatechangeResolver.promise; | |
| assert_array_equals(states, ['have-local-offer', 'stable', 'have-remote-offer', 'stable']); | |
| await new Promise(r => pc1.onnegotiationneeded = r); | |
| }, "when using addTransceiver, implicit rollback of a local offer should visit stable state, but not fire negotiationneeded until we settle in stable"); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| const states = []; | |
| const signalingstatechangeResolver = new Resolver(); | |
| pc1.onsignalingstatechange = () => { | |
| states.push(pc1.signalingState); | |
| signalingstatechangeResolver.resolve(); | |
| }; | |
| const stream1 = await getNoiseStream({audio: true}); | |
| t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop())); | |
| pc1.addTrack(stream1.getTracks()[0], stream1); | |
| const stream2 = await getNoiseStream({audio: true}); | |
| t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); | |
| pc2.addTrack(stream2.getTracks()[0], stream2); | |
| await pc1.setLocalDescription(await pc1.createOffer()); | |
| pc1.onnegotiationneeded = t.step_func(() => assert_true(false, "There should be no negotiationneeded event in this test")); | |
| await pc1.setRemoteDescription(await pc2.createOffer()); | |
| await pc1.setLocalDescription(await pc1.createAnswer()); | |
| assert_array_equals(states, ['have-local-offer', 'stable', 'have-remote-offer', 'stable']); | |
| await new Promise(r => t.step_timeout(r, 0)); | |
| }, "when using addTrack, implicit rollback of a local offer should visit stable state, but not fire negotiationneeded"); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| const stream = await getNoiseStream({audio: true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| pc1.addTrack(stream.getTracks()[0], stream); | |
| await pc1.setLocalDescription(await pc1.createOffer()); | |
| await pc2.setRemoteDescription(pc1.pendingLocalDescription); | |
| await pc2.setLocalDescription(await pc2.createAnswer()); | |
| await pc1.setRemoteDescription(pc2.localDescription); | |
| // In stable state add video on both end and make sure video transceiver is not killed. | |
| const stream1 = await getNoiseStream({video: true}); | |
| t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop())); | |
| pc1.addTrack(stream1.getTracks()[0], stream1); | |
| await pc1.setLocalDescription(await pc1.createOffer()); | |
| const stream2 = await getNoiseStream({video: true}); | |
| t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); | |
| pc2.addTrack(stream2.getTracks()[0], stream2); | |
| const offer2 = await pc2.createOffer(); | |
| await pc2.setRemoteDescription(pc1.pendingLocalDescription); | |
| await pc2.setRemoteDescription({type: "rollback"}); | |
| assert_equals(pc2.getTransceivers().length, 2); | |
| await pc2.setLocalDescription(offer2); | |
| }, "rollback of a remote offer to negotiated stable state should enable " + | |
| "applying of a local offer"); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| const stream = await getNoiseStream({audio: true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| pc1.addTrack(stream.getTracks()[0], stream); | |
| await pc1.setLocalDescription(await pc1.createOffer()); | |
| await pc2.setRemoteDescription(pc1.pendingLocalDescription); | |
| await pc2.setLocalDescription(await pc2.createAnswer()); | |
| await pc1.setRemoteDescription(pc2.localDescription); | |
| // Both ends want to add video at the same time. pc2 rolls back. | |
| const stream2 = await getNoiseStream({video: true}); | |
| t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); | |
| pc2.addTrack(stream2.getTracks()[0], stream2); | |
| await pc2.setLocalDescription(await pc2.createOffer()); | |
| assert_equals(pc2.getTransceivers().length, 2); | |
| assert_not_equals(pc2.getTransceivers()[1].sender.transport, null); | |
| await pc2.setLocalDescription({type: "rollback"}); | |
| assert_equals(pc2.getTransceivers().length, 2); | |
| // Rollback didn't touch audio transceiver and transport is intact. | |
| assert_not_equals(pc2.getTransceivers()[0].sender.transport, null); | |
| // Video transport got killed. | |
| assert_equals(pc2.getTransceivers()[1].sender.transport, null); | |
| const stream1 = await getNoiseStream({video: true}); | |
| t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop())); | |
| pc1.addTrack(stream1.getTracks()[0], stream1); | |
| await pc1.setLocalDescription(await pc1.createOffer()); | |
| await pc2.setRemoteDescription(pc1.pendingLocalDescription); | |
| }, "rollback of a local offer to negotiated stable state should enable " + | |
| "applying of a remote offer"); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| const stream = await getNoiseStream({audio: true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| pc1.addTrack(stream.getTracks()[0], stream); | |
| await pc1.setLocalDescription(await pc1.createOffer()); | |
| await pc2.setRemoteDescription(pc1.pendingLocalDescription); | |
| await pc2.setLocalDescription(await pc2.createAnswer()); | |
| await pc1.setRemoteDescription(pc2.localDescription); | |
| // pc1 adds video and pc2 adds audio. pc2 rolls back. | |
| assert_equals(pc2.getTransceivers()[0].direction, "recvonly"); | |
| const stream2 = await getNoiseStream({audio: true}); | |
| t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); | |
| pc2.addTrack(stream2.getTracks()[0], stream2); | |
| assert_equals(pc2.getTransceivers()[0].direction, "sendrecv"); | |
| await pc2.setLocalDescription(await pc2.createOffer()); | |
| assert_equals(pc2.getTransceivers()[0].direction, "sendrecv"); | |
| await pc2.setLocalDescription({type: "rollback"}); | |
| assert_equals(pc2.getTransceivers().length, 1); | |
| // setLocalDescription didn't change direction. So direction remains "sendrecv" | |
| assert_equals(pc2.getTransceivers()[0].direction, "sendrecv"); | |
| // Rollback didn't touch audio transceiver and transport is intact. Still can receive audio. | |
| assert_not_equals(pc2.getTransceivers()[0].receiver.transport, null); | |
| const stream1 = await getNoiseStream({video: true}); | |
| t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop())); | |
| pc1.addTrack(stream1.getTracks()[0], stream1); | |
| await pc1.setLocalDescription(await pc1.createOffer()); | |
| await pc2.setRemoteDescription(pc1.pendingLocalDescription); | |
| }, "rollback a local offer with audio direction change to negotiated " + | |
| "stable state and then add video receiver"); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| pc1.addTransceiver('video', {direction: 'sendonly'}); | |
| pc2.addTransceiver('video', {direction: 'sendonly'}); | |
| await pc1.setLocalDescription(await pc1.createOffer()); | |
| const pc1FirstMid = pc1.getTransceivers()[0].mid; | |
| await pc2.setLocalDescription(await pc2.createOffer()); | |
| const pc2FirstMid = pc2.getTransceivers()[0].mid; | |
| // I don't think it is mandated that this has to be true, but any implementation I know of would | |
| // have predictable mids (e.g. 0, 1, 2...) so pc1 and pc2 should offer with the same mids. | |
| assert_equals(pc1FirstMid, pc2FirstMid); | |
| await pc1.setRemoteDescription(pc2.pendingLocalDescription); | |
| // We've implicitly rolled back and the SRD caused a second transceiver to be created. | |
| // As such, the first transceiver's mid will now be null, and the second transceiver's mid will | |
| // match the remote offer. | |
| assert_equals(pc1.getTransceivers().length, 2); | |
| assert_equals(pc1.getTransceivers()[0].mid, null); | |
| assert_equals(pc1.getTransceivers()[1].mid, pc2FirstMid); | |
| // If we now do an offer the first transceiver will get a different mid than in the first | |
| // pc1.createOffer()! | |
| pc1.setLocalDescription(await pc1.createAnswer()); | |
| await pc1.setLocalDescription(await pc1.createOffer()); | |
| assert_not_equals(pc1.getTransceivers()[0].mid, pc1FirstMid); | |
| }, "two transceivers with same mids"); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| const stream = await getNoiseStream({audio: true, video: true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const audio = stream.getAudioTracks()[0]; | |
| pc1.addTrack(audio, stream); | |
| const video = stream.getVideoTracks()[0]; | |
| pc1.addTrack(video, stream); | |
| let remoteStream = null; | |
| pc2.ontrack = e => { remoteStream = e.streams[0]; } | |
| await pc2.setRemoteDescription(await pc1.createOffer()); | |
| assert_true(remoteStream != null); | |
| let remoteTracks = remoteStream.getTracks(); | |
| const removedTracks = []; | |
| remoteStream.onremovetrack = e => { removedTracks.push(e.track.id); } | |
| await pc2.setRemoteDescription({type: "rollback"}); | |
| assert_equals(removedTracks.length, 2, | |
| "Rollback should have removed two tracks"); | |
| assert_true(removedTracks.includes(remoteTracks[0].id), | |
| "First track should be removed"); | |
| assert_true(removedTracks.includes(remoteTracks[1].id), | |
| "Second track should be removed"); | |
| }, "onremovetrack fires during remote rollback"); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| const stream1 = await getNoiseStream({audio: true}); | |
| t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop())); | |
| pc1.addTrack(stream1.getTracks()[0], stream1); | |
| const offer1 = await pc1.createOffer(); | |
| const remoteStreams = []; | |
| pc2.ontrack = e => { remoteStreams.push(e.streams[0]); } | |
| await pc1.setLocalDescription(offer1); | |
| await pc2.setRemoteDescription(pc1.pendingLocalDescription); | |
| await pc2.setLocalDescription(await pc2.createAnswer()); | |
| await pc1.setRemoteDescription(pc2.localDescription); | |
| assert_equals(remoteStreams.length, 1, "Number of remote streams"); | |
| assert_equals(remoteStreams[0].getTracks().length, 1, "Number of remote tracks"); | |
| const track = remoteStreams[0].getTracks()[0]; | |
| const stream2 = await getNoiseStream({audio: true}); | |
| t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); | |
| pc1.getTransceivers()[0].sender.setStreams(stream2); | |
| const offer2 = await pc1.createOffer(); | |
| await pc2.setRemoteDescription(offer2); | |
| assert_equals(remoteStreams.length, 2); | |
| assert_equals(remoteStreams[0].getTracks().length, 0); | |
| assert_equals(remoteStreams[1].getTracks()[0].id, track.id); | |
| await pc2.setRemoteDescription({type: "rollback"}); | |
| assert_equals(remoteStreams.length, 3); | |
| assert_equals(remoteStreams[0].id, remoteStreams[2].id); | |
| assert_equals(remoteStreams[1].getTracks().length, 0); | |
| assert_equals(remoteStreams[2].getTracks().length, 1); | |
| assert_equals(remoteStreams[2].getTracks()[0].id, track.id); | |
| }, "rollback of a remote offer with stream changes"); | |
| 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'); | |
| const offer = await pc2.createOffer(); | |
| await pc1.setRemoteDescription(offer); | |
| const [transceiver] = pc1.getTransceivers(); | |
| pc1.setRemoteDescription({type:'rollback'}); | |
| pc1.removeTrack(transceiver.sender); | |
| }, 'removeTrack() with a sender being rolled back does not crash or throw'); | |
| 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 channel = pc2.createDataChannel('dummy'); | |
| await pc2.setLocalDescription(await pc2.createOffer()); | |
| await pc2.setRemoteDescription(await pc1.createOffer()); | |
| assert_equals(pc2.signalingState, 'have-remote-offer'); | |
| await pc2.setLocalDescription(await pc2.createAnswer()); | |
| await pc2.setLocalDescription(await pc2.createOffer()); | |
| assert_equals(channel.readyState, 'connecting'); | |
| }, 'Implicit rollback with only a datachannel works'); | |
| } | |
| </script> |