Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WebRTC getting Failed to execute 'addIceCandidate' on RTCPeerConnection error on console but can still display remote and local videos

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?

like image 277
Isaac Kwon Avatar asked Nov 18 '19 04:11

Isaac Kwon


1 Answers

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.

like image 178
jib Avatar answered Oct 20 '22 09:10

jib