Twilio Video API + Twilio Function で 1対1 ビデオ通話を実装する

Twilio Video API + Twilio Function で 1対1 ビデオ通話を実装する

Twilio Video SDK を使って、ブラウザ上で動作するビデオ通話アプリケーションを構築する方法を、実際に動作するサンプルコードとともに詳しく説明します。WebRTC を使ったリアルタイム通信を手軽に実装したい開発者におすすめの内容です。
2025.09.22

はじめに

本記事では、Twilio Video API を使用して最小限の構成で 1対1 ビデオ通話機能を実装する方法を説明します。Twilio Function でバックエンド処理を行い、Pure JavaScript でフロントエンドを構築することで、シンプルかつ効率的なビデオ通話システムを構築できます。

Twilio とは

Twilio は、開発者向けの通信プラットフォームサービスです。SMS、音声通話、ビデオ通話などの通信機能を API 経由で簡単にアプリケーションに組み込めます。豊富な SDK と充実したドキュメントにより、複雑な通信インフラを自前で構築することなく、高品質な通信機能を実装できます。

Twilio Video とは

Twilio Video は、リアルタイムビデオ通話機能を提供するサービスです。WebRTC 技術をベースとし、ブラウザやモバイルアプリ間でのビデオ・音声通話を実現します。Room という概念で参加者を管理し、複数の参加者が同一の Room に接続することで通話が成立します。

主な特徴は以下の通りです。

  • WebRTC ベースの高品質な映像・音声伝送
  • クロスプラットフォーム対応 (Web、iOS、Android)
  • 自動的なメディアリレーとファイアウォール越え
  • レコーディング機能

対象読者

  • JavaScript の基本的な文法を理解している
  • HTTP/HTTPS の概念を理解している
  • REST API の使用経験がある
  • ビデオ通話機能をアプリケーションに組み込みたい開発者

参考

構成

  1. Room 作成: ブラウザから Twilio Function を呼び出し、Video Room を作成
  2. Access Token 取得: 参加者ごとに Twilio Function から Access Token を取得
  3. Room 接続: Access Token を使用して Twilio Video Room に接続
  4. メディアストリーム: WebRTC 経由で参加者間のビデオ・音声を伝送

実装

Twilio Function の作成

Twilio Console で Functions を作成し、Room の作成と Access Token の生成を行います。

Environment Variables の設定

Twilio Console の Functions > Configuration で以下の環境変数を設定します:

			
			TWILIO_ACCOUNT_SID: あなたの Account SID
TWILIO_API_KEY_SID: API Key SID
TWILIO_API_KEY_SECRET: API Key Secret

		

Environment Variables

video-token Function の作成

			
			// /video-token
exports.handler = async function(context, event, callback) {
  // CORS対応のレスポンスオブジェクトを作成
  const response = new Twilio.Response();
  response.appendHeader('Content-Type', 'application/json');
  response.appendHeader('Access-Control-Allow-Origin', '*');
  response.appendHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
  response.appendHeader('Access-Control-Allow-Headers', 'Content-Type');

  // OPTIONSメソッド(プリフライトリクエスト)への対応
  if (event.httpMethod === 'OPTIONS') {
    response.setStatusCode(200);
    response.setBody({});
    return callback(null, response);
  }

  try {
    const AccessToken = Twilio.jwt.AccessToken;
    const VideoGrant = AccessToken.VideoGrant;

    // リクエストパラメータの取得
    const identity = event.identity;
    const roomName = event.roomName;

    if (!identity || !roomName) {
      response.setStatusCode(400);
      response.setBody({ 
        error: 'identity と roomName が必要です' 
      });
      return callback(null, response);
    }

    // 環境変数の確認
    if (!context.ACCOUNT_SID || !context.TWILIO_API_KEY_SID || !context.TWILIO_API_KEY_SECRET) {
      response.setStatusCode(500);
      response.setBody({ 
        error: '環境変数が正しく設定されていません' 
      });
      return callback(null, response);
    }

    // Access Token の作成
    const token = new AccessToken(
      context.ACCOUNT_SID,
      context.TWILIO_API_KEY_SID,      
      context.TWILIO_API_KEY_SECRET,   
      { identity: identity }
    );

    // Video Grant の追加
    const videoGrant = new VideoGrant({
      room: roomName
    });

    token.addGrant(videoGrant);

    // 成功レスポンス
    response.setStatusCode(200);
    response.setBody({
      token: token.toJwt(),
      identity: identity,
      roomName: roomName
    });

    callback(null, response);

  } catch (error) {
    console.error('Token生成エラー:', error);
    response.setStatusCode(500);
    response.setBody({ 
      error: 'Token生成に失敗しました',
      details: error.message
    });
    callback(null, response);
  }
};

		

