3

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?

Crossy
  • 43
  • 4
  • ithink you may use signalling server, but I am not really sure. ALSO Scheck https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/onconnectionstatechange – Chris P Feb 20 '21 at 22:57

2 Answers2

2

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.

Philipp Hancke
  • 15,855
  • 2
  • 23
  • 31
2

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");
jib
  • 40,579
  • 17
  • 100
  • 158
  • Could you clarify your last point about closing the browser tab? Did you mean `dc.close()`? Either way, when the remote user closes their browser I get an error on the data channel and peer connection (Chrome). – Crossy Feb 23 '21 at 06:05
  • @Crossy You get an error with the fiddle I included? What error do you get? Works for me in all 4 major browsers. – jib Feb 23 '21 at 13:14
  • I don't think I can test the closing browser tab using your example (which is what I was referring to). Closing browser tab does not seem to behave the same as `dc.close()`. – Crossy Feb 23 '21 at 17:06
  • @Crossy I've updated the answer. And no I mean `pc.close()` on the peer connection itself, which is implicitly [called on page unload](https://w3c.github.io/webrtc-pc/#ref-for-dfn-close-the-connection-1). – jib Feb 24 '21 at 00:40
  • 1
    Thanks for that demo - it works exactly as you describe! I wonder why it's not working for me - I'll have to investigate. Thanks a lot for the help. – Crossy Feb 24 '21 at 16:58