Search code examples
webrtcrtcdatachannel

WebRTC Data Channel connection established but messages do not come


I am having trouble receiving WebRTC Data Channels messages between two peers.

Below is an MVP where both peers are one the same page and signalling channel is replaced with plain JavaScript object exchange.

I do not use trickle ICE by choice and I am aware of the drawbacks of not using it, I do not believe it should interfere with my MVP below in any way.

The code behaves exactly as expected and all the logging messages go through. The only ones missing are the ones for data channel message handling, as if the message never came from either peer to the other. That's with the event handler that attempts to send Test. The send method call fails with a null reference error which I couldn't track the root cause of.

I have followed Untangling the WebRTC Flow to get where I am with this MVP. You can see it live here so you don't have to run it yourself.

class Peer {
    constructor(name) {
        this.name = name;
    }
    offer() {
        const peerConnection = new webkitRTCPeerConnection({ iceServers: [ { url: "stun:stun.l.google.com:19302" } ] });
        peerConnection.onnegotiationneeded = event => console.debug(this.name, "onnegotiationneeded");
        peerConnection.onsignalingstatechange = event => console.debug(this.name, "onsignalingstatechange", peerConnection.signalingState);
        peerConnection.onicegatheringstatechange = event => console.debug(this.name, "onicegatheringstatechange", peerConnection.iceGatheringState);
        peerConnection.oniceconnectionstatechange = event => console.debug(this.name, "oniceconnectionstatechange", peerConnection.iceConnectionState);
        peerConnection.onconnectionstatechange = event => console.debug(this.name, "onconnectionstatechange", peerConnection.connectionState);
        peerConnection.ondatachannel = event => {
        const dataChannel = event.channel;
        dataChannel.onopen = event => {
            console.debug(this.name, "onopen");
            dataChannel.send("TEST");
        };
        dataChannel.onclose = event => console.debug(this.name, "onclose");
        dataChannel.onerror = event => console.debug(this.name, "onerror");
        dataChannel.onmessage = event => console.debug(this.name, "onmessage");
        console.debug(this.name, "ondatachannelO");
        this.dataChannel = dataChannel;
        };

        return new Promise((resolve, reject) => {
        peerConnection.onicecandidate = event => {
            if (!event.candidate) {
            peerConnection.createOffer()
                .then(offer => {
                console.debug(this.name, "created an offer with candidates.");
                this.peerConnection = peerConnection;
                resolve(peerConnection.localDescription);
                })
                .catch(reject);
            }
        };
        peerConnection.createDataChannel("datachannel");
        peerConnection.createOffer()
            .then(offer => {
            console.debug(this.name, "created an offer without candidates.");
            peerConnection.setLocalDescription(offer)
                .then(() => {
                console.debug(this.name, "set local description. Collecting candidates…");
                })
                .catch(reject);
            })
            .catch(reject);
        });
    }
    answer(offer) {
        const peerConnection = new webkitRTCPeerConnection({ iceServers: [ { url: "stun:stun.l.google.com:19302" } ] });
        peerConnection.onnegotiationneeded = event => console.debug(this.name, "onnegotiationneeded");
        peerConnection.onsignalingstatechange = event => console.debug(this.name, "onsignalingstatechange", peerConnection.signalingState);
        peerConnection.onicegatheringstatechange = event => console.debug(this.name, "onicegatheringstatechange", peerConnection.iceGatheringState);
        peerConnection.oniceconnectionstatechange = event => console.debug(this.name, "oniceconnectionstatechange", peerConnection.iceConnectionState);
        peerConnection.onconnectionstatechange = event => console.debug(this.name, "onconnectionstatechange", peerConnection.connectionState);
        peerConnection.ondatachannel = event => {
        const dataChannel = event.channel;
        dataChannel.onopen = event => {
            console.debug(this.name, "onopen");
            dataChannel.send("TEST");
        };
        dataChannel.onclose = event => console.debug(this.name, "onclose");
        dataChannel.onerror = event => console.debug(this.name, "onerror");
        dataChannel.onmessage = event => console.debug(this.name, "onmessage");
        console.debug(this.name, "ondatachannelA");
        this.dataChannel = dataChannel;
        };
        return new Promise((resolve, reject) => {
        peerConnection.onicecandidate = event => {
            if (!event.candidate) {
            peerConnection.createAnswer()
                .then(answer => {
                console.debug(this.name, "created an answer with candidates.");
                resolve(peerConnection.localDescription);
                })
                .catch(reject);
            }
        };
        peerConnection.setRemoteDescription(offer)
            .then(() => {
            console.debug(this.name, "set remote description.");
            peerConnection.createAnswer()
                .then(answer => {
                console.debug(this.name, "created an answer without candidates.");
                peerConnection.setLocalDescription(answer)
                    .then(() => {
                    console.debug(this.name, "set local description.");
                    })
                    .catch(reject);
                })
                .catch(reject);
            })
            .catch(reject);
        });
    }
    sealTheDeal(proffer) {
        return new Promise((resolve, reject) => {
        this.peerConnection.setRemoteDescription(proffer)
            .then(() => {
            console.debug(this.name, "set remote description.");
            resolve();
            })
            .catch(console.e);
        });
    }
    send() {
        this.dataChannel.send("TEST");
    }
    }
    function flow() {
    const peerA = new Peer("Alice");
    const peerB = new Peer("Bob");
    peerA.offer()
        .then(offer => {
        console.debug("Signal transfering offer from Alice to Bob.");
        peerB.answer(offer)
            .then(proffer => {
            console.debug("Signal transfering proffer from Bob to Alice.");
            peerA.sealTheDeal(proffer)
                .then(() => {
                peerB.offer()
                    .then(offer => {
                    console.debug("Signal transfering offer from Bob to Alice.");
                    peerA.answer(offer)
                        .then(proffer => {
                        console.debug("Signal transfering proffer from Alice to Bob.");
                        peerB.sealTheDeal(proffer)
                            .then(() => {
                            console.debug("HYPE");
                            peerA.send("From Alice to Bob.");
                            peerB.send("From Bob to Alice.");
                            })
                            .catch(console.error);
                        })
                        .catch(console.error);
                    })
                    .catch(console.error);
                })
                .catch(console.error);
            })
            .catch(console.error);
        })
        .catch(console.error);
        window.peerA = peerA;
        window.peerB = peerB;
    }
    flow();

