Agora.ioを使用したライブ配信機能の実装チュートリアル

Agora.ioを使用したライブ配信機能の実装チュートリアル

こんにちは、チャリセです!
最近のブログではビデオ通話機能の実装について説明しましたが、今回はAgora.ioを使用したライブ配信機能の実装方法について解説します。ライブ配信は1人の配信者と多数の視聴者という構成で、教育、エンターテインメント、イベント配信など様々な用途に活用できます。

1. ライブ配信とビデオ通話の違い

ライブ配信とビデオ通話には以下のような主な違いがあります:

  1. 通信モード
    1. ライブ配信:Live-streaming mode
    2. ビデオ通話:RTC (Real-time Communication) mode
  2. レイテンシー
    1. ライブ配信:2-3秒程度の遅延
    2. ビデオ通話:400-800ミリ秒の遅延
  3. 参加者の役割
    1. ライブ配信:配信者(host)と視聴者(audience)の役割が明確
    2. ビデオ通話:全ての参加者が対等

2. 準備

  1. Agora.ioのアカウントとApp IDが必要です
  2. プロジェクトで以下の2つのファイルを作成します:
    1. index.html:ユーザーインターフェース
    2. streaming.js:ライブ配信機能の実装

3. HTML (index.html)

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Agora ライブ配信デモ</title>
    <style>
        .stream-container {
            display: flex;
            flex-wrap: wrap;
            gap: 20px;
            padding: 20px;
        }
        .stream-player {
            width: 640px;
            height: 480px;
            background-color: #f0f0f0;
            border: 1px solid #ccc;
        }
        .controls {
            margin: 20px;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
        }
        .role-selector {
            margin-bottom: 10px;
        }
        button {
            margin: 5px;
            padding: 8px 16px;
            cursor: pointer;
        }
        .status {
            margin: 10px;
            padding: 5px;
            background-color: #f8f9fa;
            border-radius: 3px;
        }
    </style>
</head>
<body>
    <h1>Agora ライブ配信デモ</h1>
    
    <div class="controls">
        <div class="role-selector">
            <input type="radio" id="host" name="role" value="host" checked>
            <label for="host">配信者</label>
            <input type="radio" id="audience" name="role" value="audience">
            <label for="audience">視聴者</label>
        </div>
        
        <button id="join">配信に参加</button>
        <button id="leave">配信を終了</button>
        <button id="toggleAudio">音声ミュート切替</button>
        <button id="toggleVideo">ビデオミュート切替</button>
        
        <div id="status" class="status">ステータス: 未接続</div>
    </div>

    <div id="stream-container" class="stream-container"></div>

    <script src="https://download.agora.io/sdk/release/AgoraRTC_N-4.17.2.js"></script>
    <script src="streaming.js"></script>
</body>
</html>

4. JavaScript (streaming.js)

// グローバル変数の定義
const rtc = {
    client: null,
    localAudioTrack: null,
    localVideoTrack: null
};

const options = {
    appId: "あなたのApp ID",
    channel: "livestream",
    token: null, // 本番環境では適切なトークンを使用してください
    uid: Math.floor(Math.random() * 100000),
    role: "host",
    audioMuted: false,
    videoMuted: false
};

// ステータス表示の更新関数
function updateStatus(message) {
    document.getElementById("status").textContent = `ステータス: ${message}`;
}

// Agoraクライアントの初期化
async function initializeAgoraClient() {
    rtc.client = AgoraRTC.createClient({
        mode: "live",
        codec: "vp8"
    });

    // ユーザーが配信を公開した時のイベントハンドラ
    rtc.client.on("user-published", async (user, mediaType) => {
        await rtc.client.subscribe(user, mediaType);
        
        if (mediaType === "video") {
            const playerContainer = document.createElement("div");
            playerContainer.id = `player-${user.uid}`;
            playerContainer.className = "stream-player";
            document.getElementById("stream-container").append(playerContainer);
            user.videoTrack.play(playerContainer);
            updateStatus(`${user.uid}の配信を受信中`);
        }
        if (mediaType === "audio") {
            user.audioTrack.play();
        }
    });

    // ユーザーが配信を停止した時のイベントハンドラ
    rtc.client.on("user-unpublished", (user) => {
        const playerContainer = document.getElementById(`player-${user.uid}`);
        playerContainer?.remove();
        updateStatus(`${user.uid}の配信が終了`);
    });

    // 接続状態の監視
    rtc.client.on("connection-state-change", (curState, prevState) => {
        updateStatus(`接続状態: ${curState}`);
    });
}

