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");