10

I am trying to connect two peers using webRTC. I am able to display both local and remote videos correctly but as soon as the remote video appears, the candidate object becomes null and on the console it logs the following error message.

TypeError: Failed to execute 'addIceCandidate' on 'RTCPeerConnection': Candidate missing values for both sdpMid and sdpMLineIndex

I am using two separate laptops to test the connection and since both remote and local videos are showing, I think that I have connected two peers successfully but I'm not sure because of the error message.

Any idea on why this might be happening? Am I even successfully connecting the two peers?

Below is the code.

Thank you!

Frontend

import React, { Component } from 'react';
import io from 'socket.io-client';
class App extends Component {
  constructor(props) {
    super(props);
    this.room = 'test-room';
    this.socket = io.connect('http://localhost:5000');
    this.localPeerConnection = new RTCPeerConnection({
      iceServers: [
        {
          urls: 'stun:stun.l.google.com:19302'
        }
      ]
    });
    this.remotePeerConnection = new RTCPeerConnection({
      iceServers: [
        {
          urls: 'stun:stun.l.google.com:19302'
        }
      ]
    });
  };

  componentDidMount() {
    this.socket.on('connect', () => {
      this.socket.emit('join', this.room, err => {
        if (err) {
          console.error(err);
        } else {
          this.socket.on('offer', offer => {
            console.log('OFFER RECEIVED: ', offer);
            this.createAnswer(offer);
          });

          this.socket.on('candidate', candidate => {
            console.log('CANDIDATE RECEIVED', candidate);
            this.localPeerConnection.addIceCandidate(candidate).catch(error => console.error(error));
            this.remotePeerConnection.addIceCandidate(candidate).catch(error => console.error(error));
          });

          this.socket.on('answer', answer => {
            console.log('ANSWER RECEIVED:', answer);
            this.localPeerConnection.setRemoteDescription(answer);
          });
        }
      });
    });
  }

  startCall = async () => {
    this.localPeerConnection.onicecandidate = e => {
      const iceCandidate = e.candidate;
      this.socket.emit('candidate', { room: this.room, candidate: iceCandidate });
      console.log('candidate generated', e.candidate);
    };

    this.localPeerConnection.ontrack = e => {
      this.remoteVideo.srcObject = e.streams[0];
      console.log('REMOTE STREAM?: ', e.streams[0]);
    };

    try {
      const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 150, height: 150 }, audio: false });
      for (const track of stream.getTracks()) {
        this.localPeerConnection.addTrack(track, stream);
      }

      this.localVideo.srcObject = stream;
      console.log('LOCAL STREAMS: ', this.localPeerConnection.getLocalStreams())

      return this.createOffer();
    } catch (error) {
      console.error(error);
    }
  }

  createOffer = async () => {
    try {
      const offer = await this.localPeerConnection.createOffer();
      await this.localPeerConnection.setLocalDescription(offer);
      await this.remotePeerConnection.setRemoteDescription(offer);

      this.socket.emit('offer', { room: this.room, offer });
      console.log('SENDING OFFER: ', offer);
    } catch (error) {
      console.error(error);
    }
  }

  createAnswer = async description => {
    this.remotePeerConnection.onicecandidate = e => {
      const iceCandidate = e.candidate;
      this.socket.emit('candidate', { room: this.room, candidate: iceCandidate });
      console.log('candidate generated', e.candidate);
    };

    this.remotePeerConnection.ontrack = e => {
      this.remoteVideo.srcObject = e.streams[0];
    };

    this.remotePeerConnection.setRemoteDescription(description)
      .then(() => navigator.mediaDevices.getUserMedia({ video: { width: 150, height: 150 }, audio: false }))
      .then(stream => {
        for (const track of stream.getTracks()) {
          this.remotePeerConnection.addTrack(track, stream);
        }

        this.localVideo.srcObject = stream;

        return this.remotePeerConnection.createAnswer();
      })
      .then(answer => {
        this.remotePeerConnection.setLocalDescription(answer);
        return answer;
      })
      .then(answer => {
        this.socket.emit('answer', { room: this.room, answer });
        console.log('SENDING ANSWER: ', answer);
      })
      .catch(error => console.error(error))
  }

  render() {
    return (
      <div>
        <h1>Webrtc</h1>
        <div>
          <button onClick={this.startCall}>CALL</button>
        </div>
        <div style={{ display: 'flex' }}>
          <div>
            <video id='localVideo' autoPlay muted playsInline ref={ref => this.localVideo = ref} />
            <p>LOCAL VIDEO</p>
          </div>
          <div>
            <video id='remoteVideo' autoPlay muted playsInline ref={ref => this.remoteVideo = ref} />
            <p>REMOTE VIDEO</p>
          </div>
        </div>
      </div>
    );
  }
}

