Common issues with WebRTC, React and PeerJS

Building a P2P video application requires traversing different networks and that can cause issues to arise. Network firewalls blocking specific ports, incorrect RTCPeerConnection instantiations due to browser type, SDP handshake failures, errors in ICE configurations by using the wrong STUN or TURN servers, and much more.

Not only that, some browser(s) or platforms will work perfectly fine, but then you discover that iPhone Chrome, Android Firefox, will fail mysteriously. The rabbit hole even goes deeper with different OS and browser versions..

We’re going to take a brief look at some of the common issues that usually occur when trying to communicate over WebRTC. Again many of these issues will fail silently with a black screen of death so they can be difficult and frustrating to debug.


Browser Permissions

  1. Verify the browser is actually requesting the user for audio, video and location perms (if needed) and not already implicitly saved.

Instantiating PeerJS correctly

  1. Create a Peer object. Documentation: https://peerjs.com/
  2. Pass STUN servers in as the correct data structure
    • NOTE: This has been known to fail silently when wrong, especially on mobile
  3. Verify the port and signaling server are set correctly
// Ensure config is properly formatted for PeerJS / RTCPeerConnection
// NOTE: Always have fallback ICE servers
const rtcConfig = config && config.iceServers ? config : {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    { urls: 'stun:global.stun.twilio.com:3478' }
  ]
};
...
new Peer(id, {
  secure: serverApiSignalSecure,
  host: serverApiSignalUrl,
  port: Number(serverApiSignalPort),
  path: serverApiSignalPath,
  config: rtcConfig,
  metadata: { id },
  // Mobile optimization settings
  pingInterval: 5000,
  // Ensure connection is reliable for mobile
  reliable: true,
});
...

Verify the Signaling server and Websocket connection are succeeding

  1. Verify your websocket connection is staying open and the signaling server is communicating to the other clients correctly when emitting from the client.
socket.emit('join-room', roomId, id, {
  userId: id,
  // NOTE: Pass a data object to your signaling server containing
  // client info to distribute among the peers
  data: {
    id,
    coordinates,
    isAudioEnabled,
    isVideoEnabled,
    isLocationEnabled,
    isScreenShareEnabled,
  },
});

Verify your connection with the Peer is actually established

Verify the user actually connected once given their info from the signaling server.

    peer.on('open', (id) => {
      console.info('✓ Peer connection established. Peer ID: ', id);
      ...
    });
    
    // Also log peer connection errors
    peer.on('error', (err) => {
      console.error('PEER ERROR: ', {
        type: err?.type,
        message: err?.message || err,
        name: err?.name
      });
      ...
    });

    Verify the Stream settings

    Another common issue is a wrongly instantiated Media Stream or the settings passed to it are incorrect. This can create all kinds of weird video and audio issues that will seemingly not happen for every client in the connection and depending on the browser the error will be eaten.

      const createUserMediaStream = async (
        handleCallback,
        handleErrorCallback,
        options = { video: true, audio: true }
      ) => {
        try {
          const vConstraints = {};
          // Video options / constraints
          const video = { video: options.video, ...vConstraints };
      
          // Audio options / constraints
          const additionalAudioConstraints = window.chrome
            ? { mandatory: audioConstraints }
            : audioConstraints;
      
          const aConstraints = options.audio ? additionalAudioConstraints : {};
          const audio = { audio: options.audio, ...aConstraints };
      
          await navigator.mediaDevices
            .getUserMedia({ video, audio })
            .then((stream) => {
              handleCallback(stream);
            })
            .catch((err) => handleErrorCallback(err));
        } catch (err) {
          console.error('Error CREATING USER MEDIA STREAM: ', err.message);
        }
        ...
      };

      Video and Audio Tracks

      Verify that your video and audio tracks are not null and that they tracks are REPLACED if the user stops either of them.

      const newStream = createUserMediaStream((s) => {
        ...
        const newVideoTrack = s.getVideoTracks()[0];
        ...
      
        // NOTE: Users is each client(s) version of the current connection info.
        // I'm keeping everything in sync via a Zustand store -- done in another
        // part of the code that handles incoming peer data
        const users = usersStore.getUsersData((u) => u.id);
        
        users.forEach((user) => {
          const call = peer.call(user.id, stream);
      
          // First element is Audio and second is Video - Replace the one
          // that was paused or stopped
          call.peerConnection.getSenders()[1].replaceTrack(newVideoTrack);
      
          // Pass to your React Ref
          videoRef.current.srcObject = s;
        });

      Leave a Reply

      Your email address will not be published. Required fields are marked *

      This site uses Akismet to reduce spam. Learn how your comment data is processed.