| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>Candidate exchange</title> | |
| <meta name=timeout content=long> | |
| <script src="/resources/testharness.js"></script> | |
| <script src="/resources/testharnessreport.js"></script> | |
| <script src="../RTCPeerConnection-helper.js"></script> | |
| </head> | |
| <body> | |
| <script> | |
| class StateLogger { | |
| constructor(source, eventname, field) { | |
| source.addEventListener(eventname, event => { | |
| this.events.push(source[field]); | |
| }); | |
| this.events = [source[field]]; | |
| } | |
| } | |
| class IceStateLogger extends StateLogger { | |
| constructor(source) { | |
| super(source, 'iceconnectionstatechange', 'iceConnectionState'); | |
| } | |
| } | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| pc1.createDataChannel('datachannel'); | |
| pc1IceStates = new IceStateLogger(pc1); | |
| pc2IceStates = new IceStateLogger(pc1); | |
| exchangeIceCandidates(pc1, pc2); | |
| await exchangeOfferAnswer(pc1, pc2); | |
| // Note - it's been claimed that this state sometimes jumps straight | |
| // to "completed". If so, this test should be flaky. | |
| await waitForIceStateChange(pc1, ['connected']); | |
| assert_array_equals(pc1IceStates.events, ['new', 'checking', 'connected']); | |
| assert_array_equals(pc2IceStates.events, ['new', 'checking', 'connected']); | |
| }, 'Two way ICE exchange works'); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| pc1IceStates = new IceStateLogger(pc1); | |
| pc2IceStates = new IceStateLogger(pc1); | |
| let candidates = []; | |
| pc1.createDataChannel('datachannel'); | |
| pc1.onicecandidate = e => { | |
| candidates.push(e.candidate); | |
| } | |
| // Candidates from PC2 are not delivered to pc1, so pc1 will use | |
| // peer-reflexive candidates. | |
| await exchangeOfferAnswer(pc1, pc2); | |
| const waiter = waitForIceGatheringState(pc1, ['complete']); | |
| await waiter; | |
| for (const candidate of candidates) { | |
| if (candidate) { | |
| pc2.addIceCandidate(candidate); | |
| } | |
| } | |
| await Promise.all([waitForIceStateChange(pc1, ['connected', 'completed']), | |
| waitForIceStateChange(pc2, ['connected', 'completed'])]); | |
| const candidate_pair = pc1.sctp.transport.iceTransport.getSelectedCandidatePair(); | |
| assert_equals(candidate_pair.local.type, 'host'); | |
| assert_equals(candidate_pair.remote.type, 'prflx'); | |
| assert_array_equals(pc1IceStates.events, ['new', 'checking', 'connected']); | |
| assert_array_equals(pc2IceStates.events, ['new', 'checking', 'connected']); | |
| }, 'Adding only caller -> callee candidates gives a connection'); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| pc1IceStates = new IceStateLogger(pc1); | |
| pc2IceStates = new IceStateLogger(pc1); | |
| let candidates = []; | |
| pc1.createDataChannel('datachannel'); | |
| pc2.onicecandidate = e => { | |
| candidates.push(e.candidate); | |
| } | |
| // Candidates from pc1 are not delivered to pc2. so pc2 will use | |
| // peer-reflexive candidates. | |
| await exchangeOfferAnswer(pc1, pc2); | |
| const waiter = waitForIceGatheringState(pc2, ['complete']); | |
| await waiter; | |
| for (const candidate of candidates) { | |
| if (candidate) { | |
| pc1.addIceCandidate(candidate); | |
| } | |
| } | |
| await Promise.all([waitForIceStateChange(pc1, ['connected', 'completed']), | |
| waitForIceStateChange(pc2, ['connected', 'completed'])]); | |
| const candidate_pair = pc2.sctp.transport.iceTransport.getSelectedCandidatePair(); | |
| assert_equals(candidate_pair.local.type, 'host'); | |
| assert_equals(candidate_pair.remote.type, 'prflx'); | |
| assert_array_equals(pc1IceStates.events, ['new', 'checking', 'connected']); | |
| assert_array_equals(pc2IceStates.events, ['new', 'checking', 'connected']); | |
| }, 'Adding only callee -> caller candidates gives a connection'); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| pc1IceStates = new IceStateLogger(pc1); | |
| pc2IceStates = new IceStateLogger(pc1); | |
| let pc2ToPc1Candidates = []; | |
| pc1.createDataChannel('datachannel'); | |
| pc2.onicecandidate = e => { | |
| pc2ToPc1Candidates.push(e.candidate); | |
| // This particular test verifies that candidates work | |
| // properly if added from the pc2 onicecandidate event. | |
| if (!e.candidate) { | |
| for (const candidate of pc2ToPc1Candidates) { | |
| if (candidate) { | |
| pc1.addIceCandidate(candidate); | |
| } | |
| } | |
| } | |
| } | |
| // Candidates from |pc1| are not delivered to |pc2|. |pc2| will use | |
| // peer-reflexive candidates. | |
| await exchangeOfferAnswer(pc1, pc2); | |
| await Promise.all([waitForIceStateChange(pc1, ['connected', 'completed']), | |
| waitForIceStateChange(pc2, ['connected', 'completed'])]); | |
| const candidate_pair = pc2.sctp.transport.iceTransport.getSelectedCandidatePair(); | |
| assert_equals(candidate_pair.local.type, 'host'); | |
| assert_equals(candidate_pair.remote.type, 'prflx'); | |
| assert_array_equals(pc1IceStates.events, ['new', 'checking', 'connected']); | |
| assert_array_equals(pc2IceStates.events, ['new', 'checking', 'connected']); | |
| }, 'Adding callee -> caller candidates from end-of-candidates gives a connection'); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| pc1IceStates = new IceStateLogger(pc1); | |
| pc2IceStates = new IceStateLogger(pc1); | |
| let pc1ToPc2Candidates = []; | |
| let pc2ToPc1Candidates = []; | |
| pc1.createDataChannel('datachannel'); | |
| pc1.onicecandidate = e => { | |
| pc1ToPc2Candidates.push(e.candidate); | |
| } | |
| pc2.onicecandidate = e => { | |
| pc2ToPc1Candidates.push(e.candidate); | |
| } | |
| const offer = await pc1.createOffer(); | |
| await Promise.all([pc1.setLocalDescription(offer), | |
| pc2.setRemoteDescription(offer)]); | |
| const answer = await pc2.createAnswer(); | |
| await waitForIceGatheringState(pc1, ['complete']); | |
| await pc2.setLocalDescription(answer).then(() => { | |
| for (const candidate of pc1ToPc2Candidates) { | |
| if (candidate) { | |
| pc2.addIceCandidate(candidate); | |
| } | |
| } | |
| }); | |
| await waitForIceGatheringState(pc2, ['complete']); | |
| pc1.setRemoteDescription(answer).then(async () => { | |
| for (const candidate of pc2ToPc1Candidates) { | |
| if (candidate) { | |
| await pc1.addIceCandidate(candidate); | |
| } | |
| } | |
| }); | |
| await Promise.all([waitForIceStateChange(pc1, ['connected', 'completed']), | |
| waitForIceStateChange(pc2, ['connected', 'completed'])]); | |
| const candidate_pair = | |
| pc1.sctp.transport.iceTransport.getSelectedCandidatePair(); | |
| assert_equals(candidate_pair.local.type, 'host'); | |
| // When we supply remote candidates, we expect a jump to the 'host' candidate, | |
| // but it might also remain as 'prflx'. | |
| assert_true(candidate_pair.remote.type == 'host' || | |
| candidate_pair.remote.type == 'prflx'); | |
| assert_array_equals(pc1IceStates.events, ['new', 'checking', 'connected']); | |
| assert_array_equals(pc2IceStates.events, ['new', 'checking', 'connected']); | |
| }, 'Explicit offer/answer exchange gives a connection'); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| pc1.createDataChannel('datachannel'); | |
| pc1.onicecandidate = assert_unreached; | |
| const offer = await pc1.createOffer(); | |
| await pc1.setLocalDescription(offer); | |
| await new Promise(resolve => { | |
| pc1.onicecandidate = resolve; | |
| }); | |
| }, 'Candidates always arrive after setLocalDescription(offer) resolves'); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| pc1.createDataChannel('datachannel'); | |
| pc2.onicecandidate = assert_unreached; | |
| const offer = await pc1.createOffer(); | |
| await pc2.setRemoteDescription(offer); | |
| await pc2.setLocalDescription(await pc2.createAnswer()); | |
| await new Promise(resolve => { | |
| pc2.onicecandidate = resolve; | |
| }); | |
| }, 'Candidates always arrive after setLocalDescription(answer) resolves'); | |
| </script> | |
| </body> | |
| </html> |