I'm trying to implement WebRTC communication inside playCanvas but I'm having trouble establishing a connection between Peers.
To start with the local environment, I'm using Brave Browser and Ubuntu OS, and the connection is established and I can send messages between peers. Below is a correct offer that is created with the correct Protocol RTP.
"v=0
o=- 6768969287305795266 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0
a=msid-semantic: WMS
m=application 9 UDP/TLS/RTP/SAVPF 118
c=IN IP4 0.0.0.0
b=AS:30
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:kpEN
a=ice-pwd:0cEFgezZPzFBSsTbtx03XZUv
a=ice-options:trickle
a=fingerprint:sha-256 CD:93:28:C9:A1:17:59:C7:3C:E3:F9:03:48:02:59:08:50:FF:A1:8A:AF:53:9B:65:3B:62:B0:5E:E6:23:46:9F
a=setup:actpass
a=mid:0
a=sendrecv
a=msid:channel1 channel1
a=rtcp-mux
a=rtpmap:118 google-data/90000
a=ssrc:3090418315 cname:qaxoIuO1ybfnQaqY
a=ssrc:3090418315 msid:channel1 channel1
a=ssrc:3090418315 mslabel:channel1
a=ssrc:3090418315 label:channel1
"
This is the offer that is valid and on a local PC, I can connect and send messages between peers. But this works only on the Brave browser, if I switch on Chrome or another PC with Brave browser it will not work and also if I implement WebRTC on playcanvas it will also not work.
Below is the data I get from an offer that I think is the main reason for not working. Because it uses SCTP protocol.
"v=0
o=- 5811252825165405755 3 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0
a=extmap-allow-mixed
a=msid-semantic: WMS
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
c=IN IP4 0.0.0.0
a=candidate:1528355922 1 udp 2113937151 920a8f71-bd47-4dac-9a24-497783686050.local 59907 typ host generation 0 network-cost 999
a=ice-ufrag:NjXz
a=ice-pwd:M3FdXA3wNEA25FQaFU3GwcVd
a=ice-options:trickle
a=fingerprint:sha-256 F0:59:BF:FF:AB:8B:4A:1A:A2:D8:0E:57:DD:AD:7B:14:24:7E:A7:6F:CC:C3:B2:BB:83:96:BB:53:49:71:CE:9A
a=setup:actpass
a=mid:0
a=sctp-port:5000
a=max-message-size:262144
"
This is the error that I receive on playcanvas.
Uncaught (in promise) DOMException: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection': Failed to set remote offer sdp: Data channel type mismatch. Expected RTP, got SCTP.
Below is the implementation inside playcanvas.
NetworkManager.id = null;
NetworkManager.socket = null;
NetworkManager.clients = null;
NetworkManager.user = null;
NetworkManager.username = null;
NetworkManager.connectedUser = null;
NetworkManager.yourConn = null;
NetworkManager.dataChannel = null;
// initialize code called once per entity
NetworkManager.prototype.initialize = function() {
// Context
var self = this;
this.socket = new WebSocket(this.address, "wss");
this.clients = [];
// Listeners
this.socket.onopen = function(event){
self.initState();
};
this.socket.onmessage = function(message) {
const data = JSON.parse(message.data);
switch (data.type) {
case 'login':
self.handleLogin(data.success, data.name);
break;
case 'offer':
self.handleOffer(data.offer, data.name);
break;
case 'answer':
self.handleAnswer(data.answer);
break;
case 'candidate':
self.handleCandidate(data.candidate);
break;
default:
break;
}
};
this.socket.onerror = function (err) {
console.log('Got error', err);
};
};
// Called every frame
NetworkManager.prototype.update = function(dt) {
// this.updatePosition();
// if(this.app.keyboard.wasPressed(pc.KEY_SPACE)){
// this.sendPayload("Soldo");
// }
};
// Functions
NetworkManager.prototype.handleMembers = function(users) {
this.clients = users;
this.initializePlayers(this.clients);
};
NetworkManager.prototype.handleLogin = function(success, username) {
if (!success) {
alert('try different username');
} else {
this.clients.push({username});
var configuration = {
iceServers: [{ url: 'stun:stun2.1.google.com:19302' }],
};
this.yourConn = new RTCPeerConnection(configuration, {
optional: [{ RtpDataChannels: true }],
});
this.yourConn.onicecandidate = event => {
if (event.candidate) {
this.sendMessage({
type: 'candidate',
candidate: event.candidate,
});
}
};
this.dataChannel = this.yourConn.createDataChannel('channel1', {
reliable: true,
});
this.dataChannel.onerror = function(error) {
console.log('Error: ', error);
};
this.dataChannel.onmessage = function(event) {
console.log('on message data channel');
console.log(event.data.data);
};
this.dataChannel.onclose = () => {
console.log('data channel is closed.');
};
}
};
NetworkManager.prototype.login = function(name) {
username = name;
if (username.length > 0) {
this.sendMessage({
type: 'login',
name: username,
});
}
};
NetworkManager.prototype.createOffer = function(name) {
const callToUsername = name;
if (callToUsername.length > 0) {
this.connectedUser = callToUsername;
this.yourConn.createOffer(
offer => {
this.sendMessage({
type: 'offer',
offer: offer,
});
this.yourConn.setLocalDescription(offer);
},
error => {
alert('Error when creating an offer');
}
);
}
};
NetworkManager.prototype.sendMessage = function(message) {
if (this.connectedUser) {
message.name = this.connectedUser;
}
this.socket.send(JSON.stringify(message));
};
NetworkManager.prototype.initState = function() {
// this.login(this.username);
};
NetworkManager.prototype.handleCandidate = function(candidate) {
this.yourConn.addIceCandidate(new RTCIceCandidate(candidate));
};
NetworkManager.prototype.handleOffer = function(offer, name) {
this.connectedUser = name;
this.yourConn.setRemoteDescription(new RTCSessionDescription(offer));
this.yourConn.createAnswer(
answer => {
this.yourConn.setLocalDescription(answer);
this.sendMessage({
type: 'answer',
answer: answer,
});
},
error => {
alert('Error while creating an answer');
}
);
};
NetworkManager.prototype.handleAnswer = function(answer) {
this.yourConn.setRemoteDescription(new RTCSessionDescription(answer));
};
NetworkManager.prototype.sendPayload = function (message) {
this.dataChannel.send(message);
};
I will appreciate any help.
Data Channels based on RTP were a non standard Chrome-only extension and have been deprecated for a long time in favor of SCTP Data Channels (see https://www.chromestatus.com/feature/6485681910054912). Your Brave Browser might be a very old version.
Related
I am creating a webRTC video chat that shows a caller all active members when initiating a call from firefox and the receiver is using chrome this error is displayed "Uncaught (in promise) DOMException: Failed to execute 'addIceCandidate' on 'RTCPeerConnection': Error processing ICE candidate". And when a call is initiated from firefox and receiver uses firefox I get two errors Invalidstate: cannot add ICE candidate when there is no remote SDP and ICE failed, add a STUN and see about:webrtc for details
I dont know where I am making a mistake
/ define all data here
var usersOnline,id,currentCaller,room,caller,localUser,media,memberInfo;
// All subscribed members.
var users = [];
var token = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
// create random user id
var userId = Math.random().toString(36).substring(2, 15);
// create random username
var username = token;
// authonticating user
var currentUser = {
username: token,
userId: userId
}
// stringify user data
currentUser = JSON.stringify(currentUser);
var pusher = new Pusher('KEY', {
authEndpoint: '../auth.php',
auth: {
params: JSON.parse(currentUser)
},
cluster: 'ap2',
forceTLS: true
});
var state = pusher.connection.state;
var channel = pusher.subscribe('presence-conference');
channel.bind("pusher:subscription_succeeded", function (members) {
console.log(members);
id = channel.members.me.id;
document.getElementById('mydetails').innerHTML = 'Online Now: ' + ' ( ' + (members.count - 1) +')';
members.each(member => {
if (member.id != channel.members.me.id) {
users.push(member.id);
}
});
renderOnline();
});
// Add user online
channel.bind("pusher:member_added", member => {
users.push(member.id);
renderOnline();
});
channel.bind("pusher:member_removed", member => {
// for remove member from list:
var index = users.indexOf(member.id);
users.splice(index, 1);
if (member.id == room) {
endCall();
}
renderOnline();
});
function renderOnline(){
var list = "";
users.forEach(function(user) {
list +=
`<li>` +
user +//this will call user
` <input type="button" style="float:right;" value="Call" onclick="callUser('` +
user +
`')" id="makeCall" /></li>`;
});
document.getElementById("userDetails").innerHTML = list;
}
//To iron over browser implementation anomalies like prefixes
GetRTCPeerConnection();
GetRTCSessionDescription();
GetRTCIceCandidate();
prepareCaller();
function prepareCaller() {
//Initializing a peer connection
caller = new window.RTCPeerConnection();
//Listen for ICE Candidates and send them to remote peers
caller.onicecandidate = function(evt) {
if (!evt.candidate) return;
console.log("onicecandidate called");
onIceCandidate(caller, evt);
};
//onaddstream handler to receive remote feed and show in remoteview video element
caller.onaddstream = function(evt) {
console.log("onaddstream called");
if("srcObject" in document.getElementById("selfview")){
document.getElementById("selfview").srcObject = evt.stream;
}else{
if (window.URL) {
document.getElementById("remoteview").src = window.URL.createObjectURL(
evt.stream
);
} else {
document.getElementById("remoteview").src = evt.stream;
}
}
};
}
function getCam() {
//Get local audio/video feed and show it in selfview video element
return navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
sampleSize:8
},
video: {
width: 1080,
height: 720,
aspectRatio: { ideal: 1.777778 }
}
});
}
function GetRTCIceCandidate() {
window.RTCIceCandidate =
window.RTCIceCandidate ||
window.webkitRTCIceCandidate ||
window.mozRTCIceCandidate ||
window.msRTCIceCandidate;
return window.RTCIceCandidate;
}
function GetRTCPeerConnection() {
window.RTCPeerConnection =
window.RTCPeerConnection ||
window.webkitRTCPeerConnection ||
window.mozRTCPeerConnection ||
window.msRTCPeerConnection;
return window.RTCPeerConnection;
}
function GetRTCSessionDescription() {
window.RTCSessionDescription =
window.RTCSessionDescription ||
window.webkitRTCSessionDescription ||
window.mozRTCSessionDescription ||
window.msRTCSessionDescription;
return window.RTCSessionDescription;
}
//Create and send offer to remote peer on button click
function callUser(user) {
getCam()
.then(stream => {
if("srcObject" in document.getElementById("selfview")){
document.getElementById("selfview").srcObject = stream;
}else{
if (window.URL) {
document.getElementById("selfview").src = window.URL.createObjectURL(
stream
);
} else {
document.getElementById("selfview").src = stream;
}
}
toggleEndCallButton();
caller.addStream(stream);
localUserMedia = stream;
caller.createOffer().then(function(desc) {
caller.setLocalDescription(new RTCSessionDescription(desc));
channel.trigger("client-sdp", {
sdp: desc,
room: user,
from: id
});
room = user;
});
})
.catch(error => {
console.log("an error occured", error);
});
}
function endCall() {
room = undefined;
caller.close();
for (let track of localUserMedia.getTracks()) {
track.stop();
}
prepareCaller();
toggleEndCallButton();
}
function endCurrentCall() {
channel.trigger("client-endcall", {
room: room
});
endCall();
}
//Send the ICE Candidate to the remote peer
function onIceCandidate(peer, evt) {
if (evt.candidate) {
channel.trigger("client-candidate", {
candidate: evt.candidate,
room: room
});
}
}
function toggleEndCallButton() {
if (document.getElementById("endCall").style.display == "block") {
document.getElementById("endCall").style.display = "none";
} else {
document.getElementById("endCall").style.display = "block";
}
}
//Listening for the candidate message from a peer sent from onicecandidate handler
channel.bind("client-candidate", function(msg) {
if (msg.room == room) {
console.log("candidate received");
caller.addIceCandidate(new RTCIceCandidate(msg.candidate));
}
});
//Listening for Session Description Protocol message with session details from remote peer
channel.bind("client-sdp", function(msg) {
if (msg.room == id) {
console.log("sdp received");
var answer = confirm(
"You have a call from: " + msg.from + "Would you like to answer?"
);
if (!answer) {
return channel.trigger("client-reject", { room: msg.room, rejected: id });
}
room = msg.room;
getCam()
.then(stream => {
localUserMedia = stream;
toggleEndCallButton();
if("srcObject" in document.getElementById("selfview")){
document.getElementById("selfview").srcObject = stream;
}else{
if (window.URL) {
document.getElementById("selfview").src = window.URL.createObjectURL(
stream
);
} else {
document.getElementById("selfview").src = stream;
}
}
caller.addStream(stream);
var sessionDesc = new RTCSessionDescription(msg.sdp);
caller.setRemoteDescription(sessionDesc);
caller.createAnswer().then(function(sdp) {
caller.setLocalDescription(new RTCSessionDescription(sdp));
channel.trigger("client-answer", {
sdp: sdp,
room: room
});
});
})
.catch(error => {
console.log("an error occured", error);
});
}
});
//Listening for answer to offer sent to remote peer
channel.bind("client-answer", function(answer) {
if (answer.room == room) {
console.log("answer received");
caller.setRemoteDescription(new RTCSessionDescription(answer.sdp));
}
});
channel.bind("client-reject", function(answer) {
if (answer.room == room) {
console.log("Call declined");
alert("call to " + answer.rejected + "was politely declined");
endCall();
}
});
channel.bind("client-endcall", function(answer) {
if (answer.room == room) {
console.log("Call Ended");
endCall();
}
});
I EXPECTED that the video call will work don't want to use any API, help me see where I went wrong.
Call setRemoteDescription(offer) before requesting the camera.
This puts the RTCPeerConnection in the right signaling state ("have-remote-offer") to receive and process remote ICE candidates correctly.
There's no time to request the camera first when an offer comes in. Incoming offers are typically followed closely by trickled ICE candidates on your signaling channel. addIceCandidate won't know what to do with those if it hasn't seen an offer.
Move the setRemoteDescription call ahead of the getMedia call in the promise chain to fix it. You have more time then before returning an answer.
Though that's still not great, since this approach often ends up blocking initial WebRTC negotiation on a user permission prompt for the camera. This is called tight coupling. Sadly, the current state of WebRTC encourages it, since getting the best IP mode is gated on getUserMedia in most browsers.
Lastly, there's a lot of old API usage here. See my other answer for newer APIs to use.
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 fairly new in webRTC finding problems with its documentation. I cannot figure out why joiner does not receive stream from initiator since the messages on console look to me quite normal. Also i receive warnings about deprecated methods but not sure what to correct.
html:
navigator.getUserMedia = navigator.getUserMedia ||
navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.mediaDevices.getUserMedia ||
navigator.msGetUserMedia;
var isInitiator = false
, isChannelReady = false
, isStarted = false;
//Send 'create' or 'join' message to singnalling server
console.log('Create or join room', room);
socket.emit('create or join', room);
//Handle 'created' message coming back from server:
//this peer is the initiator
socket.on('created', function (room){
console.log('Created room ' + room);
isInitiator = true;
var video = $('#sidebar').append("<video class='student' autoplay='true'></video>");
});
//Handle 'join' message coming back from server:
//another peer is joining the channel
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;
var video = $('#sidebar').append("<video class='student' autoplay='true'></video>");
});
//Handle 'joined' message coming back from server:
//this is the second peer joining the channel
socket.on('joined', function (room){
console.log('This peer has joined room ' + room);
isChannelReady = true;
var video = $('#sidebar').append("<video class='student' autoplay='true'></video>");
navigator.getUserMedia({ video: true, audio: true },
function (stream) {
$('#sidebar').children().last().attr('src', window.URL.createObjectURL(stream))
if(!isStarted && typeof stream!= 'undefined' && isChannelReady) {
createPeerConnectionInit(stream);
isStarted = true;
} else{
}
}, function (error) {
console.log('error'+error);
});
});
socket.on('message', function (message){
if (message === 'got user media') {
}
else if (message.type === 'offer') {
if(isChannelReady){
console.log('Im the initiator. Channel is ready!!!');
createPeerConnectionNotInit(message);
isStarted = true;
}
}
else if (message.type === 'answer' && isStarted) {
peer.addAnswerSDP(message);
console.log('addAnswerSDP:', message);
}
else if (message.type === 'candidate' && isStarted) {
console.log("im adding candidate!!!");
var candidate = new RTCIceCandidate({sdpMLineIndex:message.label,
candidate:message.candidate});
peer.addICE(candidate);// εδω ο initiator προσθέτει στο ice τον candidate
}
else if (message === 'bye' && isStarted) {
}
});
function createPeerConnectionInit(stream){
peer = RTCPeerConnection({
attachStream : stream,
onICE : function (candidate) {
if (candidate) {
sendMessage({
type: 'candidate',
label: candidate.sdpMLineIndex,
id: candidate.sdpMid,
candidate: candidate.candidate});
} else {
console.log('End of candidates.');
}
},
onRemoteStream : function (stream) {
console.log('onRemoteStream Init = yes');
document.getElementById('video').src = stream;
},
onOfferSDP : function(sdp) {
console.log('sdp ='+JSON.stringify(sdp));
sendMessage(sdp);
}
});
}
function createPeerConnectionNotInit(offer_sdp){
peer = RTCPeerConnection({
onICE : function (candidate) {
if (candidate) {
sendMessage({
type: 'candidate',
label: candidate.sdpMLineIndex,
id: candidate.sdpMid,
candidate: candidate.candidate});
} else {
console.log('End of candidates.');
}
},
onRemoteStream : function (stream) {
console.log('onRemoteStream Not Init = yes');
document.getElementById('video').src = URL.createObjectURL(stream);;
},
// see below two additions ↓
offerSDP : offer_sdp,
onAnswerSDP : function(sdp) {
sendMessage(sdp);
}
});
}
Console log from initiator view:
sdp ={"type":"offer","sdp":"v=0\r\no=mozilla...THIS_IS_SDPARTA-53.0.3 8347568228516099874 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=fingerprint:sha-256
Sending message: Object { type: "candidate", label: 0, id: "sdparta_0", candidate: "candidate:0 1 UDP 2122187007 2a02:5…" } boardWithD3.js.html:798:13
Many same messages following..
addAnswerSDP: Object { type: "answer", sdp: "v=0 o=mozilla...THIS_IS_SDPARTA-53.…" } boardWithD3.js.html:727:17
adding-ice candidate:0 1 UDP 2122187007 2a02:582:1096:a500:f03d:34da:c0a:75b0 50729 typ host RTCPeerConnection-v1.5.js:264:13
A couple similar messages following…..
Console log from joiner view:
offer_sdp:{"type":"offer","sdp":"v=0\r\no=mozilla...THIS_IS_SDPARTA-53.0.3 8347568228516099874 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=fingerprint:sha-256 ………
ice-servers [{"url": "stun:23.21.150.121" },
{"url": "stun:stun.services.mozilla.com" }]
sdp-constraints {
"OfferToReceiveAudio": true,
"OfferToReceiveVideo": true
} RTCPeerConnection-v1.5.js:123:5
offer-sdp v=0 o=mozilla...THIS_IS_SDPARTA-53.0.3 8347568228516099874 0 IN IP4 0.0.0.0
……
WebRTC interfaces with the “moz” prefix (mozRTCPeerConnection, mozRTCSessionDescription, mozRTCIceCandidate) have been deprecated. RTCPeerConnection-v1.5.js:79:15
RTCIceServer.url is deprecated! Use urls instead. RTCPeerConnection-v1.5.js:79
onaddstream is deprecated! Use peerConnection.ontrack instead. RTCPeerConnection-v1.5.js:101
onRemoteStream Not Init = yes boardWithD3.js.html:784:21
Sending message: Object { type: "answer", sdp: "v=0 o=mozilla...THIS_IS_SDPARTA-53.…" } boardWithD3.js.html:798:13……
adding-ice candidate:0 1 UDP 2122187007 2a02:582:1096:a500:f03d:34da:c0a:75b0 50006 typ host RTCPeerConnection-v1.5.js:264:13
**Uncaught TypeError: Cannot set property 'src' of null
at Object.onRemoteStream (boardWithD3.js.html?andora:785)
at RTCPeerConnection.peer.onaddstream (RTCPeerConnection-v1.5.js:110)
**
adding-ice candidate:3 1 UDP 2122252543 2a02:582:1096:a500:7de6:9361:ecf4:476a 50007 typ host RTCPeerConnection-v1.5.js:264:13
followed by many similar messages…..
Sending message: Object { type: "candidate", label: 0, id: "sdparta_0", candidate: "candidate:0 1 UDP 2122187007 2a02:5…" } boardWithD3.js.html:798:13
followed by many similar messages…..
Thanks in advance
As per below log
Uncaught TypeError: Cannot set property 'src' of null
at Object.onRemoteStream (boardWithD3.js.html?andora:785)
at RTCPeerConnection.peer.onaddstream (RTCPeerConnection-v1.5.js:110)
Issue is in below method with video element.
onRemoteStream : function (stream) {
console.log('onRemoteStream stream', stream);
//document.getElementById('video').src = stream; // Issue could be here, add a video element with id="video" in html body.
document.getElementById('video').srcObject = stream; //As per latest API use srcObject instead of src
}
As #jib mentioned, you using old API's.
See the samples and demo for latest API's
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
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);
}