This was indeed a race condition, and to understand how we got there, we need the full code example:
useEffect(() => {
if (!localParticipant) {
return;
}
for (const localTrack of localTracks) {
if (localTrack.kind === 'video') {
localParticipant.publishTrack(localTrack, {
priority: 'low',
});
} else {
localParticipant.publishTrack(localTrack, {
priority: 'standard',
});
}
}
return () => {
localParticipant.audioTracks.forEach((publication) => {
publication.unpublish();
});
localParticipant.videoTracks.forEach((publication) => {
publication.unpublish();
});
};
}, [localParticipant, localTracks]);
What is happening here is that every time localParticipant
or localTracks
change, we do two things:
- We clean-up by unsetting any existing audio/ video tracks
- We bind new tracks
Somehow the clean up logic causes the localParticipant.publishTrack
method to go into an error state ("Track name is duplicated") publishTrack
is invoked just after unpublish
.
The fix is to simply move unpublish
logic into a separate hook that does not depend on localTracks
.
useEffect(() => {
if (!localParticipant) {
return;
}
return () => {
localParticipant.audioTracks.forEach((publication) => {
publication.unpublish();
});
localParticipant.videoTracks.forEach((publication) => {
publication.unpublish();
});
};
}, [localParticipant]);
useEffect(() => {
if (!localParticipant) {
return;
}
for (const localTrack of localTracks) {
if (localTrack.kind === 'video') {
localParticipant.publishTrack(localTrack, {
priority: 'low',
});
} else {
localParticipant.publishTrack(localTrack, {
priority: 'standard',
});
}
}
}, [localParticipant, localTracks]);
Note that you need to do this in addition for handling events. The unmount clean-up strategy is used here primarily to enable react hot reloading.