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?
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."
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With