WebRTC와 Agora SDK (하)

myung hun kang·2023년 2월 20일
0

음 ....

맞다 너무 오랜만에 이 주제를 마무리하는 것 같다. ㅜㅜ

그동안 여러 일이 있었는데 여튼 바빴고, 이 글을 미루고 미뤘다. 반성해야겠다. 😭

그럼 바로 들어가겠다.


🌐 Room.tsx

내 앱에서 처음 RTC를 사용하는 부분이 바로 Room.tsx 파일이다. 여기서 video와 message를 사용하기 위한 셋팅을 하게되고, 셋팅이 마무리되면 video와 message 작업을 하는 컴포넌트로 넘어가게 된다.

  const [isLoading, setIsLoading] = useState(false);
  const [start, setStart] = useState(false);


  const client = useClient();

  const { ready, tracks } = MicrophoneAndCameraTracks();

우선 위의 셋팅을 해야한다. state 값들은 video와 message를 사용할 수 있는지 여부를 저장하는 상태이고,

밑에 부분이 중요하다.

지난 글에서 셋팅한 셋팅 값을 불러오는 값이다.

  const client = useClient(); // 생성된 유저 불러오기
  const { ready, tracks } = MicrophoneAndCameraTracks();
	// 유저의 비디오 & 오디오 값 가져오고 비디오 오디오가 준비 되었는지 ready 값 가져오기

