simple-peer webrtc error when trying to share screen - javascript

I am building a video chat application using simple-peer where users can also share the screen. The flow of the app is such that when user A creates a room, he can grab the page url and share with user B. When user B joins, an initiator peer is created for him as seen here.
function createPeer(partnerID, callerID, stream) {
const peer = new Peer({
initiator: true,
trickle: false,
stream,
});
peer.on("signal", signal => {
const payload = {
partnerID,
callerID,
signal
}
socketRef.current.emit("call partner", payload);
});
peer.on("stream", handleStream);
return peer;
}
When user A gets the offer from user B, a non initiator peer is created for him as seen here.
function addPeer(incomingSignal, callerID, stream) {
const peer = new Peer({
initiator: false,
trickle: false,
stream,
});
peer.on("signal", signal => {
const payload = {
callerID,
signal
}
socketRef.current.emit("accept call", payload);
});
peer.on("stream", handleStream);
peer.signal(incomingSignal);
return peer;
}
Now when any user decides to share their screen, this function gets called.
function shareScreen() {
navigator.mediaDevices.getDisplayMedia().then(stream => {
const track = stream.getTracks()[0];
peerRef.current.removeStream(videoStream.current);
peerRef.current.addStream(stream);
userVideoRef.current.srcObject = stream;
track.onended = function () {
userVideoRef.current.srcObject = videoStream.current;
peerRef.current.removeTrack(track, stream);
};
});
}
What's really strange about the behavior that I am getting, is that when user B, in other words the calling peer, wants to share his screen, all works well, but when user A, the callee peer, wants to share his screen, I get the following error.
index.js:17 Uncaught Error: [object RTCErrorEvent]
at makeError (index.js:17)
at RTCDataChannel._channel.onerror (index.js:490)
I am not really sure where I am going wrong.

After some testing, I discovered where the error is coming from.
What you need to do is to call the function peer.signal(incomingSignal) after calling the function addPeer(), along with the peer variable returned from the addPeer() function.
Here's an example:
function addPeer(incomingSignal, callerID, stream) {
const peer = new Peer({
initiator: false,
trickle: false,
stream,
});
peer.on("signal", signal => {
const payload = {
callerID,
signal
}
socketRef.current.emit("accept call", payload);
});
peer.on("stream", handleStream);
// peer.signal(incomingSignal);
return peer;
}
let peer = addPeer(incomingSignal, callerId, stream);
peer.signal(incomingSignal);
and it will work fine

Related

Why can WebRTC not reconnect after disconnecting?

