😊

初学者がWebRTCでプロダクト開発できるようになるまで【第3回:ビデオ通話アプリ作成】

に公開

はじめに

ついに実装の時がやってきました!前回まででWebRTCの理論を学んできましたが、今回は実際に手を動かして完全に動作するビデオ通話アプリケーションを作成します。

前回までのおさらい:

  • 第1回:WebRTCの基本概念と主要API
  • 第2回:シグナリング、NAT越え、接続確立の仕組み

この記事で学べること:

  • WebRTC samplesを使った実装学習法
  • 公式サンプルコードの解読と改良
  • 実際のWebRTCアプリケーションの動作原理
  • これまで学んだ理論の実装での活用方法

前提知識:

  • HTML/CSS/JavaScript の基本
  • 前回記事の内容

コードは、Google Chrome チームが提供するWebRTC samplesを引用しながら、学習していきます。

WebRTC Samples:最適な学習リソース

サンプルの取得と準備

まずは公式のWebRTC samplesを取得しましょう。

# サンプルをクローン
git clone https://github.com/webrtc/samples.git
cd samples

# 依存関係をインストール
npm install

# ローカルサーバーを起動
npm start
# http://localhost:8080 でサンプルにアクセス

今回使用するサンプル

Basic peer connection を使用します。

選んだ理由:

  • 最もシンプルでWebRTCの基本が理解できる
  • 同一ページ内でのP2P接続(シグナリングサーバー不要)
  • これまで学んだ概念がすべて含まれている
  • 約300行のコードで完結

ファイル構成:

samples/src/content/peerconnection/pc1/
├── index.html          # メインページ
├── css/main.css        # スタイル
└── js/main.js          # WebRTC実装

サンプルコードの解読

HTML構造の理解

<!-- samples/src/content/peerconnection/pc1/index.html から抜粋 -->
<div>
  <div class="box">
    <h3>Local video</h3>
    <video id="localVideo" playsinline autoplay muted></video>
  </div>
  <div class="box">
    <h3>Remote video</h3>
    <video id="remoteVideo" playsinline autoplay></video>
  </div>
</div>

<div>
  <button id="startButton">Start</button>
  <button id="callButton" disabled>Call</button>
  <button id="hangupButton" disabled>Hang Up</button>
</div>

ポイント解説:

  • localVideo: 自分のカメラ映像を表示
  • remoteVideo: 相手の映像を表示
  • muted: ローカル映像は音響フィードバック防止のためミュート
  • playsinline: iOS Safariでインライン再生を有効化

WebRTC実装の核心部分

1. PeerConnection の作成

// samples の main.js から抜粋・解説付き
const servers = {
  iceServers: [{
    urls: 'stun:stun.l.google.com:19302'
  }]
};

let localPeerConnection;
let remotePeerConnection;

function createPeerConnection() {
  localPeerConnection = new RTCPeerConnection(servers);
  remotePeerConnection = new RTCPeerConnection(servers);
  
  // 【第1回で学んだ内容】ICE候補の処理
  localPeerConnection.onicecandidate = e => {
    onIceCandidate(localPeerConnection, e);
  };
  
  // 【第2回で学んだ内容】リモートストリームの受信
  localPeerConnection.ontrack = gotRemoteStream;
  
  console.log('Created local and remote peer connections');
}

実装のポイント:

  • このサンプルでは同一ページ内に2つのPeerConnectionを作成
  • 実際のアプリでは1つのPeerConnectionで異なるブラウザ間を接続
  • STUNサーバーにGoogleの公開サーバーを使用

2. メディアストリームの取得

// 【第1回で学んだMediaStream API】
async function startAction() {
  startButton.disabled = true;
  
  try {
    // カメラ・マイクへのアクセス
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: true,
      video: true
    });
    
    // ローカルビデオに表示
    localVideo.srcObject = stream;
    localStream = stream;
    
    // 通話ボタンを有効化
    callButton.disabled = false;
    console.log('Got local stream');
    
  } catch (e) {
    console.error('getUserMedia error:', e);
    alert('カメラまたはマイクにアクセスできません');
  }
}

学習ポイント:

  • 第1回で学んだMediaStream APIの実際の使用例
  • エラーハンドリングの重要性
  • ユーザーの許可が必要な点

3. オファー/アンサー交換の実装

