React HooksによるTwilioビデオチャットの構築方法

以前、本ブログでReactを使用したビデオチャット (英文記事) について取り上げましたが、その後 (Reactの) バージョン16.8においてHooksがリリースされました。Hooksによって、クラスでコンポーネントを記述する代わりに関数型のコンポーネント中にStateや他のReactの機能を使用できるようになります。

本稿では、useState, useCallback, useEffect, useRef Hooksによる関数型コンポーネントのみを使用して、Twilio VideoとReactによるビデオチャットアプリケーションを構築します。

必要なもの

このビデオチャットアプリケーションの構築には、以下のものが必要になります。

上記がひととおり揃ったら、開発環境の準備をしていきます。

早速始めてみる

筆者の作成したReact and Express Starter appを使用して、すぐに開発をはじめることができます。 新規のディレクトリーにこのスターターアプリケーションの「twilio」ブランチをダウンロードまたはクローンして、依存関係をインストールしてください。

git clone -b twilio git@github.com:philnash/react-express-starter.git 
twilio-video-react-hooks
cd twilio-video-react-hooks
npm install

.env.exampleファイルを.envという名前でコピーします。

cp .env.example .env

アプリケーションを実行して、すべて問題なく動作しているか確認してください。

npm run dev

下図のようなページがブラウザーにロードされることを確認してください。

Twilioのクレデンシャルを準備する

Twilio Videoに接続するには、いくつかのクレデンシャル (認証情報) が必要になります。
TwilioコンソールからアカウントSIDをコピーして、.envファイルのTWILIO_ACCOUNT_SIDに入力してください。
また、APIキーおよびシークレットも必要になります。 これらは、コンソール内のProgrammable Videoツール内で作成することができます。
キーペアを作成し、SIDおよびシークレットを.envファイルのTWILIO_API_KEYおよびTWILIO_API_SECRETにそれぞれ追加してください。

スタイルを追加する

本稿ではCSSについて取り上げるわけではありませんが、若干のCSSを追加して見栄えを少し良くしておくことをオススメします。
src/App.cssファイルの内容をこちらのURLのCSSをコピーしたものに置き換えます。

これで開発の準備が整いました。

コンポーネントの計画を練る

ヘッダーおよびフッター、そしてVideoChatコンポーネントをレイアウトするAppコンポーネントから始めます。
ViddeoChatコンポーネント内ではユーザーが名前と参加したいルームを入力できるLobbyコンポーネントを表示します。
ユーザーがこれらを入力し終えたら、このLobbyコンポーネントからルームへの接続とビデオチャットの参加者を表示するRoomコンポーネントへと置き換えます。
最後に、ルーム内の各参加者用のメディアの表示を処理するParticipantコンポーネントをレンダリングします。

コンポートネントの構築

Appコンポーネント

src/App.jsファイルを開くと、初期状態のサンプルアプリケーションのコードが多くあるのでこれをまず削除します。
また、Appコンポーネントはクラスベースのコンポーネントです。
冒頭で今回のアプリケーション全体を関数型で構築すると説明したように変更することをオススメします。
インポートのセクションから、Componentおよびlogo.svgのインポートを取り除いてください。
続いてAppクラス全体を、今回のアプリケーションの骨組みをレンダリングする関数に置き換えます。
ファイル全体は以下のようになります。

import React from 'react';
import React from 'react';
import './App.css';

const App = () => {
  return (
    <div className="app">
      <header>
        <h1>Video Chat with Hooks</h1>
      </header>
      <main>
        <p>VideoChat goes here.</p>
      </main>
      <footer>
        <p>
          Made with{' '}
          <span role="img" aria-label="React">
            ⚛
          </span>{' '}
          by <a href="https://twitter.com/philnash">philnash</a>
        </p>
      </footer>
    </div>
  );
};

export default App;

VideoChatコンポーネント

このコンポーネントはユーザーがユーザー名およびルーム名を入力したかどうかに応じてロビーまたはルームを表示します。
src/VideoChat.jsという名前で新しいコンポーネントファイルを作成し、下記のボイラープレートコードをペーストするところから始めてください。

