Why does calling `RTCPeerConnection.close()` not send `closed` event?

Viewed 624

When calling RTCPeerConnection.close() on a local connection, I would expect the remote connection to receive a closed connectionstatechange event.

Instead, there is a disconnected (followed by a failed) connectionstatechange a few seconds later, which suggests I am not closing the connection properly?

Are there any other steps required to close an RTCPeerConnection (e.g. do I need to remove any of the tracks?). Am I correct to expect a closed rather than failed event to be sent - and should this occur sooner than a few seconds?

2 Answers

This is expected. While closing a RTCPeerConnection will generate a DTLS alert there is no way to ensure this actually reaches the remote end since it is sent over UDP.

The common solution here is send a reliable message via your signalling protocol that you are closing to the other end who will then also close the connection.

The best way to detect the other peer closing the connection is to always negotiate SCTP data channels with your connection and listen for a data channel's closed event:

const pc1 = new RTCPeerConnection(), pc2 = new RTCPeerConnection();

const dc1 = pc1.createDataChannel("");
dc1.onopen = () => console.log("open");
dc1.onclose = () => console.log("remote closed");
button.onclick = () => pc2.close();

pc1.onnegotiationneeded = async () => {
  await pc1.setLocalDescription(await pc1.createOffer());
  await pc2.setRemoteDescription(pc1.localDescription);
  await pc2.setLocalDescription(await pc2.createAnswer());
  await pc1.setRemoteDescription(pc2.localDescription);
};
pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
pc2.oniceconnectionstatechange = () => console.log(pc2.iceConnectionState);

pc2.ondatachannel = e => window.keepalive = e.channel; // Firefox GC workaround
<button id="button">Close remote</button>

The remote browser sends an SCTP ABORT chunk over the wire when either the remote JS calls pc2.close() explicitly, or the remote user closes the browser tab or navigates away, i.e. a clean intentional connection shutdown on their part, distinct from any network drop.

This is one advantage of negotiating data channels, without which a peer connection has no other direct wire signal for intentional closing of the connection. But with it it totally does.

This is instant (peer to peer) and more reliable than using a signaling message which might not cover the case of the remote user closing their tab without writing additional JS.

Tab close demo

To demonstrate detection of a remote tab close, open this fiddle in two windows of the same browser, and click the Connect button in one of them. Now close either window and look in the other for the message "remote closed". It uses a localStorage hack for signaling, but if you look at the JavaScript you'll see the message is from the data channel:

dc.onclose = () => console.log("remote closed");
Related