create-room Function の作成

			
			// /create-room
exports.handler = async function(context, event, callback) {
  // CORS対応のレスポンスオブジェクトを作成
  const response = new Twilio.Response();
  response.appendHeader('Content-Type', 'application/json');
  response.appendHeader('Access-Control-Allow-Origin', '*');
  response.appendHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
  response.appendHeader('Access-Control-Allow-Headers', 'Content-Type');

  // OPTIONSメソッド(プリフライトリクエスト)への対応
  if (event.httpMethod === 'OPTIONS') {
    response.setStatusCode(200);
    response.setBody({});
    return callback(null, response);
  }

  try {
    const client = context.getTwilioClient();
    const roomName = event.roomName;

    if (!roomName) {
      response.setStatusCode(400);
      response.setBody({ 
        error: 'roomName が必要です' 
      });
      return callback(null, response);
    }

    // Room の作成
    const room = await client.video.v1.rooms.create({
      uniqueName: roomName,
      type: 'group'
    });

    // 成功レスポンス
    response.setStatusCode(200);
    response.setBody({
      roomSid: room.sid,
      roomName: room.uniqueName,
      status: room.status
    });

    callback(null, response);

  } catch (error) {
    console.error('Room作成エラー:', error);

    // Room が既に存在する場合
    if (error.code === 53113) {
      response.setStatusCode(200);
      response.setBody({
        message: 'Room は既に存在します',
        roomName: roomName
      });
    } else {
      response.setStatusCode(500);
      response.setBody({ 
        error: 'Room作成に失敗しました',
        details: error.message
      });
    }

    callback(null, response);
  }
};

		

作成した Function の Visibility を Public に設定します。

フロントエンド実装

