I was trying to make video call with webrtc but when it comes to test I acounter bugs. I intend to integrate this project in a webview for my android app. I uses phone-pc and phone-phone for testing.
Case A when pc initialize the call everything works well. there is no problem
Case B when phone initialize the call to pc or other phone the Receiver sends back recvonly information in the SDp.
Here is the Log:
v=0\r\no=- 8723618501842184555 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1\r\na=msid-semantic: WMS\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111 103 9 0 8 105 13 110 113 126\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:dfKu\r\na=ice-pwd:XEe1Yy0kmjgMoiA0dVhymtDc\r\na=ice-options:trickle\r\na=fingerprint:sha-256 69:0D:95:A9:41:C7:C8:EF:57:0F:65:44:62:64:59:96:7C:40:6A:61:CE:62:0F:A3:E9:D7:1B:D0:F1:4C:13:BC\r\na=setup:active\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\na=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\na=recvonly\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=rtcp-fb:111 transport-cc\r\na=fmtp:111 minptime=10;useinbandfec=1\r\na=rtpmap:103 ISAC/16000\r\na=rtpmap:9 G722/8000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:105 CN/16000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:110 telephone-event/48000\r\na=rtpmap:113 telephone-event/16000\r\na=rtpmap:126 telephone-event/8000\r\nm=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:dfKu\r\na=ice-pwd:XEe1Yy0kmjgMoiA0dVhymtDc\r\na=ice-options:trickle\r\na=fingerprint:sha-256 69:0D:95:A9:41:C7:C8:EF:57:0F:65:44:62:64:59:96:7C:40:6A:61:CE:62:0F:A3:E9:D7:1B:D0:F1:4C:13:BC\r\na=setup:active\r\na=mid:1\r\na=extmap:14 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:13 urn:3gpp:video-orientation\r\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:12 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\na=extmap:11 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r\na=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r\na=extmap:8 http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07\r\na=extmap:9 http://www.webrtc.org/experiments/rtp-hdrext/color-space\r\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\na=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\na=recvonly\r\na=rtcp-mux\r\na=rtcp-rsize\r\na=rtpmap:96 VP8/90000\r\na=rtcp-fb:96 goog-remb\r\na=rtcp-fb:96 transport-cc\r\na=rtcp-fb:96 ccm fir\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 nack pli\r\na=rtpmap:97 rtx/90000\r\na=fmtp:97 apt=96\r\na=rtpmap:98 VP9/90000\r\na=rtcp-fb:98 goog-remb\r\na=rtcp-fb:98 transport-cc\r\na=rtcp-fb:98 ccm fir\r\na=rtcp-fb:98 nack\r\na=rtcp-fb:98 nack pli\r\na=fmtp:98 profile-id=0\r\na=rtpmap:99 rtx/90000\r\na=fmtp:99 apt=98\r\na=rtpmap:100 red/90000\r\na=rtpmap:101 rtx/90000\r\na=fmtp:101 apt=100\r\na=rtpmap:102 ulpfec/90000\r\n"
Here is my signaling server
'use strict';
const HTTPS_PORT = 8443;
const fs = require('fs');
const https = require('https');
const WebSockets = require('ws');
const WebSocketServer = WebSockets.Server;
// Yes, TLS is required
const serverConfig = {
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem'),
requestCert: false
};
// ----------------------------------------------------------------------------------------
// Create a server for the client html page
const handleRequest = function(request, response) {
// Render the single client html file for any request the HTTP server receives
console.log('request received: ' + request.url);
switch(request.url) {
case '/':
response.writeHead(200, {'Content-Type': 'text/html'});
response.end(fs.readFileSync('client/index.html'));
break;
case '/webrtc.js':
response.writeHead(200, {'Content-Type': 'application/javascript'});
response.end(fs.readFileSync('client/webrtc.js'));
break;
default:
// code block
}
};
const httpsServer = https.createServer(serverConfig, handleRequest);
httpsServer.listen(HTTPS_PORT, '0.0.0.0');
// ----------------------------------------------------------------------------------------
// Create a server for handling websocket calls
const wss = new WebSocketServer({server: httpsServer});
//No Sql Database to Store Rooms
var rooms = {};
wss.on('connection', function(socket) {
var socketId;
var socketRoomId;
socket.on('message', function(message) {
const msg = JSON.parse(message);
const action = msg.action;
const userId = msg.uuid;
const roomId = msg.roomId;
socketId = userId;
socketRoomId = roomId;
console.log('************* \n\ Action : ' + action +" \n\ **************");
switch (action) {
case "join":
const joined = joinRoom(socket, roomId, userId);
if(joined){
console.log('************* \n\ The user joined successfully the room : ' + action +" \n\ **************");
msg.action = "ready";
wss.broadcast(message);
console.log('************************ \n\ The Socket : ' + userId +" is READY \n\***************************");
}else{
console.log('************* \n\ The Room : ' + action +" is full \n\ **************");
msg.action = "full";
wss.broadcast(message);
}
break;
case "signal":
console.log('************* \n\ user '+userId+' Sent signlals: ' + message +" \n\ **************");
wss.broadcast(message);
break;
case "bye":
leave(roomId, userId);
msg.action = "leave";
// emit(roomId, JSON.stringify(msg));
wss.broadcast(message);
console.log(`************* \n\ Peer said bye on room ${roomId}. \n\ **************`);
break;
default:
console.log(`************* \n\ No action was supplied \n\ **************`);
break;
}
});
socket.on("close", () => {
if(socketId !== null) leave(socketRoomId, socketId);
console.log(`************* \n\ Socket is closed \n\ **************`);
});
});
//Remove the socket in the room
function leave(roomId, userId){
// not present: do nothing
if(!(roomId in rooms) || !(userId in rooms[roomId])) return;
// if the one exiting is the last one, destroy the room
if(Object.keys(rooms[roomId]).length === 1) delete rooms[roomId];
// otherwise simply leave the room
else delete rooms[roomId][userId];
}
function joinRoom(socket, room, userId){
if(room in rooms){
console.log('************* \n\ Room ' + room +" exists \n\ **************");
var clients = Object.keys(rooms[room]).length;
if(clients < 2){
console.log('************* \n\ No body is in the room ' + room +" \n\ **************");
if(!(userId in rooms[room])) rooms[room][userId] = socket;
console.log('************* \n\ the Room ' + room +" now has "+Object.keys(rooms[room]).length+" \n\ **************");
return true;
}else{
console.log('************* \n\ the room ' + room +" is full \n\ **************");
return false;
}
}
else{
console.log('************* \n\ the Room ' + room +" does not exists yet \n\ **************");
rooms[room] = {};
rooms[room][userId] = socket;
console.log('************* \n\ the Room ' + room +" now has "+Object.keys(rooms[room]).length+" \n\ **************");
return true;
}
}
//Send the message to everyone in a room
function emit(room, message){
if(room in rooms)
Object.entries(rooms[room]).forEach(([, sock]) => sock.send(message));
}
//Send the message to everyone but me in a room
function emitButMe(ownerId, room, message){
if(room in rooms)
Object.entries(rooms[room]).forEach(([userid, sock]) => sends(ownerId, userid, sock, message));
}
function sends(ownerId, userId, socket, message){
if(ownerId !== userId) socket.send(message);
}
wss.broadcast = function(data) {
this.clients.forEach(function(client) {
if(client.readyState === WebSockets.OPEN) {
client.send(data);
}
});
};
console.log('Server running. Visit https://localhost:' + HTTPS_PORT + ' in Firefox/Chrome.\n\n\
Some important notes:\n\
* Note the HTTPS; there is no HTTP -> HTTPS redirect.\n\
* You\'ll also need to accept the invalid TLS certificate.\n'
);
And in the client side I implemented the below javascript in order to communicate with the Server and the peer.
let localVideo;
let localStream;
let remoteVideo;
let peerConnection;
let uuid;
let serverConnection;
let peerConnectionConfig = {
'iceServers': [
{ 'urls': 'stun:stun.stunprotocol.org:3478' },
{ 'urls': 'stun:stun.l.google.com:19302' },
]
};
window.onload = pageReady;
btnStart.onclick = _ => start(true);
async function pageReady() {
uuid = (new MediaStream()).id;
localVideo = document.getElementById('localVideo');
remoteVideo = document.getElementById('remoteVideo');
serverConnection = new WebSocket('wss://' + window.location.hostname + ':8443');
serverConnection.onmessage = gotMessageFromServer;
serverConnection.onopen = joinRoom;
const constraints = {
video: true,
audio: true,
};
if (navigator.mediaDevices.getUserMedia) {
try {
localStream = await navigator.mediaDevices.getUserMedia(constraints);
localVideo.srcObject = localStream;
} catch (err) {
errorHandler(err);
}
} else {
alert('Your browser does not support getUserMedia API');
}
}
//Join a Room
function joinRoom() {
serverConnection.send(JSON.stringify({ action: 'join', roomId: 'salon', uuid }));
}
async function start(isCaller) {
peerConnection = new RTCPeerConnection(peerConnectionConfig);
peerConnection.onicecandidate = gotIceCandidate;
peerConnection.ontrack = gotRemoteStream;
localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
if (isCaller) {
const offer = await peerConnection.createOffer();
await createdDescription(offer);
}
}
async function gotMessageFromServer(message) {
if (!peerConnection) start(false);
const signal = JSON.parse(message);
// Ignore messages from ourself
if (signal.uuid == uuid) return;
try {
if (signal.sdp) {
await peerConnection.setRemoteDescription(signal.sdp);
// Only create answers in response to offers
if (signal.sdp.type == 'offer') {
const answer = await peerConnection.createAnswer();
await createdDescription(answer);
}
} else if (signal.ice) {
peerConnection.addIceCandidate(signal.ice);
}
} catch (err) {
errorHandler(err);
}
}
function gotIceCandidate(event) {
if (event.candidate != null) {
serverConnection.send(JSON.stringify({ ice: event.candidate, uuid, action: 'signal', roomId: 'salon' }));
}
}
async function createdDescription(description) {
console.log('got description');
try {
await peerConnection.setLocalDescription(description);
serverConnection.send(JSON.stringify({ sdp: peerConnection.localDescription, uuid, action: 'signal', roomId: 'salon' }));
} catch (err) {
errorHandler(err);
}
}
function gotRemoteStream(event) {
console.log('got remote stream');
remoteVideo.srcObject = event.streams[0];
}
function errorHandler(error) {
console.error(error);
}
And the Html page is like:
<!DOCTYPE html>
<html>
<head>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="webrtc.js"></script>
</head>
<body>
<video id="localVideo" autoplay muted style="width:40%;"></video>
<video id="remoteVideo" autoplay style="width:40%;"></video>
<br />
<input type="button" id="start" onclick="start(true)" value="Start Video"></input>
<script type="text/javascript">
pageReady();
</script>
</body>
</html>
Because you are using then() of Promise, it is because when remote peerConnection is created it is creating and sending the answer before it is peerConnection.addStream().
If you don't use then() and rewrite to async/await, it should work.
Also peerConnection.addStream() is an old specification and has been removed from the new specification. You should change to peerConnection.addTrack().
executable sample
https://codepen.io/gtk2k/pen/JjGmZaz?editors=1010
(Open the sample page in two tabs and click the Start button)
let localVideo;
let localStream;
let remoteVideo;
let peerConnection;
let uuid;
let serverConnection;
let peerConnectionConfig = {
'iceServers': [
{ 'urls': 'stun:stun.stunprotocol.org:3478' },
{ 'urls': 'stun:stun.l.google.com:19302' },
]
};
window.onload = pageReady;
btnStart.onclick = _ => start(true);
async function pageReady() {
uuid = (new MediaStream()).id;
localVideo = document.getElementById('localVideo');
remoteVideo = document.getElementById('remoteVideo');
serverConnection = new WebSocket('wss://' + window.location.hostname + ':8443');
serverConnection.onmessage = gotMessageFromServer;
serverConnection.onopen = joinRoom;
const constraints = {
video: true,
audio: true,
};
if (navigator.mediaDevices.getUserMedia) {
try {
localStream = await navigator.mediaDevices.getUserMedia(constraints);
localVideo.srcObject = localStream;
} catch (err) {
errorHandler(err);
}
} else {
alert('Your browser does not support getUserMedia API');
}
}
//Join a Room
function joinRoom() {
serverConnection.send(JSON.stringify({ action: 'join', roomId: 'salon', uuid }));
}
async function start(isCaller) {
peerConnection = new RTCPeerConnection(peerConnectionConfig);
peerConnection.onicecandidate = gotIceCandidate;
peerConnection.ontrack = gotRemoteStream;
localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
if (isCaller) {
const offer = await peerConnection.createOffer();
await createdDescription(offer);
}
}
async function gotMessageFromServer(message) {
if (!peerConnection) start(false);
const signal = JSON.parse(message);
// Ignore messages from ourself
if (signal.uuid == uuid) return;
try {
if (signal.sdp) {
await peerConnection.setRemoteDescription(signal.sdp);
// Only create answers in response to offers
if (signal.sdp.type == 'offer') {
const answer = await peerConnection.createAnswer();
await createdDescription(answer);
}
} else if (signal.ice) {
peerConnection.addIceCandidate(signal.ice);
}
} catch (err) {
errorHandler(err);
}
}
function gotIceCandidate(event) {
if (event.candidate != null) {
serverConnection.send(JSON.stringify({ ice: event.candidate, uuid, action: 'signal', roomId: 'salon' }));
}
}
async function createdDescription(description) {
console.log('got description');
try {
await peerConnection.setLocalDescription(description);
serverConnection.send(JSON.stringify({ sdp: peerConnection.localDescription, uuid, action: 'signal', roomId: 'salon' }));
} catch (err) {
errorHandler(err);
}
}
function gotRemoteStream(event) {
console.log('got remote stream');
remoteVideo.srcObject = event.streams[0];
}
function errorHandler(error) {
console.error(error);
}
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
For certain types of messages, I want to target users by FIRTokens vs topic, which are stored in my real-time database. I load these tokens with async/await and then decide if I want to send notifications to a topic vs a smaller list of users. The data loading code works as expected. But what's odd is that if I use .sendMulticast(payload), the notifications fail for all tokens in the list. On the other hand if I use .sendToDevice(adminFIRTokens, payload) the notification goes successfully to all my users. Right now my list has 2 tokens and with sendMulticast I have 2 failures and with sendToDevice I have 2 successes. Am I missing the point of what sendMulticast is supposed to do? According to the docs: Send messages to multiple devices:
The REST API and the Admin FCM APIs allow you to multicast a message to a list of device registration tokens. You can specify up to 500 device registration tokens per invocation.
So both should logically work. Then why does one fail and the other work? In fact with sendToDevice I get a multicastId in the response!
Here are some console outputs:
sendToDevice:
Sent filtered message notification successfully:
{
results:
[
{ messageId: '0:1...45' },
{ messageId: '16...55' }
],
canonicalRegistrationTokenCount: 0,
failureCount: 0,
successCount: 2,
multicastId: 3008...7000
}
sendMulticast:
List of tokens that caused failures: dJP03n-RC_Y:...MvPkTbuV,fDo1S8jPbCM:...2YETyXef
Cloud function to send the notification:
functions.database
.ref("/discussionMessages/{autoId}/")
.onCreate(async (snapshot, context) => {
// console.log("Snapshot: ", snapshot);
try {
const groupsRef = admin.database().ref("people/groups");
const adminUsersRef = groupsRef.child("admin");
const filteredUsersRef = groupsRef.child("filtered");
const filteredUsersSnapshot = await filteredUsersRef.once("value");
const adminUsersSnapshot = await adminUsersRef.once("value");
var adminUsersFIRTokens = {};
var filteredUsersFIRTokens = {};
if (filteredUsersSnapshot.exists()) {
filteredUsersFIRTokens = filteredUsersSnapshot.val();
}
if (adminUsersSnapshot.exists()) {
adminUsersFIRTokens = adminUsersSnapshot.val();
}
const topicName = "SpeechDrillDiscussions";
const message = snapshot.val();
const senderName = message.userName;
const senderCountry = message.userCountryEmoji;
const title = senderName + " " + senderCountry;
const messageText = message.message;
const messageTimestamp = message.messageTimestamp.toString();
const messageID = message.hasOwnProperty("messageID")
? message.messageID
: undefined;
const senderEmailId = message.userEmailAddress;
const senderUserName = getUserNameFromEmail(senderEmailId);
const isSenderFiltered = filteredUsersFIRTokens.hasOwnProperty(
senderUserName
);
var payload = {
notification: {
title: title,
body: messageText,
sound: "default",
},
data: {
messageID: messageID,
messageTimestamp: messageTimestamp,
},
};
if (isSenderFiltered) {
adminFIRTokens = Object.values(adminUsersFIRTokens);
// payload.tokens = adminFIRTokens; //Needed for sendMulticast
return (
admin
.messaging()
.sendToDevice(adminFIRTokens, payload)
// .sendMulticast(payload)
.then(function (response) {
if (response.failureCount === 0) {
console.log(
"Sent filtered message notification successfully:",
response
);
} else {
console.log(
"Sending filtered message notification failed for some tokens:",
response
);
}
// if (response.failureCount > 0) {
// const failedTokens = [];
// response.responses.forEach((resp, idx) => {
// if (!resp.success) {
// failedTokens.push(adminFIRTokens[idx]);
// }
// });
// console.log(
// "List of tokens that caused failures: " + failedTokens
// );
// }
return true;
})
);
} else {
payload.topic = topicName;
return admin
.messaging()
.send(payload)
.then(function (response) {
console.log("Notification sent successfully:", response);
return true;
});
}
} catch (error) {
console.log("Notification sent failed:", error);
return false;
}
});
I think it's an issue of using a different payload structure.
This is the old one (without iOS specific info):
var payload = {
notification: {
title: title,
body: messageText,
sound: "default",
},
data: {
messageID: messageID,
messageTimestamp: messageTimestamp,
},
};
Whereas this is the new version (apns has iOS specific info)
var payload = {
notification: {
title: title,
body: messageText,
},
data: {
messageID: messageID,
messageTimestamp: messageTimestamp,
},
apns: {
payload: {
aps: {
sound: "default",
},
},
},
};
With the new structure, both send and sendMulticast are working properly. Which would fail to send or give errors like apns key is not supported in payload.
The new function:
functions.database
.ref("/discussionMessages/{autoId}/")
.onCreate(async (snapshot, context) => {
// console.log("Snapshot: ", snapshot);
try {
const groupsRef = admin.database().ref("people/groups");
const adminUsersRef = groupsRef.child("admin");
const filteredUsersRef = groupsRef.child("filtered");
const filteredUsersSnapshot = await filteredUsersRef.once("value");
const adminUsersSnapshot = await adminUsersRef.once("value");
var adminUsersFIRTokens = {};
var filteredUsersFIRTokens = {};
if (filteredUsersSnapshot.exists()) {
filteredUsersFIRTokens = filteredUsersSnapshot.val();
}
if (adminUsersSnapshot.exists()) {
adminUsersFIRTokens = adminUsersSnapshot.val();
}
// console.log(
// "Admin and Filtered Users: ",
// adminUsersFIRTokens,
// " ",
// filteredUsersFIRTokens
// );
const topicName = "SpeechDrillDiscussions";
const message = snapshot.val();
// console.log("Received new message: ", message);
const senderName = message.userName;
const senderCountry = message.userCountryEmoji;
const title = senderName + " " + senderCountry;
const messageText = message.message;
const messageTimestamp = message.messageTimestamp.toString();
const messageID = message.hasOwnProperty("messageID")
? message.messageID
: undefined;
const senderEmailId = message.userEmailAddress;
const senderUserName = getUserNameFromEmail(senderEmailId);
const isSenderFiltered = filteredUsersFIRTokens.hasOwnProperty(
senderUserName
);
console.log(
"Will attempt to send notification for message with message id: ",
messageID
);
var payload = {
notification: {
title: title,
body: messageText,
},
data: {
messageID: messageID,
messageTimestamp: messageTimestamp,
},
apns: {
payload: {
aps: {
sound: "default",
},
},
},
};
console.log("Is sender filtered? ", isSenderFiltered);
if (isSenderFiltered) {
adminFIRTokens = Object.values(adminUsersFIRTokens);
console.log("Sending filtered notification with sendMulticast()");
payload.tokens = adminFIRTokens; //Needed for sendMulticast
return admin
.messaging()
.sendMulticast(payload)
.then((response) => {
console.log(
"Sent filtered message (using sendMulticast) notification: ",
JSON.stringify(response)
);
if (response.failureCount > 0) {
const failedTokens = [];
response.responses.forEach((resp, idx) => {
if (!resp.success) {
failedTokens.push(adminFIRTokens[idx]);
}
});
console.log(
"List of tokens that caused failures: " + failedTokens
);
}
return true;
});
} else {
console.log("Sending topic message with send()");
payload.topic = topicName;
return admin
.messaging()
.send(payload)
.then((response) => {
console.log(
"Sent topic message (using send) notification: ",
JSON.stringify(response)
);
return true;
});
}
} catch (error) {
console.log("Notification sent failed:", error);
return false;
}
});
I am currently figuring out how to configure webRTC for my test application. I am able to get the user media API part of webrtc to work but the ICE config is what causes me issues. I have tried to look here on stackoverflow and it seems like noone else have had a similar error returned from the client. I am testing it on a live server where the inital communcation is through WSS. I have left out the markup and websocket config because it's irrelevant.
let myPeerConnection;
/* step 1: get users media stream inputs */
function getUserMediaClient(type = ""){
let mediaConstraints = {video:true, audio:true};
createPeerConnection();
navigator.mediaDevices.getUserMedia(mediaConstraints)
.then(function(localStream) {
document.getElementById("myVideo").srcObject = localStream;
document.getElementById("myVideo").onloadedmetadata = function(){
document.getElementById("myVideo").play();
}
localStream.getTracks().forEach(track => myPeerConnection.addTrack(track, localStream));
})
.catch(handleGetUserMediaError);
}
getUserMediaClient();
/*
preparation for step 2:
get users Peer connection information when the user calls createPeerCandidate()
to send the offer or handleVideoOfferMsg() to send the answer
*/
function createPeerConnection(){
myPeerConnection = new RTCPeerConnection(
{
iceServers: [
{
urls: [
'stun:stun.l.google.com:19302',
'stun:stun1.l.google.com:19302'
]
}/*,
{
urls: 'turn:192.158.29.39:3478?transport=tcp',
credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
username: '28224511:1379330808'
}*/
]
}
);
return myPeerConnection;
}
/*
step 2:
send information to opposite part through websocket with SDP info
*/
function handleNegotiationNeededEvent(myUsername = "", targetUsername = "") {
myPeerConnection.createOffer().then(function(offer) {
return myPeerConnection.setLocalDescription(offer);
})
.then(function() {
sendToServer({
name: myUsername,
target: targetUsername,
type: "video-offer",
sdp: myPeerConnection.localDescription
});
})
.catch(reportError);
}
/*
step 3:
"if" answering user accepts - load tracks to stream and respond with SDP config info
*/
function handleVideoOfferMsg(msg) {
let mediaConstraints = {video:true, audio:true};
var localStream = null;
targetUsername = msg.name;
let myUsername = document.getElementById("user1").value;
createPeerConnection();
var desc = new RTCSessionDescription(msg.sdp);
myPeerConnection.setRemoteDescription(desc).then(function () {
return navigator.mediaDevices.getUserMedia(mediaConstraints);
})
.then(function(stream) {
localStream = stream;
document.getElementById("myVideo").srcObject = localStream;
localStream.getTracks().forEach(track => myPeerConnection.addTrack(track, localStream));
})
.then(function() {
return myPeerConnection.createAnswer();
})
.then(function(answer) {
return myPeerConnection.setLocalDescription(answer);
})
.then(function() {
var msg = {
name: myUsername,
target: targetUsername,
type: "video-answer",
sdp: myPeerConnection.localDescription
};
sendToServer(msg);
})
.catch(handleGetUserMediaError);
}
/*
step 4:
when both users have exchanged information - the ice processing can begin
the user that initially sent the request can now reply with a communication method
*/
var candidateData = null;
function handleICECandidateEvent(event) {
if ((event.sdp)||(event.candidate)){
if (event.candidate){
candidateData = event.candidate;
} else if (event.sdp) {
candidateData = event.sdp;
}
sendToServer({
type: "new-ice-candidate",
target: event.target,
candidate: candidateData
});
}
}
///////////////////// non functional part under ////////////////////////////
function handleNewICECandidateMsg(msg) {
candidateData = msg.candidate;
myPeerConnection.addIceCandidate(new RTCIceCandidate({sdpMLineIndex:1,candidate: candidateData})).catch(e => {
console.log("Failure during addIceCandidate(): " + JSON.stringify(e));
});
////////////// non functional part above ///////////////
console.log("MSG: " + JSON.stringify(msg));
}
function handleGetUserMediaError(e){
//removed for simplicity
}
//wss connection estabilshment from client removed
wss.onmessage = function(e){
if (type == "video-offer" && document.getElementById("user1").value == target){
// create counteroffer
handleVideoOfferMsg(data);
} else if (type == "video-answer"){
handleICECandidateEvent(data);
} else if (type == "new-ice-candidate"){
handleNewICECandidateMsg(data);
}
}
Most of the code is from MDN's tutorial here: https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Signaling_and_video_calling
The issue is within the handleNewICECandidateMsg() function (or so i think). Because when i enter sdpMLineIndex and sdpMid i get an empty json string and when i leave it with only the candidates sdp info it throws a typeerror saying it needs either sdpMid or sdpMLineIndex.
Any thoughts, links. Anything is appreciated!
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.
RTCPeerConnection gets established and receives the clients reply which is 'answer' but does not show the remote video stream. The console log is shown below
console.log
From the log, the offer is sent to the peer which the peer sends back an answer that is console logged to display that it did actually send the answer back. I would greatly appreciate if you could take a look at my code pasted below and advise me how to go about rectifying it.
'use strict';
var localStream;
var remoteStream;
var isInitiator;
var configuration = {
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
}
]
};
var pc = new RTCPeerConnection(configuration);
// Define action buttons.
const callButton = document.getElementById('callButton');
const hangupButton = document.getElementById('hangupButton');
/////////////////////////////////////////////
window.room = prompt('Enter room name:');
var socket = io.connect();
if (room !== '') {
console.log('Message from client: Asking to join room ' + room);
socket.emit('create or join', room);
}
socket.on('created', function(room) {
console.log('Created room ' + room);
isInitiator = true;
startVideo();
});
socket.on('joined', function(room) {
console.log('joined: ' + room);
startVideo();
});
socket.on('log', function(array) {
console.log.apply(console, array);
});
////////////////////////////////////////////////
function sendMessage(message) {
socket.emit('message', message);
}
// This client receives a message
socket.on('message', function(message) {
if (message.type === 'offer') {
pc.setRemoteDescription(message);
console.log('Sending answer to peer.');
pc.createAnswer().then(
setLocalAndSendMessage,
onCreateSessionDescriptionError
);
} else if (message.type === 'answer') {
console.log('This is to check if answer was returned');
remoteStream = event.stream;
remoteVideo.srcObject = remoteStream;
pc.setRemoteDescription(message);
} else if (message.type === 'candidate') {
pc.addIceCandidate(candidate);
}
});
////////////////////////////////////////////////////
const localVideo = document.querySelector('#localVideo');
const remoteVideo = document.querySelector('#remoteVideo');
// Set up initial action buttons status: disable call and hangup.
callButton.disabled = true;
hangupButton.disabled = true;
// Add click event handlers for buttons.
callButton.addEventListener('click', callStart);
hangupButton.addEventListener('click', hangupCall);
function startVideo() {
navigator.mediaDevices
.getUserMedia({
audio: true,
video: true
})
.then(gotStream)
.catch(function(e) {
alert('getUserMedia() error: ' + e.name);
});
}
function gotStream(stream) {
localVideo.srcObject = stream;
localStream = stream;
callButton.disabled = false;
}
function callStart() {
createPeerConnection();
callButton.disabled = true;
hangupButton.disabled = false;
if (isInitiator) {
console.log('Sending offer to peer');
pc.createOffer(setLocalAndSendMessage, handleCreateOfferError);
}
}
/////////////////////////////////////////////////////////
function createPeerConnection() {
try {
pc.onicecandidate = ({ candidate }) => sendMessage({ candidate });
pc.ontrack = event => {
if (remoteVideo.srcObject) return;
remoteVideo.srcObject = event.stream;
};
console.log('Created RTCPeerConnnection');
} catch (e) {
console.log('Failed to create PeerConnection, exception: ' + e.message);
alert('Cannot create RTCPeerConnection object.');
return;
}
}
function handleCreateOfferError(event) {
console.log('createOffer() error: ', event);
}
function setLocalAndSendMessage(sessionDescription) {
console.log('setLocalAndSendMessage sending message', sessionDescription);
pc.setLocalDescription(sessionDescription);
sendMessage(sessionDescription);
}
function onCreateSessionDescriptionError(error) {
console.log('Failed to create session description: ' + error.toString());
}
function hangupCall() {
pc.close();
pc = null;
}