2020.12.23

Twilio Programmable VideoとReactで簡単なビデオチャットを作成してみた

はじめに

こんにちは。上川です。

今回は、Twilio Programmable VideoとReactで簡単なビデオチャットを作成してみます。
結構簡単にビデオチャットが作成できますので、皆さんもぜひ試していただけたらと思います。

この記事は、twilio Advent Calendar 2020の16日目の記事です。

設定手順

今回はこちらのTwilio社の公式ブログを参考に簡単なビデオチャットを事前に作成しました。 上記ブログでは、穴埋め方式でコードを書きながら処理を理解できます。

今回は上記ブログで作成したものを試す方法を説明します。 以下手順です。

  • Git Hubリポジトリのクローン
  • API Key作成
  • アクセストークンを生成するバックエンド側の作成
  • フロントエンド側の設定

Git Hubリポジトリのクローン

まず、完成版のこちらをクローンしてください。

git clone https://github.com/kkamikawa/twilio-video-starter-kit-complete

API Key作成

こちらからAPI KeyをTypeはStandardで作成します。

その際にAPIの「SID」と「SECRET」は後で使用するため控えておきます。

アクセストークンを生成するバックエンド側の作成

こちらを参考にTwilio cliからアクセストークンを生成するserverless projectを作成しデプロイします。 少し手順が多いのですが、ここが完了すればあと少しです。

①次のディレクトリへ移動します。

cd twilio-video-starter-kit-complete\token-service

②環境変数のファイルをコピーします。

copy .env.sample .env

③コピーした.envに以下の値を設定します。

※ACCOUNT_SIDとAUTH_TOKENは、こちらのLIVE Credentialsの欄にあります。

ACCOUNT_SID=LIVE Credentialsの欄

AUTH_TOKEN=LIVE Credentialsの欄

API_KEY=先ほど作成した値

API_SECRET=先ほど作成した値

④TwilioのCLIをインストールしていない方は次のコマンドを打ってください。※もし、インストールがお済の方は⑦の手順からお願い致します。

npm install twilio-cli -g

⑤そして、Twilio Serverless Toolkit pluginを追加します。

twilio plugins:install @twilio-labs/plugin-serverless

⑥完了したら、デプロイ先の認証情報を設定します。

twilio login

⑦次のコマンドでデプロイします。表示されているFunctionsのURL(https://token-service-×××-dev.twil.io)をコピーしておきます。

twilio serverless:deploy

⑧curlなどでアクセストークンが取得できれば成功です。

curl token-service-XXXX-dev.twil.io/token

デプロイしたものは以下になります。
トークンを発行し、Videoの権限を与えています。
exports.handler = function(context, event, callback) {
    //環境変数からTwilio認証情報を読み込む
    const {ACCOUNT_SID, API_KEY, API_SECRET} = context;
    
    //呼び出し時に渡されたパラメータ
    const {identity} = event;

    //token発行
    const AccessToken = Twilio.jwt.AccessToken;
    const token = new AccessToken(
        ACCOUNT_SID,
        API_KEY,
        API_SECRET,
        {identity: identity}
    );

    //tokenにProgrammable Videoへのアクセス許可を与える
    const VideoGrant = AccessToken.VideoGrant;
    const videoGrant = new VideoGrant({
        room: 'cool room' // 特定のルームに制限可能、例えばeventとして渡して動的に切り替えも可能
    });
    token.addGrant(videoGrant);

    //レスポンス作成
    const response = new Twilio.Response();
    const headers = {
        "Access-Control-Allow-Origin": "*", // クライアント側のURLに変更してください
        "Access-Control-Allow-Methods": "GET,PUT,POST,DELETE,OPTIONS",
        "Access-Control-Allow-Headers": "Content-Type",
        "Content-Type": "application/json"
    };

    response.setHeaders(headers);
    response.setBody({
        accessToken: token.toJwt()
    });

    return callback(null, response);
}
token.js

フロントエンド側の設定

twilio-video-starter-kit-completeのディレクトリへ移動します。

①以下のコマンドでnpmライブラリをインストールします。

npm install

②環境変数のファイルをコピーします。

copy .env.sample .env

③コピーした.envファイルに、デプロイしたときに表示されていたFunctionsのURLを以下のようにして入れます。 もしくは、こちらからも参照可能です。

REACT_APP_ENDPOINT="https://token-service-XXXX-dev.twil.io"

作成したアプリは以下です。

コンポーネントの階層はApp>Room>Paticipant>Trackとなっています。

Appではアクセストークンを取得し、最終的にTrackでVideoとAudioを描画しています。
import './App.scss';
import React, {Component} from 'react';
import Room from './Room';
const { connect } = require('twilio-video');

class App extends Component {
    constructor(props){
        super(props)

        this.state = {
            identity: '',
            room: null
        }

        this.inputRef = React.createRef();

    }

    //ルームに参加する
    joinRoom = async () => {
        try {
            //作成したアクセストークン取得のエンドポイントを指定
            const {REACT_APP_ENDPOINT} = process.env;
            const response = await fetch(`${REACT_APP_ENDPOINT}/token?identity=${this.state.identity}`);
            const data = await response.json();
            const room = await connect(data.accessToken, {
            name: 'cool-room',//今回はテストなのでルーム名は固定
            audio: true,
            video: true
            });

            this.setState({ room: room });
        } catch(err) {
            console.log(err);
        }
    }

    //ルーム名を空にしてロビー画面にもどる
    returnToLobby = () => {
        this.setState({ room: null });
    }

    //入力時にプレースホルダーを空にする
    removePlaceholderText = () => {
        this.inputRef.current.placeholder = '';
    }

    //入力時にStateにidentityとして入力値をセット
    updateIdentity = (event) => {
        this.setState({
            identity: event.target.value
        });
    }

    render() {
        //ユーザーが名前を入力したときのみボタンを有効にするためのフラグ
        const disabled = this.state.identity === '' ? true : false;

        return (
            <div className="app">
            {
                this.state.room === null
                ? <div className="lobby">
                    <input
                        value={this.state.identity} 
                        onChange={this.updateIdentity} 
                        ref={this.inputRef}
                        onClick={this.removePlaceholderText}
                        placeholder="名前を入力してください"/>
                    <button
                        disabled={disabled}
                        onClick={this.joinRoom}>
                        ルームに参加
                    </button>
                </div>
                : <Room returnToLobby={this.returnToLobby} room={this.state.room} />
            }
            </div>
        );
    }

}

export default App;
App.js
import React, {Component} from 'react';
import './App.scss';
import Participant from './Participant';

class Room extends Component {
    constructor(props) {
        super(props);

        this.state = {
            //参加者一覧をmapオブジェクトから配列に変更する
            remoteParticipants: Array.from(this.props.room.participants.values())
        }

    }

    //コンポーネントがマウント時に一回呼ばれる
    componentDidMount() {
        // 参加者が接続・切断された時のためのイベントリスナーを設置
        this.props.room.on('participantConnected', participant => this.addParticipant(participant));
        this.props.room.on('participantDisconnected', participant => this.removeParticipant(participant));

        //ローカル参加者がブラウザウィンドウを閉じると、参加者がルームから削除される
        window.addEventListener("beforeunload", this.leaveRoom);
    }

    //コンポーネントがアンマウントされたときにも切断
    componentWillUnmount() {
        this.leaveRoom();
    }

    //新規参加者をStateに追加
    addParticipant = (participant) => {
        console.log(`${participant.identity} `);

        this.setState({
            remoteParticipants: [...this.state.remoteParticipants, participant]
        });
        }

    //退出者をStateから除外
    removeParticipant = (participant) => {
        console.log(`${participant.identity} 退`);

        this.setState({
            remoteParticipants: this.state.remoteParticipants.filter(p => p.identity !== participant.identity)
        });
    }

    //ルームを退出
    leaveRoom = () => {
        this.props.room.disconnect();
        this.props.returnToLobby();
    }

    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}>ルームから退出</button>
            </div>
        );
    }

}