import React from 'react';

const VideoChat = () => {
  return <div></div> // we'll build up our response later
};

export default VideoChat;

VideoChatコンポーネントはチャットに関連するデータを扱うための最上位階層のコンポーネントになります。
ここではチャットに参加するユーザーのユーザー名、接続先のルームのルーム名、そしてサーバーから取得したアクセストークンを保存しておくことが必要になります。
続くコンポーネントではこうしたデータを入力するためのフォームを組み立てていくことになります。

React Hooksでは、useState Hookを使用してこのデータ保存を行います。

useState

useStateはStateの初期値を単一引数に取り、そして現在のState、およびこのStateを更新するための関数を含む配列を返す関数です。
この配列を分割代入 (Destructuring) すると、stateおよびsetStateといったような2つの個別の変数に分けることができます。
ここではsetStateを使用して、コンポーネント内のユーザー名、ルーム、およびトークンを捕捉します。
まずはReactからuseStateをインポートして、ユーザー名、ルーム、そしてトークンをセットアップします。

import React, { useState } from 'react';

const VideoChat = () => {
  const [username, setUsername] = useState('');
  const [roomName, setRoomName] = useState('');
  const [token, setToken] = useState(null);

  return <div></div> // we'll build up our response later
};

次に、ユーザーがusernameおよびroomNameを対応するinput要素に入力した際に、これを処理するための2つの関数が必要になります。

import React, { useState } from 'react';

const VideoChat = () => {
  const [username, setUsername] = useState('');
  const [roomName, setRoomName] = useState('');
  const [token, setToken] = useState(null);

  const handleUsernameChange = event => {
    setUsername(event.target.value);
  };

  const handleRoomNameChange = event => {
    setRoomName(event.target.value);
  };

  return <div></div> // we'll build up our response later
};


これはこれで動作しますが、useCallbackという別のReact Hookを使用してコンポーネントを最適化することができます。

useCallback

この関数型コンポーネントが呼び出されると、handle〜という関数が毎回再定義されます。
これら関数はsetUsernameおよびsetRoomName関数に依存するためコンポーネントの一部である必要がありますが、その内容は毎回同じになります。
useCallbackは、関数をメモ化しておくことを可能にするReact Hookです。

useCallbackはメモ化対象の関数、および関数の依存関係を含む配列という2つの引数を取ります。
関数の依存関係のいずれかが変更されると、メモ化された関数は期限切れと見なされ、関数は再定義されて再びメモ化されます。
ここでは2つの関数にはいずれも依存関係がないため、空の配列を渡すだけで問題ありません (useState HookのsetState関数は、関数内の定数と考えることができます) 。
関数を書き換えるには、useCallbackをファイルの先頭部分のインポートに追加し、続いて対象の各関数をuseCallbackで囲みます。

import React, { useState, useCallback } from 'react';

const VideoChat = () => {
  const [username, setUsername] = useState('');
  const [roomName, setRoomName] = useState('');
  const [token, setToken] = useState(null);

  const handleUsernameChange = useCallback(event => {
    setUsername(event.target.value);
  }, []);

  const handleRoomNameChange = useCallback(event => {
    setRoomName(event.target.value);
  }, []);

  return <div></div> // we'll build up our response later
};

ユーザーがフォームを送信すると、ユーザー名とルーム名をサーバーに送信して、ルームに入室するためのアクセストークンと交換しましょう。これを行う関数もコンポーネント内に作成します。
ここではfetch APIを使用してエンドポイントに対してJSONデータを送信し、レスポンスを受信、パースを行い、そしてsetToken関数を使用してStateにトークンを保存します。 この関数もまた、useCallbackで囲むことになりますが、今回usernameroomNameに依存しますので、これらをuseCallbackに対して依存関係として追加します。

const handleRoomNameChange = useCallback(event => {
    setRoomName(event.target.value);
  }, []);

  const handleSubmit = useCallback(async event => {
    event.preventDefault();
    const data = await fetch('/video/token', {
      method: 'POST',
      body: JSON.stringify({
        identity: username,
        room: roomName
      }),
      headers: {
        'Content-Type': 'application/json'
      }
    }).then(res => res.json());
    setToken(data.token);
  }, [username, roomName]);

  return <div></div>// we'll build up our response later
};