Solution

  • peerConnection.ondatachannel only fires on the answerer.

    The offerer gets the data channel like this:

    this.dataChannel = peerConnection.createDataChannel("datachannel");
    

    Other comments:

    Example:

    var pc1 = new RTCPeerConnection(), pc2 = new RTCPeerConnection();
    
    pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
    pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
    pc1.oniceconnectionstatechange = e => console.log(pc1.iceConnectionState);
    
    pc1.onnegotiationneeded = e =>
      pc1.createOffer().then(d => pc1.setLocalDescription(d))
      .then(() => pc2.setRemoteDescription(pc1.localDescription))
      .then(() => pc2.createAnswer()).then(d => pc2.setLocalDescription(d))
      .then(() => pc1.setRemoteDescription(pc2.localDescription))
      .catch(e => console.log(e));
    
    var dc1, dc2;
    pc2.ondatachannel = e => {
      dc2 = e.channel;
      dc2.onopen = () => console.log("Chat!");
      dc2.onmessage = e => console.log("> " + e.data);
    };
    
    dc1 = pc1.createDataChannel("chat");
    dc1.onopen = () => (chat.disabled = false, chat.select());
    
    chat.onkeypress = e => {
      if (e.keyCode != 13) return;
      dc1.send(chat.value);
      chat.value = "";
    };
    Chat: <input id="chat" disabled></input>
    <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>