HTML ファイルの作成

			
			<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Twilio Video 1対1 通話</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
        }

        .container {
            max-width: 800px;
            margin: 0 auto;
        }

        .controls {
            margin-bottom: 20px;
        }

        input, button {
            margin: 5px;
            padding: 10px;
        }

        .video-container {
            display: flex;
            gap: 20px;
            flex-wrap: wrap;
        }

        video {
            width: 300px;
            height: 200px;
            background-color: #000;
            border: 1px solid #ccc;
        }

        .local-video {
            border: 2px solid blue;
        }

        .remote-video {
            border: 2px solid green;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Twilio Video 1対1 通話</h1>

        <div class="controls">
            <input type="text" id="roomNameInput" placeholder="Room 名を入力" />
            <input type="text" id="identityInput" placeholder="あなたの名前を入力" />
            <button id="createRoomBtn">Room 作成</button>
            <button id="joinRoomBtn">Room 参加</button>
            <button id="leaveRoomBtn" disabled>Room 退出</button>
        </div>

        <div id="status"></div>

        <div class="video-container">
            <div>
                <h3>あなたの映像</h3>
                <video id="localVideo" class="local-video" muted autoplay playsinline></video>
            </div>
            <div>
                <h3>相手の映像</h3>
                <video id="remoteVideo" class="remote-video" autoplay playsinline></video>
            </div>
        </div>
    </div>

    <script src="https://sdk.twilio.com/js/video/releases/2.28.1/twilio-video.min.js"></script>
    <script src="app.js"></script>
</body>
</html>

		

JavaScript ファイルの作成

			
			// app.js
const TWILIO_FUNCTION_BASE_URL = 'https://your-function-domain.twil.io'; // あなたの Function URL に置き換え
// 注意: 本記事では説明を簡単にするため、Twilio Function の URL を直接コードに記載していますが、本番環境では Vercel の環境変数などの方法で管理してください。

let currentRoom = null;
let localTracks = [];

// DOM 要素の取得
const roomNameInput = document.getElementById('roomNameInput');
const identityInput = document.getElementById('identityInput');
const createRoomBtn = document.getElementById('createRoomBtn');
const joinRoomBtn = document.getElementById('joinRoomBtn');
const leaveRoomBtn = document.getElementById('leaveRoomBtn');
const statusDiv = document.getElementById('status');
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');

// イベントリスナーの設定
createRoomBtn.addEventListener('click', createRoom);
joinRoomBtn.addEventListener('click', joinRoom);
leaveRoomBtn.addEventListener('click', leaveRoom);

// ステータス表示関数
function updateStatus(message) {
    statusDiv.textContent = message;
    console.log(message);
}

// Room 作成
async function createRoom() {
    const roomName = roomNameInput.value.trim();

    if (!roomName) {
        alert('Room 名を入力してください');
        return;
    }

    try {
        updateStatus('Room を作成中...');

        const response = await fetch(`${TWILIO_FUNCTION_BASE_URL}/create-room`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: `roomName=${encodeURIComponent(roomName)}`
        });

        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        const data = await response.json();
        updateStatus(`Room "${data.roomName}" を作成しました (SID: ${data.roomSid})`);

    } catch (error) {
        console.error('Room 作成エラー:', error);
        updateStatus('Room の作成に失敗しました: ' + error.message);
    }
}

// Room 参加
async function joinRoom() {
    const roomName = roomNameInput.value.trim();
    const identity = identityInput.value.trim();

    if (!roomName || !identity) {
        alert('Room 名と名前を入力してください');
        return;
    }

    try {
        updateStatus('Access Token を取得中...');

        // Access Token の取得
        const response = await fetch(`${TWILIO_FUNCTION_BASE_URL}/video-token`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: `identity=${encodeURIComponent(identity)}&roomName=${encodeURIComponent(roomName)}`
        });

        // レスポンスのエラーチェック
        if (!response.ok) {
            const errorText = await response.text();
            console.error('API Error:', response.status, errorText);
            throw new Error(`API Error: ${response.status}`);
        }

        // JSONパース
        const data = await response.json();

        // tokenの存在チェック
        if (!data.token) {
            throw new Error('Token が取得できませんでした');
        }

        updateStatus('ローカルメディアを取得中...');

        // ローカルメディアの取得
        localTracks = await Twilio.Video.createLocalTracks({
            audio: true,
            video: true
        });

        // ローカル映像の表示
        const localVideoTrack = localTracks.find(track => track.kind === 'video');
        if (localVideoTrack) {
            localVideo.srcObject = new MediaStream([localVideoTrack.mediaStreamTrack]);
        }

        updateStatus('Room に接続中...');

        // Room への接続
        currentRoom = await Twilio.Video.connect(data.token, {
            name: roomName,
            tracks: localTracks
        });

        updateStatus(`Room "${roomName}" に接続しました`);

        // UI の更新
        createRoomBtn.disabled = true;
        joinRoomBtn.disabled = true;
        leaveRoomBtn.disabled = false;

        // Room イベントの設定
        setupRoomEvents(currentRoom);

        // 既存の参加者の処理
        currentRoom.participants.forEach(participantConnected);

    } catch (error) {
        console.error('Room 参加エラー:', error);
        updateStatus('Room への参加に失敗しました: ' + error.message);

        // ローカルトラックのクリーンアップ
        localTracks.forEach(track => track.stop());
        localTracks = [];
    }
}