このコンポーネントの最後の関数として、ログアウト機能を追加しましょう。
これでユーザーをルームから退出させてロビーに戻します。
これを行うにはトークンをnullに設定します。
ここでもこの関数をuseCallbackで囲みます。依存関係はありません。

const handleLogout = useCallback(event => {
    setToken(null);
  }, []);

  return  <div></div> // we'll build up our response later
};

このコンポーネントの仕事の大半は配下のコンポーネントを取りまとめることです。
そのためこれらコンポーネントを作成するまではレンダリング内容はごくわずかです。
続いて、ユーザー名とルーム名の入力用フォームをレンダリングするLobbyコンポーネントを作成することにしましょう。

Lobbyコンポーネント

src/Lobby.jsという名前の新規ファイルを作成します。
このコンポーネントでは親であるVideoChatコンポーネントに対して全イベントを渡すため、それ自体にデータを一切保存しておく必要はありません。
コンポーネントのレンダリング時に、usernanmeおよびroomName、そしてこれらの変更とフォーム送信を処理する関数を渡します。
これらのpropsを分割代入して、後々使いやすくなるようにしておくことができます。
Lobbyコンポーネントの主な仕事は、下記のようにこれらのpropsを使用するフォームをレンダリングすることです。

import React from 'react';

const Lobby = ({
  username,
  handleUsernameChange,
  roomName,
  handleRoomNameChange,
  handleSubmit
}) => {
  return (
    <form onSubmit={handleSubmit}>
      <h2>Enter a room</h2>
      <div>
        <label htmlFor="name">Name:</label>
        <input
          type="text"
          id="field"
          value={username}
          onChange={handleUsernameChange}
          required
        />
      </div>

      <div>
        <label htmlFor="room">Room name:</label>
        <input
          type="text"
          id="room"
          value={roomName}
          onChange={handleRoomNameChange}
          required
        />
      </div>
      <button type="submit">Submit</button>
    </form>
  );
};

export default Lobby;

videoChatコンポーネントを更新して、トークンを持っていない場合はLobbyをレンダリングしてください。
トークンがある場合はusernameroomName、そしてtokenをレンダリングします。
ここではファイルの冒頭でLobbyコンポーネントをインポートし、コンポーネント関数の下部にJSXをレンダリングすることが必要になります。

import React, { useState, useCallback } from 'react';
import Lobby from './Lobby';

const VideoChat = () => {
  // ...

  const handleLogout = useCallback(event => {
    setToken(null);
  }, []);
  
  let render;
  if (token) {
    render = (
      <div>
        <p>Username: {username}</p>
        <p>Room name: {roomName}</p>
        <p>Token: {token}</p>
      </div>
    );
  } else {
    render = (
      <Lobby
         username={username}
         roomName={roomName}
         handleUsernameChange={handleUsernameChange}
         handleRoomNameChange={handleRoomNameChange}
         handleSubmit={handleSubmit}
      />
    );
  }
  return render;
};

これをページ上に表示させるには、VideoChatコンポーネントをAppコンポーネントにインポートし、レンダリングを行うことが必要です。 src/App.jsを再度開いて、下記の変更を行ってください。

import React from 'react';
import './App.css';
import VideoChat from './VideoChat';

const App = () => {
  return (
    <div className="app">
      <header>
        <h1>Video Chat with Hooks</h1>
      </header>
      <main>
        <VideoChat />
      </main>
      <footer>
        <p>
          Made with{' '}
          <span role="img" aria-label="React">
            ⚛️
          </span>{' '}
          by <a href="https://twitter.com/philnash">philnash</a>
        </p>
      </footer>
    </div>
  );
};

export default App;

アプリケーションがまだ実行中である (またはnpm run devコマンドで再起動している) ことを確認し、ブラウザーでページを開くとフォームが確認できます。
ユーザー名とルーム名を入力して送信を行うと、入力された各種の名前とサーバーから取得されたトークンの表示に変更されることとなります。

