I am developing a simple webRTC application, using my own server for signaling.
the javascript code is as follow (I have removed the signaling process and unnecessary logic):
const configuration = {
iceServers: [
{
urls: [
"stun:stun1.l.google.com:19302",
"stun:stun2.l.google.com:19302",
],
},
],
iceCandidatePoolSize: 10,
};
const callerCandidatesString = "callerCandidates";
const calleeCandidatesString = "calleeCandidates";
var received_offer = null;
var offer = null;
var answer = null;
var peerConnection = null;
let localStream = null;
let remoteStream = null;
var constraints = {
optional: [],
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true
}
}
async function startMedia(e) {
const localStream = await navigator.mediaDevices.getUserMedia({video: true, audio: true});
document.getElementById("video1").srcObject = localStream;
remoteStream = new MediaStream();
document.getElementById("video2").srcObject = remoteStream;
if (I am the caller) {
create_the_offer();
}
if (I am the callee) {
get_the_offer();
}
}
async function create_the_offer() {
peerConnection = new RTCPeerConnection(configuration);
registerPeerConnectionListeners();
localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream);
});
var offer = await peerConnection.createOffer(constraints);
peerConnection.setLocalDescription(offer);
peerConnection.onicecandidate = function(candidate) {
if (candidate.candidate == null) {
//save the offer in the server
--> offer: JSON.stringify(peerConnection.localDescription)},
}
}
check_if_there_is_an_answer();
}
async function get_the_offer() {
// --> retrieve the offer from the server, then
create_answer(offer_from_server);
}
async function create_answer(received_offer) {
console.log("Create PeerConnection with configuration: ", configuration);
peerConnection = new RTCPeerConnection(configuration);
registerPeerConnectionListeners();
localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream);
});
console.log('received offer:' + received_offer)
my_offer = new RTCSessionDescription(JSON.parse(received_offer));
peerConnection.setRemoteDescription(my_offer);
// collectIceCandidates(peerConnection, calleeCandidatesString, callerCandidatesString);
peerConnection.addEventListener("track", event => {
console.log("Got remote track:", event.streams[0]);
event.streams[0].getTracks().forEach(track => {
console.log("Add a track to the remoteStream:", track);
remoteStream.addTrack(track);
});
});
const answer = await peerConnection.createAnswer(constraints);
console.log("Created answer:", answer);
await peerConnection.setLocalDescription(answer);
peerConnection.onicecandidate = function (e) {
if (e.candidate == null) {
// --> send the answer to the server
}
}
function check_if_there_is_an_answer() {
// retrieve answer from server. this function is executed several times until the answer is received.
// when there is an aswer:
start_remote_connection(answer);
}
async function start_remote_connection(passed_answer) {
my_answer = new RTCSessionDescription(JSON.parse(passed_answer));
peerConnection.setRemoteDescription(my_answer);
peerConnection.addEventListener("track", event => {
console.log("Got remote track:", event.streams[0]);
event.streams[0].getTracks().forEach(track => {
console.log("Add a track to the remoteStream:", track);
remoteStream.addTrack(track);
});
console.log("stream remoto: " + JSON.stringify(remoteStream.getVideoTracks()));
});
document.getElementById("video1").srcObject = localStream;
document.getElementById("video2").srcObject = remoteStream;
}
async function hangUp(e) {
const tracks = document.getElementById("video1").srcObject.getTracks();
tracks.forEach(track => {
track.stop();
});
remoteStream.getTracks().forEach(track => track.stop());
peerConnection.close();
document.getElementById("video1").srcObject = null;
document.getElementById("video2").srcObject = null;
}
// collect ICE Candidates function below
async function collectIceCandidates(peerConnection, localName, remoteName) {
const candidatesCollection = null;
peerConnection.addEventListener("icecandidate", event => {
if (event.candidate) {
const json = event.candidate.toJSON();
candidatesCollection.add(json);
}
});
}
// collect ICE Candidates function above
function registerPeerConnectionListeners() {
peerConnection.addEventListener("icegatheringstatechange", () => {
console.log(
`ICE gathering state changed: ${peerConnection.iceGatheringState}`);
});
peerConnection.addEventListener("connectionstatechange", () => {
console.log(`Connection state change: ${peerConnection.connectionState}`);
});
peerConnection.addEventListener("signalingstatechange", () => {
console.log(`Signaling state change: ${peerConnection.signalingState}`);
});
peerConnection.addEventListener("iceconnectionstatechange ", () => {
console.log(
`ICE connection state change: ${peerConnection.iceConnectionState}`);
});
}
window.onload = startMedia();
If the caller uses chrome and the callee uses FireFox (on localhost, same PC) the code works fine and both users can share their screen.
output with chrome
If the caller uses FireFox and the callee uses Chrome (still on localhost) the code still works fine, but the connection is not established and users cannot see the screen of the other person. I get no error in the console.
output with FireFox
In particular, with FF I am not getting "connection state change: connecting" and then "connection state change: connected".
My guess is that FF and chrome manage the async/await differently, and somehow with FF some values are not ready when actually needed, but cannot figure out why ...
with safari (MacOS) it does not work!!
I then tried it between the computer (with chrome) and an Android phone (chrome browser). It worked the first time I test it, and then never again :(
Does anyone of you has a clue ?
Related
I'm currently working on a webrtc project that was written before from another developer.
While I'm currently new to webrtc and I tried hard to make it work after deleting obsolete functions and other things, now can I identify what is wrong here in my steps?
I'm following steps in here
my code here is about trigger .call button
$(dod)
.find(".call")
and I run throw signaling
wbsc.emit("SEND_EVENT_EMIT_CALL_AUDIO", {
data: { type: "login", id: id },
});
$(dod).hide();
//call*donecallProccess 1
setTimeout(() => {
wbsc.emit("SEND_EVENT_EMIT_CALL_AUDIO", {
data: { type: "doneoif", id: id },
});
}, 2e3);
that can trigger and process here
case "donecall":
call(data.id);
break;
case "showcall":
handleLogin(data.success, data.id);
break;
case "offercall":
handleOffer(data.offer, data.name);
break;
case "answercall":
handleAnswer(data.answer);
break;
case "candidatecall":
handleCandidate(data.candidate);
break;
case "leavecall":
handleLeave();
then this code run one after each handle login and getUserMediaSuccess
gather permission from media stream and create a new RTCPeerConnection(servers)
get tracks from my streams with addtrack if I implement it correctly
and if ontrack happened can I collect streaming to add to remote peer like this way or should just add
yourConn.ontrack = (event) => {
if (event.candidate !== null) {
remoteVideo.srcObject = event.streams[0];
} else {
console.log("there is an error with on trackevent", event);
}
};
complete code for previous handlelogin and call is
let handleLogin = async (success) => {
try {
if (success) {
localVideo = document.getElementById("wbrtclocal");
remoteVideo = document.getElementById("wbrtcremote");
var getUserMedia = navigator.mediaDevices.getUserMedia|| navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ;
if (getUserMedia) {
getUserMedia({
audio: { noiseSuppression: false },
})
.then(getUserMediaSuccess)
.catch(errorHandler);
} else {
alert("Your browser does not support getUserMedia API");
}
} else {
alert("Ooops...try a different username");
}
} catch (err) {
errorHandler(error);
}
};
let getUserMediaSuccess = async (streams) => {
try {
yourConn = new RTCPeerConnection(servers);
if(streams){
localStream = streams;
localVideo.srcObject = streams;
streams.getTracks().forEach((track) => yourConn.addTrack(track, streams));
}
yourConn.onicecandidate = (event) => {
if (event.candidate) {
wbsc.emit("SEND_EVENT_EMIT_CALL_AUDIO", {
data: {
type: "candidate",
candidate: event.candidate,
id: connectedUser,
},
});
}
console.log("nwe ice candidate is", event.candidate);
console.log("nwe stream is", streams);
};
remoteStream = new MediaStream();
remoteVideo.srcObject = remoteStream;
yourConn.ontrack = (event) => {
if (event.candidate !== null) {
event.streams[0].getTracks().forEach((track) => {
remoteStream.addTrack(track);
});
} else {
console.log("there is an error with on trackevent", event);
}
};
} catch (err) {
errorHandler(error);
}
console.log("stream is", streams);
};
call function
async function call(id) {
$("#videoCall").show();
if (id.length > 0) {
connectedUser = id;
try {
RTCPeerConnection.createOffer().
offer.
await yourConn
.createOffer()
.then((offer) => successCallback)
.catch((e) => {
fl(e);
});
let successCallback = async (offer) => {
try {
yourConn
.setLocalDescription(offer)
.then(
wbsc.emit("SEND_EVENT_EMIT_CALL_AUDIO", {
data: { type: "offer", offer: offer, id: connectedUser },
})
)
.catch((e) => {
fl(e);
});
} catch (e) {
fl(e);
}
};
} catch (e) {
fl(e);
}
const user = U_CASH[id];
if (user) {
$("#videoCall")
.find(".u-pic")
.css("background-image", "url(" + removegifpic(user.pic + ")"));
$("#videoCall").find(".u-topic").text(user.topic);
}
$(".statecall").text("جاري الإتصال");
hl($(".statecall"), "warning");
} else {
alert("username can't be blank!");
}
console.log("connectedUser", id);
console.log("offer", offer);
console.log();
console.log();
console.log();
}
and this about each one for those
[handleOffer , handleAnswer , handleCandidate , handleLeave]
let handleOffer = async (offer, name) => {
$("#callvideonot").show();
const user = U_CASH[name];
if (user) {
$("#callvideonot")
.find(".u-pic")
.css("background-image", "url(" + removegifpic(user.pic + ")"));
$("#callvideonot").find(".u-topic").text(user.topic);
}
$(".callvideoaccept").on("click", async () => {
connectedUser = name;
await yourConn.setRemoteDescription(offer);
yourConn
.createAnswer()
.then((answer) => t.setLocalDescription(answer))
.then(() => {
wbsc.emit("SEND_EVENT_EMIT_CALL_AUDIO", {
data: { type: "answer", answer: answer, id: connectedUser },
});
})
.catch(fl);
const user = U_CASH[name];
if (user) {
$(".statecall").text("متصل");
hl($(".statecall"), "success");
$("#videoCall")
.find(".u-pic")
.css("background-image", "url(" + removegifpic(user.pic + ")"));
$("#videoCall").find(".u-topic").text(user.topic);
}
$("#callvideonot").hide();
$("#videoCall").show();
});
$(".callvideodeny").on("click", function () {
wbsc.emit("SEND_EVENT_EMIT_CALL_AUDIO", {
data: { type: "leave", id: name },
});
});
};
let handleAnswer = async (answer) => {
try {
$(".statecall").text("متصل");
hl($(".statecall"), "success");
//here we delete new RTCSessionDescription because constructor is deprecated.
await yourConn.setRemoteDescription(answer);
} catch (e) {
fl(e);
}
};
let handleCandidate = async (candidate) => {
try {
var NewlyIceCandidate = new RTCIceCandidate(candidate)
.setRemoteDescription().
await yourConn.addIceCandidate(NewlyIceCandidate);
} catch (e) {
fl(e);
}
};
function handleLeave() {
$("#callvideonot").hide();
$(".statecall").text("رفض");
hl($(".statecall"), "danger");
$(".vloumemic").removeClass("fa-volume-off");
$(".vloumemic").addClass("fa-volume-up");
$(".mutemic").removeClass("fa-microphone-slash");
$(".mutemic").addClass("fa-microphone");
setTimeout(() => {
$("#videoCall").hide();
}, 1e3);
if (localStream) {
localStream.getTracks().forEach((e) => e.stop());
}
if (connectedUser) {
connectedUser = null;
}
remoteVideo.src = null;
if (yourConn) {
yourConn.close();
yourConn.onicecandidate = null;
yourConn.ontrack = null;
localStream = null;
}
}
in here here number 7 instruction they said should I add Wait for an incoming remote SDP description from the signaling service and set it using RTCPeerConnection.setRemoteDescription(). as the caller where can I add it? in handleCandidate function?
and in the callee side number 1 instruction the said that I should
Create a new RTCPeerConnection instance with the appropriate ICE configuration.
can I reuse
yourConn = new RTCPeerConnection(servers);
or should instantiate a new one to prevent conflict in website server process
as a not yourConn it's global value and in top level of this file and reuse it over all
the connection, is peers 2 or the callee should have another new RTCPeerConnection?
and for remoteVideo.srcObject = remoteStream; the remoteStream value is global and I overwrite it. can I here add new media stream or just it's good to dealing with the present one which is remoteVideo element?
// remoteStream = new MediaStream();
remoteVideo.srcObject = remoteStream;
the issue that was here is to split the RTCPeerConnection object and just create new one for each peer local and remote that what i did and it working corectly after i remove addtrack completly and replace it with addtranceiver and gettranceiver
u can find it here from previous issue's answer and i fix it here
and no need to overwrite or reset the srcObect value. The track transition on receiver side in the same MediaStream should be "seamless" RTCRtpSender.replaceTrack
This allows you to seamlessly change which track is being sent without having to renegotiate at the expense of another offer/answer cycle
I've been trying to work on this for a while. I'm working with google drive api -> and I'm trying to try to get the main script to re-run the request of the accessToken is incorrect and causes an error.
Can that be sent to the main script somehow?
Adding some code to show I've actually worked on this lol - left out some bc it's alot of other unrelated stuff.
I am using IndexedDB to pass info between SW and main Script
//////////////////////////////////////////////////////////////
//////////////// Check Database onload ///////////////////////
//////////////////////////////////////////////////////////////
window.addEventListener("load", checkUpload(), false);
function checkUpload() {
if (supportCheck()) {
let openRequest = indexedDB.open("GoogleDrive", 1);
openRequest.onsuccess = (e) => {
var db = e.target.result;
var objectStore = db
.transaction(["backups"], "readwrite")
.objectStore("backups");
var request = objectStore.get("1");
request.onerror = function () {
// Handle errors!
};
request.onsuccess = function (event) {
var data = event.target.result;
if (googleSignin.isAuthorizedForGDrive()) {
// Call SW Function
}
else {
//Google Sign in Error
}
let accessToken = gapi.auth.getToken().access_token;
data.access = accessToken;
// Put this updated object back into the database.
var requestUpdate = objectStore.put(data);
requestUpdate.onerror = function (event) {
// Do something with the error
};
requestUpdate.onsuccess = function (event) {
// Success - the data is updated!
// Call SW Function
};
}
}
}
}
}
//////////////////////////////////////////////////////////////
//////////////// Initialize Database Function ///////////////
//////////////////////////////////////////////////////////////
uploadBtn.addEventListener("click", handleUploadClick, false);
save.addEventListener("click", handleUploadClick, false);
//Adds/Create Data that is stored in IndexedDB so that the Service Worker can
access and use it
//ServiceWorker Call Function
function initSW() {
console.log("Script: Called InitSW()");
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("serviceWorker.js")
.then((registration) => navigator.serviceWorker.ready)
.then((registration) => {
registration.sync.register("sendFile-sync").then(() => {
//Do Function using sync
try {
console.log("Script : Sync Registered");
} catch {
console.log("Script : Sync Not Registered");
}
});
});
}
}
SW
self.addEventListener("sync", (e) => {
if (e.tag === "sendFile-sync") {
console.log("SW Sync : Sync Found!");
e.waitUntil(fetchFile());
} else {
console.log("SW Sync : No Sync Found");
}
});
//Function Called above when sync is fired
function fetchFile() {
let openRequest = indexedDB.open("GoogleDrive", 1);
openRequest.onerror = function () {
};
openRequest.onsuccess = function () {
let db = openRequest.result;
let transaction = db.transaction(["backups"], 'readwrite');
let backups = transaction.objectStore("backups");
let request = backups.get("1");
request.onsuccess = function (event) {
let date = Date();
let accessToken = request.result.access;
console.log("SW Sync: Access Token - " + accessToken);
let BlobContent = request.result.text;
let file = BlobContent;
let metadata = {
name: "Backup " + date, // Filename
mimeType: "application/pdf", // mimeType at Google Drive
parents: ["root"], // Root Folder ID for testing
};
let form = new FormData();
form.append(
"metadata",
new Blob([JSON.stringify(metadata)], { type: "application/json" })
);
form.append("file", file);
fetch(
"https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id",
{
method: "POST",
headers: new Headers({ Authorization: "Bearer " + accessToken }),
body: form,
}
)
.then((res) => {
return res.json();
})
.then(function (val) {
console.log(val.error.message);
<!-- message is "invalid credentials --> in our scenario
}
})
}
request.onerror = function () {
console.log("SW Sync : Getting IndexedDB values error");
};
};
}
Sure, the ServiceWorker's clients are exposed in its self.clients property, from where you can find the correct Client with which you can communicate thanks to its postMessage() method.
How to find the correct client will depend on the situation, for instance in the install or activate events there should be only one client, so you should be able to reach it by doing
const clients = await self.clients.matchAll();
const client = clients[0];
client.postMessage("something bad happenned...");
In a fetch event, the clientId is exposed on the event instance, so you can do
const client = await self.clients.get(evt.clientId);
client.postMessage("something bad happenned...");
I must admit I don't know well the BackgroundSync API, so I'm not sure if in this sync event your page would be the only one client, however, you can certainly make your page open a private communication channel with the SW even before, which by the way, sounds like a better mean of passing your API's credentials than through IDB:
const channel = new MessageChannel();
channel.port1.onmessage = SWTalksToMe; // handle messages from SW
navigator.serviceWorker
.register("serviceWorker.js")
.then((registration) => navigator.serviceWorker.ready)
.then((registration) => {
registration.postMessage("", [channel.port2]));
return registration.sync.register("sendFile-sync")
})
//...
And in your ServiceWorker
self.addEventListener("message", evt => {
if(evt.ports) {
client_port = evt.ports[0];
}
});
Finally, if you wanted to communicate with all the clients, you could use a BroadcastChannel.
With two peers connected via WebRTC, we can see the peer connection is complete, yet only one client is receiving the video stream (the "offering" peer). The "answering" peer is never receiving "track" events (peerConnection.ontrack).
Code for the "answering" peer that displays the problem:
const peerConnection = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun1.l.google.com:19302' }]});
const localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
localStream.getTracks().forEach((track) => { peerConnection.addTrack(track, localStream) });
const { data } = await axios.get("http://{HIDDEN}/offer");
if (data) {
// console.log('received offer', data.offer)
await peerConnection.setRemoteDescription(data.offer);
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
connectionInfo.current.answer = peerConnection.localDescription;
data.iceCandidates.forEach(async (candidate: any) => {
await peerConnection.addIceCandidate(candidate);
});
// Set peer connection event handlers
peerConnection.onicecandidate = (event) => {
if (event.candidate) connectionInfo.current.iceCandidates.push(event.candidate);
};
peerConnection.onicegatheringstatechange = async () => {
if (peerConnection.iceGatheringState === "complete" && connectionInfo.current.answer) {
await axios.post("http://{HIDDEN}/answer", connectionInfo.current);
}
};
peerConnection.ontrack = async (event) => {
remoteStream.addTrack(event.track);
};
if (peerConnection.iceGatheringState === "complete") {
await axios.post("http://{HIDDEN}/answer", connectionInfo.current);
}
}
I found this post which mentions your problem and suggests that the order in which event listeners are created relative to the peerConnection.setRemoteDescription() call is the problem: https://github.com/w3c/webrtc-pc/issues/198
Moving the event handlers immediately after the new RTCPeerConnection initialization (before the description setting) should fix the issue. For example:
const peerConnection = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun1.l.google.com:19302' }]});
// Set peer connection event handlers - MOVED UP HERE
peerConnection.onicecandidate = (event) => {
if (event.candidate) connectionInfo.current.iceCandidates.push(event.candidate);
};
peerConnection.onicegatheringstatechange = async () => {
if (peerConnection.iceGatheringState === "complete" && connectionInfo.current.answer) {
await axios.post("http://{HIDDEN}/answer", connectionInfo.current);
}
};
peerConnection.ontrack = async (event) => {
remoteStream.addTrack(event.track);
};
const localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
localStream.getTracks().forEach((track) => { peerConnection.addTrack(track, localStream) });
const { data } = await axios.get("http://{HIDDEN}/offer");
if (data) {
// console.log('received offer', data.offer)
await peerConnection.setRemoteDescription(data.offer);
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
connectionInfo.current.answer = peerConnection.localDescription;
data.iceCandidates.forEach(async (candidate: any) => {
await peerConnection.addIceCandidate(candidate);
});
if (peerConnection.iceGatheringState === "complete") {
await axios.post("http://{HIDDEN}/answer", connectionInfo.current);
}
}
I'm new to webrtc and react. I'm developing a peer to peer video chat app. On the calling side, both remote video and local video shows up. But on the callee side, only local video shows up. I've been trying to find out where I'm doing wrong but not able to figure it out. One thing I noticed when I console.log peerconnection variable inside handle ice candidate function is, the 'connectionState' is still 'connecting' on the callee side.('connected' on the caller side).
EDIT: I have modified code for readability and using async await. Now I am getting "Failed to execute 'addIceCandidate' on 'RTCPeerConnection': Error processing ICE candidate" error.
EDIT 2: Modified code according to answer
Here is my ORIGINAL code
//refs for my video tag
const localVideoRef = useRef("");
const remoteVideoRef = useRef("");
//video elements
<video ref={remoteVideoRef} playsInline autoPlay className="remoteVideo"></video>
<video ref={localVideoRef} playsInline autoPlay muted className="localVideo"></video>
//button to start call
<button onClick={handleCall}>Call</button>
const handleCall = async () => {
createPeerConnection();
navigator.mediaDevices
.getUserMedia({
audio: true,
video: true
})
.then(function(localStream) {
let videoObj = localVideoRef.current;
videoObj.srcObject = localStream;
localStream
.getTracks()
.forEach(track => myPeerConnection.addTrack(track, localStream));
})
.catch("getUserMedia() error: ");
};
let myPeerConnection = null;
function createPeerConnection() {
myPeerConnection = new RTCPeerConnection({
iceServers: [
{
urls: "stun:stun2.1.google.com:19302"
}
]
});
myPeerConnection.onnegotiationneeded = handleNegotiationNeededEvent;
myPeerConnection.onicecandidate = handleICECandidateEvent;
myPeerConnection.ontrack = handleTrackEvent;
myPeerConnection.onsignalingstatechange = handleSignalingStateChangeEvent;
}
var isNegotiating = false;
const handleSignalingStateChangeEvent = () => {
isNegotiating = myPeerConnection.signalingState != "stable";
};
function handleNegotiationNeededEvent() {
if (isNegotiating) {
return;
}
isNegotiating = true;
myPeerConnection
.createOffer()
.then(function(offer) {
return myPeerConnection.setLocalDescription(offer);
})
.then(function() {
socket.emit("video-offer", {
from: authContext.user.name,
to: connectedTo,
sdp: myPeerConnection.localDescription
});
});
}
//checking if socket is initialized
if (socket) {
socket.on("gotOffer", data => {
handleVideoOfferMsg(data);
});
socket.on("gotCandidate", data => {
handleNewICECandidateMsg(data);
});
socket.on("gotAnswer", data => {
console.log("inside got answer");
handleGotAnswer(data);
});
}
function handleVideoOfferMsg(msg) {
createPeerConnection();
var desc = new RTCSessionDescription(msg.sdp);
myPeerConnection
.setRemoteDescription(desc)
.then(function() {
return navigator.mediaDevices.getUserMedia({
audio: true,
video: true
});
})
.then(function(stream) {
let localStream = stream;
let videoObj = localVideoRef.current;
videoObj.srcObject = stream;
localStream
.getTracks()
.forEach(track => myPeerConnection.addTrack(track, localStream));
})
.then(function() {
return myPeerConnection.createAnswer();
})
.then(function(answer) {
return myPeerConnection.setLocalDescription(answer);
})
.then(function() {
socket.emit("video-answer", {
from: authContext.user.name,
to: connectedTo,
sdp: myPeerConnection.localDescription
});
})
.catch("error");
}
async function handleGotAnswer(msg) {
if (!myPeerConnection) return;
// if (isNegotiating) return;
//I don't know why it's not working (no remote video on the caller side too) when I add above line. So, I am checking signaling state in the below line
if (myPeerConnection.signalingState == "stable") return;
await myPeerConnection.setRemoteDescription(
new RTCSessionDescription(msg.sdp)
);
}
function handleICECandidateEvent(event) {
if (!myPeerConnection) return;
if (isNegotiating) return;
if (event.candidate) {
socket.emit("candidate", {
to: connectedTo,
from: authContext.user.name,
candidate: event.candidate
});
}
}
function handleNewICECandidateMsg(msg) {
if (myPeerConnection.signalingState == "stable") return;
var candidate = new RTCIceCandidate(msg.candidate);
myPeerConnection.addIceCandidate(candidate).catch("error");
}
function handleTrackEvent(event) {
let videoObj = remoteVideoRef.current;
videoObj.srcObject = event.streams[0];
}
Here is my NEW code:
let pc1 = new RTCPeerConnection({
iceServers: [
{
urls: "stun:stun2.1.google.com:19302"
}
]
});
let pc2 = new RTCPeerConnection({
iceServers: [
{
urls: "stun:stun2.1.google.com:19302"
}
]
});
const handleCall = async () => {
let stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true
});
let videoObj = localVideoRef.current;
videoObj.srcObject = stream;
let localStream = stream;
stream
.getTracks()
.forEach(async track => await pc1.addTrack(track, localStream));
pc1.onnegotiationneeded = async function() {
let offer = await pc1.createOffer();
await pc1.setLocalDescription(offer);
socket.emit("video-offer", {
from: authContext.user.name,
to: connectedTo,
sdp: pc1.localDescription
});
pc1.onicecandidate = function(event) {
if (event.candidate) {
socket.emit("candidate", {
pc: "pc1",
to: connectedTo,
from: authContext.user.name,
candidate: event.candidate
});
}
};
};
pc1.ontrack = function(event) {
let videoObj = remoteVideoRef.current;
videoObj.srcObject = event.streams[0];
};
};
//listening to socket emits from server related to video chat
if (socket) {
socket.on("gotOffer", data => {
//step 1 of callee
handleVideoOfferMsg(data);
});
socket.on("gotCandidate", data => {
handleNewICECandidateMsg(data);
});
socket.on("gotAnswer", data => {
handleGotAnswer(data);
});
}
async function handleVideoOfferMsg(msg) {
var desc = new RTCSessionDescription(msg.sdp);
await pc2.setRemoteDescription(desc);
let stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true
});
let videoObj = localVideoRef.current;
videoObj.srcObject = stream;
let localStream = stream;
stream
.getTracks()
.forEach(async track => await pc2.addTrack(track, localStream));
let answer = await pc2.createAnswer();
await pc2.setLocalDescription(answer);
socket.emit("video-answer", {
from: authContext.user.name,
to: connectedTo,
sdp: pc2.localDescription
});
pc2.ontrack = function(event) {
let videoObj = remoteVideoRef.current;
videoObj.srcObject = event.streams[0];
};
pc2.onicecandidate = function(event) {
if (event.candidate) {
socket.emit("candidate", {
pc: "pc2",
to: connectedTo,
from: authContext.user.name,
candidate: event.candidate
});
}
};
}
async function handleGotAnswer(msg) {
if (pc1.signalingState == "stable") {
console.log("negotiating");
return;
}
await pc1.setRemoteDescription(new RTCSessionDescription(msg.sdp));
//INSERTED THIS
if (candidatesArray.length) {
candidatesArray.forEach(async msg => {
var candidate = new RTCIceCandidate(msg.candidate);
await pc1.addIceCandidate(candidate);
});
}
}
let candidatesArray = [];
async function handleNewICECandidateMsg(msg) {
if (msg.pc == "pc1") {
var candidate = new RTCIceCandidate(msg.candidate);
await pc2.addIceCandidate(candidate);
}
if (msg.pc == "pc2") {
try {
if (pc1.connectionState != "stable" && !pc1.remoteDescription) {
candidatesArray.push(msg);
return;
}
var candidate = new RTCIceCandidate(msg.candidate);
await pc1.addIceCandidate(candidate);
} catch (error) {
//this is where error is triggered.
console.log("error adding ice candidate: " + error);
}
}
}
I have not put my server side code, because I find no issues in it.
From what I understand, the error is because remotedescription is not being set when addicecandidate is called. May be because I am skipping setting remote description when signalingState is stable. But if I remove that line of code, I am getting another error - "Failed to set remote answer sdp: Called in wrong state: kStable"
Where am I going wrong?
Immediately after calling pc.setLocalDescription(), the PeerConnection will start emitting onicecandidate events, thanks to Trickle ICE. However, this means that maybe the first candidates are generated too fast and they get sent to the remote peer even before sending SDP Offer/Answer!
Maybe that's what happens in your case, and the first candidates are arriving too early from the other side. For this reason it's a good idea to check the PeerConnection signaling state: if it is stable and the remote description has been already set, then you can call pc.addIceCandidate(). If not, you store the candidate in a queue.
Later, when the remote description finally arrives, after setting it you manually add all the candidates that are waiting in the queue.
Here you can see code with this idea. Candidates are first queued, and later when the PeerConnection signaling state becomes stable, queued items are added.
I am trying to put together a little video-chat prototype using WebRTC.
I've been testing this on localhost for the last couple of days (using 2 browser instances) but I simply can't get chrome(V32) to display the remote stream correctly as it is always a black square.
I can see the streams arrive to both peers, get attached but always black.
In firefox (V26) everything works very well.
I am using SignalR as the signaling mechanism and adapter.js for browser interoperability.
This is the code I have for the webrtc module, what can I possibly be doing wrong for chrome to fail?
Thank you very much.
define(['services/logger', 'services/dataservice', 'services/messenger', 'knockout'], function (logger, dataservice, messenger, ko) {
var
webrtc = {
init: init,
call: call
},
_myMediaStream = null,
_myConstraints = null,
_myConnection = null,
_iceServers = [{ url: 'stun:74.125.142.127:19302' }]; // stun.l.google.com - Firefox does not support DNS names.
function init(constraints) {
getUserMedia(constraints, function (stream) {
var videoElement = document.querySelector('#myVideo');
//videoElement.muted = true;
videoElement.controls = true;
_myMediaStream = stream;
_myConstraints = constraints;
attachMediaStream(videoElement, _myMediaStream);
messenger.publish('LocalMediaStreamSet');
}, function (error) {
logger.logError(JSON.stringify(error), null, 'webrtc/init', true);
});
}
function call(user) {
_myConnection = _myConnection || _createConnection();
_myConnection.addStream(_myMediaStream);
_myConnection.createOffer(function (desc) {
_myConnection.setLocalDescription(desc, function () {
dataservice.sendRTCMessage(JSON.stringify({ sdp: desc, origin: '', target: user, constraints: _myConstraints, type:'offer' }));
});
});
}
function _createConnection() {
console.log('creating RTCPeerConnection...');
var connection = new RTCPeerConnection({ iceServers: _iceServers }); // null = no ICE servers
connection.onicecandidate = function (event) {
if (event.candidate) {
dataservice.sendICECandidate(JSON.stringify({ "candidate": event.candidate }))
.then(function () {
console.log('ice candidate sent to remote peer.')
});
}
};
connection.onaddstream = function (event) {
var videoElement = document.querySelector('#theirsVideo');
videoElement.controls = true;
console.log('attaching remote stream...')
attachMediaStream(videoElement, event.stream);
console.log('attaching remote stream done.')
};
connection.onremovestream = function () {
console.log('Remote stream removed.');
};
return connection;
}
function _subscribeToEvents() {
//subscribe to new RTCMessage events
messenger.subscribe(document, 'newRTCMessage', function (e, message) {
var
isConfirmed = true,
connection = _myConnection || _createConnection();
if (message.sdp.type === 'offer') {
//need confirmation to accept the call
isConfirmed = confirm("Incoming call from " + message.origin + ", accept?");
}
if (message.sdp && isConfirmed) {
connection.setRemoteDescription(new RTCSessionDescription(message.sdp), function () {
if (connection.remoteDescription.type === 'offer') {
//subscrive to localstream when ready (is setted on the init call below)
messenger.subscribe(document, 'LocalMediaStreamSet', function (e) {
console.log('received offer, sending answer...');
connection.addStream(_myMediaStream);
connection.createAnswer(function (desc) {
connection.setLocalDescription(desc, function () {
dataservice.sendRTCMessage(JSON.stringify({ sdp: connection.localDescription, origin: '', target: message.origin, callId: '', type: 'answer' }));
});
});
});
init(message.constraints);
} else if (connection.remoteDescription.type === 'answer') {
console.log('got an answer');
}
});
} else if (message.candidate) {
console.log('adding ice candidate from remote peer...');
connection.addIceCandidate(new RTCIceCandidate(message.candidate));
}
_myConnection = connection;
});
}
_subscribeToEvents();
return webrtc;
});
I finally made it work!!
It had to do with the fact that on connection.onicecandidate event I should be calling the dataservice.sendRTCMessage() function instead of the dataservice.sendICECandidate() one that was failing on my controller.
Both peers were not attaching remote ice candidates and thus the stream could not be shared.
Thanks anyways!