I am trying to setup video chat where two peer connections exchange video. This happens after creating a data channel. So this is the process of events:
- offerer creates data channel, offerer creates and offer, answerer creates an answer. So far so good. We have a datachannel.
- offerer gets video stream through getUserMedia and adds it to the peer connection, then the onnegotiation event of offerer fires, offerer creates a new offer, while answerer responds with an answer. Still all good. The offerer is streaming.
- answerer gets video stream through getUserMedia and adds it to the peer connection, offerer creates a new offer, while answerer responds with an answer. Still ok. The answerer is streaming too.
However, if I switch step 2 and 3 (so answerer starts streaming first) then things start to go wrong. Both sides have to start streaming only after steps 1, 3 and 2 all have taken place.
I am pretty sure it has something to do with the order of SDP offers and answers.
When I let the answerer create a new offer when it has an onnegotiationneeded event the behavior is different but still erratic.
I'm quite clueless now about how to add the offers and answers.
Here is the code:
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
PeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection || window.RTCPeerConnection;
IceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate || window.RTCIceCandidate;
SessionDescription = window.mozRTCSessionDescription || window.webkitRTCSessionDescription || window.RTCSessionDescription;
var videoOfferer = document.getElementById('videoOfferer');
var videoAnswerer = document.getElementById('videoAnswerer');
var buttonOfferer = document.getElementById('buttonOfferer');
var buttonAnswerer = document.getElementById('buttonAnswerer');
var servers = {
iceServers: [
{url: "stun:23.21.150.121"},
{url: "stun:stun.1.google.com:19302"}
]
};
var offerer = new PeerConnection(servers), answerer = new PeerConnection(servers);
var channelOfferer = null, channelAnswerer = null;
offerer.onicecandidate = function(e) {
if(e.candidate == null) return;
answerer.addIceCandidate(new IceCandidate(e.candidate), function(){}, error);
};
offerer.onaddstream = function(e) {
videoOfferer.src = URL.createObjectURL(e.stream);
videoOfferer.play();
};
answerer.onicecandidate = function(e) {
if(e.candidate == null) return;
offerer.addIceCandidate(new IceCandidate(e.candidate), function(){}, error);
};
answerer.onaddstream = function(e) {
videoAnswerer.src = URL.createObjectURL(e.stream);
videoAnswerer.play();
};
function offerCreated(sdp) {
console.log('offer');
offerer.setLocalDescription(new SessionDescription(sdp), function() {
answerer.setRemoteDescription(new SessionDescription(sdp), function() {
answerer.createAnswer(answerCreated, error);
}, error);
}, error);
}
function answerCreated(sdp) {
console.log('answer');
answerer.setLocalDescription(new SessionDescription(sdp), function() {
}, error);
offerer.setRemoteDescription(new SessionDescription(sdp), function() {
}, error);
}
function error() {}
buttonOfferer.addEventListener('click', function() {
navigator.getUserMedia({audio: true, video: true}, function(stream) {
offerer.addStream(stream);
}, function(){});
});
buttonAnswerer.addEventListener('click', function() {
navigator.getUserMedia({audio: true, video: true}, function(stream) {
answerer.addStream(stream);
}, function(){});
});
channelOfferer = offerer.createDataChannel('channel', {reliable: true});
offerer.createOffer(offerCreated, error);
answerer.ondatachannel = function(e) {
channelOfferer = e.channel;
channelOfferer.onmessage = function(e) {
console.log(e.data);
};
channelOfferer.onmessage = function(e) {
console.log(e.data);
};
// these are added later
offerer.onnegotiationneeded = function() {
offerer.createOffer(offerCreated, error);
};
answerer.onnegotiationneeded = function() {
offerer.createOffer(offerCreated, error);
};
};
I think the problem is in step 3 where you have the offerer initiating when the answerer adds a video. In a real remote call, how would the offerer even know to do that?
As I understand it, when the answerer needs to renegotiate then the roles are effectively reversed, because for the purposes of renegotiation: the renegotiator acts as the offerer, while the non-renegotiator acts as the answerer.
In other words: the response to
pc.onnegotiationneeded
is always:createOffer()
,setLocalDescription(description)
pc.localDescription
to the other sideregardless of side.
I'm not an authority on SDP, so I don't know for sure that this is the right approach, but the examples in the spec at least suggest to me that the steps above are right, and I got it working this way.
I've tested this out in this Firefox jsfiddle and it seems to work. Instructions for using the fiddle:
Offer
button and copy the offer.addTrack
on either end and video should show up on the other end.addTrack
in the other direction, and you should have video going both ways.Can this produce glare? I'm sure, and there may be better ways of handling this, but this seems to work for me.