Roomコンポーネント

アプリケーションにユーザー名とルーム名を追加したので、これらを使用してTwilio Videoのチャットルームに参加できるようになります。
Twilio Videoサービスと連携するには、下記コマンドを使用したJavaScript SDKのインストールが必要になります。

npm install twilio-video --save

srcディレクトリーにRoom.jsという名前の新規ファイルを作成します。
下記のボイラープレートコードから始めていきます。
このコンポーネントではTwilio Video SDKとuseStateおよびuseEffect Hookを使用します。 
またroomNametoken、そしてhandleLogoutを親コンポーネントからpropsとして取得します。

import React, { useState, useEffect } from 'react';
import Video from 'twilio-video';

const Room = ({ roomName, token, handleLogout }) => {

});

export default Room;


コンポーネントがまず行うことは、tokenroomNameを使用したTwilio Videoサービスへの接続です。
接続が行われると、Stateとして保存すべきroomオブジェクトを取得します。
roomには時間の経過に伴って変化する参加者のリストも含まれますので、これも保存しておく必要があります。
それにはuseStateを使用します。 初期値はルームに対してはnull、参加者に対しては空の配列を使用します。

const Room = ({ roomName, token, handleLogout }) => {
  const [room, setRoom] = useState(null);
  const [participants, setParticipants] = useState([]);
});

ルームに参加できるようになるにするため、このコンポーネントに何かレンダリングしてください。
participants配列に対してmap関数を適用し、各参加者の識別子を表示し、加えてルーム内のローカル参加者の識別子も表示します。

const Room = ({ roomName, token, handleLogout }) => {
  const [room, setRoom] = useState(null);
  const [participants, setParticipants] = useState([]);

  const remoteParticipants = participants.map(participant => (
    <p key={participant.sid}>participant.identity</p>
  ));

  return (
    <div className="room">
      <h2>Room: {roomName}</h2>
      <button onClick={handleLogout}>Log out</button>
      <div className="local-participant">
        {room ? (
          <p key={room.localParticipant.sid}>{room.localParticipant.identity}</p>
        ) : (
          ''
        )}
      </div>
      <h3>Remote Participants</h3>
      <div className="remote-participants">{remoteParticipants}</div>
    </div>
  );
});


VideoChatコンポーネントを更新して、これまでのプレースホルダー情報の代わりにRoomコンポーネントをレンダリングしてください。

import React, { useState, useCallback } from 'react';
import Lobby from './Lobby';
import Room from './Room';

const VideoChat = () => {
  // ...

  const handleLogout = useCallback(event => {
    setToken(null);
  }, []);
  
  let render;
  if (token) {
    render = (
      <Room roomName={roomName} token={token} handleLogout={handleLogout} />
    );
  } else {
    render = (
      <Lobby
         username={username}
         roomName={roomName}
         handleUsernameChange={handleUsernameChange}
         handleRoomNameChange={handleRoomNameChange}
         handleSubmit={handleSubmit}
      />
    );
  }
  return render;
};


これをブラウザーで実行するとルーム名とログアウトボタンが表示されますが、まだルームに接続および参加をしていないため、参加者の識別子は表示されません。

すでにルームへの参加に必要な情報をすべて持っているので、コンポーネントの初回レンダリング時に接続のアクションをトリガーさせます。 またコンポーネントの破棄時には (これ以上WebRTC接続をバックグラウンドで維持しておく意味がないため) 、ルームからの退出を行った方が良いです。 これら2つの処理には副作用があります。 クラスベースのコンポーネントでは、このような状況ではcomponentDidMountおよびcomponentWillUnmountライフサイクルメソッドが使用されますが、React HooksではuseEffect Hookを使用します。

useEffect

useEffectは関数 (メソッド) を引数に取り、これはコンポーネントがレンダリングされると実行されます。
ここではコンポーネントのロード時にビデオサービスに接続を行うので、ルームへの参加者の入退室が行われる際にStateからの参加者の追加と削除をそれぞれ実行できる関数も必要になります。
Room.jsファイル内のJSXの手前に、下記のコードを追加してHookの組み込みを始めてください。

