1

I'm trying to establish a WebRTC connection between two browsers. I have a node.js server for them to communicate through, which essentially just forwards the messages from one client to the other. I am running the server and two tabs all on my laptop, but I have not been able to make a connection. I have been able to send the offers and answers between the two tabs successfully resulting in pc.signalingState = 'stable' in both tabs. I believe once this is done then the RTCPeerConnection objects should start producing icecandidate events, but this is not happening and I do not know why. Here is my code (I've omitted the server code):

'use strict';
// This is mostly copy pasted from webrtc.org/getting-started/peer-connections.

import { io } from 'socket.io-client';

const configuration = {
    'iceServers': [
        { 'urls': 'stun:stun4.l.google.com:19302' },
        { 'urls': 'stun:stunserver.stunprotocol.org:3478' },
    ]
}

// Returns a promise for an RTCDataChannel
function join() {
    const socket = io('ws://localhost:8090');
    const pc = new RTCPeerConnection(configuration);

    socket.on('error', error => {
        socket.close();
        throw error;
    });

    pc.addEventListener('signalingstatechange', event => {
        // Prints 'have-local-offer' then 'stable' in one tab,
        // 'have-remote-offer' then 'stable' in the other.
        console.log(pc.signalingState);
    })

    pc.addEventListener('icegatheringstatechange', event => {
        console.log(pc.iceGatheringState); // This line is never reached.
    })


    // Listen for local ICE candidates on the local RTCPeerConnection
    pc.addEventListener('icecandidate', event => {
        if (event.candidate) {
            console.log('Sending ICE candidate'); // This line is never reached.
            socket.emit('icecandidate', event.candidate);
        }
    });

    // Listen for remote ICE candidates and add them to the local RTCPeerConnection
    socket.on('icecandidate', async candidate => {
        try {
            await pc.addIceCandidate(candidate);
        } catch (e) {
            console.error('Error adding received ice candidate', e);
        }
    });

    // Listen for connectionstatechange on the local RTCPeerConnection
    pc.addEventListener('connectionstatechange', event => {
        if (pc.connectionState === 'connected') {
            socket.close();
        }
    });

    // When both browsers send this signal they will both receive the 'matched' signal,
    // one with the payload true and the other with false.
    socket.emit('join');
    
    return new Promise((res, rej) => {
        socket.on('matched', async first => {
            if (first) {
                // caller side
                socket.on('answer', async answer => {
                    await pc.setRemoteDescription(new RTCSessionDescription(answer))
                        .catch(console.error);
                });
                const offer = await pc.createOffer();
                await pc.setLocalDescription(offer)
                    .catch(console.error);
                socket.emit('offer', offer);

                // Listen for connectionstatechange on the local RTCPeerConnection
                pc.addEventListener('connectionstatechange', event => {
                    if (pc.connectionState === 'connected') {
                        res(pc.createDataChannel('data'));
                    }
                });

            } else {
                // recipient side
                socket.on('offer', async offer => {
                    pc.setRemoteDescription(new RTCSessionDescription(offer))
                        .catch(console.error);
                    const answer = await pc.createAnswer();
                    await pc.setLocalDescription(answer)
                        .catch(console.error);
                    socket.emit('answer', answer);
                });

                pc.addEventListener('datachannel', event => {
                    res(event.channel);
                });
            }
        });
    });
}

join().then(dc => {
    dc.addEventListener('open', event => {
        dc.send('Hello');
    });
    dc.addEventListener('message', event => {
        console.log(event.data);
    });
});

The behavior is the same in both Firefox and Chrome. That behavior is, again, that the offers and answers are signalled successfully, but no ICE candidates are ever created. Does anyone know what I'm missing?

1 Answer 1

4

Okay, I found the problem. I have to create the RTCDataChannel before creating the offer. Here's a before and after comparison of the SDP offers:

# offer created before data channel:
{
  type: 'offer',
  sdp: 'v=0\r\n' +
    'o=- 9150577729961293316 2 IN IP4 127.0.0.1\r\n' +
    's=-\r\n' +
    't=0 0\r\n' +
    'a=extmap-allow-mixed\r\n' +
    'a=msid-semantic: WMS\r\n'
}

# data channel created before offer:
{
  type: 'offer',
  sdp: 'v=0\r\n' +
    'o=- 1578211649345353372 2 IN IP4 127.0.0.1\r\n' +
    's=-\r\n' +
    't=0 0\r\n' +
    'a=group:BUNDLE 0\r\n' +
    'a=extmap-allow-mixed\r\n' +
    'a=msid-semantic: WMS\r\n' +
    'm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n' +
    'c=IN IP4 0.0.0.0\r\n' +
    'a=ice-ufrag:MZWR\r\n' +
    'a=ice-pwd:LfptE6PDVughzmQBPoOtvaU8\r\n' +
    'a=ice-options:trickle\r\n' +
    'a=fingerprint:sha-256 1B:C4:38:9A:CD:7F:34:20:B8:8D:78:CA:4A:3F:81:AE:C5:55:B3:27:6A:BD:E5:49:5A:F9:07:AE:0C:F6:6F:C8\r\n' +
    'a=setup:actpass\r\n' +
    'a=mid:0\r\n' +
    'a=sctp-port:5000\r\n' +
    'a=max-message-size:262144\r\n'
}

In both cases the answer looked similar to the offer. You an see the offer is much longer and mentions webrtc-datachannel in the second case. And sure enough, I started getting icecandidate events and everything is working now.

2
  • 1
    Thank you so much sir for sharing the solution for us, you saved me a lot of time .. there is no place in the internet where I find anything about data channel as a solution for this .. even though it doesn't make sense to me, but for now it works perfectly fine !
    – akrem bc
    Commented Apr 2 at 12:16
  • np! It's definitely poorly documented. Tbc, I'm pretty sure this isn't just to do with data channels. I think the idea is that whatever channels you're using, you're supposed to create them before creating the offer. Does that help? Commented Apr 15 at 0:18

Not the answer you're looking for? Browse other questions tagged or ask your own question.