A. Kriegman
A. Kriegman

Reputation: 660

WebRTC succesfully signalled offer and answer, but not getting any ICE candidates

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?

Upvotes: 1

Views: 2216

Answers (1)

A. Kriegman
A. Kriegman

Reputation: 660

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.

Upvotes: 4

Related Questions