// 【第2回で学んだシグナリング】同一ページ内での実装
async function callAction() {
  callButton.disabled = true;
  hangupButton.disabled = false;
  
  console.log('Starting call');
  createPeerConnection();
  
  // ローカルストリームをPeerConnectionに追加
  localStream.getTracks().forEach(track => {
    localPeerConnection.addTrack(track, localStream);
  });
  
  try {
    // オファーの作成
    console.log('localPeerConnection createOffer start');
    const offer = await localPeerConnection.createOffer();
    
    // ローカル記述子として設定
    await localPeerConnection.setLocalDescription(offer);
    console.log('localPeerConnection setLocalDescription complete');
    
    // 【通常はシグナリングサーバー経由、ここでは直接設定】
    await remotePeerConnection.setRemoteDescription(offer);
    console.log('remotePeerConnection setRemoteDescription complete');
    
    // アンサーの作成
    const answer = await remotePeerConnection.createAnswer();
    await remotePeerConnection.setLocalDescription(answer);
    console.log('remotePeerConnection setLocalDescription complete');
    
    // 【通常はシグナリングサーバー経由、ここでは直接設定】
    await localPeerConnection.setRemoteDescription(answer);
    console.log('localPeerConnection setRemoteDescription complete');
    
  } catch (e) {
    console.error('Call failed:', e);
  }
}

実装の理解ポイント:

  • 第2回で学んだオファー/アンサー交換の実際の流れ
  • 通常のアプリではシグナリングサーバー経由で交換
  • このサンプルでは学習のため直接設定

4. ICE候補の処理

// 【第2回で学んだICE/STUN/TURN】
function onIceCandidate(pc, event) {
  if (event.candidate) {
    console.log('ICE candidate:', event.candidate);
    
    // 【通常はシグナリングサーバー経由、ここでは直接追加】
    const otherPc = (pc === localPeerConnection) ? 
                    remotePeerConnection : localPeerConnection;
    
    otherPc.addIceCandidate(event.candidate)
      .then(() => {
        console.log('ICE candidate added successfully');
      })
      .catch(e => {
        console.error('Failed to add ICE candidate:', e);
      });
  }
}

学習ポイント:

  • 第2回で学んだICE候補の実際の処理方法
  • STUNサーバーを使った接続経路の発見
  • 実際のアプリではシグナリングサーバー経由で交換

5. リモートストリームの受信

// 相手の映像・音声を受信
function gotRemoteStream(e) {
  if (remoteVideo.srcObject !== e.streams[0]) {
    remoteVideo.srcObject = e.streams[0];
    console.log('Received remote stream');
  }
}

サンプルを改良してみよう

1. 接続状態の可視化

サンプルに接続状態の監視機能を追加してみましょう:

// 接続状態の監視を追加
function createPeerConnection() {
  localPeerConnection = new RTCPeerConnection(servers);
  
  // 接続状態の変化を監視
  localPeerConnection.onconnectionstatechange = () => {
    console.log('Connection state:', localPeerConnection.connectionState);
    updateConnectionStatus(localPeerConnection.connectionState);
  };
  
  // ICE接続状態の監視
  localPeerConnection.oniceconnectionstatechange = () => {
    console.log('ICE connection state:', localPeerConnection.iceConnectionState);
  };
}

function updateConnectionStatus(state) {
  const statusElement = document.getElementById('connectionStatus');
  if (statusElement) {
    statusElement.textContent = `接続状態: ${state}`;
    statusElement.className = `status-${state}`;
  }
}

2. 統計情報の表示

WebRTCの品質監視機能を追加:

// 統計情報の取得
function startStatsDisplay() {
  setInterval(async () => {
    if (localPeerConnection) {
      const stats = await localPeerConnection.getStats();
      displayStats(stats);
    }
  }, 1000);
}

function displayStats(stats) {
  stats.forEach(report => {
    if (report.type === 'inbound-rtp' && report.mediaType === 'video') {
      console.log('受信品質:', {
        packetsReceived: report.packetsReceived,
        packetsLost: report.packetsLost,
        framesDecoded: report.framesDecoded
      });
    }
  });
}

実際のプロダクトへの発展

1. シグナリングサーバーとの統合

サンプルコードをベースに、実際のシグナリングサーバーと統合する方法:

// Socket.IOとの統合例
const socket = io('wss://your-signaling-server.com');

// オファーの送信(サンプルの直接設定を置き換え)
async function sendOffer() {
  const offer = await localPeerConnection.createOffer();
  await localPeerConnection.setLocalDescription(offer);
  
  // サンプル: remotePeerConnection.setRemoteDescription(offer);
  // 実際: シグナリングサーバー経由で送信
  socket.emit('offer', {
    sdp: offer,
    roomId: currentRoom
  });
}