export default App;

Server

const express = require('express');

const app = express();
const server = require('http').createServer(app);
const io = require('socket.io')(server);
const PORT = process.env.PORT || 5000;

const connections = [];
const clients = [];

io.set('origins', '*:*');
io.on('connection', socket => {
  connections.push(socket);
  clients.push({ socket_id: socket.id });
  console.log('Connected: %s sockets connected ', connections.length);

  socket.on('join', (room, callback) => {
    const clients = io.sockets.adapter.rooms[room];
    const numClients = (typeof clients !== 'undefined') ? clients.length : 0;
    console.log('joined room', room);
    if (numClients > 1) {
      return callback('already_full');
    }
    else if (numClients === 1) {
      socket.join(room);
      io.in(room).emit('ready');
    }
    else {
      socket.join(room);
    }

    callback();
  });

  socket.on('offer', (data) => {
    const { room, offer } = data;
    console.log('offer from: ', offer);
    socket.to(room).emit('offer', offer);
  });

  socket.on('answer', (data) => {
    const { room, answer } = data;
    console.log('answer from: ', answer);
    socket.to(room).emit('answer', answer);
  });

  socket.on('candidate', (data) => {
    const { room, candidate } = data;
    console.log('candidate: ', candidate);
    socket.to(room).emit('candidate', candidate);
  });

  socket.on('disconnect', () => {
    connections.splice(connections.indexOf(socket), 1);
    console.log('Disconnected: %s sockets connected, ', connections.length);
    clients.forEach((client, i) => {
      if (client.socket_id === socket.id) {
        clients.splice(i, 1);
      }
    });
  });
});

