WebRTC needing setup on pageload? - webrtc

I have a webRTC implementation working, but it requires a setup function to be run on page load.
setup = () => {
peerConnection = new RTCPeerConnection(servers)
peerConnection.onicecandidate = onicecandidate
peerConnection.onconnectionstatechange = onconnectionstatechange(
peerConnection,
)
peerConnection.onnegotiationneeded = onnegotiationneeded
peerConnection.ontrack = function({streams: [stream]}) {
const remoteVideo = document.getElementById('remote-video')
if (remoteVideo) {
remoteVideo.srcObject = stream
}
}
mediaStream.getTracks().forEach(track => peerConnection.addTrack(track, mediaStream))
}
Only then can I make the offer:
makeOffer = () => {
peerConnection
.createOffer()
.then(offer => {
return peerConnection
.setLocalDescription(new RTCSessionDescription(offer))
.then(() => offer)
})
.then(offer => {
sendOffer(offer)
})
}
If i attempt to run the setup just prior to the makeOffer function I get the error:
DOMException: Failed to execute 'setLocalDescription' on
'RTCPeerConnection': Failed to set local offer sdp: The order of
m-lines in subsequent offer doesn't match order from previous
offer/answer.
My assumption is that it is due to the makeOffer running before something in the setup has completed, but nothing in the setup seems to be asynchronous. What do I need to do to be able to setup the connection and send the offer all at once?

Related

Keep client connected to WebSocket in react native and express server

