0

I am trying to implement a video call room where users can share their audio and video using Agora and React. In fact, I have a working example but only with native javascript without React.

what I try to do is each time the handleUserPublished is running to store the remote user who published.

The problem with that is when the user publishes, and if he is sharing both video and audio tracks then the event is triggered twice since each track i.e. audio and video causes handleUserPublished to run. this is my code for the Room component:

  let handleUserPublished = async (user, mediaType) => {

    let dataElement = // create dataElement based on user

    if (mediaType === "video") {    
      setNewRemoteVideo(user);
      setPlayers((prev) => [...prev, dataElement]); // I am not sure put this here or in the other case but I have to update 'players' state only one time
    }
    if (mediaType === "audio") {
      setNewRemoteAudio(user);
      // or put setPlayers((prev) => [...prev, dataElement]) here
    }
  };
  useEffect(() => {
    const joinRoomInit = async () => {
      // create Agora connection
      client.on("user-published", handleUserPublished);
    };
    joinRoomInit();
  }, []);

As you can see in the above code I create two different states, newRemoteAudio and newRemoteVideo this is because depending on mediaType I chose what state to update with the received user.

Note that user when mediaType === "video" is different from user when mediaType === "audio" however the user object coming with the second handleUserPublished event is enough to store because it contains both data but I can never guess which one is going to be the second one the order is not always the same that's why I store both users in different state so after that, I can merge them. This somehow helps but I would like to store the remote user once and for all (the second run), the same for players.

My goal is to add dataElement to players state once the remote user is added to the other state because in JSX I am returning players but I am struggling to do that since the event handleUserPublished runs twice and I don't know which order gonna happen so I don't know when exactly to update players state, I want to do it the second time handleUserPublished runs so I don't have to set audio data and video data separately but only once (the second run) and the same of players state.

I need your help, thank you

Ahmed Sbai
  • 10,695
  • 9
  • 19
  • 38
nebil
  • 52
  • 9

1 Answers1

0

I've been where you are right now, I faced the same behavior but from what I remember the same user is returned with the event no matter which mediaType it is.
anyways, you still want to handle the event only once

Behavior

Here your problem is that you cannot differentiate the first handleUserPublished run from the second run because when it runs the second time, changes made by the first one are not reflected yet, since the component didn't get the time to rerender so the state is not updated yet. the old story of setState is asynchronous.

Solution

Use a ref, you can think of useRef hook as a javascript regular variable that is updated immediately and does not get initiated when the component renders.
Now life become easier, the idea is each time handleUserPublished runs you check if the user exists in remoteUsersRef.current.
If it is the case, you know it is the second time handleUserPublished is running therefore you do what you have to, and if it is not, you just add the user to remoteUsersRef.current

Note: even if the received user objects don't share the same data as you said, they should share the same id since it is the same user after all. You check the existence based on the id property.


Good to know

You might don't have to use a state for remoteUsers, maybe remoteUsersRef is enough to store remote users, that's what I did.
I see people using state everywhere when a ref is enough. you want to store data in a state only when you have to rerender the component, and so reflect changes in the UI when this data changes.


Global solution

Note: it is also possible that handleUserPublished runs only once (if the user is only publishing audio tracks) so in fact you have to update players state each time and check each time if the user exists so then you just return the same previous value to the state otherwise you add the new data to it:

setPlayers((prev) => {
 let isUserDataExistsInPrev = // return 'true' if user data exists in 'prev', 'false' otherwise
 if(isUserDataExistsInPrev){
   return prev
 }else{
   let dataElement = // create 'dataElement' from 'user' 
   return [...prev, dataElement]
 }
});

You do also the same logic with remoteUsersRef.current, each time you check if the user exists so you do nothing (or just replace it with the new one if you are sure that the second one contains more data) otherwise you add it


Finally, I can't pass without admitting that

setPlayers((prev) => prev)

Is ugly, I know that, but in our particular case here, I didn't find better, I think there's no way around it, and there's no other way to access the state value in real time without using the functional form of setState ‍♂️. anyways, this is not our topic.

Ahmed Sbai
  • 10,695
  • 9
  • 19
  • 38
  • Thank you, I don't believe I didn't think of using a ref this should solve it but I want to use a state for `remoteUsers` to display their names on the chat bar also yeah I am sure the user comming with each event is different – nebil Aug 13 '23 at 18:04
  • 1
    @nebil In my case I used only `players` state, I was following the same tutorial, you still can get remote users names from `players` if you include it there, but you are free, if you want to store remote users in a state you can still do that, you do the same logic, you still have to use the ref to diffirentiate the two runs. finally for the user object, I don't know, maybe I was not using the same Agora version or maybe I missed that but anyways as mentionned you can update the user value in your state/ref with the one provided by the second run. – Ahmed Sbai Aug 13 '23 at 18:17