useEffect(() => {
    const participantConnected = participant => {
      setParticipants(prevParticipants => [...prevParticipants, participant]);
    };
    const participantDisconnected = participant => {
      setParticipants(prevParticipants =>
        prevParticipants.filter(p => p !== participant)
      );
    };
    Video.connect(token, {
      name: roomName
    }).then(room => {
      setRoom(room);
      room.on('participantConnected', participantConnected);
      room.on('participantDisconnected', participantDisconnected);
      room.participants.forEach(participantConnected);
    });
  });

ここではtokenroomNameを使用してTwilio Videoサービスへの接続を行っています。
接続が完了すると、room Stateの設定、接続または切断してくる参加者に対するリスナーのセットアップ、そして参加者がいる場合はそれらをループで回して、先に記述したparticipantConnected関数を使用してparticipants配列Stateへの追加を行います。
ここまでは順調ですが、コンポーネントが削除されてもルームには接続されたままです。 そのためクリーンアップについても自前で行わなければなりません。

useEffectに渡したコールバックの戻り値として関数を返すと、この関数はコンポーネントがアンマウントされるタイミングで実行されます。 useEffectを使用するコンポーネントが再レンダリングされるとき、この関数もコールされ再実行前にEffectのクリーンアップが行われます。
接続済みの場合にルームから切断する関数を返します。

Video.connect(token, {
      name: roomName
    }).then(room => {
      setRoom(room);
      room.on('participantConnected', participantConnected);
      room.participants.forEach(participantConnected);
    });

    return () => {
      setRoom(currentRoom => {
        if (currentRoom && currentRoom.localParticipant.state === 'connected') {
          currentRoom.disconnect();
          return null;
        } else {
          return currentRoom;
        }
      });
    };
  });

ここでは先のuseStateから取得した、コールバック版のsetRoom関数を使用していることに注目してください。
setRoomに対して関数を渡すと、この関数は直前の値を引数にコールされるので、ここではcurrentRoomという名前にしてください。
一方、関数から返される値は新たなStateとして設定されます。
しかし、これでまだ終わりではありません。

現状では再レンダリングのたびに毎回、コンポーネントは参加済みのルームから退出してから再接続します。
これは理想的ではないので、クリーンナップを行い、Effectを再度実行すべきタイミングを指示してあげる必要があります。
ちょうどuseCallbackで行なったように、依存関係のある変数の配列を渡してこれを行います。
変数の値が変更されたらまずクリーンアップを行い、それからEffectを再実行します。
変更がなかった場合はEffectの再実行は必要ありません。
関数を見てみると、他のルームに、あるいは別のユーザーとして接続する場合には、roomNameまたはtokenの変更が見込まれることが分かります。 これら変数をuseEffectに配列として渡すことにしましょう。


   return () => {
      setRoom(currentRoom => {
        if (currentRoom && currentRoom.localParticipant.state === 'connected') {
          currentRoom.disconnect();
          return null;
        } else {
          return currentRoom;
        }
      });
    };
  }, [roomName, token]);

このEffectにおいては、コールバック関数を2つ定義していることに注目してください。
これを先に行ったようにuseCallbackで囲む必要があるのではないかとお考えになるかもしれませんが、それは違います。
これらはEffectの一部であるため、依存関係の更新時のみに実行されるためです。
また、Hookの中でコールバック関数を使用することはできません。 これらは必ず、コンポーネント内から直接、あるいはカスタムHookから使用されなければなりません。
これでこのコンポーネントはほぼ出来上がりです。 アプリケーションをリロードし、ユーザー名とルーム名を入力して、ここまで問題なく動作するか確認してください。
ルームに入室すると識別子の表示が確認できます。 ログアウトボタンをクリックするとロビーに戻ります。

残る作業はビデオ通話の参加者のレンダリングと、そのビデオおよびオーディオのページへの追加です。

Participantコンポーネント

