In my multi peer webrtc client (testing on chrome) a stable connection is successfully established but after I receive the answer from the callee, the ontrack event is not firing and thus the stream sourceObj is not attached to my DOM. Why? The callee shows both videos (local and remote) but on the caller side the remote video is not added and it seems that this is due to the ontrack event not firing.
I create all the RTCPeerConnection before sending the Offer/Answer and add the local Tracks at creation time to it after I had bound the ontrack events to it. Then I send the offer/answer and follow the signaling proccess.
class WebRTC_Client {
private serversCfg = {
iceServers: [{
urls: ["stun:stun.l.google.com:19302"]
}]
};
...
private gotStream(stream) {
window.localStream = stream;
...
}
private stopLocalTracks(){
if (window.localStream) {
window.localStream.getTracks().forEach(function (track) {
track.stop();
});
var videoTracks = window.localStream.getVideoTracks();
for (var i = 0; i !== videoTracks.length; ++i) {
videoTracks[i].stop();
}
}
}
private start() {
var self = this;
...
this.stopLocalTracks();
...
navigator.mediaDevices.getUserMedia(this.getConstrains())
.then((stream) => {
self.gotStream(stream);
trace('Send signal to peers that I am ready to be called: onReadyForTeamspeak');
self.SignalingChannel.send(JSON.stringify({type: 'onReadyForTeamspeak'}));
})
.catch( self.errorHandler );
}
public addPeerId(peerId){
this.availablePeerIds[peerId] = peerId;
this.preparePeerConnection(peerId);
}
private preparePeerConnection(peerId){
var self = this;
if( this.peerConns[peerId] ){
return;
}
this.peerConns[peerId] = new RTCPeerConnection(this.serversCfg);
this.peerConns[peerId].ontrack = function (evt) { self.gotRemoteStream(evt, peerId); };
this.peerConns[peerId].onicecandidate = function (evt) { self.iceCallback(evt, peerId); };
this.addTracks(peerId);
}
private addTracks(peerId){
var self = this;
var localTracksCount = 0;
window.localStream.getTracks().forEach(
function (track) {
self.peerConns[peerId].addTrack(
track,
window.localStream
);
}
);
}
private call() {
var self = this;
for( let peerId in this.availablePeerIds ){
if( !this.availablePeerIds.hasOwnProperty(peerId) ){
continue;
}
if( this.isCaller(peerId) ) {
this.preparePeerConnection(peerId);
this.createOffer(peerId);
}
}
}
private createOffer(peerId){
var self = this;
this.peerConns[peerId].createOffer( this.offerOptions )
.then( function (offer) { return self.peerConns[peerId].setLocalDescription(offer); } )
.then( function () {
self.SignalingChannel.send(JSON.stringify({ "sdp": self.peerConns[peerId].localDescription, "remotePeerId": peerId, "type": "onWebRTCPeerConn" }));
})
.catch( this.errorHandler );
}
private answerCall(peerId){
var self = this;
this.peerConns[peerId].createAnswer()
.then( function (answer) { return self.peerConns[peerId].setLocalDescription(answer); } )
.then( function () {
self.SignalingChannel.send(JSON.stringify({ "sdp": self.peerConns[peerId].localDescription, "remotePeerId": peerId, "type": "onWebRTCPeerConn" }));
})
.catch( this.errorHandler );
}
...
private gotRemoteStream(e, peerId) {
if (!this.videoBillboards[peerId]) {
this.createMediaElements(peerId);
}
if (this.videoAssets[peerId].srcObject !== e.streams[0]) {
this.videoAssets[peerId].srcObject = e.streams[0];
}
}
private iceCallback(event, peerId) {
this.SignalingChannel.send(JSON.stringify({ "candidate": event.candidate, "remotePeerId": peerId, "type": "onWebRTCPeerConn" }));
}
private handleCandidate(candidate, peerId) {
this.peerConns[peerId].addIceCandidate(candidate)
.then(
this.onAddIceCandidateSuccess,
this.onAddIceCandidateError
);
}
...
private handleSignals(signal){
var self = this,
peerId = signal.connectionId;
this.addPeerId(peerId);
if( signal.sdp ) {
var desc = new RTCSessionDescription(signal.sdp);
this.peerConns[peerId].setRemoteDescription(desc)
.then(function () {
if (desc.type === 'offer') {
self.answerCall(peerId);
}
})
.catch( this.errorHandler );
} else if( signal.candidate ){
this.handleCandidate(new RTCIceCandidate(signal.candidate), peerId);
} else if( signal.closeConn ){
this.endCall(peerId,true);
}
}
}
Found a solution! Something was wrong with the options I put in to this.peerConns[peerId].createOffer( this.offerOptions ).
Actually in the code I provided one could not see it but the method that was dynamically creating my class member variable this.offerOptions had a bug. This was obviously telling the callee to NOT send any streams back to the caller.
Related
Is there anyway to notify offerer that non-existing track before just added to get the new stream from the answerer from the code below?
For my current issue now here is that the offerer can add new non-existing track and onnegotiationneeded will be fired and will also be able to createOffer and update media successfully, but when answerer do same process onnegotiationneeded fired normally also from the answerer but no media will be exchanged just because offerer do not have any new track on his end!
I use replaceOrAddTrack(remotePartiID, track, TrackKind) in adding and replacing of tracks
Only the replace works with either ends if it has same track kind from initial connection
_cfg = {
sdpConstraints: {
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true,
VoiceActivityDetection: true,
IceRestart: true
},
optional: []
}
...
};
var channels_wrap = (function() {
return {
...
init: function() {
_cfg.defaultChannel.on('message', (message) => {
if (_cfg.enableLog) {
console.log('Client received message:', message);
}
if (message.type === 'newparticipant') {
var partID = message.from;
var partData = message.fromData;
// Open a new communication channel to the new participant
_cfg.offerChannels[partID] = this.openSignalingChannel(partID);
// Wait for answers (to offers) from the new participant
_cfg.offerChannels[partID].on('message', (msg) => {
if (msg.dest === _cfg.myID) {
if (msg.type === 'reoffer') {
if (_cfg.opc.hasOwnProperty(msg.from)) {
console.log('reoffering')
_cfg.opc[msg.from].negotiationNeeded();
}
} else
if (msg.type === 'answer') {
_cfg.opc[msg.from].peer.setRemoteDescription(new RTCSessionDescription(msg.snDescription),
handlers_wrap.setRemoteDescriptionSuccess,
handlers_wrap.setRemoteDescriptionError);
} else if (msg.type === 'candidate') {
var candidate = new RTCIceCandidate({
sdpMLineIndex: msg.label,
candidate: msg.candidate
});
if (_cfg.enableLog) {
console.log('got ice candidate from ' + msg.from);
}
_cfg.opc[msg.from].peer.addIceCandidate(candidate, handlers_wrap.addIceCandidateSuccess, handlers_wrap.addIceCandidateError);
}
}
});
// Send an offer to the new participant
dialogs_wrap.createOffer(partID, partData);
} else if (message.type === 'bye') {
handlers_wrap.hangup(message.from, message.fromData);
}
});
},
initPrivateChannel: function() {
// Open a private channel (namespace = _cfg.myID) to receive offers
_cfg.privateAnswerChannel = this.openSignalingChannel(_cfg.myID);
// Wait for offers or ice candidates
_cfg.privateAnswerChannel.on('message', (message) => {
if (message.dest === _cfg.myID) {
if (message.type === 'offer') {
var to = message.from;
dialogs_wrap.createAnswer(message.snDescription, _cfg.privateAnswerChannel, to, message.fromData);
} else if (message.type === 'candidate') {
var candidate = new RTCIceCandidate({
sdpMLineIndex: message.label,
candidate: message.candidate
});
_cfg.apc[message.from].peer.addIceCandidate(candidate, handlers_wrap.addIceCandidateSuccess, handlers_wrap.addIceCandidateError);
}
}
});
}
};
})();
var tracks_wrap = (function() {
return {
getParticipants: function(partID = null) {
var participants = {};
if (partID) {
if (_cfg.opc.hasOwnProperty(partID)) {
participants[partID] = {
ID: partID,
type: 'opc'
};
} else
if (_cfg.apc.hasOwnProperty(partID)) {
participants[partID] = {
ID: partID,
type: 'apc'
};
}
} else {
for (let key in _cfg.opc) {
participants[key] = {
ID: key,
type: 'opc'
};
}
for (let key in _cfg.apc) {
participants[key] = {
ID: key,
type: 'apc'
};
}
}
return participants;
},
replaceOrAddTrack: function(remotePartiID, track, TrackKind) {
if (!TrackKind) {
return;
}
var participants = this.getParticipants(remotePartiID);
for (var partiID in participants) {
var peer = null;
if (participants[partiID].type === 'apc' && _cfg.apc.hasOwnProperty(partiID)) {
peer = _cfg.apc[partiID].peer;
} else if (participants[partiID].type === 'opc' && _cfg.opc.hasOwnProperty(partiID)) {
peer = _cfg.opc[partiID].peer;
} else {
continue;
}
var foundTrack = null;
peer.getSenders().forEach(function(rtpSender) {
if (rtpSender.track && TrackKind === rtpSender.track.kind) {
foundTrack = true;
rtpSender.replaceTrack(track);
}
});
if (!foundTrack) {
peer.addTrack(track, _cfg.localStream); //This work only if it is offerrer that add track but not working with answerer even if i tell the offerer to send offer again
}
}
}
};
})();
var dialogs_wrap = (function() {
return {
/**
*
* Send an offer to peer with id partID and metadata as partData
*
*/
createOffer: function(partID, partData) {
if (_cfg.enableLog) {
console.log('Creating offer for peer ' + partID, partData);
}
var opcPeer = new RTCPeerConnection(_cfg.pcConfig, _cfg.peerSetup);
_cfg.opc[partID] = {};
_cfg.opc[partID].peer = opcPeer;
_cfg.opc[partID].peer.onicecandidate = handlers_wrap.handleIceCandidateAnswer(_cfg.offerChannels[partID], partID, partData);
_cfg.opc[partID].peer.ontrack = handlers_wrap.handleRemoteStreamAdded(partID, partData);
_cfg.opc[partID].peer.onremovetrack = handlers_wrap.handleRemoteStreamRemoved(partID, partData);
_cfg.localStream.getTracks().forEach(track => _cfg.opc[partID].peer.addTrack(track, _cfg.localStream));
try {
_cfg.sendChannel[partID] = _cfg.opc[partID].peer.createDataChannel("sendDataChannel", {
reliable: false
});
_cfg.sendChannel[partID].onmessage = handlers_wrap.handleMessage;
if (_cfg.enableLog) {
console.log('Created send data channel');
}
} catch (e) {
alert('Failed to create data channel. \n You need supported RtpDataChannel enabled browser');
console.log('createDataChannel() failed with exception: ', e.message);
}
_cfg.sendChannel[partID].onopen = handlers_wrap.handleSendChannelStateChange(partID);
_cfg.sendChannel[partID].onclose = handlers_wrap.handleSendChannelStateChange(partID);
var onSuccess = (partID, partData) => {
var channel = _cfg.offerChannels[partID];
if (_cfg.enableLog) {
console.log('Sending offering');
}
channel.emit('message', {
snDescription: _cfg.opc[partID].peer.localDescription,
from: _cfg.myID,
fromData: _cfg.myData,
type: 'offer',
dest: partID,
destData: partData
});
}
_cfg.opc[partID].negotiationNeeded = () => {
_cfg.opc[partID].peer.createOffer(_cfg.sdpConstraints).then(offer => {
offer.sdp = sdp_wrap.SDPController(offer.sdp);
return _cfg.opc[partID].peer.setLocalDescription(offer)
})
.then(() => onSuccess(partID, partData)).catch(handlers_wrap.handleCreateOfferError);
}
_cfg.opc[partID].peer.onnegotiationneeded = () => {
_cfg.opc[partID].negotiationNeeded();
}
},
createAnswer: function(snDescription, cnl, to, toData) {
if (_cfg.enableLog) {
console.log('Creating answer for peer ' + to);
}
if (!_cfg.apc.hasOwnProperty(to)) {
var apcPeer = new RTCPeerConnection(_cfg.pcConfig, _cfg.peerSetup);
//apcPeer.setConfiguration(_cfg.pcConfig);
_cfg.apc[to] = {};
_cfg.apc[to].peer = apcPeer;
_cfg.apc[to].peer.onicecandidate = handlers_wrap.handleIceCandidateAnswer(cnl, to, toData);
_cfg.apc[to].peer.ontrack = handlers_wrap.handleRemoteStreamAdded(to, toData);
_cfg.apc[to].peer.onremovetrack = handlers_wrap.handleRemoteStreamRemoved(to, toData);
_cfg.localStream.getTracks().forEach(track => _cfg.apc[to].peer.addTrack(track, _cfg.localStream));
_cfg.apc[to].peer.ondatachannel = handlers_wrap.gotReceiveChannel(to);
}
_cfg.apc[to].peer.setRemoteDescription(new RTCSessionDescription(snDescription), handlers_wrap.setRemoteDescriptionSuccess, handlers_wrap.setRemoteDescriptionError);
var onSuccess = (channel) => {
if (_cfg.enableLog) {
console.log('Sending answering');
}
channel.emit('message', {
snDescription: _cfg.apc[to].peer.localDescription,
from: _cfg.myID,
fromData: _cfg.myData,
type: 'answer',
dest: to,
destData: toData
});
}
_cfg.apc[to].peer.createAnswer().then(function(answer) {
answer.sdp = sdp_wrap.SDPController(answer.sdp);
return _cfg.apc[to].peer.setLocalDescription(answer);
})
.then(() => onSuccess(cnl))
.catch(handlers_wrap.handleCreateAnswerError);
var negotiationNeeded = false;
_cfg.apc[to].peer.onnegotiationneeded = (ev) => {
if (!negotiationNeeded) {
negotiationNeeded = true;
return;
}
//So i tried to create this to tell the offerer to do offer again, offerer do resend offer but nothing seem to happen
cnl.emit('message', {
from: _cfg.myID,
fromData: _cfg.myData,
type: 'reoffer',
dest: to,
destData: toData
});
}
}
};
})();
I am trying to implement video chat with webrtc using code from google code labs . It works fine on same network but does not work on different network. i have installed coturn on my server. I am not sure if it is working correctly.
Any one used google codelabs code and made it working?
This is the modified main.js file
'use strict';
var isChannelReady = false;
var isInitiator = false;
var isStarted = false;
var localStream;
var pc;
var remoteStream;
var turnReady;
var pcConfig = {
"iceServers":[
{'urls': 'stun:stun.l.google.com:19302'},
{"urls":["turn:78.129.167.90"],"username":"wyzturner1","credential":"wyzturnp2ss"}]
};
// Set up audio and video regardless of what devices are present.
var sdpConstraints = {
offerToReceiveAudio: true,
offerToReceiveVideo: true
};
/////////////////////////////////////////////
var room = 'foo';
// Could prompt for room name:
// room = prompt('Enter room name:');
var socket = io.connect('https://www.samplesite.com:8080');
if (room !== '') {
socket.emit('create or join', room);
console.log('Attempted to create or join room', room);
}
socket.on('created', function(room) {
console.log('Created room ' + room);
isInitiator = true;
});
socket.on('full', function(room) {
console.log('Room ' + room + ' is full');
});
socket.on('join', function (room){
console.log('Another peer made a request to join room ' + room);
console.log('This peer is the initiator of room ' + room + '!');
isChannelReady = true;
});
socket.on('joined', function(room) {
console.log('joined: ' + room);
isChannelReady = true;
});
socket.on('log', function(array) {
console.log.apply(console, array);
});
////////////////////////////////////////////////
function sendMessage(message) {
console.log('Client sending message: ', message);
socket.emit('message', message);
}
// This client receives a message
socket.on('message', function(message) {
console.log('Client received message:', message);
if (message === 'got user media') {
maybeStart();
} else if (message.type === 'offer') {
if (!isInitiator && !isStarted) {
maybeStart();
}
pc.setRemoteDescription(new RTCSessionDescription(message));
doAnswer();
} else if (message.type === 'answer' && isStarted) {
pc.setRemoteDescription(new RTCSessionDescription(message));
} else if (message.type === 'candidate' && isStarted) {
var candidate = new RTCIceCandidate({
sdpMLineIndex: message.label,
candidate: message.candidate
});
pc.addIceCandidate(candidate);
} else if (message === 'bye' && isStarted) {
handleRemoteHangup();
}
});
////////////////////////////////////////////////////
var localVideo = document.querySelector('#localVideo');
var remoteVideo = document.querySelector('#remoteVideo');
navigator.mediaDevices.getUserMedia({
audio: false,
video: true
})
.then(gotStream)
.catch(function(e) {
alert('getUserMedia() error: ' + e.name);
});
function gotStream(stream) {
console.log('Adding local stream.');
localVideo.src = window.URL.createObjectURL(stream);
localStream = stream;
sendMessage('got user media');
if (isInitiator) {
maybeStart();
}
}
var constraints = {
video: true
};
console.log('Getting user media with constraints', constraints);
//if (location.hostname !== 'localhost') {
// requestTurn(
// 'https://computeengineondemand.appspot.com/turn?username=41784574&key=4080218913'
// );
//}
function maybeStart() {
console.log('>>>>>>> maybeStart() ', isStarted, localStream, isChannelReady);
if (!isStarted && typeof localStream !== 'undefined' && isChannelReady) {
console.log('>>>>>> creating peer connection');
createPeerConnection();
isStarted = true;
console.log('isInitiator', isInitiator);
if (isInitiator) {
doCall();
}
}
}
window.onbeforeunload = function() {
sendMessage('bye');
};
/////////////////////////////////////////////////////////
function createPeerConnection() {
try {
pc = new RTCPeerConnection(pcConfig);
pc.addStream(localStream);
pc.onicecandidate = handleIceCandidate;
pc.onaddstream = handleRemoteStreamAdded;
pc.oniceconnectionstatechange = function(){
console.log('ICE state: ',pc.iceConnectionState);
}
pc.onremovestream = handleRemoteStreamRemoved;
console.log('Created RTCPeerConnnection');
} catch (e) {
console.log('Failed to create PeerConnection, exception: ' + e.message);
alert('Cannot create RTCPeerConnection object.');
return;
}
}
function handleIceCandidate(event) {
console.log('icecandidate event: ', event);
if (event.candidate) {
sendMessage({
type: 'candidate',
label: event.candidate.sdpMLineIndex,
id: event.candidate.sdpMid,
candidate: event.candidate.candidate
});
} else {
console.log('End of candidates.');
}
}
function handleRemoteStreamAdded(event) {
console.log('Remote stream added.');
remoteVideo.src = window.URL.createObjectURL(event.stream);
remoteStream = event.stream;
}
function handleCreateOfferError(event) {
console.log('createOffer() error: ', event);
}
function doCall() {
console.log('Sending offer to peer');
pc.createOffer(setLocalAndSendMessage, handleCreateOfferError);
}
function doAnswer() {
console.log('Sending answer to peer.');
pc.createAnswer().then(
setLocalAndSendMessage,
onCreateSessionDescriptionError
);
}
function setLocalAndSendMessage(sessionDescription) {
// Set Opus as the preferred codec in SDP if Opus is present.
// sessionDescription.sdp = preferOpus(sessionDescription.sdp);
pc.setLocalDescription(sessionDescription);
console.log('setLocalAndSendMessage sending message', sessionDescription);
sendMessage(sessionDescription);
}
function onCreateSessionDescriptionError(error) {
trace('Failed to create session description: ' + error.toString());
}
function requestTurn(turnURL) {
var turnExists = false;
for (var i in pcConfig.iceServers) {
if (pcConfig.iceServers[i].urls.substr(0, 5) === 'turn:') {
turnExists = true;
turnReady = true;
break;
}
}
// if (!turnExists) {
// console.log('Getting TURN server from ', turnURL);
// // No TURN server. Get one from computeengineondemand.appspot.com:
// var xhr = new XMLHttpRequest();
// xhr.onreadystatechange = function() {
// if (xhr.readyState === 4 && xhr.status === 200) {
// var turnServer = JSON.parse(xhr.responseText);
// console.log('Got TURN server: ', turnServer);
// pcConfig.iceServers.push({
// 'url': 'turn:' + turnServer.username + '#' + turnServer.turn,
// 'credential': turnServer.password
// });
// turnReady = true;
// }
// };
// xhr.open('GET', turnURL, true);
// xhr.send();
// }
}
function handleRemoteStreamAdded(event) {
console.log('Remote stream added.');
remoteVideo.src = window.URL.createObjectURL(event.stream);
remoteStream = event.stream;
}
function handleRemoteStreamRemoved(event) {
console.log('Remote stream removed. Event: ', event);
}
function hangup() {
console.log('Hanging up.');
stop();
sendMessage('bye');
}
function handleRemoteHangup() {
console.log('Session terminated.');
stop();
isInitiator = false;
}
function stop() {
isStarted = false;
// isAudioMuted = false;
// isVideoMuted = false;
pc.close();
pc = null;
}
///////////////////////////////////////////
// Set Opus as the default audio codec if it's present.
function preferOpus(sdp) {
var sdpLines = sdp.split('\r\n');
var mLineIndex;
// Search for m line.
for (var i = 0; i < sdpLines.length; i++) {
if (sdpLines[i].search('m=audio') !== -1) {
mLineIndex = i;
break;
}
}
if (mLineIndex === null) {
return sdp;
}
// If Opus is available, set it as the default in m line.
for (i = 0; i < sdpLines.length; i++) {
if (sdpLines[i].search('opus/48000') !== -1) {
var opusPayload = extractSdp(sdpLines[i], /:(\d+) opus\/48000/i);
if (opusPayload) {
sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex],
opusPayload);
}
break;
}
}
// Remove CN in m line and sdp.
sdpLines = removeCN(sdpLines, mLineIndex);
sdp = sdpLines.join('\r\n');
return sdp;
}
function extractSdp(sdpLine, pattern) {
var result = sdpLine.match(pattern);
return result && result.length === 2 ? result[1] : null;
}
// Set the selected codec to the first in m line.
function setDefaultCodec(mLine, payload) {
var elements = mLine.split(' ');
var newLine = [];
var index = 0;
for (var i = 0; i < elements.length; i++) {
if (index === 3) { // Format of media starts from the fourth.
newLine[index++] = payload; // Put target payload to the first.
}
if (elements[i] !== payload) {
newLine[index++] = elements[i];
}
}
return newLine.join(' ');
}
// Strip CN from sdp before CN constraints is ready.
function removeCN(sdpLines, mLineIndex) {
var mLineElements = sdpLines[mLineIndex].split(' ');
// Scan from end for the convenience of removing an item.
for (var i = sdpLines.length - 1; i >= 0; i--) {
var payload = extractSdp(sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i);
if (payload) {
var cnPos = mLineElements.indexOf(payload);
if (cnPos !== -1) {
// Remove CN payload from m line.
mLineElements.splice(cnPos, 1);
}
// Remove CN line in sdp
sdpLines.splice(i, 1);
}
}
sdpLines[mLineIndex] = mLineElements.join(' ');
return sdpLines;
}
I'm trying to connect 2 peers with webrtc and datachannel without camera and microphone.
try {
socket = new WebSocket("ws://localhost:1337/");
var servers = {iceServers:[{url:"stun:stun.l.google.com:19302"}]};
peerConn = new webkitRTCPeerConnection(servers, {optional:[{RtpDataChannels: true}]});
channel = peerConn.createDataChannel("abcd1234", {reliable: false});
peerConn.onicecandidate = function(evt) {
if(evt.candidate) {
socket.send(JSON.stringify({"candidate": evt.candidate}));
}
};
channel.onopen = function () {
console.log("channel is open");
channel.send('first text message over RTP data ports');
};
channel.onmessage = function (event) {
console.log('received a message:', event.data);
};
peerConn.createOffer(function(desc) {
peerConn.setLocalDescription(desc);
socket.send(JSON.stringify({"sdp": desc}));
});
socket.onmessage = function(evt) {
var signal = JSON.parse(evt.data);
if(signal.sdp) {
peerConn.setRemoteDescription(new RTCSessionDescription(signal.sdp));
alert("desc");
} else {
peerConn.addIceCandidate(new RTCIceCandidate(signal.candidate));
alert("ice");
}
}
} catch(e) {
console.log(e.message);
}
In Chrome this errors out with:
Uncaught Error: InvalidStateError: DOM Exception 11
Open two tabs; click "Create Offer" button from 1st tab; and watch console logs:
<script>
// webkitRTCPeerConnection && RTCDataChannel specific code goes here
var iceServers = {
iceServers: [{
url: 'stun:stun.l.google.com:19302'
}]
};
var optionalRtpDataChannels = {
optional: [{
RtpDataChannels: true
}]
};
var mediaConstraints = {
optional: [],
mandatory: {
OfferToReceiveAudio: false, // Hmm!!
OfferToReceiveVideo: false // Hmm!!
}
};
var offerer, answerer, answererDataChannel, offererDataChannel;
function createOffer() {
offerer = new webkitRTCPeerConnection(iceServers, optionalRtpDataChannels);
offererDataChannel = offerer.createDataChannel('RTCDataChannel', {
reliable: false
});
setChannelEvents(offererDataChannel, 'offerer');
offerer.onicecandidate = function (event) {
if (!event.candidate) returnSDP();
};
offerer.ongatheringchange = function (event) {
if (event.currentTarget && event.currentTarget.iceGatheringState === 'complete') returnSDP();
};
function returnSDP() {
socket.send({
sender: 'offerer',
sdp: offerer.localDescription
});
}
offerer.createOffer(function (sessionDescription) {
offerer.setLocalDescription(sessionDescription);
}, null, mediaConstraints);
}
function createAnswer(offerSDP) {
answerer = new webkitRTCPeerConnection(iceServers, optionalRtpDataChannels);
answererDataChannel = answerer.createDataChannel('RTCDataChannel', {
reliable: false
});
setChannelEvents(answererDataChannel, 'answerer');
answerer.onicecandidate = function (event) {
if (!event.candidate) returnSDP();
};
answerer.ongatheringchange = function (event) {
if (event.currentTarget && event.currentTarget.iceGatheringState === 'complete') returnSDP();
};
function returnSDP() {
socket.send({
sender: 'answerer',
sdp: answerer.localDescription
});
}
answerer.setRemoteDescription(new RTCSessionDescription(offerSDP));
answerer.createAnswer(function (sessionDescription) {
answerer.setLocalDescription(sessionDescription);
}, null, mediaConstraints);
}
function setChannelEvents(channel, channelNameForConsoleOutput) {
channel.onmessage = function (event) {
console.debug(channelNameForConsoleOutput, 'received a message:', event.data);
};
channel.onopen = function () {
channel.send('first text message over RTP data ports');
};
}
// WebSocket specific code goes here
var socket = new WebSocket('ws://localhost:1337');
socket.onmessage = function (e) {
var data = JSON.parse(e.data);
console.log(data);
if (data.sdp) {
if (data.sender == 'offerer') createAnswer(data.sdp);
else offerer.setRemoteDescription(new RTCSessionDescription(data.sdp));
}
};
socket.push = socket.send;
socket.send = function (data) {
socket.push(JSON.stringify(data));
};
</script>
<button id="create-offer">Create Offer</button>
<script>
document.getElementById('create-offer').onclick = function () {
this.disabled = true;
createOffer();
};
</script>
now i'm trying the following:
First i create an offer on client #1 and send the description:
try {
peerConn = new webkitRTCPeerConnection(stunServers, {optional:[{RtpDataChannels: true}]});
peerConn.createOffer(function(desc) {
peerConn.setLocalDescription(desc);
socket.send("createpeer|" + JSON.stringify(desc));
}, null, mediaConstraints);
peerConn.onconnection = function () {
console.log("[webrtc] connected with peer");
peerChannel = peerConn.createDataChannel("test", {reliable: false});
peerChannel.onmessage = function (event) {
alert("Server: " + event.data);
};
peerChannel.onopen = function () {
peerChannel.send("Hello Server!");
};
};
} catch(error) {
console.log(error);
}
Client #2 receives that and sends his description:
case "createpeer":
console.log("[websocket] received create peer request from " + cmd[1] + " on " + cmd[2]);
try {
peerConn = new webkitRTCPeerConnection(stunServers, {optional:[{RtpDataChannels: true}]});
peerConn.setRemoteDescription(new RTCSessionDescription(JSON.parse(cmd[3])));
peerConn.createAnswer(function(desc) {
peerConn.setLocalDescription(desc);
socket.send("openpeer|" + cmd[1] + "|" + cmd[2] + "|" + JSON.stringify(desc));
}, null, mediaConstraints);
peerConn.ondatachannel = function (channel) {
channel.onmessage = function (event) {
alert("Client: " + event.data);
};
channel.onopen = function () {
channel.send("Hello Client!");
};
};
} catch(error) {
console.log(error);
}
break;
Finally client #1 recives the description from client #2
case "openpeer":
console.log("[websocket] received open peer");
peerConn.setRemoteDescription(new RTCSessionDescription(JSON.parse(cmd[1])));
break;
Everything works fine without errors, but the connection is not established and the onconnection method is not called.
Greetings
I've been trying to get this works, but I don't know what's wrong, can you guys help me? I tried WebRTC code with modification like this.
Both caller and callee enter the web
Web will create channel based on their username
Caller clicks the call button and sends message to spesific channel to some user's username. When caller clicks the call button, he then creates peerConnection and adds localStream
Callee will receive message, and the process goes on like WebRTC sample code. When callee receives an offer, he then creates peerConnection and adds localStream, then creates and sends answer
My code goes like this
var my_username = '{{ current_username }}';
var friend;
var localVideo;
var remoteVideo;
var localStream;
var remoteStream;
var channel;
var channelReady = false;
var pc;
var socket;
var started = false;
// Set up audio and video regardless of what devices are present.
var mediaConstraints = {'mandatory': {
'OfferToReceiveAudio':true,
'OfferToReceiveVideo':true }};
var isVideoMuted = false;
var isAudioMuted = false;
function choiceFriendInitialize() {
var choice_visible = false;
$('ul#friendlist > li').click(function(e) {
$(this).css('background-color', '#808080');
friend = $(this).text();
var choice = $('ul#choice');
choice_visible = true;
choice.css('display', 'inline-block');
choice.css('top', e.pageY);
choice.css('left', e.pageX);
});
// trigger call from here
$('li#call').click(function() {
$('ul#friendlist > li').css('background-color', 'transparent');
$('ul#choice').css('display', 'none');
choice_visible = false;
// call
maybeStart();
doCall();
});
$('li#unfriend').click(function() {
$('ul#friendlist > li').css('background-color', 'transparent');
$('ul#choice').css('display', 'none');
choice_visible = false;
});
$("body").mouseup(function(){
if (choice_visible) {
$('ul#friendlist > li').css('background-color', 'transparent');
$('ul#choice').css('display', 'none');
choice_visible = false;
}
});
}
function initialize() {
console.log("Initializing..");
choiceFriendInitialize();
localVideo = document.getElementById("localVideo");
remoteVideo = document.getElementById("remoteVideo");
openChannel();
doGetUserMedia();
}
function openChannel() {
console.log("Opening channel.");
var channel = new goog.appengine.Channel('{{ token }}');
var handler = {
'onopen': onChannelOpened,
'onmessage': onChannelMessage,
'onerror': onChannelError,
'onclose': onChannelClosed
};
socket = channel.open(handler);
}
function doGetUserMedia() {
// Call into getUserMedia via the polyfill (adapter.js).
var constraints = {"mandatory": {}, "optional": []};
try {
getUserMedia({'audio':true, 'video':constraints}, onUserMediaSuccess, onUserMediaError);
console.log("Requested access to local media with mediaConstraints:\n" + " \"" + JSON.stringify(constraints) + "\"");
} catch (e) {
alert("getUserMedia() failed. Is this a WebRTC capable browser?");
console.log("getUserMedia failed with exception: " + e.message);
}
}
function createPeerConnection() {
var pc_config = {"iceServers": [{"url": "stun:stun.l.google.com:19302"}]};
try {
// Create an RTCPeerConnection via the polyfill (adapter.js).
pc = new RTCPeerConnection(pc_config);
pc.onicecandidate = onIceCandidate;
console.log("Created RTCPeerConnnection with config:\n" + " \"" + JSON.stringify(pc_config) + "\".");
} catch (e) {
console.log("Failed to create PeerConnection, exception: " + e.message);
alert("Cannot create RTCPeerConnection object; WebRTC is not supported by this browser.");
return;
}
pc.onconnecting = onSessionConnecting;
pc.onopen = onSessionOpened;
pc.onaddstream = onRemoteStreamAdded;
pc.onremovestream = onRemoteStreamRemoved;
}
function maybeStart() {
if (!started && localStream && channelReady) {
console.log("Creating PeerConnection.");
createPeerConnection();
console.log("Adding local stream.");
pc.addStream(localStream);
started = true;
// Caller initiates offer to peer.
//if (initiator)
//doCall();
}
}
function doCall() {
console.log("Sending offer to peer.");
pc.createOffer(setLocalAndSendMessage, null, mediaConstraints);
}
function doAnswer() {
console.log("Sending answer to peer.");
pc.createAnswer(setLocalAndSendMessage, null, mediaConstraints);
}
function setLocalAndSendMessage(sessionDescription) {
// Set Opus as the preferred codec in SDP if Opus is present.
sessionDescription.sdp = preferOpus(sessionDescription.sdp);
pc.setLocalDescription(sessionDescription);
sendMessage({from: my_username, to: friend}, sessionDescription);
}
function sendMessage(client, message) {
console.log('C->S: ' + JSON.stringify(message));
var xhr = new XMLHttpRequest();
xhr.open('POST', '/send', true);
xhr.setRequestHeader("Content-type","application/json");
var msgString = {send_info: client, data_message: message};
xhr.send(JSON.stringify(msgString));
}
function processSignalingMessage(message) {
var msg = JSON.parse(message);
var data_message = msg.data_message;
var send_info = msg.send_info;
if (data_message.type === 'offer') {
// Callee creates PeerConnection
if (!started)
maybeStart();
pc.setRemoteDescription(new RTCSessionDescription(data_message));
friend = send_info.from;
doAnswer();
} else if (data_message.type === 'answer' && started) {
pc.setRemoteDescription(new RTCSessionDescription(data_message));
} else if (data_message.type === 'candidate' && started) {
var candidate = new RTCIceCandidate({sdpMLineIndex:data_message.label, candidate:data_message.candidate});
pc.addIceCandidate(candidate);
} else if (data_message.type === 'bye' && started) {
onRemoteHangup();
}
}
function onChannelOpened() {
console.log('Channel opened.');
channelReady = true;
}
function onChannelMessage(message) {
console.log('S->C: ' + message.data);
processSignalingMessage(message.data);
}
function onChannelError() {
console.log('Channel error.');
}
function onChannelClosed() {
console.log('Channel closed.');
}
function onUserMediaSuccess(stream) {
console.log("User has granted access to local media.");
// Call the polyfill wrapper to attach the media stream to this element.
attachMediaStream(localVideo, stream);
localVideo.style.opacity = 1;
localStream = stream;
// Caller creates PeerConnection.
//if (initiator) maybeStart();
}
function onUserMediaError(error) {
console.log("Failed to get access to local media. Error code was " + error.code);
alert("Failed to get access to local media. Error code was " + error.code + ".");
}
function onIceCandidate(event) {
if (event.candidate) {
sendMessage({from: my_username, to: friend}, {type: 'candidate',
label: event.candidate.sdpMLineIndex,
id: event.candidate.sdpMid,
candidate: event.candidate.candidate});
} else {
console.log("End of candidates.");
}
}
function onSessionConnecting(message) {
console.log("Session connecting.");
}
function onSessionOpened(message) {
console.log("Session opened.");
}
function onRemoteStreamAdded(event) {
console.log("Remote stream added.");
attachMediaStream(remoteVideo, event.stream);
remoteStream = event.stream;
waitForRemoteVideo();
}
function onRemoteStreamRemoved(event) {
console.log("Remote stream removed.");
}
function onHangup() {
console.log("Hanging up.");
transitionToDone();
stop();
// will trigger BYE from server
socket.close();
}
function onRemoteHangup() {
console.log('Session terminated.');
transitionToWaiting();
stop();
}
function stop() {
started = false;
isAudioMuted = false;
isVideoMuted = false;
pc.close();
pc = null;
}
function waitForRemoteVideo() {
if (remoteStream.videoTracks.length === 0 || remoteVideo.currentTime > 0) {
console.log('ada remote stream');
transitionToActive();
} else {
console.log('ga ada remote stream');
setTimeout(waitForRemoteVideo, 100);
}
}
function transitionToActive() {
remoteVideo.style.opacity = 1;
}
function transitionToWaiting() {
remoteVideo.style.opacity = 0;
}
function transitionToDone() {
localVideo.style.opacity = 0;
remoteVideo.style.opacity = 0;
}
function toggleVideoMute() {
if (localStream.videoTracks.length === 0) {
console.log("No local video available.");
return;
}
if (isVideoMuted) {
for (i = 0; i < localStream.videoTracks.length; i++) {
localStream.videoTracks[i].enabled = true;
}
console.log("Video unmuted.");
} else {
for (i = 0; i < localStream.videoTracks.length; i++) {
localStream.videoTracks[i].enabled = false;
}
console.log("Video muted.");
}
isVideoMuted = !isVideoMuted;
}
function toggleAudioMute() {
if (localStream.audioTracks.length === 0) {
console.log("No local audio available.");
return;
}
if (isAudioMuted) {
for (i = 0; i < localStream.audioTracks.length; i++) {
localStream.audioTracks[i].enabled = true;
}
console.log("Audio unmuted.");
} else {
for (i = 0; i < localStream.audioTracks.length; i++){
localStream.audioTracks[i].enabled = false;
}
console.log("Audio muted.");
}
isAudioMuted = !isAudioMuted;
}
setTimeout(initialize, 1);
// Send BYE on refreshing(or leaving) a demo page
// to ensure the room is cleaned for next session.
window.onbeforeunload = function() {
sendMessage({from: my_username, to: friend}, {type: 'bye'});
//Delay 100ms to ensure 'bye' arrives first.
setTimeout(function(){}, 100);
}
// Ctrl-D: toggle audio mute; Ctrl-E: toggle video mute.
// On Mac, Command key is instead of Ctrl.
// Return false to screen out original Chrome shortcuts.
document.onkeydown = function() {
if (navigator.appVersion.indexOf("Mac") != -1) {
if (event.metaKey && event.keyCode === 68) {
toggleAudioMute();
return false;
}
if (event.metaKey && event.keyCode === 69) {
toggleVideoMute();
return false;
}
} else {
if (event.ctrlKey && event.keyCode === 68) {
toggleAudioMute();
return false;
}
if (event.ctrlKey && event.keyCode === 69) {
toggleVideoMute();
return false;
}
}
}
// Set Opus as the default audio codec if it's present.
function preferOpus(sdp) {
var sdpLines = sdp.split('\r\n');
// Search for m line.
for (var i = 0; i < sdpLines.length; i++) {
if (sdpLines[i].search('m=audio') !== -1) {
var mLineIndex = i;
break;
}
}
if (mLineIndex === null)
return sdp;
// If Opus is available, set it as the default in m line.
for (var i = 0; i < sdpLines.length; i++) {
if (sdpLines[i].search('opus/48000') !== -1) {
var opusPayload = extractSdp(sdpLines[i], /:(\d+) opus\/48000/i);
if (opusPayload)
sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], opusPayload);
break;
}
}
// Remove CN in m line and sdp.
sdpLines = removeCN(sdpLines, mLineIndex);
sdp = sdpLines.join('\r\n');
return sdp;
}
function extractSdp(sdpLine, pattern) {
var result = sdpLine.match(pattern);
return (result && result.length == 2)? result[1]: null;
}
// Set the selected codec to the first in m line.
function setDefaultCodec(mLine, payload) {
var elements = mLine.split(' ');
var newLine = new Array();
var index = 0;
for (var i = 0; i < elements.length; i++) {
if (index === 3) // Format of media starts from the fourth.
newLine[index++] = payload; // Put target payload to the first.
if (elements[i] !== payload)
newLine[index++] = elements[i];
}
return newLine.join(' ');
}
// Strip CN from sdp before CN constraints is ready.
function removeCN(sdpLines, mLineIndex) {
var mLineElements = sdpLines[mLineIndex].split(' ');
// Scan from end for the convenience of removing an item.
for (var i = sdpLines.length-1; i >= 0; i--) {
var payload = extractSdp(sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i);
if (payload) {
var cnPos = mLineElements.indexOf(payload);
if (cnPos !== -1) {
// Remove CN payload from m line.
mLineElements.splice(cnPos, 1);
}
// Remove CN line in sdp
sdpLines.splice(i, 1);
}
}
sdpLines[mLineIndex] = mLineElements.join(' ');
return sdpLines;
}
When it comes to waitForRemoteVideo, the function calls the else condition. But the blob url for remoteVideo exists.
It's funny that I've been searching for the error and re-writing the code for like three times, and suddenly after posted my question here, I realized my mistake.
Here in the function processSignallingMessage..
if (data_message.type === 'offer') {
// Callee creates PeerConnection
if (!started)
maybeStart();
pc.setRemoteDescription(new RTCSessionDescription(data_message));
friend = send_info.from;
doAnswer();
} else ....
Should be like this:
if (data_message.type === 'offer') {
friend = send_info.from;
// Callee creates PeerConnection
if (!started)
maybeStart();
pc.setRemoteDescription(new RTCSessionDescription(data_message));
doAnswer();
} else ....
because callee need variable friend to be filled before sending candidate message.
for a couple of days I'm now stuck with trying to get my webRTC client to work and I can't figure out what I'm doing wrong.
I'm trying to create multi peer webrtc client and am testing both sides with Chrome.
When the callee receives the call and create the Answer, I get the following error:
Failed to set local answer sdp: Called in wrong state: kStable
The receiving side correctly establishes both video connnections and is showing the local and remote streams. But the caller seems not to receive the callees answer. Can someone hint me what I am doing wrong here?
Here is the code I am using (it's a stripped version to just show the relevant parts and make it better readable)
class WebRTC_Client
{
private peerConns = {};
private sendAudioByDefault = true;
private sendVideoByDefault = true;
private offerOptions = {
offerToReceiveAudio: true,
offerToReceiveVideo: true
};
private constraints = {
"audio": true,
"video": {
frameRate: 5,
width: 256,
height: 194
}
};
private serversCfg = {
iceServers: [{
urls: ["stun:stun.l.google.com:19302"]
}]
};
private SignalingChannel;
public constructor(SignalingChannel){
this.SignalingChannel = SignalingChannel;
this.bindSignalingHandlers();
}
/*...*/
private gotStream(stream) {
(<any>window).localStream = stream;
this.videoAssets[0].srcObject = stream;
}
private stopLocalTracks(){}
private start() {
var self = this;
if( !this.isReady() ){
console.error('Could not start WebRTC because no WebSocket user connectionId had been assigned yet');
}
this.buttonStart.disabled = true;
this.stopLocalTracks();
navigator.mediaDevices.getUserMedia(this.getConstrains())
.then((stream) => {
self.gotStream(stream);
self.SignalingChannel.send(JSON.stringify({type: 'onReadyForTeamspeak'}));
})
.catch(function(error) { trace('getUserMedia error: ', error); });
}
public addPeerId(peerId){
this.availablePeerIds[peerId] = peerId;
this.preparePeerConnection(peerId);
}
private preparePeerConnection(peerId){
var self = this;
if( this.peerConns[peerId] ){
return;
}
this.peerConns[peerId] = new RTCPeerConnection(this.serversCfg);
this.peerConns[peerId].ontrack = function (evt) { self.gotRemoteStream(evt, peerId); };
this.peerConns[peerId].onicecandidate = function (evt) { self.iceCallback(evt, peerId); };
this.peerConns[peerId].onnegotiationneeded = function (evt) { if( self.isCallingTo(peerId) ) { self.createOffer(peerId); } };
this.addLocalTracks(peerId);
}
private addLocalTracks(peerId){
var self = this;
var localTracksCount = 0;
(<any>window).localStream.getTracks().forEach(
function (track) {
self.peerConns[peerId].addTrack(
track,
(<any>window).localStream
);
localTracksCount++;
}
);
trace('Added ' + localTracksCount + ' local tracks to remote peer #' + peerId);
}
private call() {
var self = this;
trace('Start calling all available new peers if any available');
// only call if there is anyone to call
if( !Object.keys(this.availablePeerIds).length ){
trace('There are no callable peers available that I know of');
return;
}
for( let peerId in this.availablePeerIds ){
if( !this.availablePeerIds.hasOwnProperty(peerId) ){
continue;
}
this.preparePeerConnection(peerId);
}
}
private createOffer(peerId){
var self = this;
this.peerConns[peerId].createOffer( this.offerOptions )
.then( function (offer) { return self.peerConns[peerId].setLocalDescription(offer); } )
.then( function () {
trace('Send offer to peer #' + peerId);
self.SignalingChannel.send(JSON.stringify({ "sdp": self.peerConns[peerId].localDescription, "remotePeerId": peerId, "type": "onWebRTCPeerConn" }));
})
.catch(function(error) { self.onCreateSessionDescriptionError(error); });
}
private answerCall(peerId){
var self = this;
trace('Answering call from peer #' + peerId);
this.peerConns[peerId].createAnswer()
.then( function (answer) { return self.peerConns[peerId].setLocalDescription(answer); } )
.then( function () {
trace('Send answer to peer #' + peerId);
self.SignalingChannel.send(JSON.stringify({ "sdp": self.peerConns[peerId].localDescription, "remotePeerId": peerId, "type": "onWebRTCPeerConn" }));
})
.catch(function(error) { self.onCreateSessionDescriptionError(error); });
}
private onCreateSessionDescriptionError(error) {
console.warn('Failed to create session description: ' + error.toString());
}
private gotRemoteStream(e, peerId) {
if (this.audioAssets[peerId].srcObject !== e.streams[0]) {
this.videoAssets[peerId].srcObject = e.streams[0];
trace('Added stream source of remote peer #' + peerId + ' to DOM');
}
}
private iceCallback(event, peerId) {
this.SignalingChannel.send(JSON.stringify({ "candidate": event.candidate, "remotePeerId": peerId, "type": "onWebRTCPeerConn" }));
}
private handleCandidate(candidate, peerId) {
this.peerConns[peerId].addIceCandidate(candidate)
.then(
this.onAddIceCandidateSuccess,
this.onAddIceCandidateError
);
trace('Peer #' + peerId + ': New ICE candidate: ' + (candidate ? candidate.candidate : '(null)'));
}
private onAddIceCandidateSuccess() {
trace('AddIceCandidate success.');
}
private onAddIceCandidateError(error) {
console.warn('Failed to add ICE candidate: ' + error.toString());
}
private hangup() {}
private bindSignalingHandlers(){
this.SignalingChannel.registerHandler('onWebRTCPeerConn', (signal) => this.handleSignals(signal));
}
private handleSignals(signal){
var self = this,
peerId = signal.connectionId;
if( signal.sdp ) {
trace('Received sdp from peer #' + peerId);
this.peerConns[peerId].setRemoteDescription(new RTCSessionDescription(signal.sdp))
.then( function () {
if( self.peerConns[peerId].remoteDescription.type === 'answer' ){
trace('Received sdp answer from peer #' + peerId);
} else if( self.peerConns[peerId].remoteDescription.type === 'offer' ){
trace('Received sdp offer from peer #' + peerId);
self.answerCall(peerId);
} else {
trace('Received sdp ' + self.peerConns[peerId].remoteDescription.type + ' from peer #' + peerId);
}
})
.catch(function(error) { trace('Unable to set remote description for peer #' + peerId + ': ' + error); });
} else if( signal.candidate ){
this.handleCandidate(new RTCIceCandidate(signal.candidate), peerId);
} else if( signal.closeConn ){
trace('Closing signal received from peer #' + peerId);
this.endCall(peerId,true);
}
}
}
I have been using a similar construct to build up the WebRTC connections between sender and receiver peers, by calling the method RTCPeerConnection.addTrack twice (one for the audio track, and one for the video track).
I used the same structure as shown in the Stage 2 example shown in The evolution of WebRTC 1.0:
let pc1 = new RTCPeerConnection(), pc2 = new RTCPeerConnection(), stream, videoTrack, videoSender;
(async () => {
try {
stream = await navigator.mediaDevices.getUserMedia({video: true, audio: true});
videoTrack = stream.getVideoTracks()[0];
pc1.addTrack(stream.getAudioTracks()[0], stream);
} catch (e) {
console.log(e);
}
})();
checkbox.onclick = () => {
if (checkbox.checked) {
videoSender = pc1.addTrack(videoTrack, stream);
} else {
pc1.removeTrack(videoSender);
}
}
pc2.ontrack = e => {
video.srcObject = e.streams[0];
e.track.onended = e => video.srcObject = video.srcObject; // Chrome/Firefox bug
}
pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
pc1.onnegotiationneeded = async e => {
try {
await pc1.setLocalDescription(await pc1.createOffer());
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription(await pc2.createAnswer());
await pc1.setRemoteDescription(pc2.localDescription);
} catch (e) {
console.log(e);
}
}
Test it here: https://jsfiddle.net/q8Lw39fd/
As you'll notice, in this example the method createOffer is never called directly; instead, it is indirectly called via addTrack triggering an RTCPeerConnection.onnegotiationneeded event.
However, just as in your case, Chrome triggers this event twice, once for each track, and this causes the error message you mentioned:
DOMException: Failed to set local answer sdp: Called in wrong state: kStable
This doesn't happen in Firefox, by the way: it triggers the event only once.
The solution to this issue is to write a workaround for the Chrome behavior: a guard that prevents nested calls to the (re)negotiation mechanism.
The relevant part of the fixed example would be like this:
pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
var isNegotiating = false; // Workaround for Chrome: skip nested negotiations
pc1.onnegotiationneeded = async e => {
if (isNegotiating) {
console.log("SKIP nested negotiations");
return;
}
isNegotiating = true;
try {
await pc1.setLocalDescription(await pc1.createOffer());
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription(await pc2.createAnswer());
await pc1.setRemoteDescription(pc2.localDescription);
} catch (e) {
console.log(e);
}
}
pc1.onsignalingstatechange = (e) => { // Workaround for Chrome: skip nested negotiations
isNegotiating = (pc1.signalingState != "stable");
}
Test it here: https://jsfiddle.net/q8Lw39fd/8/
You should be able to easily implement this guard mechanism into your own code.
You're sending the answer here:
.then( function (answer) { return self.peerConns[peerId].setLocalDescription(answer); } )
Look at mine:
var callback = function (answer) {
createdDescription(answer, fromId);
};
peerConnection[fromId].createAnswer().then(callback).catch(errorHandler);
function createdDescription(description, fromId) {
console.log('Got description');
peerConnection[fromId].setLocalDescription(description).then(function() {
console.log("Sending SDP:", fromId, description);
serverConnection.emit('signal', fromId, {'sdp': description});
}).catch(errorHandler);
}