I am building a web application using React where users can enter a group call. I have a NodeJS server that runs Socket.IO to manage the client events, and the users are connected through a peer-to-peer connection using simple-peers (WebRTC).
Everything is functional, from joining the call, seeing other users, and being able to leave. The call is "always open", similar to discord, and users can come and go as they please. However, if you leave and then try to rejoin the call without refreshing the page, the call breaks on all sides. The user leaving and rejoining gets the following error:
Error: cannot signal after peer is destroyed
For another user in the call, it logs the "user joined" event multiple times for the one user that tried to rejoin. Before it would add multiple peers as well, but I now make sure duplicates cannot exist.
However, to me, the strangest part is that when the user leaves, they send a call out to all other users in the room. The other users successfully destroy the peer connection and then remove the user from their peer array. The user who left on his turn also destroys each connection and resets the peer array to an empty array. So I'm very confused as to what PTP connection it is trying to re-establish.
const [roomSize, setRoomSize] = useState(0);
const socketRef = useRef();
const userVideo = useRef();
const peersRef = useRef([]);
const roomId = 'meeting_' + props.zoneId;
useEffect(async () => {
socketRef.current = io.connect(SERVER_URI, {
jsonp: false,
forceNew: true,
extraHeaders: {
"x-access-token": window.localStorage.getItem('accessToken'),
"zone-id": props.zoneId
}
});
}, []);
useEffect(async () => {
if (props.active) {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
height: window.innerHeight / 2,
width: window.innerWidth / 2
},
audio: true });
userVideo.current.srcObject = stream;
console.log(`%cJoined socket at ${SERVER_URI}, connected=${socketRef.current.connected}`, 'color: pink');
socketRef.current.emit("join room", roomId);
socketRef.current.on("all users", users => {
users.forEach(userID => {
const peer = createPeer(userID, socketRef.current.id, stream);
const peerObj = {
peerID: userID,
peer,
};
if (!peersRef.current.find(p => p.peerID === userID))
peersRef.current.push(peerObj);
});
setRoomSize(peersRef.current.length);
console.log(`%cNew Room Members: ${peersRef.current.length}; %o`, 'color: cyan', {peersRef: peersRef.current});
})
socketRef.current.on("user joined", payload => {
const peer = addPeer(payload.signal, payload.callerID, stream);
const peerObj = {
peerID: payload.callerID,
peer,
};
if (!peersRef.current.find(p => p.peerID === payload.callerID))
peersRef.current.push(peerObj);
setRoomSize(peersRef.current.length);
console.log(`%cSomeone Joined. Members: ${peersRef.current.length}; %o`, 'color: cyan', {peersRef: peersRef.current});
});
socketRef.current.on("receiving returned signal", payload => {
/** #type {Peer} */
const item = peersRef.current.find(p => p.peerID === payload.id);
item.peer.signal(payload.signal);
console.log("%creceiving return signal", 'color: lightgreen');
});
socketRef.current.on('user left', id => {
const peerObj = peersRef.current.find(p => p.peerID === id);
// console.log("user left", { peerObj });
if (peerObj)
peerObj.peer.destroy();
const peers = peersRef.current.filter(p => p.peerID !== id);
peersRef.current = peers;
setRoomSize(peersRef.current.length);
console.log(`%cSomeone Left. Members: ${peersRef.current.length}`, 'color: cyan');
});
} catch (err) {
console.trace(err);
}
}
else if (socketRef.current && socketRef.current.connected) {
socketRef.current.emit("leave room");
peersRef.current.forEach(peerObj => {
peerObj.peer.destroy();
});
peersRef.current = [];
setRoomSize(peersRef.current.length);
}
}, [props.active, peersRef.current]);
const createPeer = (userToSignal, callerID, stream) => {
const peer = new Peer({
initiator: true,
trickle: false,
stream,
});
peer.on("signal", signal => {
socketRef.current.emit("sending signal", { userToSignal, callerID, signal })
})
return peer;
}
const addPeer = (incomingSignal, callerID, stream) => {
const peer = new Peer({
initiator: false,
trickle: false,
stream,
})
peer.on("signal", signal => {
socketRef.current.emit("returning signal", { signal, callerID })
})
peer.signal(incomingSignal);
return peer;
}
Quick Edit: The above code is part of a React component that renders a video element for each peer.
When props.active becomes false is when the user leaves the call. This happens at the end of the second useEffect hook, where the client who left should have removed all their peer objects after destroying them. Why does this user receive the above error on a reconnect? And how do I keep this error from occurring?
Edit: I just noticed that when both users leave the call, and both try to rejoin without refreshing, the error does not occur. So something is different when removing a peer upon a user leaving compared to leaving yourself is my best guess.
TLDR; Put all refs you use in the useEffect body in the useEffect deps array.
I'd be sure to first check the useEffect deps array. It looks like socketRef is required in multiple places throughout that hook body, but it doesn't appear to be in the deps array. This can cause the hook to use less-than-current data.
It's also because of this that the socketRef ref object may never actually update, meaning, it may correctly remove the user from peers, as peerRefs is in the useEffect deps array, but the internal session (the room) may not recognize this; the room's internal representation of the user still exists.
Repeating myself, but just to make it clear, you mentioned:
So something is different when removing a peer upon a user leaving compared to leaving yourself is my best guess.
This is the same reason as listed above. The reason it happens when a peer leaves is because the peerRefs ref object IS in the useEffect deps array, so the effect you're describing is just 'perfect timing', if you will, since the applications state (all the refs) are correctly sync'd up with each other.

Request video during PeerJs ongoing live connection (stream)