server.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`);
});

UPDATE

After reading jib's comment, I have modified my client js as follows.

import React, { Component } from 'react';
import io from 'socket.io-client';

class App extends Component {
  constructor(props) {
    super(props);
    this.room = 'test-room';
    this.socket = io.connect('http://localhost:5000');
    this.peerConnection = new RTCPeerConnection({
      iceServers: [
        {
          urls: 'stun:stun.l.google.com:19302'
        }
      ]
    });
  };

  componentDidMount() {
    this.socket.on('connect', () => {
      this.socket.emit('join', this.room, err => {
        if (err) {
          console.error(err);
        } else {
          this.socket.on('offer', offer => {
            console.log('OFFER RECEIVED: ', offer);
            this.createAnswer(offer);
          });

          this.socket.on('candidate', candidate => {
            this.peerConnection.addIceCandidate(candidate).catch(error => console.error(error));
            console.log('CANDIDATE RECEIVED', candidate);
          });

          this.socket.on('answer', answer => {
            console.log('ANSWER RECEIVED:', answer);
            this.peerConnection.setRemoteDescription(answer);
          });
        }
      });
    });
  }

  startCall = async () => {
    this.peerConnection.oniceconnectionstatechange = () => console.log('ICE CONNECTION STATE: ', this.peerConnection.iceConnectionState);

    this.peerConnection.onicecandidate = e => {
      const iceCandidate = e.candidate;
      this.socket.emit('candidate', { room: this.room, candidate: iceCandidate });
      console.log('candidate generated', e.candidate);
    };

    this.peerConnection.ontrack = e => {
      this.remoteVideo.srcObject = e.streams[0];
      console.log('REMOTE STREAMS: ', this.peerConnection.getRemoteStreams());
    };

    try {
      const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 150, height: 150 }, audio: false });
      for (const track of stream.getTracks()) {
        this.peerConnection.addTrack(track, stream);
      }

      this.localVideo.srcObject = stream;
      console.log('LOCAL STREAMS: ', this.peerConnection.getLocalStreams())

      return this.createOffer();
    } catch (error) {
      console.error(error);
    }
  }

  createOffer = async () => {
    try {
      const offer = await this.peerConnection.createOffer();
      await this.peerConnection.setLocalDescription(offer);
      this.socket.emit('offer', { room: this.room, offer });
      console.log('SENDING OFFER: ', offer);
    } catch (error) {
      console.error(error);
    }
  }

  createAnswer = async description => {
    this.peerConnection.onicecandidate = e => {
      const iceCandidate = e.candidate;
      this.socket.emit('candidate', { room: this.room, candidate: iceCandidate });
      console.log('candidate generated', e.candidate);
    };

    this.peerConnection.ontrack = e => {
      this.remoteVideo.srcObject = e.streams[0];
    };

    this.peerConnection.setRemoteDescription(description)
      .then(() => navigator.mediaDevices.getUserMedia({ video: { width: 150, height: 150 }, audio: false }))
      .then(stream => {
        for (const track of stream.getTracks()) {
          this.peerConnection.addTrack(track, stream);
        }

        this.localVideo.srcObject = stream;

        return this.peerConnection.createAnswer();
      })
      .then(answer => {
        this.peerConnection.setLocalDescription(answer);
        return answer;
      })
      .then(answer => {
        this.socket.emit('answer', { room: this.room, answer });
        console.log('SENDING ANSWER: ', answer);
      })
      .catch(error => console.error(error))
  }

  render() {
    return (
      <div>
        <h1>Webrtc</h1>
        <div>
          <button onClick={this.startCall}>CALL</button>
        </div>
        <div style={{ display: 'flex' }}>
          <div>
            <video id='localVideo' autoPlay muted playsInline ref={ref => this.localVideo = ref} />
            <p>LOCAL VIDEO</p>
          </div>
          <div>
            <video id='remoteVideo' autoPlay muted playsInline ref={ref => this.remoteVideo = ref} />
            <p>REMOTE VIDEO</p>
          </div>
        </div>
      </div>
    );
  }
}

export default App;

The error on my console still persists... any idea why?

jib
  • 40,579
  • 17
  • 100
  • 158
Isaac Kwon
  • 103
  • 1
  • 1
  • 7
  • You're setting the same candidates on both sides. Why do you have two peers in the same JS? You don't need signaling at all if you're just doing a local-loop demo. See [this answer](https://stackoverflow.com/questions/53398178/webrtc-both-remote-and-local-video-is-displayed-with-local-stream/53403324#53403324) for help. – jib Nov 19 '19 at 20:15
  • Hi jib, after looking at the answer from the link, I have modified the code a little bit. But the error on my console still persists. For further clarification, I have my signaling server hosted on heroku and I am also able to see both local and remote video when I make a call. Any idea on what could be causing the error on the console? – Isaac Kwon Nov 20 '19 at 01:55
  • Maybe there is an issue with Google Chrome. I tested this on Firefox and Safari and I'm not getting any errors on those browsers. – Isaac Kwon Nov 20 '19 at 05:16

1 Answers1

19

The error on my console still persists... any idea why?

This is a known bug in Chrome (please ★ it to have it fixed!)

To see it, type the following into web console in Chrome 78:

const pc = new RTCPeerConnection(); pc.setRemoteDescription(await pc.createOffer())

then

pc.addIceCandidate(undefined)

and it produces:

TypeError: Failed to execute 'addIceCandidate' on 'RTCPeerConnection':
 Candidate missing values for both sdpMid and sdpMLineIndex

Now try

pc.addIceCandidate()

and it says:

TypeError: Failed to execute 'addIceCandidate' on 'RTCPeerConnection':
 1 argument required, but only 0 present.

Both are in violation of the latest spec, which says that pc.addIceCandidate() is "an end-of-candidates indication" that "applies to all media descriptions."

Workaround

You can safely ignore this error until Chrome fixes it, or catch the TypeError and suppress it.

I recommend against if (candidate) pc.addIceCandidate(candidate) as a workaround, as once Chrome fixes this, it would prevent iceConnectionState from ever going to the "completed" state in Chrome or any other browser.

jib
  • 40,579
  • 17
  • 100
  • 158
  • 1
    After reading your answer, I just decided to ignore the error. Thank you so much for your help! – Isaac Kwon Nov 21 '19 at 01:13
  • @jib, I am facing this error and from your answer, it seems that chrome didn't solve it yet!! Is it so? – Jayna Tanawala Aug 07 '20 at 07:50
  • @WagnerPatriota Actually, the upstream bug was recently fixed, and this works for me now in Chrome 90. – jib May 27 '21 at 01:35
  • 1
    Still seeing this issue in Chrome 91.0.4472.101 – Azimjon Ilkhomov Jun 10 '21 at 00:53
  • @AzimjonIlkhomov WFM in 91.0.4472.101 on mac. I see `Promise: undefined` which I believe is correct. Are you entering the above verbatim into the console? What errors are you seeing? – jib Jun 12 '21 at 15:16