export default Room;
Room.js
import React, {Component} from 'react';
import './App.scss';
import Track from './Track';

class Participant extends Component {
    constructor(props) {
        super(props);
        
        //既存のトラック
        const existingPublications = Array.from(this.props.participant.tracks.values());
        const existingTracks = existingPublications.map(publication => publication.track);
        const nonNullTracks = existingTracks.filter(track => track !== null)
        
        this.state = {
            tracks: nonNullTracks
        }
    }

    componentDidMount() {
        if (!this.props.localParticipant) {
            //トラックが登録された時のイベントリスナー
            this.props.participant.on('trackSubscribed', track => this.addTrack(track));
        }
    }

    addTrack = (track) => {
        this.setState({
            tracks: [...this.state.tracks, track]
        });
    }

    render() {
        return (
            <div className="participant" id={this.props.participant.identity}>
            <div className="identity">{ this.props.participant.identity}</div>
            {
                this.state.tracks.map(track =>
                <Track key={track} filter={this.state.filter} track={track}/>)
            }
            </div>
        );
    }
}

export default Participant;
Participant.js
import React, {Component} from 'react';
import './App.scss';

class Track extends Component {
    constructor(props) {
        super(props)
        this.ref = React.createRef();
    }

    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)
        }
    }

    render() {
        return (
            <div className="track" ref={this.ref}>
            </div>
        )
    }
}

export default Track;
Track.js

結果

設定が終わったら、以下のコマンドでローカルでアプリを起動します。

npm start

適当に名前を入れて、2つのタブで開けば完成です。

作成したビデオチャット

補足

デフォルトの場合、RoomのTypeがGroupに設定されます。 例えば、無料のTypeであるGoに変更したい場合はこちらのコンソールからROOM TYPEをGoに選択することで、2名までという制限はありますが無料で試せます。

まとめ

今回は、Twilio Programmable VideoとReactで簡単なビデオチャットを作成してみましたが、いかがでしたでしょうか。比較的簡単に実装することができたかと思います。
Twilio Programmable Videoを使用することで、柔軟にビデオチャットアプリケーションを作成できると思います。またいろいろと試してみたいです。
最後まで読んでいただきありがとうございました。

Twilioの導入をご検討の方は、お気軽にご相談ください。
28 件
     
  • banner
  • banner

関連する記事