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
- Verify the browser is actually requesting the user for audio, video and location perms (if needed) and not already implicitly saved.
Instantiating PeerJS correctly
- Create a Peer object. Documentation: https://peerjs.com/
- Pass STUN servers in as the correct data structure
- NOTE: This has been known to fail silently when wrong, especially on mobile
- 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
- 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