[React×Agora] Webアプリ上でビデオ通話機能を簡単に実装してみたくなりますよね？

# はじめに
皆さん、初めまして！DevToxicClubに所属している@takakouと申します。
普段はフルリモートで地方勤務をしており、動画配信プラットフォームのフロントエンドエンジニアとして働いています。趣味は猫とのんびり過ごすことと、ドライブです🚗✨

今回は「**[React×Agora] Webアプリ上でビデオ通話機能を実装してみたくなりますよね？**」というテーマで記事を書いてみました。この記事では、初めてSDK(今回はAgora)を使ってビデオ通話機能を実装する方に向けて、手順をわかりやすく紹介します！まずは、この記事を書こうと思った経緯からお話しします。


# 経緯
この記事を書いた経緯としては、「自分のプロダクトにビデオ通話機能を組み込んでみたい」と思ったことです。
ただ、1からWebSocketを使って実装するのは大変な工数がかかりますし、予算が限られたプロダクトだと難しいですよね。

そんな時、便利なのがSaaS。とはいえ、コストが気になります。開発段階から課金が発生するのは少しハードルが高いと感じました。

そこで今回は、無料プランが用意されているAgoraを利用してみることにしました。Agoraを使えば、ビデオ通話機能を簡単に実装できます。この記事では、Agoraを活用したWebアプリへの通話機能の導入方法をイメージしやすい形で紹介していきます。

# 対象者
今回の記事の対象となるのは、下記の知識がある方々になります。
- React,Viteの知識がある方
- ある程度、独走でフロントエンドの実装ができる方

上記の条件を設けたのは、本記事では、フロントエンドの基礎から解説をするつもりがないからです。
もし本記事が気になってはいるが、フロントエンド周りの技術や内容がわからないという方は、下記の記事が初心者向けでわかりやすいと思います。ある程度知識をつけていただいたうえで記事の内容を読み進めていただけると幸いです。

https://ja.react.dev/learn

## 参考にして進める資料
下記の資料を参考に進めていくことにします。
基本的には下記の資料に沿って進めますが、補足や説明などは適時、自分の方で翻訳した内容を書いていきます。

https://docs.agora.io/en/video-calling/get-started/get-started-sdk?platform=react-js

# 環境について
今回開発を行っていく環境について記述しておきます。

| |バージョン等|
|----|-----|
|PC|Desktop PC|
|CPU|AMD Ryzen 7 5700X|
|RAM|32GB|
|OS| windows11|
|node| v20.10.0|


# Agoraの初期設定
Agoraは利用するのにアカウントの設定などが必要になります。
下記のページにアクセスし登録を進めていきましょう。

https://sso2.agora.io/en/v6/signup?redirectUri=https%3A%2F%2Fsso2.agora.io%2Fapi%2Fv0%2Foauth%2Fauthorize%3Fresponse_type%3Dcode%26client_id%3Dconsole%26redirect_uri%3Dhttps%253A%252F%252Fconsole.agora.io%252Fapi%252Fv2%252Foauth%252Fen%26scope%3Dbasic_info

アクセスすると下記のような綺麗な新規登録ページが表示されると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/a983ec88-c08c-bc65-8358-9f5cc334bdcf.png)



**10,000 free minutes per month. No credit card required.**
と書いてある通り、10000分までは無料で、クレジットカードは不要とのことです。
クレジットカードが最初に必要なサービスって結構あるのでここに関してはとても安心ですね。

とりあえず必要な項目を入力して「**Create Account**」を押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/ca822132-8c16-0116-452c-5749fad40ce0.png)

Gmailに認証コードが送られているのでそれを入力して「**Verify**」を押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/e2ae84ba-fef5-33ee-6a23-a675afaba525.png)

アンケートの項目が出てくるのですが、この選択で何か大きく設定が変更されるとかはないので自分に適した項目をチェックしておきましょう。
選択し終わったら「**Start Buildding**」を押下します。

そうすると下記のようなかっこいいアカウントページに飛べると思います。
自分はダークモードを常に設定しているのですが、他のモードであれば多分色が違うと思います。
チュートリアルに関するPopupが出たり、勝手にページ遷移したりしますが、基本的にskipで良いと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/33d55a7e-b4ce-55fd-06fb-f9e43b13443b.png)

# 実装
それでは実装を進めていきたいと思います。

## プロジェクトの用意
下記のコマンドでプロジェクトをセットアップしましょう。
プロジェクト名は正直何でもよいですが、agoraのサイトと合わせて`agora-sdk-quickstart`としておきます。

```sh:Terminal
npm create vite@latest agora-sdk-quickstart -- --template react-ts
```

問題なく実行出来たらどんなテンプレートなのか気になるのでまず起動してみることにします。

```sh:Terminal
cd agora-sdk-quickstart
npm install
npm run dev
```

はい、いつも見る景色が広がってたらOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/538c0bc8-ab44-3a29-d4f5-36664d958c8f.png)

その後、AgoraのSCKをimportします。
```sh:Terminal
npm i agora-rtc-react
```

プロジェクトのルートディレクトリにある、`index.html`を下記のように修正します。
```html:index.html
<!Doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Agora React Quickstart</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
```

## 初期化処理
AgoraのSDKが提供しているProviderを設定します。
AgoraのSDKを利用するうえで必須な記述となるので忘れずに書いておきます。

```jsx:main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import AgoraRTC, { AgoraRTCProvider } from "agora-rtc-react";
import './index.css'
import App from './App.tsx'

const client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <AgoraRTCProvider client={client}>
      <App />
    </AgoraRTCProvider>
  </StrictMode>
)

```


