Your signaling is fine — the failure happens because the peers never complete the ICE connection.
Make sure you:
1. Call pc.addIceCandidate(new RTCIceCandidate(msg.data)) when receiving ICE from signaling.
2. Don’t send ICE candidates before setting the remote description — store them until pc.setRemoteDescription() is done.
3. Handle pc.ondatachannel on the non-initiator side.
4. Use the same STUN server config on both peers.
5. If still failing, test with a TURN server — STUN alone won’t relay traffic across NAT.
Most “connectionState: failed” issues come from missing addIceCandidate() or using only STUN behind NAT.