I am new to PeerJs and recently starting developing an app for my school during this Covid pandemic.
I have been able to deploy code to NodeJs server with express and was able to establish connection between 2 users.
But the problem arises when video is turned off from the beginning of stream for both users and a user wants to initiate a video call.
What I need is, to send some kind of notification to user 2 that user 1 is requesting for video. So that user 2 will turn on video.
My existing code is:
var url = new URL(window.location.href);
var disableStreamInBeginning = url.searchParams.get("disableStreamInBeginning"); // To disable video in the beginning
var passwordProtectedRoom = url.searchParams.get("passwordProtectedRoom");
var muteAllInBeginning = url.searchParams.get("muteAllInBeginning");
const socket = io('/')
const localVideoDiv = document.getElementById('local-video-div')
const oneOnOneSelf = document.getElementById('local-video')
const oneOnOneRemote = document.getElementById('remote-video')
if(typeof disableStreamInBeginning !== 'undefined' && disableStreamInBeginning == 'true'){
var disbaleSelfStream = true
} else {
var disbaleSelfStream = false
}
if(typeof passwordProtectedRoom !== 'undefined' && passwordProtectedRoom == 'true'){
var passwordProtected = true
} else {
var passwordProtected = false
}
if(typeof muteAllInBeginning !== 'undefined' && muteAllInBeginning == 'true'){
var muteAll = true
} else {
var muteAll = false
}
var systemStream
oneOnOneSelf.style.opacity = 0
oneOnOneRemote.style.opacity = 0
const myPeer = new Peer(undefined, {
host: '/',
port: '443',
path: '/myapp',
secure: true
})
const ownVideoView = document.createElement('video')
const peers = {}
navigator.mediaDevices.getUserMedia({
video: true,
audio: true
}).then(ownStream => {
systemStream = ownStream
addVideoStream(ownStream, oneOnOneSelf)
myPeer.on('call', call => {
call.answer(ownStream)
call.on('stream', remoteStream => {
addVideoStream(remoteStream, oneOnOneRemote)
})
})
socket.on('user-connected', userId => {
//connectToNewUser(userId, stream)
setTimeout(connectToNewUser, 1000, userId, ownStream)
})
})
socket.on('user-disconnected', userId => {
if (peers[userId]) peers[userId].close()
})
myPeer.on('open', id => {
//Android.onPeerConnected();
socket.emit('join-room', ROOM_ID, id)
})
function connectToNewUser(userId, stream) {
const call = myPeer.call(userId, stream)
call.on('stream', remoteStream => {
//console.log('Testing')
addVideoStream(remoteStream, oneOnOneRemote)
})
call.on('close', () => {
oneOnOneRemote.remove()
})
peers[userId] = call
}
function addVideoStream(stream, videoView) {
videoView.srcObject = stream
videoView.addEventListener('loadedmetadata', () => {
if(disbaleSelfStream){
audioVideo(true)
} else {
localVideoDiv.style.opacity = 0
videoView.style.opacity = 1
videoView.play()
}
})
}
function audioVideo(bool) {
if(bool == true){
localVideoDiv.style.opacity = 1
oneOnOneSelf.style.opacity = 0
systemStream.getVideoTracks()[0].enabled = false
} else {
if(disbaleSelfStream){
console.log('Waiting For Another User To Accept') // Here is need to inform user 2 to tun on video call
} else {
localVideoDiv.style.opacity = 0
oneOnOneSelf.style.opacity = 1
systemStream.getVideoTracks()[0].enabled = true
}
}
}
function muteUnmute(bool) {
if(bool == true){
systemStream.getAudioTracks()[0].enabled = true
} else {
systemStream.getAudioTracks()[0].enabled = false
}
}
function remoteVideoClick(){
alert('Hi');
}
Please help.
You can send messages back and forth directly using peer itself
const dataConnection = peer.connect(id) will connect you to the remote peer, it returns a dataConnection class instance that you can later use with the send method of that class.
Just remember that you also want to setup listener on the other side to listen for this events, like "open" to know when the data channel is open:
dataConnection.on('open', and dataConnection.on('data...
You have a bug in your code above, I know you didn't ask about it, it is hard to see and not always will manifest. The problem will occur when your originator sends a call before the destination has had time to receive the promise back with its local video/audio stream. The solution is to invert the order of the calls and to start by setting up the event handler for peer.on("call", ... rather than by starting by waiting for a promise to return when we ask for the video stream. The failure mode will depend on how long does it take for your destination client to signal it wants and call to the originator plus how long it takes for the originator to respond versus how long it takes for the stream promise to return on the destination client. You can see a complete working example, where messages are also sent back and forth here.
// Function to obtain stream and then await until after it is obtained to go into video chat call and answer code. Critical to start the event listener ahead of everything to ensure not to miss an incoming call.
peer.on("call", async (call) => {
let stream = null;
console.log('*** "call" event received, calling call.answer(strem)');
// Obtain the stream object
try {
stream = await navigator.mediaDevices.getUserMedia(
{
audio: true,
video: true,
});
// Set up event listener for a peer media call -- peer.call, returns a mediaConnection that I name call
// Answer the call by sending this clients video stream --myVideo-- to calling remote user
call.answer(stream);
// Create new DOM element to place the remote user video when it comes
const video = document.createElement('video');
// Set up event listener for a stream coming from the remote user in response to this client answering its call
call.on("stream", (userVideoStream) => {
console.log('***"stream" event received, calling addVideoStream(UserVideoStream)');
// Add remote user video stream to this client's active videos in the DOM
addVideoStream(video, userVideoStream);
});
} catch (err) {
/* handle the error */
console.log('*** ERROR returning the stream: ' + err);
};
});

Issue with seting up a pub/ sub implementation with Pubnub

First off let me start by saying I am a web developer who is very new to the Blockchain space so I apologize in advance if I am missing something obvious. With that being said I am having issues implementing a broadcast transaction method into a cryptocurrency project I am working on. Every time I am able to successfully make transaction requests to my API and I am able to see the correct transaction pool on the main dev network, however my peer network does not see any new transactions until I restart it. I know this means my broadcast transaction implementation is missing something but I am not sure what I need to fix it.
please refer to the following images
Here is the code snippets for my pubsub implementation using PUBNUB
const CHANNELS = {
TEST: "TEST",
BLOCKCHAIN: "BLOCKCHAIN",
TRANSACTION: "TRANSACTION"
};
class PubSub {
constructor({blockchain, transactionPool}) {
this.blockchain = blockchain;
this.transactionPool = transactionPool;
this.pubnub = new PubNub(credentials); // defined above but omitted for obvious reasons
this.pubnub.subscribe({channels: Object.values(CHANNELS)}); // defined in channels object
this.pubnub.addListener(this.listener());
};
listener() {
return {
message: messageObject => {
const { channel, message } = messageObject;
console.log(`Message received. Channel: ${channel}. Message: ${message}`);
const parsedMessage = JSON.parse(message);
switch(channel) {
case CHANNELS.BLOCKCHAIN:
this.blockchain.replaceChain(parsedMessage);
break;
case CHANNELS.TRANSACTION:
this.transactionPool.setTransaction(parsedMessage);
break;
default:
return;
}
}
};
}
publish({channel, message}) {
this.pubnub.unsubscribe(channel, () => {
this.pubnub.publish(channel, message, () => {
this.pubnub.subscribe(channel);
});
});
}
broadcastChain() {
this.publish({
channel: CHANNELS.BLOCKCHAIN,
message: JSON.stringify(this.blockchain.chain)
})
}
broadcastTransaction(transaction) {
this.publish({
channel: CHANNELS.TRANSACTION,
message: JSON.stringify(transaction)
});
}
};
And here is the snippets for where the broadcastTransaction method is called
const pubsub = new PubSub({blockchain, transactionPool});
let transaction = transactionPool.existingTransaction({inputAddress: wallet.publicKey}); // Creates global binding
// Sends transaction to the network
app.post("/api/transact", (req, res) => {
const {amount, recipient} = req.body;
try {
if(transaction) {
transaction.update({senderWallet: wallet, recipient, amount });
} else {
transaction = wallet.createTransaction({recipient, amount});
}
} catch(error) {
return res.status(400).json({type: "error", message: error.message});
};
transactionPool.setTransaction(transaction);
pubsub.broadcastTransaction(transaction); // Calls broadcastTransaction from pubsub class (does not work for peers)
res.json({type: "success", transaction});
});
I tried to be as specific as possible but If I missed anything please let me know. Thank you in advance!
Thank you for the input. logging helped me realize that my dev and peer ports were not subscribed when starting up the nodes. I added the following method to the PubSub class and was able to sync both the dev and peer ports
subscribeToChannels() {
this.pubnub.subscribe({
channels: [Object.values(CHANNELS)]
});
}

Multipeer connection onicecandidate event won't fire

I'm having problems with the logic to build behind the webRTC multi peer connections handling.
Basically I'm trying to make a Room full of people in a videoconference call.
I'm using the basic WebSocket library provided by js, and React for the frontend and Java (spring boot) for the backend.
As of my understanding right now this is what I managed to write down (filtered based on what I "think" is relevant)
This is my web socket init method (adding listeners)
let webSocketConnection = new WebSocket(webSocketUrl);
webSocketConnection.onmessage = (msg) => {
const message = JSON.parse(msg.data);
switch (message.type) {
case "offer":
handleOfferMessage(message);
break;
case "text":
handleReceivedTextMessage(message);
break;
case "answer":
handleAnswerMessage(message);
break;
case "ice":
handleNewICECandidateMessage(message);
break;
case "join":
initFirstUserMedia(message);
break;
case "room":
setRoomID(message.data);
break;
case "peer-init":
handlePeerConnection(message);
break;
default:
console.error("Wrong type message received from server");
}
Plus of course the 'on error', 'on close' and 'on open' listeners
This is the method handling the incoming offer
const handleOfferMessage = (message) => {
console.log("Accepting Offer Message");
console.log(message);
let desc = new RTCSessionDescription(message.sdp);
let newPeerConnection = new RTCPeerConnection(peerConnectionConfig);
newPeerConnection.onicecandidate = handleICECandidateEvent;
newPeerConnection.ontrack = handleTrackEvent;
if (desc != null && message.sdp != null) {
console.log("RTC Signalling state: " + newPeerConnection.signalingState);
newPeerConnection
.setRemoteDescription(desc)
.then(function () {
console.log("Set up local media stream");
return navigator.mediaDevices.getUserMedia(mediaConstraints);
})
.then(function (stream) {
console.log("-- Local video stream obtained");
localStream = stream;
try {
videoSelf.current.srcObject = localStream;
} catch (error) {
videoSelf.current.src = window.URL.createObjectURL(stream);
}
console.log("-- Adding stream to the RTCPeerConnection");
localStream
.getTracks()
.forEach((track) => newPeerConnection.addTrack(track, localStream));
})
.then(function () {
console.log("-- Creating answer");
return newPeerConnection.createAnswer();
})
.then(function (answer) {
console.log("-- Setting local description after creating answer");
return newPeerConnection.setLocalDescription(answer);
})
.then(function () {
console.log("Sending answer packet back to other peer");
webSocketConnection.send(
JSON.stringify({
from: user,
type: "answer",
sdp: newPeerConnection.localDescription,
destination: message.from
})
);
})
.catch(handleErrorMessage);
}
peerConnections[message.from.id] = newPeerConnection;
console.log("Peer connections updated now ", peerConnections);
};
SN: I got the peer connections defined as an array of RTCPeerConnection indexed by the user unique id
let [peerConnections, setPeerConnections] = useState([]);
And here comes the part that I think I got wrong and on which I'm having trouble understanding
const handleAnswerMessage = (message) => {
console.log("The peer has accepted request");
let currentPeerConnection = peerConnections[message.from.id];
if (currentPeerConnection) {
currentPeerConnection.setRemoteDescription(message.sdp).catch(handleErrorMessage);
peerConnections[message.from.id] = currentPeerConnection;
} else {
console.error("No user was found with id ", message.from.id);
}
console.log("Peer connections updated now ", peerConnections);
};
currentPeerConnection.setRemoteDescription(message.sdp).catch(handleErrorMessage);
peerConnections[message.from.id] = currentPeerConnection;
console.log("Peer connections updated now ", peerConnections);
};
The answer and the offer work perfectly, I can clearly see the two peers communicating one by sending the offer and the other one responding with an answer. The only problem is that after that nothing happens, but from what I read about webRTC it should actually start gathering ice candidates as soon as a local description has been set.
I can understand why the peer handling the answer (caller) actually does not fire up iceecandidate and that's probably because I do not set a local description on the answer message (I don't know if it would be correct). the callee on the other hand, handling the offer message should actually start gathering iceecandidates tho, I'm setting the local description on there.
This some additional code that might help
function getMedia(constraints, peerCnnct, initiator) {
if (localStream) {
localStream.getTracks().forEach((track) => {
track.stop();
});
}
navigator.mediaDevices
.getUserMedia(constraints)
.then(stream => {
return getLocalMediaStream(stream, peerCnnct, initiator);
})
.catch(handleGetUserMediaError);
}
function getLocalMediaStream(mediaStream, peerConnection, initiator) {
localStream = mediaStream;
const video = videoSelf.current;
if (video) {
video.srcObject = mediaStream;
video.play();
}
//localVideo.srcObject = mediaStream;
console.log("Adding stream tracks to the peer connection: ", peerConnection);
if (!initiator) {
localStream
.getTracks()
.forEach((track) => peerConnection.addTrack(track, localStream));
}
}
const handlePeerConnection = (message) => {
console.info("Creating new peer connection for user ", message.from);
let newPeerConnection = new RTCPeerConnection(peerConnectionConfig);
// event handlers for the ICE negotiation process
newPeerConnection.ontrack = handleTrackEvent;
newPeerConnection.onicecandidate = handleICECandidateEvent;
getMedia(mediaConstraints, newPeerConnection, false);
newPeerConnection.onnegotiationneeded = handleNegotiationNeededEvent(newPeerConnection, webSocketConnection, user, message.from);
peerConnections[message.from.id] = newPeerConnection;
};
Here you can clearly see my desperate attempt in finding a solution and creating a peer connection just for the sake of sending the offer.
I cannot index a peer connection that has no end user because I would need his id, that I receive only after I received an answer from him when I first join the room.
(The backend should work but either way putting a debugger on the ice candidate handler method I could clearly see that it's just not fired)
What am I doing wrong?
EDIT: Now the WebSocketMessage Server side has also a destination user. This way the the new peer that connects to the room receives as many peer-init messages as the already connected peers are. Then proceeds to make one offer per peer setting it as a destination.
The problem still persists though
This feels actually wierd but I fixed it calling the getUserMedia() (which means calling the addTrack before adding the onicecandidate event definition to the peerConnection.
This at least fixed my problem

How decline a offer o call in peerjs

hello guys How I can reject o accepted a call or offer is coming from the sender peer, I just using Peerjs client and peer server
this is my sender client
const peer = new Peer('sender', { host: '1.0.0.99', port: 9000, path: '/' })
var call = document.getElementById('call');
call.addEventListener('click',startChat);
function startChat(){
navigator.mediaDevices.getUserMedia({ video: true}).then((localStream) =>{
document.querySelector('video#local').srcObject = localStream;
var call = peer.call('receiver',localStream);
call.on('stream',remoteStream => {
document.querySelector('video#remote').srcObject = remoteStream
})
})
}
this is my receiver
const peer = new Peer('receiver', { host: '1.0.0.99', port: 9000, path: '/' })
peer.on('call', call => {
const startChat = async () => {
const localStream = await navigator.mediaDevices.getUserMedia({
video: true
})
document.querySelector('video#local').srcObject = localStream
// call.answer(localStream)
call.close(mediaStream);
call.on('stream', remoteStream => {
document.querySelector('video#remote').srcObject = remoteStream
})
}
startChat();
})
my goal on receiver can decline and accepted sorry I am new in this, thanks for any help
According to the oficial documentation, the MediaConnection API, states that the close() method should be used to reject the call, and the answer() method to accept it. In your code, you have tried both, even though you're passing an argument to the close() function, which doesn't take any. Now if you close the Media Connection, I assume that the callback on the the 'stream' message is invalid.
well I found the way
peer.on('call', call => {
var acceptsCall = confirm("Videocall incoming, do you want to accept it ?");
if (acceptsCall) {
const startChat = async () => {
const localStream = await navigator.mediaDevices.getUserMedia({
video: true
})
document.querySelector('video#local').srcObject = localStream
call.answer(localStream)
call.on('stream', remoteStream => {
document.querySelector('video#remote').srcObject = remoteStream
});
// Handle when the call finishes
call.on('close', function () {
alert("The videocall has finished");
});
}
startChat();
} else {
alert('call decline!');
}
})
one of the solutions I found was to start a conversation and after 0.1 seconds, close the connection, it is not one of the best solutions, but it was the one that served me the most.

Categories

Resources