srcディレクトリーにParticipant.jsという名前で新しいコンポーネントを作成します。
ここでもこれまでと同様のボイラープレートコードからスタートします。
このコンポーネントでは3つのHookを使用します。 このうちuseStateuseEffectについてはすでに見てきました。 残る1つはuseRefです。
またpropsのparticipantオブジェクトも渡し、useStateを用いて参加者のビデオおよびオーディオを状態を捕捉しています。

import React, { useState, useEffect, useRef } from 'react';

const Participant = ({ participant }) => {
  const [videoTracks, setVideoTracks] = useState([]);
  const [audioTracks, setAudioTracks] = useState([]);
};

export default Participant;

参加者からビデオまたはオーディオストリームを取得した際には、これを <video> および <audio> 要素に紐付けることが必要になります。
JSXは宣言的 (Declarative) であるため、DOM (Document Object Model) への直接アクセスを行うことはできません。
そのため別の方法でHTML要素への参照を取得することが必要です。
ReactではrefsおよびuseRef HookによってDOMへのアクセスを提供しています。 Refsを使用するには、前もってこれを宣言し、JSX内で参照することが必要です。 ここでは、レンダリング前にuseRef Hookを使用してrefsを作成します。

const Participant = ({ participant }) => {
  const [videoTracks, setVideoTracks] = useState([]);
  const [audioTracks, setAudioTracks] = useState([]);

  const videoRef = useRef();
  const audioRef = useRef();
 });

ここでは必要なJSXを返します。 JSXの要素をrefに接続するには、ref属性を使用します。

const Participant = ({ participant }) => {
  const [videoTracks, setVideoTracks] = useState([]);
  const [audioTracks, setAudioTracks] = useState([]);

  const videoRef = useRef();
  const audioRef = useRef();

  return (
    <div className="participant">
      <h3>{participant.identity}</h3>
      <video ref={videoRef} autoPlay={true} />
      <audio ref={audioRef} autoPlay={true} muted={true} />
    </div>
  );
 });

また <video> および <audio> タグの属性をautoplay (これにより、メディアストリームを取得するとすぐに再生が開始されます) およびmuted (これでテスト中にハウリングが発生して難聴に見舞われることもなくなります。 この過ちを犯したとき、きっと筆者に感謝すること請け合いです) に設定します。

Effectの使用がいくつか必要なため、このコンポーネントはまだあまり役に立ちません。
実際にはこのコンポーネントではuseEffect Hookを3回使用します。
その理由については、下記で説明していきます。
最初のuseEffect Hookでは、Stateにビデオおよびオーディオトラックを設定し、トラックの追加または削除用にparticipantオブジェクトに対してリスナーをセットアップします。
またコンポーネントのアンマウント時にはこれらリスナーをクリーンアップおよび削除し、Stateを空にすることも必要になります。
最初のuseEffect Hookでは、トラックがparticipantから追加または削除されたときに実行される2つの関数を追加します。 これらの関数はいずれも、トラックがオーディオであるかビデオであるかを確認し、それから関連するState関数を使用して、これをStateから追加または削除します。