I have a react native application where i have two users using the app (customer and restaurant)
So on checkout I connect the customer to websocket on the express server and once the order is placed i send a message to the restaurant which is supposed to be connected to websocket all time.
However, sometimes the restaurant is disconnected somehow, so I am trying to keep the restaurant connected, and if disconnected then reconnect again automatically.
In react native restaurant side implementation i have the following code :
this is useWebSocketLite hook to handle connection, send, receive messages and retry connection to server when closed:
function useWebSocketLite({ socketUrl, retry: defaultRetry = 3, retryInterval = 1000 }) {
const [data, setData] = useState();
const [send, setSend] = useState(() => () => undefined);
const [retry, setRetry] = useState(defaultRetry);
const [readyState, setReadyState] = useState(false);
useEffect(() => {
const ws = new WebSocket(socketUrl);
ws.onopen = () => {
setReadyState(true);
setSend(() => {
return (data) => {
try {
const d = JSON.stringify(data);
ws.send(d);
return true;
} catch (err) {
return false;
}
};
});
ws.onmessage = (event) => {
const msg = formatMessage(event.data);
setData({ message: msg, timestamp: getTimestamp() });
};
};
ws.onclose = () => {
setReadyState(false);
if (retry > 0) {
setTimeout(() => {
setRetry((retry) => retry - 1);
}, retryInterval);
}
};
return () => {
ws.close();
};
}, [retry]);
return { send, data, readyState };
}
So based on this, every-time the connection is closed, the connection will retry again.
Besides, when a restaurant launches the app the following code will be implemented:
const ws = useWebSocketLite({
socketUrl: `wss://${url}/id=${user.user_id}&role=restaurant`
});
This useEffect to establish the connection:
useEffect(() => {
if (ws.readyState === true) {
setConnectionOpen(true);
}
}, [ws.readyState]);
and this useEffect to handle incoming messages
useEffect(() => {
if (ws.data) {
const message = ws.data;
//dispatch...
}
}, [ws.data]);
Express server implementation:
This is the code where i handle socket connections and messages in express server:
var webSockets = {}
function setupWebSocket(server) {
server.on('connection', (socket, req) => {
if (req) {
var clientId = req.url
let regexReplace = /[\[\]/]/g
let regex = /([^=#&]+)=([^?&#]*)/g,
params = {},
match;
while ((match = regex.exec(clientId))) {
params[decodeURIComponent(match[1]).replace(regexReplace, '')] = decodeURIComponent(match[2])
}
if (params.role === 'restaurant') {
webSockets[params.id] = socket
}
}
socket.on('message', data => {
let sData = JSON.parse(JSON.parse(data))
let {id, data} = sData.data
sendToClient(id, 'order', data)
})
socket.on('error', (err) => {
console.log(err)
})
socket.on('close', (code, req) => {
var clientId = req.url
let regexReplace = /[\[\]/]/g
let regex = /([^=#&]+)=([^?&#]*)/g,
params = {},
match;
while ((match = regex.exec(clientId))) {
params[decodeURIComponent(match[1]).replace(regexReplace, '')] = decodeURIComponent(match[2])
}
if (params.role === 'restaurant') {
delete webSockets[clientId]
console.log(`${webSockets[clientId]} disconnected with code ${code} !`);
}
});
});
// sends a message to a specific client
const sendToClient = (clientId, type, data = {}) => {
const payload = { type, data }
const messageToSend = JSON.stringify({ error: false, message: payload })
if (webSockets[clientId]) {
webSockets[clientId].send(messageToSend)
console.log(`${clientId} client notified with this order`)
} else {
console.log(`${clientId} websocket client is not connected.`)
}
}
}
So most of the time I get 13 websocket client is not connected. which means the restaurant has already been deleted from the webSockets object and its connection already closed.
Apologise for long question and hope someone can help me regarding this.
First of all, you should know that this is not a good practice of websockets, where you are forcing the client (the restaurant) to be connected.
Whatever, at the current state of your code, there is an illogical behavior: at the end of the useEffect of your “useWebSocketLite” function, you are closing the socket connection:
return () => {
ws.close();
};
Knowing that the useEffect hook is called twice: after the first render of the component, and then after every change of the dependencies (the “retry” state in your case); Your code can be ridden like so: everytime the “retry” state changes, we will close the socket! So for me that is why you got the client disconnected.

WebRTC succesfully signalled offer and answer, but not getting any ICE candidates

I'm trying to establish a WebRTC connection between two browsers. I have a node.js server for them to communicate through, which essentially just forwards the messages from one client to the other. I am running the server and two tabs all on my laptop, but I have not been able to make a connection. I have been able to send the offers and answers between the two tabs successfully resulting in pc.signalingState = 'stable' in both tabs. I believe once this is done then the RTCPeerConnection objects should start producing icecandidate events, but this is not happening and I do not know why. Here is my code (I've omitted the server code):
'use strict';
// This is mostly copy pasted from webrtc.org/getting-started/peer-connections.
import { io } from 'socket.io-client';
const configuration = {
'iceServers': [
{ 'urls': 'stun:stun4.l.google.com:19302' },
{ 'urls': 'stun:stunserver.stunprotocol.org:3478' },
]
}
// Returns a promise for an RTCDataChannel
function join() {
const socket = io('ws://localhost:8090');
const pc = new RTCPeerConnection(configuration);
socket.on('error', error => {
socket.close();
throw error;
});
pc.addEventListener('signalingstatechange', event => {
// Prints 'have-local-offer' then 'stable' in one tab,
// 'have-remote-offer' then 'stable' in the other.
console.log(pc.signalingState);
})
pc.addEventListener('icegatheringstatechange', event => {
console.log(pc.iceGatheringState); // This line is never reached.
})
// Listen for local ICE candidates on the local RTCPeerConnection
pc.addEventListener('icecandidate', event => {
if (event.candidate) {
console.log('Sending ICE candidate'); // This line is never reached.
socket.emit('icecandidate', event.candidate);
}
});
// Listen for remote ICE candidates and add them to the local RTCPeerConnection
socket.on('icecandidate', async candidate => {
try {
await pc.addIceCandidate(candidate);
} catch (e) {
console.error('Error adding received ice candidate', e);
}
});
// Listen for connectionstatechange on the local RTCPeerConnection
pc.addEventListener('connectionstatechange', event => {
if (pc.connectionState === 'connected') {
socket.close();
}
});
// When both browsers send this signal they will both receive the 'matched' signal,
// one with the payload true and the other with false.
socket.emit('join');
return new Promise((res, rej) => {
socket.on('matched', async first => {
if (first) {
// caller side
socket.on('answer', async answer => {
await pc.setRemoteDescription(new RTCSessionDescription(answer))
.catch(console.error);
});
const offer = await pc.createOffer();
await pc.setLocalDescription(offer)
.catch(console.error);
socket.emit('offer', offer);
// Listen for connectionstatechange on the local RTCPeerConnection
pc.addEventListener('connectionstatechange', event => {
if (pc.connectionState === 'connected') {
res(pc.createDataChannel('data'));
}
});
} else {
// recipient side
socket.on('offer', async offer => {
pc.setRemoteDescription(new RTCSessionDescription(offer))
.catch(console.error);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer)
.catch(console.error);
socket.emit('answer', answer);
});
pc.addEventListener('datachannel', event => {
res(event.channel);
});
}
});
});
}
join().then(dc => {
dc.addEventListener('open', event => {
dc.send('Hello');
});
dc.addEventListener('message', event => {
console.log(event.data);
});
});
The behavior is the same in both Firefox and Chrome. That behavior is, again, that the offers and answers are signalled successfully, but no ICE candidates are ever created. Does anyone know what I'm missing?
Okay, I found the problem. I have to create the RTCDataChannel before creating the offer. Here's a before and after comparison of the SDP offers:
# offer created before data channel:
{
type: 'offer',
sdp: 'v=0\r\n' +
'o=- 9150577729961293316 2 IN IP4 127.0.0.1\r\n' +
's=-\r\n' +
't=0 0\r\n' +
'a=extmap-allow-mixed\r\n' +
'a=msid-semantic: WMS\r\n'
}
# data channel created before offer:
{
type: 'offer',
sdp: 'v=0\r\n' +
'o=- 1578211649345353372 2 IN IP4 127.0.0.1\r\n' +
's=-\r\n' +
't=0 0\r\n' +
'a=group:BUNDLE 0\r\n' +
'a=extmap-allow-mixed\r\n' +
'a=msid-semantic: WMS\r\n' +
'm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n' +
'c=IN IP4 0.0.0.0\r\n' +
'a=ice-ufrag:MZWR\r\n' +
'a=ice-pwd:LfptE6PDVughzmQBPoOtvaU8\r\n' +
'a=ice-options:trickle\r\n' +
'a=fingerprint:sha-256 1B:C4:38:9A:CD:7F:34:20:B8:8D:78:CA:4A:3F:81:AE:C5:55:B3:27:6A:BD:E5:49:5A:F9:07:AE:0C:F6:6F:C8\r\n' +
'a=setup:actpass\r\n' +
'a=mid:0\r\n' +
'a=sctp-port:5000\r\n' +
'a=max-message-size:262144\r\n'
}
In both cases the answer looked similar to the offer. You an see the offer is much longer and mentions webrtc-datachannel in the second case. And sure enough, I started getting icecandidate events and everything is working now.

addTrack after etablished connection

I need to add my track and send to other peers after having etablished the connection I've followed the MDN example
pc.onnegotiationneeded = async () => {
try {
makingOffer = true;
await pc.setLocalDescription();
signaler.send({ description: pc.localDescription });
} catch(err) {
console.error(err);
} finally {
makingOffer = false;
}
};
onnegotiationneeded is fired when we call pc.addTrack so I will remake the process offer -> answer -> ICE.
So I have a function which call getUserMedia and set the local video I've added a callback to apply addTrack to my peers
const handleActiveVideo = async (cb) => {
const devices = await getVideoDevices();
const localStream = await createLocalStream(devices[0].deviceId);
setLocalVideoEl(localStream, devices[0].deviceId);
cb((pc) => {
localStream.getTracks().forEach((track) => {
pc.connection.addTrack(track, localStream);
});
});
};
But if I do that with an etablished connection when I add my local stream to the track with one peer it's ok all work fine but with my second peer I have this error on Firefox
Remote description indicates ICE restart but offer did not request ICE
restart (new remote description changes either the ice-ufrag or
ice-pwd)
with Chrome
DOMException: Failed to execute 'setRemoteDescription' on
'RTCPeerConnection': Failed to set remote answer sdp: Failed to apply
the description for m= section with mid='0': Failed to set SSL role
for the transport.
To recapitulate
connect my 2 peers without any tracks on both sides
start the video with my Peer1 ok I can see the video with my Peer2
start the video with my Peer2 error on Peer2 in the answer
setRemoteDescription(description);

how to get socketid in socket.io(nodejs)

In my nodejs application, i am using socket.io for sockets connection.
I am configuring my server side code like this
socket.io configuration in separate file.
//socket_io.js
var socket_io = require('socket.io');
var io = socket_io();
var socketApi = {};
socketApi.io = io;
module.exports = socketApi;
below is my server.js file in which i am attaching my socket io to the server like this
var socketApi = require('./server/socket_io');
// Create HTTP server.
const server = http.createServer(app);
// Attach Socket IO
var io = socketApi.io;
io.attach(server);
// Listen on provided port, on all network interfaces.
server.listen(port, () => console.log(`API running on localhost:${port}`));
and then i am using socket.io in my game.js file to emit updated user coins like this.
//game.js
var socketIO = require('../socket_io');
function updateUserCoins(userBet) {
userId = mongoose.Types.ObjectId(userBet.user);
User.findUserWithId(userId).then((user) => {
user.coins = user.coins - userBet.betAmount;
user.save((err, updatedUser) => {
socketIO.io.sockets.emit('user coins', {
userCoins: updatedUser.coins,
});
});
})
}
and then in my client side, i am doing something like this,
socket.on('user coins', (data) => {
this.coins = data.userCoins;
});
but with the above implementation, updating coins of any user, updates all user coins at the client side, since all the clients are listening to the same socket user coins.
To solve the above problem, i know that i have to do something like this,
// sending to individual socketid (private message)
socketIO.io.sockets.to(<socketid>).emit('user coins', {
userCoins: updatedUser.coins,
});
but my concern is that how will get <socketid> with my current implementation.
You can get it by listening to connection to your socket.io server :
io.on('connection', function (socket) {
socket.emit('news', { hello: 'world' });
socket.on('my other event', function (data) {
console.log(data);
});
});
Here you have a socket object ( io.on('connection', function (socket) ) and you can get his id with socket.id
So you'll probably need to wrap your code with the connection listener.
Source of the exemple for the connection listener
Socket object description

webrtc configration return null value

I am implementing webrtc for peerconnection but not working. when i debug this code then i saw it is not coming in
theirConnection.onaddstream = function (e) {
theirVideo.src = window.URL.createObjectURL(e.stream);
};
block. please let me know where i have done wrong? I am using this on my local system and with one webcam only.
function startPeerConnection(stream) {
var configuration = {
"iceServers": [{ "url": "stun:127.0.0.1:8080" }]
};
yourConnection = new mozRTCPeerConnection(configuration);
theirConnection = new mozRTCPeerConnection(configuration);
// Setup stream listening
theirConnection.onaddstream = function (e) {
theirVideo.src = window.URL.createObjectURL(e.stream);
};
// Setup ice handling
yourConnection.onicecandidate = function (event) {
if (event.candidate) {
theirConnection.addIceCandidate(new RTCIceCandidate(event.candidate));
}
};
theirConnection.onicecandidate = function (event) {
if (event.candidate) {
yourConnection.addIceCandidate(new RTCIceCandidate(event.candidate));
}
};
// Begin the offer
yourConnection.createOffer(function (offer) {
yourConnection.setLocalDescription(offer);
theirConnection.setRemoteDescription(offer);
theirConnection.createAnswer(function (offer) {
theirConnection.setLocalDescription(offer);
yourConnection.setRemoteDescription(offer);
});
});
}
You're calling the promise methods. The problem is literally that you're not checking for errors.
Specifically: The legacy callback methods you seek expect at least two arguments: a success callback, and a failure callback.
Without a failure callback, WebIDL name-overloading calls the newer promise methods instead, which take no callbacks and return a promise instead.
Either pass in the required error callbacks everywhere, e.g.:
yourConnection.createOffer(offer => { ... }, err => console.log(err));
Or better, avoid the legacy methods altogether:
function startPeerConnection(stream) {
var config = { iceServers: [{ urls: "stun:127.0.0.1:8080" }] };
yours = new RTCPeerConnection(config);
theirs = new RTCPeerConnection(config);
theirs.onaddstream = e => theirVideo.srcObject = e.stream;
yours.onicecandidate = e => theirs.addIceCandidate(e.candidate);
theirs.onicecandidate = e => yours.addIceCandidate(e.candidate);
yours.addStream(stream); // <-- don't forget
return yours.createOffer()
.then(offer => Promise.all([yours.setLocalDescription(offer),
theirs.setRemoteDescription(offer)]))
.then(() => theirs.createAnswer())
.then(answer => Promise.all([theirs.setLocalDescription(answer),
yours.setRemoteDescription(answer)]));
}
navigator.mediaDevices.getUserMedia({video: true, audio: true})
.then(stream => startPeerConnection(stream))
.catch(e => console.log(e));
<video id="theirVideo" height="120" width="160" autoplay></video>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
And always check for errors. A createOffer error told me you'd forgotten .addStream(stream).
The promise API is available natively in Firefox, and through adapter.js in Chrome.