// Room 退出
function leaveRoom() {
    if (currentRoom) {
        currentRoom.disconnect();
        currentRoom = null;
    }

    // ローカルトラックの停止
    localTracks.forEach(track => track.stop());
    localTracks = [];

    // 映像の初期化
    localVideo.srcObject = null;
    remoteVideo.srcObject = null;

    // UI の更新
    createRoomBtn.disabled = false;
    joinRoomBtn.disabled = false;
    leaveRoomBtn.disabled = true;

    updateStatus('Room から退出しました');
}

// Room イベントの設定
function setupRoomEvents(room) {
    // 参加者が接続した時
    room.on('participantConnected', participantConnected);

    // 参加者が切断した時
    room.on('participantDisconnected', participantDisconnected);

    // Room から切断された時
    room.on('disconnected', (room, error) => {
        updateStatus('Room から切断されました');
        leaveRoom();
    });
}

// 参加者接続時の処理
function participantConnected(participant) {
    updateStatus(`${participant.identity} が参加しました`);

    // 既存のトラックの処理
    participant.tracks.forEach(publication => {
        if (publication.isSubscribed) {
            trackSubscribed(publication.track);
        }
    });

    // トラック購読イベント
    participant.on('trackSubscribed', trackSubscribed);
    participant.on('trackUnsubscribed', trackUnsubscribed);
}

// 参加者切断時の処理
function participantDisconnected(participant) {
    updateStatus(`${participant.identity} が退出しました`);

    // リモート映像をクリア
    remoteVideo.srcObject = null;
}

// トラック購読時の処理
function trackSubscribed(track) {
    if (track.kind === 'video') {
        remoteVideo.srcObject = new MediaStream([track.mediaStreamTrack]);
    }
}

// トラック購読解除時の処理
function trackUnsubscribed(track) {
    if (track.kind === 'video') {
        remoteVideo.srcObject = null;
    }
}

// ページ離脱時のクリーンアップ
window.addEventListener('beforeunload', () => {
    leaveRoom();
});

		

Vercel でのデプロイ

ブラウザのセキュリティポリシーにより、カメラ・マイクへのアクセスには HTTPS が必要です。Vercel を使用することで、簡単に HTTPS 対応のアプリケーションをデプロイできます。

GitHub リポジトリの作成

			
			test-twilio-video/
├── index.html
└── app.js

		

Vercel でのデプロイ手順

GitHub にリポジトリをプッシュ

			
			git init
git add .
git commit -m "Initial commit"
git remote add origin https://github.com/your-username/test-twilio-video.git
git push -u origin main

		

Vercel でインポート

  • vercel.com にアクセスしてサインアップ
  • "New Project" をクリック
  • GitHub リポジトリを選択
  • Framework Preset: Other を選択
  • "Deploy" をクリック
  • 数分で https://your-project-name.vercel.app の URL が発行される

Vercel でのデプロイ

環境変数の設定

app.jsTWILIO_FUNCTION_BASE_URL を Twilio Function の実際の URL に更新してください。

			
			const TWILIO_FUNCTION_BASE_URL = 'https://your-function-domain.twil.io';

		

動作確認

デバイス A での操作 (PC ブラウザ)

  • Vercel の URL にアクセス
  • Room 名 (例: test) を入力
  • 名前 (例: user1) を入力
  • "Room 作成" → "Room 参加" をクリック
  • カメラ・マイクの許可を与える

デバイス B での操作 (スマートフォンブラウザ)

  • 同じ Vercel URL にアクセス
  • 同じ Room 名 (test) を入力
  • 異なる名前 (例: user2) を入力
  • "Room 参加" をクリック
  • カメラ・マイクの許可を与える

通話確認

  • 両デバイスでお互いの映像が表示される
  • 音声が双方向で聞こえる

動作確認

まとめ

本記事では、Twilio Video API を使用して最小限の構成で 1対1 ビデオ通話機能を実装しました。この基本実装をベースに、以下のような拡張案が考えられます。

  • エラーハンドリングの強化
  • UI/UX の改善
  • 画面共有機能
  • チャット機能
  • 通話録画機能

Twilio Video API の豊富な機能により、用途に応じてさらに高度なビデオ通話システムを構築できます。

この記事をシェアする

FacebookHatena blogX

関連記事