I've put together a quick snippet to test establishing a WebRTC peer connection within the same browser tab context.
const peerConnection1 = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.l.google:19302' } ] });
peerConnection1.addEventListener('signalingstatechange', _ => log('1 signaling state ' + peerConnection1.signalingState));
peerConnection1.addEventListener('icegatheringstatechange', _ => log('1 ICE gathering state ' + peerConnection1.iceGatheringState));
peerConnection1.addEventListener('connectionstatechange', _ => log('1 connection state ' + peerConnection1.connectionState));
const peerConnection2 = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.l.google:19302' } ] });
peerConnection2.addEventListener('signalingstatechange', _ => log('2 signaling state ' + peerConnection1.signalingState));
peerConnection2.addEventListener('icegatheringstatechange', _ => log('2 ICE gathering state ' + peerConnection1.iceGatheringState));
peerConnection2.addEventListener('connectionstatechange', _ => log('2 connection state ' + peerConnection1.connectionState));
const dataChannel = peerConnection1.createDataChannel(null);
const offer = await peerConnection1.createOffer();
await peerConnection1.setLocalDescription(offer);
await peerConnection2.setRemoteDescription(offer);
const answer = await peerConnection2.createAnswer();
await peerConnection2.setLocalDescription(answer);
await peerConnection1.setRemoteDescription(answer);
peerConnection1.addEventListener('icecandidate', event => {
log('1 ICE candidate ' + (event.candidate ? event.candidate.candidate : 'null'))
if (event.candidate !== null) {
peerConnection2.addIceCandidate(event.candidate);
}
});
peerConnection2.addEventListener('icecandidate', event => {
log('2 ICE candidate ' + (event.candidate ? event.candidate.candidate : 'null'))
if (event.candidate !== null) {
peerConnection1.addIceCandidate(event.candidate);
}
});
dataChannel.addEventListener('open', () => {
dataChannel.send('message from 1 to 2');
});
dataChannel.addEventListener('message', event => {
log('2: ' + event.data);
});
peerConnection2.addEventListener('datachannel', event => {
monitor(event.channel, 'dc 2');
event.channel.addEventListener('open', () => {
event.channel.send('message from 2 to 1');
});
event.channel.addEventListener('message', event => {
log('1: ' + event.data);
});
});
This snippet works in Chrome and Firefox (tried both latest versions on Windows), but does not work in Safari, neither on iOS nor on macOS.
The log as seen in working browsers:
1 onnegotiationneeded
1 onsignalingstatechange
1 signaling state have-local-offer
2 onsignalingstatechange
2 signaling state have-local-offer
2 onsignalingstatechange
2 signaling state have-local-offer
1 onsignalingstatechange
1 signaling state stable
1 onicegatheringstatechange
1 ICE gathering state gathering
1 onicecandidate
1 ICE candidate candidate:0 1 UDP 2122252543 ... 59263 typ host
1 onicecandidate
1 ICE candidate candidate:2 1 TCP 2105524479 ... 9 typ host tcptype active
2 onicegatheringstatechange
2 ICE gathering state gathering
2 onicecandidate
2 ICE candidate candidate:0 1 UDP 2122252543 ... 59264 typ host
2 onicecandidate
2 ICE candidate candidate:2 1 TCP 2105524479 ... 9 typ host tcptype active
2 oniceconnectionstatechange
1 oniceconnectionstatechange
1 oniceconnectionstatechange
2 oniceconnectionstatechange
dc 1 onopen
2 ondatachannel
dc 2 onopen
dc 2 onmessage
1: message from 1 to 2
dc 1 onmessage
2: message from 2 to 1
1 onicecandidate
1 ICE candidate candidate:1 1 UDP 1686052863 ... 59263 typ srflx raddr ... rport 59263
1 onicegatheringstatechange
1 ICE gathering state plete
1 onicecandidate
1 ICE candidate null
2 onicecandidate
2 ICE candidate candidate:1 1 UDP 1686052863 ... 59264 typ srflx raddr ... rport 59264
2 onicegatheringstatechange
2 ICE gathering state plete
2 onicecandidate
2 ICE candidate null
The log as seen in non-working browsers:
1 onnegotiationneeded
1 onsignalingstatechange
1 signaling state have-local-offer
1 onicegatheringstatechange
1 ICE gathering state gathering
1 onconnectionstatechange
1 connection state connecting
2 onsignalingstatechange
2 signaling state have-local-offer
2 onsignalingstatechange
2 signaling state have-local-offer
2 onicegatheringstatechange
2 ICE gathering state gathering
2 onconnectionstatechange
2 connection state connecting
1 onsignalingstatechange
1 signaling state stable
1 oniceconnectionstatechange
1 onicecandidate
1 ICE candidate candidate:842163049 1 udp 1677729535 ... 55297 typ srflx raddr 0.0.0.0 rport 0 generation 0 ufrag e+HS network-cost 50
1 onicecandidate
1 ICE candidate null
1 onicegatheringstatechange
1 ICE gathering state plete
2 oniceconnectionstatechange
2 onicecandidate
2 ICE candidate candidate:842163049 1 udp 1677729535 ... 53858 typ srflx raddr 0.0.0.0 rport 0 generation 0 ufrag X+Uv network-cost 50
2 onicecandidate
2 ICE candidate null
2 onicegatheringstatechange
2 ICE gathering state plete
What could be the reason for the difference? It looks like no host candidates at all are collected in Safari. Is this a security measure? Can I turn it off in development to make this code work? How about production? Had this been a full example with ICE and peers on different devices, how could I have made sure candidates were collected in order to establish the peer connection?
I've put together a quick snippet to test establishing a WebRTC peer connection within the same browser tab context.
const peerConnection1 = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.l.google.:19302' } ] });
peerConnection1.addEventListener('signalingstatechange', _ => log('1 signaling state ' + peerConnection1.signalingState));
peerConnection1.addEventListener('icegatheringstatechange', _ => log('1 ICE gathering state ' + peerConnection1.iceGatheringState));
peerConnection1.addEventListener('connectionstatechange', _ => log('1 connection state ' + peerConnection1.connectionState));
const peerConnection2 = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.l.google.:19302' } ] });
peerConnection2.addEventListener('signalingstatechange', _ => log('2 signaling state ' + peerConnection1.signalingState));
peerConnection2.addEventListener('icegatheringstatechange', _ => log('2 ICE gathering state ' + peerConnection1.iceGatheringState));
peerConnection2.addEventListener('connectionstatechange', _ => log('2 connection state ' + peerConnection1.connectionState));
const dataChannel = peerConnection1.createDataChannel(null);
const offer = await peerConnection1.createOffer();
await peerConnection1.setLocalDescription(offer);
await peerConnection2.setRemoteDescription(offer);
const answer = await peerConnection2.createAnswer();
await peerConnection2.setLocalDescription(answer);
await peerConnection1.setRemoteDescription(answer);
peerConnection1.addEventListener('icecandidate', event => {
log('1 ICE candidate ' + (event.candidate ? event.candidate.candidate : 'null'))
if (event.candidate !== null) {
peerConnection2.addIceCandidate(event.candidate);
}
});
peerConnection2.addEventListener('icecandidate', event => {
log('2 ICE candidate ' + (event.candidate ? event.candidate.candidate : 'null'))
if (event.candidate !== null) {
peerConnection1.addIceCandidate(event.candidate);
}
});
dataChannel.addEventListener('open', () => {
dataChannel.send('message from 1 to 2');
});
dataChannel.addEventListener('message', event => {
log('2: ' + event.data);
});
peerConnection2.addEventListener('datachannel', event => {
monitor(event.channel, 'dc 2');
event.channel.addEventListener('open', () => {
event.channel.send('message from 2 to 1');
});
event.channel.addEventListener('message', event => {
log('1: ' + event.data);
});
});
This snippet works in Chrome and Firefox (tried both latest versions on Windows), but does not work in Safari, neither on iOS nor on macOS.
The log as seen in working browsers:
1 onnegotiationneeded
1 onsignalingstatechange
1 signaling state have-local-offer
2 onsignalingstatechange
2 signaling state have-local-offer
2 onsignalingstatechange
2 signaling state have-local-offer
1 onsignalingstatechange
1 signaling state stable
1 onicegatheringstatechange
1 ICE gathering state gathering
1 onicecandidate
1 ICE candidate candidate:0 1 UDP 2122252543 ... 59263 typ host
1 onicecandidate
1 ICE candidate candidate:2 1 TCP 2105524479 ... 9 typ host tcptype active
2 onicegatheringstatechange
2 ICE gathering state gathering
2 onicecandidate
2 ICE candidate candidate:0 1 UDP 2122252543 ... 59264 typ host
2 onicecandidate
2 ICE candidate candidate:2 1 TCP 2105524479 ... 9 typ host tcptype active
2 oniceconnectionstatechange
1 oniceconnectionstatechange
1 oniceconnectionstatechange
2 oniceconnectionstatechange
dc 1 onopen
2 ondatachannel
dc 2 onopen
dc 2 onmessage
1: message from 1 to 2
dc 1 onmessage
2: message from 2 to 1
1 onicecandidate
1 ICE candidate candidate:1 1 UDP 1686052863 ... 59263 typ srflx raddr ... rport 59263
1 onicegatheringstatechange
1 ICE gathering state plete
1 onicecandidate
1 ICE candidate null
2 onicecandidate
2 ICE candidate candidate:1 1 UDP 1686052863 ... 59264 typ srflx raddr ... rport 59264
2 onicegatheringstatechange
2 ICE gathering state plete
2 onicecandidate
2 ICE candidate null
The log as seen in non-working browsers:
1 onnegotiationneeded
1 onsignalingstatechange
1 signaling state have-local-offer
1 onicegatheringstatechange
1 ICE gathering state gathering
1 onconnectionstatechange
1 connection state connecting
2 onsignalingstatechange
2 signaling state have-local-offer
2 onsignalingstatechange
2 signaling state have-local-offer
2 onicegatheringstatechange
2 ICE gathering state gathering
2 onconnectionstatechange
2 connection state connecting
1 onsignalingstatechange
1 signaling state stable
1 oniceconnectionstatechange
1 onicecandidate
1 ICE candidate candidate:842163049 1 udp 1677729535 ... 55297 typ srflx raddr 0.0.0.0 rport 0 generation 0 ufrag e+HS network-cost 50
1 onicecandidate
1 ICE candidate null
1 onicegatheringstatechange
1 ICE gathering state plete
2 oniceconnectionstatechange
2 onicecandidate
2 ICE candidate candidate:842163049 1 udp 1677729535 ... 53858 typ srflx raddr 0.0.0.0 rport 0 generation 0 ufrag X+Uv network-cost 50
2 onicecandidate
2 ICE candidate null
2 onicegatheringstatechange
2 ICE gathering state plete
What could be the reason for the difference? It looks like no host candidates at all are collected in Safari. Is this a security measure? Can I turn it off in development to make this code work? How about production? Had this been a full example with ICE and peers on different devices, how could I have made sure candidates were collected in order to establish the peer connection?
Share Improve this question asked Dec 24, 2018 at 12:25 Tomáš HübelbauerTomáš Hübelbauer 10.8k17 gold badges74 silver badges141 bronze badges1 Answer
Reset to default 5I've found the source of the problem and a workaround in this WebKit bug report:
https://bugs.webkit/show_bug.cgi?id=189503
The key is to call navigator.mediaDevices.getUserMedia({ video: true })
before trying to establish the peer connection. Safari seems to avoid disclosing the host candidates unless the permissions have been given first. After introducing this line to my example, the connection now succeeds.