// 配信開始処理
async function startStreaming() {
    try {
        // ラジオボタンから選択された役割を取得
        options.role = document.querySelector('input[name="role"]:checked').value;
        
        // クライアントの役割を設定
        await rtc.client.setClientRole(options.role);
        updateStatus(`役割を${options.role}に設定`);
        
        // チャンネルに参加
        await rtc.client.join(options.appId, options.channel, options.token, options.uid);
        updateStatus("チャンネルに参加しました");
        
        // 配信者の場合のみ、トラックを作成して配信
        if (options.role === "host") {
            try {
                rtc.localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack();
                rtc.localVideoTrack = await AgoraRTC.createCameraVideoTrack({
                    encoderConfig: {
                        width: 640,
                        height: 360,
                        frameRate: 15,
                        bitrateMin: 400,
                        bitrateMax: 1000
                    }
                });
                
                // ローカルビデオの表示
                const localContainer = document.createElement("div");
                localContainer.id = `player-${options.uid}`;
                localContainer.className = "stream-player";
                document.getElementById("stream-container").append(localContainer);
                rtc.localVideoTrack.play(localContainer);
                
                // トラックの公開
                await rtc.client.publish([rtc.localAudioTrack, rtc.localVideoTrack]);
                updateStatus("配信を開始しました");
                
                // UI更新
                document.getElementById("join").disabled = true;
                document.getElementById("leave").disabled = false;
                document.getElementById("toggleAudio").disabled = false;
                document.getElementById("toggleVideo").disabled = false;
                
            } catch (error) {
                updateStatus(`デバイスのアクセスに失敗: ${error.message}`);
                await rtc.client.leave();
                return;
            }
        } else {
            updateStatus("視聴者として参加しました");
            document.getElementById("join").disabled = true;
            document.getElementById("leave").disabled = false;
        }
        
    } catch (error) {
        updateStatus(`エラーが発生しました: ${error.message}`);
    }
}

// 配信終了処理
async function stopStreaming() {
    try {
        if (options.role === "host") {
            // 配信者の場合、トラックを解放
            rtc.localAudioTrack?.close();
            rtc.localVideoTrack?.close();
        }
        
        // すべての視聴者のプレイヤーを削除
        const container = document.getElementById("stream-container");
        container.innerHTML = "";
        
        // チャンネルから退出
        await rtc.client.leave();
        
        // UI更新
        document.getElementById("join").disabled = false;
        document.getElementById("leave").disabled = true;
        document.getElementById("toggleAudio").disabled = true;
        document.getElementById("toggleVideo").disabled = true;
        
        updateStatus("配信を終了しました");
        
    } catch (error) {
        updateStatus(`エラーが発生しました: ${error.message}`);
    }
}

// 音声ミュート切替
function toggleAudio() {
    if (rtc.localAudioTrack) {
        if (options.audioMuted) {
            rtc.localAudioTrack.setEnabled(true);
            options.audioMuted = false;
            this.textContent = "音声ミュート";
            updateStatus("音声をオンにしました");
        } else {
            rtc.localAudioTrack.setEnabled(false);
            options.audioMuted = true;
            this.textContent = "音声ミュート解除";
            updateStatus("音声をミュートにしました");
        }
    }
}

// ビデオミュート切替
function toggleVideo() {
    if (rtc.localVideoTrack) {
        if (options.videoMuted) {
            rtc.localVideoTrack.setEnabled(true);
            options.videoMuted = false;
            this.textContent = "ビデオミュート";
            updateStatus("ビデオをオンにしました");
        } else {
            rtc.localVideoTrack.setEnabled(false);
            options.videoMuted = true;
            this.textContent = "ビデオミュート解除";
            updateStatus("ビデオをミュートにしました");
        }
    }
}

// 初期化とイベントリスナーの設定
window.onload = async function() {
    // Agoraクライアントの初期化
    await initializeAgoraClient();
    
    // ボタンのイベントリスナー設定
    document.getElementById("join").onclick = startStreaming;
    document.getElementById("leave").onclick = stopStreaming;
    document.getElementById("toggleAudio").onclick = toggleAudio;
    document.getElementById("toggleVideo").onclick = toggleVideo;
    
    // 初期状態のUIセットアップ
    document.getElementById("leave").disabled = true;
    document.getElementById("toggleAudio").disabled = true;
    document.getElementById("toggleVideo").disabled = true;
};

// ページ遷移時のクリーンアップ
window.onbeforeunload = async function() {
    await stopStreaming();
};

5. 主な機能の説明

  1. 役割の選択
    1. 配信者(host)と視聴者(audience)の役割を切り替え可能
    2. 配信者のみがオーディオ・ビデオトラックを公開可能
  2. 配信の制御
    1. 配信の開始/終了
    2. 音声・ビデオのミュート制御
    3. 視聴者数の表示(実装可能)
  3. エラーハンドリング
    1. 配信開始/終了時のエラー処理
    2. ネットワーク接続の監視

6. セキュリティ対策

  1. トークンベースの認証
    1. App IDとトークンはサーバーサイドで管理
    2. 一時トークンは定期的に更新
  2. 役割の制御
    1. 配信者の権限を適切に管理
    2. なりすまし防止の実装

7. パフォーマンス最適化

  • ビデオ品質の設定
// ビデオプロファイルの設定例
rtc.localVideoTrack.setEncoderConfiguration({
    width: 640,
    height: 360,
    frameRate: 15,
    bitrateMin: 400,
    bitrateMax: 1000,
});
  • 帯域幅の管理
    1. 視聴者数に応じた品質調整
    2. ネットワーク状況に応じた動的な品質制御

まとめ

Agora.ioを使用したライブ配信機能の実装について、基本的な実装から発展的な機能まで解説しました。このコードをベースに、プロジェクトの要件に応じてカスタマイズしていくことで、様々なユースケースに対応できます。
セキュリティ、パフォーマンス、ユーザビリティなど、考慮すべき点は多岐にわたりますが、Agora.ioの充実したSDKを活用することで、安定した配信システムを構築できます。

HappyCoding!!

現在
株式会社チョモランマ
株式会社シェルパ
3Dmodeljapan株式会社
ではスタッフを大募集しております!!
Unreal Engine4、AI、プログラミングや建築パースに興味がある方!
ぜひご応募下さい!!
初心者の方、未経験の方やインターンを受けてみたい方々でも大歓迎です!!