WebRTC 对等连接不在 Safari 上建立,在 Chrome、Firefox 中建立

WebRTC peer connection does not establish on Safari, does in Chrome, Firefox

我整理了一个快速片段来测试在同一浏览器选项卡上下文中建立 WebRTC 对等连接。

const peerConnection1 = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.l.google.com: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.com: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);
  });
});

此代码片段适用于 Chrome 和 Firefox(在 Windows 上尝试了两个最新版本),但不适用于 Safari,既不适用于 iOS 也不适用于 macOS。

在工作浏览器中看到的日志:

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 complete
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 complete
2 onicecandidate
2 ICE candidate null

在非工作浏览器中看到的日志:

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

造成差异的原因可能是什么?看起来 Safari 中根本没有收集到任何候选主机。这是一种安全措施吗?我可以在开发中关闭它以使这段代码工作吗?生产怎么样?如果这是 ICE 和不同设备上的对等点的完整示例,我如何确保收集候选人以建立对等点连接?

我在这个 WebKit 错误报告中找到了问题的根源和解决方法:

https://bugs.webkit.org/show_bug.cgi?id=189503

关键是在尝试建立对等连接之前调用 navigator.mediaDevices.getUserMedia({ video: true })。 Safari 似乎避免公开候选主机,除非先获得许可。在我的示例中引入这一行后,现在连接成功了。