다음 값이 사용 가능해지면 그때 video & message 셋팅을 useEffect에서 시작한다.


  useEffect(() => {
    const init = async (roomName: string, currentUser: UserDataNId) => {
      try {
        setIsLoading(true);
        // client roomName 방에 입장
        const clientUid = await client.join(
          config.appId,
          roomName,
          config.token,
          null
        );
        //비디오 & 오디오 방안 사람들과 공유.
        if (tracks) await client.publish([tracks[0], tracks[1]]);

        
        // 메세지 유저정보 만들기
        const RTMclient = AgoraRTM.createInstance(config.appId);
        await RTMclient.login({ uid: String(clientUid) });
        // 현재 user의 displayName 넣기
        await RTMclient.addOrUpdateLocalUserAttributes({
          name: currentUser.displayName,
        });
        // roomName 방에 채널을 만들기
        const rtmChannel = RTMclient.createChannel(roomName);
        // 방에 입장
        await rtmChannel.join();

        if (clientUid && rtmChannel && tracks) {
          const localUser: {
            user: IAgoraRTCClient;
            tracks: [ICameraVideoTrack, IMicrophoneAudioTrack];
          } = {
            user: client,
            tracks: [tracks[1], tracks[0]],
          };
		
         // 만들어진 유저와 rtm channel 그리고 rtmclient는 redux에 저장한다. 
          dispatch(setLocalUser(localUser));
          dispatch(setChannel(rtmChannel));
          dispatch(setRtmClient(RTMclient));
          setStart(true);
        }
        setIsLoading(false);
      } catch (error) {
        console.log(`Room UseEffect has Error!!! : ${error}`);
      }
    };
    
    
    if (ready && tracks && client && currentUser) {
      console.log("Room starting point", roomId, client);
      if (roomId) {
        init(roomId, currentUser);
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [roomId, client, ready, tracks, currentUser]);

현재 유저가 있고 위 client, ready, tracks 값이 셋팅이 완료되었을 때 roomId가 있으면

init 함수를 실행한다.

init에서는 client의 여러 함수를 사용 방에 입장, 비디오 오디오 공유등의 작업을 한다.

추가적으로 rtm 을 쓰기위해

const RTMclient = AgoraRTM.createInstance(config.appId);

도 만들어준다.

RTC의 client는 이미 useClient로 만들었지만 RTM은 RTC의 client의 uid 값이 있어야 rtc의 client와 동일한 유저의 rtmClient를 만들 수 있기 때문에 위 useEffect에서 만들게 되었다.

위 작업으로 RTMClient랑 rtmchannel까지 만들어졌으면 값들을 redux에 저장하고

맨위에 있는 setStart값을 true로 바꿔서 video와 message에 리스너를 다는 components로 넘어가게 해준다.

 return (
    <RoomContainer>
      <VideoCallContainer>
   // 화상채팅 컴포넌트
        {ready && tracks && <VideoCall />}
      </VideoCallContainer>

      {start && (
        <>
   // 메세지 컴포넌트 
          <MessageCallContainer
            className={messageShow ? "show" : "hide"}
          >
            <MessageCall />
          </MessageCallContainer>
        </>
      )}
    </RoomContainer>
  );

📸 videoCall.tsx

videoCall 컴포넌트에서는 실질적으로 rtc client에 즉 현재 나의 video와 audio 에 설정을 더해주는 일을 한다.

또한 새로 channel에 들어오는 유저나 나가는 유저의 video와 audio 값을 듣는 일을 한다.

여기서도 위와 마찬가지로

const client = useClient();

을 하고 useEffect에서 각 상황별로 리스너를 만든다.


 useEffect(() => {
    const init = async () => {
      // remote user가 들어오고 나가고 할 때 event handler
      client.on("user-published", async (user, mediaType) => {
        try {
          await client.subscribe(user, mediaType);

          if (mediaType === "video") {
            dispatch(addRtcUser(user));
          }
          if (mediaType === "audio") {
            user.audioTrack?.play();
          }

        } catch (error) {
          console.log("rtc subscribe fail", error);
        }
      });

      client.on("user-unpublished", async (user, mediaType) => {
        try {
          await client.unsubscribe(user, mediaType);
        } catch (error) {
          console.log("rtc unpublished error", error);
        }
      });

      client.on("user-left", (user) => {
        dispatch(removeRtcUser(user));
      });
      setStart(true);
      setIsLoading(false);
    };

    if (localUser) {
      console.log("VideoCall point", localUser);
    }
    // localUser가 들어오기 전에 이미 존재하는 유저를 구독해야 하기 때문에
    init();
  }, [localUser, client, dispatch]);

"user-published", "user-unpublished", "user-left" 와 같은 상황에 리스너를 달고

실제 video 화면을 생성하면 구현은 마무리된다.

return (
    <Fragment>
      {localUser && localUser.tracks && <Controls />}
      {start && localUser && localUser.tracks && <Videos />}
    </Fragment>
  );

Controls 컴포넌트처럼 화면에 다양한 조작을 하는 기능을 넣고 싶으면 유저의 track에 접근해서 구현 할 수 있다.


⌨️ MessageCall.tsx

RTM 또한 위 videoCall처럼 구현하면 된다.
다만

  const channel = useSelector(selectRtmChannel);
  const rtmClient = useSelector(selectRtmClient);
  const messages = useSelector(selectRtmMessages);

저장했던 rtm 관련 값들을 사용한다는 차이가 있다.

rtm에서는 rtmClient보다 channel에서 message를 듣는 과정을 처리하는 일을 많이 한다.

rtm에 리스너를 다는 useEffect는 다음과 같다.


 useEffect(() => {
    const init = async (channel: RtmChannel, rtmClient: RtmClient) => {
      channel.on("MemberJoined", async (MemberId) => {
        try {
          const { name } = await rtmClient.getUserAttributesByKeys(MemberId, [
            "name",
          ]);
          if (name) {
            const botMessageData = makeBotMessage("join", name);
            if (botMessageData) dispatch(addMessages(botMessageData));
          }
   
        } catch (error) {
          console.log("Rtm Member join error", error);
        }
      });

      channel.on("MemberLeft", async (MemberId) => {
        try {
          const botMessageData = makeBotMessage("left");
          if (botMessageData) dispatch(addMessages(botMessageData));
        } catch (error) {
          console.log("Rtm MemberLeft error", error);
        }
      });

      channel.on("ChannelMessage", async (messageData, MemberId) => {
        try {
          if (messageData.messageType === "TEXT") {
            let data = JSON.parse(messageData.text);
            const reciveMessageData = {
              ...data,
              from: MESSAGE_TYPE.other,
            };
            dispatch(addMessages(reciveMessageData));
          }
          scrollToBottom();
        } catch (error) {
          console.log(error);
        }
      });
    };

    if (channel && rtmClient) {
      init(channel, rtmClient);
    }
  }, [channel, rtmClient, dispatch]);

"ChannelMessage" 만 들어도 message를 주고 받는 것은 가능하다.

"MemberJoined"와 "MemberLeft"를 듣는 이유는 채팅방 안에 특정 유저가 나가거나 들어오게 되면 간단한 봇 메세지를 보여주기 위함이다.


마치며

처음으로 자력으로 개발한 앱이기에 여러 부분에서 문제가 많다.

typescript로 변환하는 작업을 진행했지만 type도 제대로 지정을 했는지 의문이다... 🤔 (뭐 any는 없으니 그나마 다행이다 ㅠㅠ)

여튼 agora.io의 SDK는 계속해서 업그레이드되고 있어서 혹시나 agora의 rtc & rtm을 사용해보려 한다면

내 글은 정말 참고 정도만 해주면 좋겠다.

재미있는건 이 앱을 만들고 몇달이 지난후 나몰래 조용히 앱을 이용해본 외국인들이 조금 있다는 것!!!
개선점이나 후기정도 github에 남겨주고 가시지 참... ㅋㅋㅋ

여튼 그래도 시리즈가 마무리되어 다행이다.

더 양질의 글을 쓰려고 노력하도록 하겠다.

위 앱의 github주소와 agora에서 예시로 제공하는 react를 이용한 rtc앱 github주소를 남기며 마무리하겠다.



나의 앱 github주소 : https://github.com/michoball/video-chat-app

agora의 example 주소 : https://github.com/AgoraIO-Community/Agora-RTC-React/blob/master/example/src/App.tsx

profile
프론트엔드 개발자입니다.

0개의 댓글