ReactとTwilio Programmable Videoでカスタムビデオチャットアプリを作成する
読む所要時間: 32 分
このチュートリアルでは、ReactとTwilio Programmable Videoを使用してグループビデオチャット用のWebアプリケーションを作成します。ビデオチャットは友人や家族、同僚とのコミュニケーションに大変便利なツールです。それでは、以下にカスタムビデオチャットアプリを作成する基本手順を紹介します。
必要条件
- Twilioアカウント。アカウント作成方法はヘルプセンターの「Twilioアカウントの作成方法」を参照してください。
- Twilioの電話番号。
- GitHubアカウント。ReactアプリをGitHubページにデプロイするために使用します。
作成するアプリケーションは2つのパートで構成されています。
- React.jsのフロントエンド
- 任意の言語・プラットフォームで作成されたバックエンド。
バックエンドはアクセストークンの生成に使用し、ユーザーに権限を付与します。フロントエンドはTwilio Programmable Video JavaScript SDKを使用してビデオチャットロジックを処理します。バックエンドの作成は本稿では説明しませんが、Node.jsユーザー向けにブログ記事「Twilio Functionsを使ってTwilio Chat、Video、Voice用のアクセストークンを生成する」で詳しく説明しています。
クライアントの作成
Reactアプリをクローンする
プログラムの雛形となるReactアプリケーションをクローンし、スターターファイルと.cssファイルを格納します。
git clone https://github.com/ahl389/twilio-video-starter-kit
twilio-video-starter-kitに移動し、package.jsonファイルにリストされた依存関係をインストールします。以下のコマンドを実行してください。
cd twilio-video-starter-kit
npm install
Package.jsonには、このプロジェクトで使用するnode-sassが含まれています。
twilio-video
もインストールします。以下のコマンドを実行してください。
npm install twilio-video
インストールが完了したら、twilio-video-starter-kit内の/srcフォルダーを確認してみましょう。src配下のファイルのうち、次のファイルに注目してください。
- App.js
- Room.js
- Participant.js
- Track.js
この4つのコンポーネントファイルを直接編集して手順を進めていきます。
アプリの構造
App
コンポーネントはアプリケーションの最上位コンポーネントです。このコンポーネントでアプリケーションのランディングページをコントロールし、ユーザーによる入室と退室のアクションを処理します。このコンポーネントには子コンポーネントのRoom
があります。
Room
コンポーネントはビデオルームのすべての参加者のコンテナです。新しいリモート参加者の入室や退出を検知する役割もあります。このコンポーネントには複数のParticipant
子コンポーネントを含めることができます。
Participant
コンポーネントはそれぞれの参加者のオーディオ/ビデオトラックを管理します。各トラックを表しているのがTrack
子コンポーネントです。
Track
コンポーネントはpropsとして受け取ったトラックの接続とレンダリングを行います。
Appコンポーネントの作成
まずはApp
コンポーネントから開発を始めます。
クローンしたtwilio-video-starter-kitリポジトリのsrcフォルダーにあるApp.jsを開いてください。このファイルに、アプリケーションの主コンポーネントであるApp
コンポーネントのコードが含まれています。このコンポーネントを使用し、ユーザーが名前を入力し、ビデオルームに参加するロビー画面の表示の有無のほか、ビデオルーム自体の表示をコントロールする機能を作成します。
そのほか、Twilioへの接続を開始したり、ビデオルームを作成する役割もあります。
ファイルの先頭にパッケージのインポートや、App
クラスの骨組みが含まれています。
最初にTwilio JavaScript SDKをインポートします。Twilioパッケージからconnect
を特定してインポートする必要があります。以下のハイライトされた行を追加してください。
constructor()メソッド
次に、constructor
メソッドを作成してApp
コンポーネントを初期化し、初期ステートを設定します。constructor
メソッドは新しいコンポーネントが作成されると最初に呼び出されるメソッドです。
App
クラスに次のコードを追加してください。
identity
ステートは空文字列をデフォルト値として設定します。identity
は、ユーザーが名前を入力すると値が更新されます。room
ステートの値はデフォルトでnull
です。ユーザーがJoin Roomボタンをクリックするとビデオルームに接続されます。接続するとTwilioからroom
オブジェクトが返され、コンポーネントのroom
ステートに格納されます。
room
ステートがnull
のときは、ユーザーはビデオルームに参加していない状態で、ロビー画面が表示されます。入室するとステートが変わり、rerenderがトリガーされます。この時点でroom
ステートはnull
でなくなり、ユーザーにビデオルーム画面が表示されます。
入室
次に、ビデオルームに参加するメソッドを追加します。このメソッドは、Join Roomボタンをクリックすると呼び出されます。
App
クラスのconstructor()
メソッドの下に次のコードを追加してください。
joinRoom()
はアクセストークンを取得する非同期関数で、アクセストークンを使用してビデオルームに接続します。つまり、クライアントがビデオルームに接続する前にアクセストークンを返すエンドポイントが必要になります。本稿では、Twilioを使用したビデオチャットアプリ作成のクライアントサイドの手順を主に取り扱うため、アクセストークンの作成には触れません。アクセストークンの生成に関して詳しくは、ブログ記事「Twilio Functionsを使ってTwilio Chat、Video、Voice用のアクセストークンを生成する」を参照してください。エンドポイントの準備ができたら次に進みます。
アクセストークンエンドポイントが使える状態になったところで、上記のコードの3行目のfetch
URLを実際のアクセストークンエンドポイントに修正してください。
ロビーに戻る
joinRoom()
メソッドに続き、returnToLobby()
メソッドを作成します。App
クラスのjoinRoom()
メソッドのすぐ下に次のコードを追加してください。
このメソッドによりroom
ステートをnull
に戻し、再レンダリングをトリガーします。これにより再びロビー画面が表示されます。returnToLobby()
メソッドは、Leave Roomボタンのクリックにより呼び出されます。このボタンは、App
コンポーネントでなくRoom
コンポーネントの子です。後ほど使い方を説明します。
render()メソッド
次に、render()
メソッドを作成します。このメソッドは、コンポーネントのroom
ステートの値を使用し、条件に応じてロビー画面かビデオルームのどちらかを表示します。
メソッドの各部を作成していきましょう。まず、App
クラスにあるreturnToLobby()
メソッドの下に以下のコードを追加してください。
このコードはrender()
メソッドを作成し、そのメソッドから app
クラスの<div>
要素を返します。この<div>
の中で、 コードはroom
ステートがnull
であるかをチェックし、null
であれば lobby
クラスの<div>
を追加します。この<div>
には、ユーザーの識別情報のための<input>
フィールドとJoin Roomボタンを含めます。
null
でなく、ルームに接続した状態であれば、Room
コンポーネントにreturnToLobby
とroom
の2つのpropsを追加します。前者は先に作成したreturnToLobby()
メソッド、後者はTwilioから返された実際のroom
オブジェクトです。
App
コンポーネントのreturnToLobby()
メソッドをpropsとしてRoom
コンポーネントに渡します。これは、Leave Roomをクリックしたときに、Room
コンポーネント内からメソッドを呼び出せるようにするためです。Reactはデフォルトではトップダウンのデータフローをとりますが、このようにステートをリフトアップし、子コンポーネントから親コンポーネントへと上位へ伝搬することができます。
ファイルの先頭で以下のようにRoom
コンポーネントをインポートしてください。
Refの作成
render()
メソッドの基本が分かったところで、もう少し手を加えて完全に機能させます。
レポジトリに含まれるCSSは、ロビーページのすべての文字列を中央合わせにしています。ユーザーが入力フィールドをクリックして名前を入力すると、カーソルがプレースホルダーの中央に表示され、おかしな表示になります。そこで、入力した文字のクリックやフォーカスによりプレースホルダー文字が消えるようにします。
onClick()
属性を以下のように、renderメソッドの<input>
要素に追加してください。
次にremovePlaceholderText()
メソッドを作成します。
returnToLobby()
メソッドとrender()
メソッドの間に、以下のコードを追加してください。
removePlaceholderText() {
this.inputRef.current.placeholder = '';
}
これにより、this.inputRef.current
のプレースホルダーテキストが空の文字列になります。このメソッドを機能させるために、JavaScriptキーワードをバインドします。これはセクションの最後に扱います。
this.inputRef.current
は、ReactのRefへの参照です。Refは、現行コンポーネントにより作成されたDOM要素に直接アクセスできます。this.inputRef
はRef自体への参照であり、.current
プロパティはDOM要素です。
Refを作成して<input>
要素にバインドするために、2つの小さな変更を加えます。
まず、以下のハイライトされた行をconstructorメソッドに追加して、Refを作成してください。
次にrender()
メソッドに戻り、再び<input>
要素を編集してRefを以下のようにバインドしてください。
render()
メソッド、さらにApp
コンポーネント全体を完成させるにはまだステップが残っていますが、App
コンポーネントはプロジェクトで最も複雑なコンポーネントなのでお付き合いください。
identityステートのバインド
アクセストークンを取得するためにまず必要なのがidentity
です。identity
はユーザーが入力する必須の情報です。identity
を入力する前にJoin Roomボタンをクリックすると問題が発生します。
これに対処するために、入力フィールドに何か入力されるまではJoin Roomボタンを無効にしておく必要があります。
render()
メソッドの先頭、return
の前に、次のハイライトされた行を追加してください。
これにより作成されるdisabled
変数は、ブーリアン型のフラグです。identity
ステートが空のときは、true
になります。それ以外はfalse
です。
このフラグと等しい値を持つdisabled
属性を追加します。render()
メソッドの<button>
要素を以下のように編集してください。
あと少しです!
identity
ステートが空のときは、何も入力されていないと前述しました。このため、<input>
フィールドの値とidentity
ステートの値の間に双方向のバインドを作成します。
<input>
フィールドの中身が変わると、<input>
要素に追加したonChange()
属性によりidentity
の値が変わります。このステートが更新されるたびに再レンダリングがトリガーされ、入力内容が消えます。これは問題のある動作であるため、修正するために<input>
要素の値をidentity
ステートにバインドします。
以下のupdateIdentity()
メソッドをremovePlaceholderText()
メソッドの下に追加してください。
さらに、renderメソッドの<input>
要素を以下のように編集してください。
最後に、先に述べたように、特殊なJavaScriptキーワードthis
を新規のメソッドにバインドし、それぞれのメソッドでthis
が適切に使用されるようにします。constructorメソッドの末尾、}
の前に以下のコードを追加してください。
これでApp
コンポーネントが完成しました! 次に、Room
コンポーネントに移ります。
Roomコンポーネントの作成
App.jsファイルを保存して閉じ、srcフォルダーのファイルRoom.jsを開いてください。
constructor()メソッド
Room
クラスに以下のconstructor
メソッドを追加してください。
これにより、Room
コンポーネントが初期化され、remoteParticipants
ステートが設定されます。これは、propsとして渡されたroom
オブジェクトのparticipants
キーから派生する配列値です。一般にReactアプリを構築する場合は、propsからのステート派生は避けたいところですが、以下の2つの理由から使うことにします。
1)設定するデフォルトステートは参加者の入室や退室とともに後で変化する
2)participants
キーの形式はmap
オブジェクトであり、簡単にループできないこと。
ここでは、map
オブジェクトからparticipants
オブジェクトの配列に変更して使用します。
ステートを初期化した後で、this
キーワードをコンポーネントメソッドのleaveRoom()
にバインドします。このメソッドは、propsとして受け取ったreturnToLobby()
メソッドを呼び出します。
componentDidMount()へのイベントリスナーの追加
constructorメソッドの下に、新規メソッドのcomponentDidMount()
を追加します。この特殊なReactライフサイクルメソッドは、コンポーネントが最初に一回だけマウントされる場合のみ呼び出されます。このため、ネットワークリクエストをするときやイベントリスナーを追加するときに最適で、今回の目的にうってつけです。
新たなリモート参加者が入室や退室をするたびに接続イベントが発行されます。このイベントをこのコンポーネントで検知します。
新しい参加者が接続すると、イベントリスナーを介して参加者にアクセスができます。参加者をremoteParticipants
ステートに追加するため、この後使用するaddParticipant()
メソッドを使用します。
同じように、退室するときは、参加者へのアクセスを取得し、remoteParticipants
ステートから削除します。
また、componentDidMount()
によりウィンドウそのものにイベントリスナーが追加されます。ユーザー(ローカルの参加者)がブラウザウィンドウを閉じると、ウィンドウを再読み込みする前に参加者がルームから削除されます。
コンポーネントがUnmountされたときも切断が行われるようにします。以下のメソッドをコンポーネントcomponentDidMount()
の後に追加してください。
addParticipant/removeParticipantメソッド
componentWillUnmount()
メソッドの下に、以下の2つのメソッドを追加してください。
addParticipant(participant) {
console.log(`${participant.identity} has joined the room.`);
this.setState({
remoteParticipants: [...this.state.remoteParticipants, participant]
});
}
removeParticipant(participant) {
console.log(`${participant.identity} has left the room`);
this.setState({
remoteParticipants: this.state.remoteParticipants.filter(p => p.identity !== participant.identity)
});
}
これらのメソッドにより、リモート参加者の接続と切断のたびにコンポーネントのremoteParticipant
ステートを更新します。ステートの変化によりコンポーネントのrerenderがトリガーされ、新しい参加者が画面に表示されるか、画面から削除されます。
leaveRoom()メソッド
Room
コンポーネントのleaveRoom()
メソッドが呼び出されると、まずローカル参加者がルームから切断されます。これによりイベントが発行され、アプリの他のすべての実行中インスタンスで受信されます。
つまり、あなたが他のデバイスを使用する友人とチャットをしているときにLeave Roomボタンを押すと、全員に再レンダリングが実行され、友人たちはあなたを見られなくなります。
この後、App
コンポーネントのreturnToLobby()
メソッドが呼び出され、room
ステートがnull
に戻ります。ローカルユーザーに再レンダリングが実行され、ビデオルームからロビー画面の表示に戻ります。
leaveRoom()
メソッドをRoomコンポーネントに追加します。Room
クラスの閉じ括弧の直前に以下のコードをペーストしてください。
leaveRoom() {
this.props.room.disconnect();
this.props.returnToLobby();
}
render()メソッド
RoomクラスのleaveRoom()
メソッドの下に、以下のrender()
メソッドのコードをペーストしてください。
render() {
return (
<div className="room">
<div className = "participants">
<Participant key={this.props.room.localParticipant.identity} localParticipant="true" participant={this.props.room.localParticipant}/>
{
this.state.remoteParticipants.map(participant =>
<Participant key={participant.identity} participant={participant}/>
)
}
</div>
<button id="leaveRoom" onClick={this.leaveRoom}>Leave Room</button>
</div>
);
}
このメソッドは最初にローカル参加者を表示するため、ユーザーを常にビデオルームの最初の参加者として表示し、remoteParticipants
配列をマップし各リモート参加者を表示します。
render()
メソッドはParticipant
コンポーネントを参照しています。このため、以下のようにコンポーネントをRoom.jsファイルの先頭にインポートしてください。
Participantコンポーネントの作成
Participant
コンポーネントは、各参加者から公開されたトラックをレンダリングし、新しいトラックのサブスクリプションを検知します。
srcフォルダーのファイルParticipant.jsを開いてください。
前述のコンポーネントと同じく、ファイル先頭にインポートと関連クラスのシェル(ここではParticipant
)が見られます。
ここでも、Participant
クラスを作成するためにconstructorメソッドから始めます。
constructor()メソッド
constructorメソッドを作成します。以下のコードをParticipant
クラスに追加してください。
これによりParticipant
コンポーネントが初期化され、その参加者が所有する公開済みトラックによりステートが設定されます。
ローカル参加者の場合、これは入室したときに自動で公開されるオーディオ/ビデオトラックになります。ローカル参加者が入室したときに入室済みのリモート参加者にも、オーディオ/ビデオトラックが設定されます。
ただし、他の参加者が新しい参加者のトラックにアクセスするには、そのトラックへのサブスクリプションが必要になります。サブスクリプションは自動で行われ、その際イベントが発行されます。このため、リモート参加者を表すすべてのコンポーネントで検知する必要があります。
componentDidMount()へのイベントリスナーの追加
イベントリスナーを追加するため、以下のcomponentDidMount()
メソッドをconstructorメソッドの下に作成してください。
このコードは、現在のコンポーネントがローカル参加者用でなければ、トラックのサブスクリプションのイベントリスナーを追加する必要があることを表しています。このユーザーのトラックにサブスクリプションをすることにより、トラックがアクセス可能になり、コンポーネントのtrack
ステートに追加されます。これにより再レンダリングがトリガーされ、アプリがトラックをレンダリングできるようになります。
コンポーネントのステートへのトラックの追加
componentDidMount()
メソッドの下に、ステートを更新する以下のaddTrack()
メソッドを追加してください。
addTrack(track) {
this.setState({
tracks: [...this.state.tracks, track]
});
}
render()メソッド
以下のrender()
メソッドをaddTrack()
メソッドの下に追加してください。
Participant
メソッドを完成させるために、render()
メソッドに含まれるTrack
コンポーネントをインポートする必要があります。Participant.jsファイルの先頭に以下のコードを追加してください。
import React, {Component} from 'react';
import './App.scss';
import Track from './Track';
Trackコンポーネントの作成
いよいよ最後のステップです。このTrack
コンポーネントは、すべての参加者のトラックをDOMに接続する役割をします。Track
コンポーネントはtrack
オブジェクトをpropsとして受け取ります。
srcフォルダー配下のTrack.jsを開いてください。
ファイルにはいくつかのインポートとTrack
クラスのシェルが含まれています。
まず、constructorメソッドを追加します。Track
クラスに以下のコードを追加してください。
App
コンポーネントのときと同じように、TrackコンポーネントのRefも作成します。これは、このコンポーネントが作成するDOM要素にアクセスしてトラックを接続するために使用します。
constructorメソッドの下に、以下のcomponentDidMount()
メソッドを追加してください。
componentDidMount() {
if (this.props.track !== null) {
const child = this.props.track.attach();
this.ref.current.classList.add(this.props.track.kind);
this.ref.current.appendChild(child)
}
}
このメソッドは、propsに含まれるtrack
の値がnull
でないことをチェックし、null
でなければRefをもとにtrack
オブジェクトに関連するオーディオ/ビデオ要素をDOMに接続します。
render()メソッド
最後にrender()
メソッドを追加します。componentDidMount()
メソッドの下に、以下のようにTrackコンポーネントのrender()
メソッドを追加してください。
お疲れ様です。アプリケーションの開発が完了しました。
テスト
すべてのファイルを保存し、閉じてください。コマンドプロンプトにて、プロジェクトのルートフォルダであるtwilio-video-starter-kitに移動してください。
次に進む前に、アクセストークンを生成するバックエンドコードの実行も確認してください。
次のコマンドを実行してローカルのReactサーバーを起動します。以下のコマンドを実行してください。
アプリケーションが実行されたら、ブラウザでlocalhost:3000(あるいは別の使用中のポート)にアクセスしてください。
アプリのロビー画面が表示されます。
名前を入力すると、Join Roomボタンが有効になります。
Join Roomをクリックしてビデオルームに入室してください。カメラにあなたが表示されているはずです。
ブラウザの新しいタブを開き、再びlocalhost:3000(あるいはその他のポート)にアクセスしてください。今度は別の名前を入力してJoin Roomボタンをクリックしてください。
どちらかのタブでLeave Roomボタンをクリックするとロビー画面に戻ります。タブを切り替えて、参加者が退室したことを確認します。
まとめ
この記事では、Twilio Programmable Videoを使用したReactの基本的なビデオチャットアプリの作成方法を学びました。Reactの機能やProgrammable Videoを利用したアプリケーションの作成方法をご理解いただけたかと思います。
また、ビデオアプリに見栄えのするバーチャルミラーも追加できます。どんなアプリを作っているか、Twitterでお知らせください!
Ashleyは、TwilioブログのJavaScriptエディターです。Ashleyと協力し、Twilioにテクニカルストーリーを紹介するには、Twitter[@ahl389](https://twitter.com/ahl389)までご連絡ください。TwitterでAshleyが見つからない場合は、どこかのパティオでコーヒーを飲んでいることでしょう(ワインの時間かも)。