😊
初学者が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の最新動向と今後の展望
今すぐできるアクション:
-
WebRTC samplesで実験
git clone https://github.com/webrtc/samples.git cd samples npm install && npm start
-
サンプルのカスタマイズにチャレンジ
- 接続状態の可視化を追加
- 統計情報の表示機能
- モバイル対応の改良
-
他のサンプルも試してみる
WebRTC samplesは、実装学習に欠かせないリソースです。公式の実装パターンを理解してまねることで、効率的に実用的なWebRTCアプリケーションの実装が身につくと思います。
連載記事一覧:
- WebRTCの概要と仕組み
- 接続の仕組み
- 【今回】ビデオ通話アプリ作成
- トラブルシューティングと運用(次回・最終回)
この記事が役に立ったら、ぜひいいね👍とストック📚をお願いします!
Discussion