| <!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> |