// オファーの受信
socket.on('offer', async (data) => {
  await remotePeerConnection.setRemoteDescription(data.sdp);
  const answer = await remotePeerConnection.createAnswer();
  await remotePeerConnection.setLocalDescription(answer);
  
  socket.emit('answer', {
    sdp: answer,
    roomId: currentRoom
  });
});

2. モバイル対応

サンプルをモバイルデバイスに対応させる改良:

// モバイル向けの制約設定
const mobileConstraints = {
  audio: true,
  video: {
    width: { ideal: 640 },
    height: { ideal: 480 },
    frameRate: { ideal: 15 }  // モバイルでは低フレームレート
  }
};

// デバイス検出
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
const constraints = isMobile ? mobileConstraints : desktopConstraints;

Chrome DevToolsでのデバッグ

WebRTC内部統計の確認

1. chrome://webrtc-internals/ にアクセス
2. サンプルで通話を開始
3. 詳細な統計情報と接続状態を確認

確認すべき項目:

  • ICE候補の収集状況
  • 選択された接続経路
  • メディア品質の統計
  • 帯域幅の使用状況

コンソールでの動的テスト

// ブラウザコンソールで実行
// 現在の接続統計を取得
localPeerConnection.getStats().then(stats => {
  stats.forEach(report => {
    if (report.type === 'candidate-pair' && report.selected) {
      console.log('選択された接続経路:', report);
    }
  });
});

// メディアトラックの制御
const videoTrack = localStream.getVideoTracks()[0];
videoTrack.enabled = false; // ビデオを停止
videoTrack.enabled = true;  // ビデオを再開

よくある問題と対処法

1. カメラ・マイクアクセスエラー

// エラーハンドリングの改良
try {
  const stream = await navigator.mediaDevices.getUserMedia(constraints);
  localVideo.srcObject = stream;
} catch (error) {
  console.error('Media access error:', error);
  
  switch(error.name) {
    case 'NotAllowedError':
      alert('カメラとマイクの権限を許可してください');
      break;
    case 'NotFoundError':
      alert('カメラまたはマイクが見つかりません');
      break;
    case 'NotReadableError':
      alert('デバイスが他のアプリで使用中です');
      break;
    default:
      alert('メディアアクセスでエラーが発生しました');
  }
}

2. 接続失敗の対処

// 接続失敗時の再試行機能
localPeerConnection.oniceconnectionstatechange = () => {
  if (localPeerConnection.iceConnectionState === 'failed') {
    console.log('ICE connection failed, attempting restart...');
    localPeerConnection.restartIce();
  }
};

まとめ

この記事では、WebRTC samplesを活用した実装学習法を学びました。

今回学んだこと:

  • WebRTC samplesの活用法: 公式サンプルから学ぶ効率的な実装方法
  • サンプルコードの解読: これまで学んだ理論の実際の実装例
  • 改良とカスタマイズ: 基本サンプルを実用的なアプリに発展させる方法
  • デバッグとトラブルシューティング: Chrome DevToolsを使った実際の問題解決

実装のポイント:

  • 公式サンプルは学習の最良のリソース
  • 理論と実装の対応関係の理解が重要
  • 段階的な改良で実用的なアプリに発展可能
  • デバッグツールの活用が開発効率を向上させる

次回予告:
次回は最終回「トラブルシューティングと実用化編」として、本格的なプロダクション運用に向けた内容をお届けします!

  • 実際のプロダクションでの課題と対策
  • パフォーマンス最適化と品質制御
  • セキュリティ強化とスケーラビリティ
  • WebRTCの最新動向と今後の展望

今すぐできるアクション:

  1. WebRTC samplesで実験

    git clone https://github.com/webrtc/samples.git
    cd samples
    npm install && npm start
    
  2. サンプルのカスタマイズにチャレンジ

    • 接続状態の可視化を追加
    • 統計情報の表示機能
    • モバイル対応の改良
  3. 他のサンプルも試してみる

WebRTC samplesは、実装学習に欠かせないリソースです。公式の実装パターンを理解してまねることで、効率的に実用的なWebRTCアプリケーションの実装が身につくと思います。


連載記事一覧:

  1. WebRTCの概要と仕組み
  2. 接続の仕組み
  3. 【今回】ビデオ通話アプリ作成
  4. トラブルシューティングと運用(次回・最終回)

この記事が役に立ったら、ぜひいいね👍とストック📚をお願いします!

Discussion