I’m currently having an issue where joining a call for the first time works perfectly, however, when a peer who isn’t the host leaves and rejoins, the connection state never changes to connected; it gets stuck at connecting. After several hours of debugging, I realised the first set of handshake has its signalling state as “have-local-offer”, while the rejoining one has its signalling state as “stable”.
To also add, ending the call and restarting as a host works perfectly.
Here’s my code for proper explanation:
const joinCall = useCallback(async () => {
const callDoc = doc(collection(firestore, 'calls'), partyId);
const callData = (await getDoc(callDoc)).data();
if (callData?.callEnded) {
Toast.show({
type: ALERT_TYPE.DANGER,
title: "Can't join party",
textBody: 'The party has ended',
titleStyle: tw`font-poppinsRegular text-xs`,
textBodyStyle: tw`font-poppinsRegular text-xs`,
});
setCallEnded(true);
return false;
}
if (!callData?.callStarted) {
Toast.show({
type: ALERT_TYPE.DANGER,
title: "Can't join party yet",
textBody: "Host hasn't started the party",
titleStyle: tw`font-poppinsRegular text-xs`,
textBodyStyle: tw`font-poppinsRegular text-xs`,
});
setCallStarted(false);
return false;
}
setCallStarted(true);
const pc = createPeerConnection();
const stream = await setupMediaStream();
if (stream) {
stream.getTracks().forEach(track => pc.addTrack(track, stream));
}
try {
console.log('join call signaling state', pc.signalingState);
if (callData?.offer && pc.signalingState === 'stable') {
const offerDescription = new RTCSessionDescription(callData.offer);
await pc.setRemoteDescription(offerDescription);
const answerDescription = await pc.createAnswer();
await pc.setLocalDescription(answerDescription);
await updateDoc(callDoc, {answer: answerDescription});
addIceCandidatesToPeerConnection();
}
unsubscribeSnapshot = onSnapshot(callDoc, snapshot => {
const data = snapshot.data();
if (data?.callEnded) {
setCallEnded(true);
}
if (data?.candidates) {
console.log(
'ICE candidates received from Firestore:',
data.candidates,
);
data.candidates.forEach((candidateData: any) => {
const candidate = new RTCIceCandidate(candidateData);
if (pc.remoteDescription) {
console.log('Adding ICE candidate:', candidate);
pc.addIceCandidate(candidate).catch(error => {
console.error(
'Failed to add ICE candidate when joining the call',
error,
);
});
} else {
console.warn(
'Remote description not set yet, queuing ICE candidate:',
candidate,
);
ICE_CANDIDATES_QUEUE.push(candidate);
}
});
}
});
return true;
} catch (error) {
console.error('Error in joinCall:', error);
return false;
}
}, [
createPeerConnection,
setupMediaStream,
firestore,
partyId,
addIceCandidatesToPeerConnection,
]);
const leaveCall = useCallback(async () => {
const pc = PEERCONNECTION;
if (pc) {
pc.getTransceivers().forEach(transceiver => {
transceiver.stop();
});
pc.close();
PEERCONNECTION = null;
}
if (LOCAL_STREAM) {
LOCAL_STREAM.getTracks().forEach(track => {
track.stop();
});
LOCAL_STREAM = null;
}
if (REMOTE_STREAM) {
REMOTE_STREAM.getTracks().forEach(track => {
track.stop();
});
REMOTE_STREAM = null;
}
if (unsubscribeSnapshot) {
unsubscribeSnapshot();
}
setIsConnected(false);
setCallEnded(true);
}, []);
const beginParty = useCallback(async () => {
const pc = createPeerConnection();
const stream = await setupMediaStream();
if (stream) {
stream.getTracks().forEach(track => pc.addTrack(track, stream));
}
const callDoc = doc(collection(db, 'calls'), partyId);
const offerOptions = {
offerToReceiveAudio: true,
offerToReceiveVideo: false,
voiceActivityDetection: true,
};
try {
const offer = await pc.createOffer(offerOptions);
await pc.setLocalDescription(offer);
await setDoc(callDoc, {
offer: offer,
candidates: [],
callStarted: true,
callEnded: false,
});
unsubscribeSnapshot = onSnapshot(callDoc, snapshot => {
const data = snapshot.data();
if (data?.callEnded) {
setCallEnded(true);
}
console.log('signalling', pc.signalingState);
if (data?.answer && pc.signalingState === 'have-local-offer') {
const answerDescription = new RTCSessionDescription(data.answer);
pc.setRemoteDescription(answerDescription)
.then(
() => console.log('adding ice candidate'),
addIceCandidatesToPeerConnection,
)
.catch(error => {
console.error(
'Failed to set remote description when beginning party:',
error,
);
});
}
if (data?.candidates) {
data.candidates.forEach((candidateData: any) => {
const candidate = new RTCIceCandidate(candidateData);
if (pc.remoteDescription) {
pc.addIceCandidate(candidate).catch(error => {
console.error(
'Failed to add ICE candidate when beginning party:',
error,
);
});
} else {
ICE_CANDIDATES_QUEUE.push(candidate);
}
});
}
});
} catch (error) {
console.error('Error in beginParty:', error);
}
}, [
createPeerConnection,
setupMediaStream,
partyId,
addIceCandidatesToPeerConnection,
]);