const videoRef = useRef();
  const audioRef = useRef();

  useEffect(() => {
    const trackSubscribed = track => {
      if (track.kind === 'video') {
        setVideoTracks(videoTracks => [...videoTracks, track]);
      } else {
        setAudioTracks(audioTracks => [...audioTracks, track]);
      }
    };

    const trackUnsubscribed = track => {
      if (track.kind === 'video') {
        setVideoTracks(videoTracks => videoTracks.filter(v => v !== track));
      } else {
        setAudioTracks(audioTracks => audioTracks.filter(a => a !== track));
      }
    };

    // more to come


続いてparticipantオブジェクトを使用して、オーディオとビデオトラック用の初期値を設定します。
先ほど記述した関数を使用してtrackSubscribedおよびtrackUnsubscribedイベントへのリスナーをセットアップ、そして返却される関数内でクリーンアップを行います。

useEffect(() => {
    const trackSubscribed = track => {
      // implementation
    };

    const trackUnsubscribed = track => {
      // implementation
    };

    setVideoTracks(Array.from(participant.videoTracks.values()));
    setAudioTracks(Array.from(participant.audioTracks.values()));

    participant.on('trackSubscribed', trackSubscribed);
    participant.on('trackUnsubscribed', trackUnsubscribed);

    return () => {
      setVideoTracks([]);
      setAudioTracks([]);
      participant.removeAllListeners();
    };
  }, [participant]);

  return (
    <div className="participant">


Hookはparticipantオブジェクトにのみ依存しているため、これが変更されないかぎりクリーンアップは実行されない点に注目してください。
また、useEffect Hookを使用してビデオおよびオーディオトラックをDOMに紐づけることも必要になります。
ここではそのビデオバージョンだけお見せしますが、オーディオについても同様です。 
Videoaudioに置き換えてください。
HookはStateから最初のビデオトラックが存在すればこれを取得し、あらかじめrefによってキャプチャーされたDOMノードに紐付けます。
VideoRef.currentを使用してref中の現在のDOMノードを参照することができます。
ビデオトラックを紐づける場合、クリーンアップ時にこれを紐付け解除する関数を返却することも必要になります。

}, [participant]);

  useEffect(() => {
    const videoTrack = videoTracks[0];
    if (videoTrack) {
      videoTrack.attach(videoRef.current);
      return () => {
        videoTrack.detach();
      };
    }
  }, [videoTracks]);

  return (
    <div className="participant">


audioTracksに対しても同様にHookを用意すれば、RoomコンポーネントからParticipantコンポーネントに対してレンダリングを行う準備が整います。
Participantコンポーネントをファイル冒頭でインポートし、識別子表示用の段落を実際のコンポーネントに置き換えます。

import React, { useState, useEffect } from 'react';
import Video from 'twilio-video';
import Participant from './Participant';

// hooks here

  const remoteParticipants = participants.map(participant => (
    <Participant key={participant.sid} participant={participant} />
  ));

  return (
    <div className="room">
      <h2>Room: {roomName}</h2>
      <button onClick={handleLogout}>Log out</button>
      <div className="local-participant">
        {room ? (
          <Participant
            key={room.localParticipant.sid}
            participant={room.localParticipant}
          />
        ) : (
          ''
        )}
      </div>
      <h3>Remote Participants</h3>
      <div className="remote-participants">{remoteParticipants}</div>
    </div>
  );
});

ここでアプリケーションをリロードすれば、ルームに参加してご自身の映像を画面上で確認できます。
別のブラウザーを開いて同じルームに参加すれば、もうひとつ同じ映像を確認できます。
ログアウトボタンをクリックすればロビーに戻ることができます。

まとめ

Reactを使用したTwilio Videoの構築には、対処すべきあらゆる副作用が存在するため、いくらかの追加作業が必要になります。
トークン取得のためのリクエストの発行からVideoサービスへの接続、<video> および <audio> 要素へ接続するためのDOM操作まで、考慮すべきポイントが多々あります。
本稿ではuseStateuseCallbackuseEffect、およびuseRefを使用してこれらの副作用を制御し、関数型のコンポーネントのみを使用してアプリケーションを構築する方法について見てきました。

本稿がTwilio Video、そしてReact Hooks双方の理解の助けになれば幸いです。
このアプリケーションの全ソースコードはGitHub上で公開されていますので、実際に触って理解を深めてみてください。

React Hooksについてのさらに詳しい読み物としては、非常に詳細な公式ドキュメント、Hooksの仕組みが視覚的に解説されているthinking in hooks (英文記事)、そしてDan Abramov氏のDeep dive into useEffect (長い英文記事ですが相応の価値のあるものです) を参照してください。

Twilio Videoを使用した開発について学習したい場合は、Switching cameras during a Video chat または、Add screen sharing to your Twilio Video application (いずれも英文記事) をご覧ください。

Reactを使用してこれらのアプリケーション、あるいは他のクールなビデオチャット機能を構築したなら、ぜひ筆者宛にTwitter上のコメントやphilnash@twilio.comのメールでお知らせください。

この記事をシェア

最新記事

すべての記事へ