## メインの処理
```tsx:App.tsx
import {
  LocalUser,
  RemoteUser,
  useIsConnected,
  useJoin,
  useLocalMicrophoneTrack,
  useLocalCameraTrack,
  usePublish,
  useRemoteUsers,
} from "agora-rtc-react";
import { useState } from "react";

import "./index.css";

export const Basics = () => {
  const [calling, setCalling] = useState(false);
  const isConnected = useIsConnected();
  const [appId, setAppId] = useState(""); 
  const [channel, setChannel] = useState(""); 
  const [token, setToken] = useState("");

  useJoin({appid: appId, channel: channel, token: token ? token : null}, calling);
  //local user
  const [micOn, setMic] = useState(true);
  const [cameraOn, setCamera] = useState(true);
  const { localMicrophoneTrack } = useLocalMicrophoneTrack(micOn);
  const { localCameraTrack } = useLocalCameraTrack(cameraOn);
  usePublish([localMicrophoneTrack, localCameraTrack]);
  //remote users
  const remoteUsers = useRemoteUsers();

  return (
    <>
      <div className="room">
        {isConnected ? (
          <div className="user-list">
            <div className="user">
              <LocalUser
                audioTrack={localMicrophoneTrack}
                cameraOn={cameraOn}
                micOn={micOn}
                videoTrack={localCameraTrack}
                cover="https://www.agora.io/en/wp-content/uploads/2022/10/3d-spatial-audio-icon.svg"
              >
                <samp className="user-name">You</samp>
              </LocalUser>
            </div>
            {remoteUsers.map((user) => (
              <div className="user" key={user.uid}>
                <RemoteUser cover="https://www.agora.io/en/wp-content/uploads/2022/10/3d-spatial-audio-icon.svg" user={user}>
                  <samp className="user-name">{user.uid}</samp>
                </RemoteUser>
              </div>
            ))}
          </div>
        ) : (
          <div className="join-room">
            {/* <img alt="agora-logo" className="logo" src={logo} /> */}
            <input
              onChange={e => setAppId(e.target.value)}
              placeholder="<Your app ID>"
              value={appId}
            />
            <input
              onChange={e => setChannel(e.target.value)}
              placeholder="<Your channel Name>"
              value={channel}
            />
            <input
              onChange={e => setToken(e.target.value)}
              placeholder="<Your token>"
              value={token}
            />

            <button
              className={`join-channel ${!appId || !channel ? "disabled" : ""}`}
              disabled={!appId || !channel}
              onClick={() => setCalling(true)}
            >
              <span>Join Channel</span>
            </button>
          </div>
        )}
      </div>
      {isConnected && (
        <div className="control">
          <div className="left-control">
            <button className="btn" onClick={() => setMic(a => !a)}>
              <i className={`i-microphone ${!micOn ? "off" : ""}`} />
            </button>
            <button className="btn" onClick={() => setCamera(a => !a)}>
              <i className={`i-camera ${!cameraOn ? "off" : ""}`} />
            </button>
          </div>
          <button
            className={`btn btn-phone ${calling ? "btn-phone-active" : ""}`}
            onClick={() => setCalling(a => !a)}
          >
            {calling ? <i className="i-phone-hangup" /> : <i className="i-mdi-phone" />}
          </button>
        </div>
      )}
    </>
  );
};

export default Basics;
```

```css:App.css
#root {
  max-width: 1280px;
  margin: 0 auto;
  padding: 2rem;
  text-align: center;
}

.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
  filter: drop-shadow(0 0 2em #61dafbaa);
}

@keyframes logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: no-preference) {
  a:nth-of-type(2) .logo {
    animation: logo-spin infinite 20s linear;
  }
}

.card {
  padding: 2em;
}

.read-the-docs {
  color: #888;
}

```

全て記述し終わったら試しに起動してみましょう。
```sh:Terminal
npm run dev
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/04037612-a71f-eb29-ba19-fd7cf2ef6ed0.png)


## Agora側で必要な値を発行
Agoraのサイトから、今手元で起動したサイトに出ている、`app ID`,`channel Name`,`token`を取得して入力します。

Agoraのコンソールで新しいプロジェクトを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/14a599c6-51a5-d9ec-2347-96d705d02779.png)

作成したプロジェクトに`App ID`をメモしておき、generate Temp Tokenをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/e38e5015-e10a-c3c1-1592-b7efb7cc9ace.png)

適当なChannel Nameを入力して、tokenを発行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/4755bbea-a768-7a83-e1b7-9bc546b58813.png)

自分で起動したサイトに必要な値を入寮くしてjoin Channelをして下記の画面になればOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/82edcd0c-1849-8ff8-89ea-de277a9ac8cc.png)

おなじ流れでもう一つページを開いてそちらでも入室してみます。


相手拠点の映像が出てくれば完璧です。
僕のデスクトップPCはカメラがないので映りませんが、通常映るはずです！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524017/fed44551-00ef-7590-e26e-f0475f7a5351.png)

## まとめ
今回はSDKを使ったビデオ通話の実装をしてみました。
本来なら`app ID`,`channel Name`,`token`の取得はバックエンドでの処理を通して取得したりするのが王道かもしれませんが、今回はquick startと同じ形式で進めてみました。

ビデオ通話は1から実装するとかなり複雑な話になりますが、SDKを使うことで簡単に実装を行えることが伝わればいいなと思います。


## おわりに
ここまで読んでいただきありがとうございます。
久々に記事を書いたのでかなり見づらい箇所があったかと思います。ぜひコメントでご指摘いただけると幸いです。
普段は、自分のブログにて技術関連の話題を発信したりしています。
お時間のある方はこちらのブログも見ていっていただけると幸いです。

https://kt-tech.blog