3

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.

MyNameIsGuzse
  • 273
  • 1
  • 5
  • 23

1 Answers1

2

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.

Mytch
  • 380
  • 1
  • 3
  • 16
  • 1
    Definitely will try this. Awarding you the bounty since not much time is left, and you definitely gave some great insight as to how to proceed. Thanks! – MyNameIsGuzse May 24 '22 at 13:41