最終更新日:2026/03/21 原本2025-08-27
😎

初学者がWebRTCでプロダクト開発できるようになるまで【第2回:接続の仕組み】

に公開

はじめに

前回は「WebRTCって何?」という基本的な疑問から、主要APIの概要まで学習しました。今回は、WebRTCの最も重要で複雑な部分である「どうやってブラウザ同士が直接つながるのか?」という仕組みを深掘りします。

前回のおさらい:

  • WebRTCはブラウザネイティブなP2P通信技術
  • MediaStream、RTCPeerConnection、RTCDataChannelの3つのAPI
  • UDPベースでリアルタイム性を重視

この記事で学べること:

  • シグナリングの役割と実装方法
  • NAT越えの仕組み(ICE/STUN/TURN)
  • オファー/アンサー交換の詳細プロセス
  • 実際の接続確立手順

前提知識:

  • 前回記事の内容
  • ネットワーク通信の基本概念

この記事を読み終える頃には、「なぜWebRTCでP2P接続ができるのか」が手に取るように理解できるはずです!

シグナリング:P2P接続の仲人役

シグナリングって何をしてるの?

シグナリングは、WebRTCでP2P接続を確立するために必要な情報を交換するプロセスです。よく「お見合いの仲人」に例えられます。

【お見合いの例】
仲人:「田中さん(IP: 192.168.1.10)は音楽好きで、佐藤さん(IP: 10.0.1.5)は映画好きです」
田中さん:「佐藤さんとお話ししたいです」
仲人:「佐藤さん、田中さんがお話ししたがってます」
佐藤さん:「わかりました、お話ししましょう」
→ その後は二人で直接会話(P2P通信)

【WebRTCの場合】
シグナリングサーバー:「Aブラウザの接続情報はこれで、Bブラウザの接続情報はこれです」
Aブラウザ:「Bブラウザと接続したいです」
シグナリングサーバー:「Bブラウザ、Aブラウザが接続したがってます」
Bブラウザ:「わかりました、接続しましょう」
→ その後は直接P2P通信

シグナリングの4つの役割

1. P2P接続確立のための情報交換

// 交換される主な情報
const connectionInfo = {
  // SDP (Session Description Protocol) - メディア情報
  sdp: {
    mediaTypes: ['audio', 'video'],      // 音声・映像の種類
    codecs: ['H.264', 'VP8', 'Opus'],    // 使用可能なコーデック
    networkInfo: 'IPアドレス:ポート'      // ネットワーク情報
  },
  // ICE候補 - 接続経路情報
  iceCandidates: [
    { ip: '192.168.1.10', port: 54321, type: 'host' },      // ローカルIP
    { ip: '203.0.113.5', port: 12345, type: 'reflexive' },  // パブリックIP
    { ip: '198.51.100.1', port: 9876, type: 'relay' }       // TURNサーバー経由
  ]
};

2. セッション管理

// セッション管理の例
class SessionManager {
  constructor() {
    this.sessions = new Map(); // アクティブなセッション
  }
  
  createSession(userId, roomId) {
    const sessionId = `${roomId}_${userId}_${Date.now()}`;
    this.sessions.set(sessionId, {
      userId,
      roomId,
      status: 'connecting',
      createdAt: new Date()
    });
    return sessionId;
  }
  
  endSession(sessionId) {
    const session = this.sessions.get(sessionId);
    if (session) {
      session.status = 'disconnected';
      session.endedAt = new Date();
      console.log(`セッション ${sessionId} を終了しました`);
    }
  }
}

3. ユーザー認証

// シグナリングサーバーでの認証例
const io = require('socket.io')(server);

io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  
  // JWTトークンの検証
  jwt.verify(token, SECRET_KEY, (err, decoded) => {
    if (err) {
      return next(new Error('認証エラー'));
    }
    socket.userId = decoded.userId;
    socket.username = decoded.username;
    next();
  });
});

io.on('connection', (socket) => {
  console.log(`認証済みユーザー ${socket.username} が接続しました`);
});

4. ルーム管理

// ルーム管理の実装例
const rooms = new Map(); // roomId -> Set of socketIds

socket.on('join-room', (roomId) => {
  // 既存のルームから退出
  leaveAllRooms(socket);
  
  // 新しいルームに参加
  socket.join(roomId);
  
  if (!rooms.has(roomId)) {
    rooms.set(roomId, new Set());
  }
  rooms.get(roomId).add(socket.id);
  
  // ルーム内の他のユーザーに通知
  socket.to(roomId).emit('user-joined', {
    userId: socket.userId,
    username: socket.username
  });
  
  console.log(`${socket.username} がルーム ${roomId} に参加(現在 ${rooms.get(roomId).size} 人)`);
});

シグナリングを実現する技術選択肢

WebRTCのシグナリングは「情報交換の仲介」が役割でしたが、実際にどんな技術でこの仲介を行うかは開発者が選択する必要があります。

「WebRTCの標準には、シグナリングの実装方法は含まれていない」ため、既存の通信技術を活用してシグナリングサーバーを構築します。

「どの技術を選べばよいか」を判断できるよう、主要な選択肢とその特徴を見ていきましょう。

WebSocket:最も一般的で実装しやすい選択

WebSocketは、WebRTCシグナリングで最もよく使われる技術です。

特徴:

  • ブラウザとサーバー間でリアルタイム双方向通信が可能
  • WebRTCと同じくブラウザネイティブでサポート
  • Socket.IOなどのライブラリで簡単に実装できる

簡単な実装例:

// シグナリングサーバー(Node.js + Socket.IO)
const io = require('socket.io')(server);

io.on('connection', (socket) => {
  // ルーム参加
  socket.on('join-room', (roomId) => {
    socket.join(roomId);
  });

  // オファー・アンサー・ICE候補の転送
  socket.on('offer', (offer, roomId) => {
    socket.to(roomId).emit('offer', offer);
  });
  // 他の処理も同様...
});

その他の技術選択肢

SIP(Session Initiation Protocol)

  • 従来の電話システムで使われる標準プロトコル
  • 既存の電話インフラとの連携が必要な場合に選択
  • 実装が複雑で、WebRTC初学者には推奨しない

カスタムソリューション(Firebase、Pusherなど)

  • クラウドサービスの提供するリアルタイム通信機能を活用
  • サーバー運用の負担を軽減できる
  • サービス依存のリスクがある

シグナリング技術選択の指針

各技術とWebRTCの関係を整理すると以下のようになります:

技術 実装難易度 学習コスト スケーラビリティ 推奨度(初学者)
WebSocket ⭐⭐⭐⭐⭐
SIP ⭐⭐
カスタム 中〜高 ⭐⭐⭐

結論:初学者はWebSocketから始めるのがよさそう

WebRTCを学習する初学者には、以下の理由でWebSocketをお勧めです。

  1. 実装が簡単: Socket.IOなどのライブラリで数十行で実装可能
  2. 学習リソースが豊富: WebRTC + WebSocketの組み合わせは情報が多い
  3. デバッグしやすい: ブラウザの開発者ツールで通信内容を確認可能
  4. 将来性: より高度な技術への移行もしやすい

重要なポイント: シグナリング技術は「手段」であり、WebRTCの「本質」ではありません。まずはWebSocketでWebRTCの動作原理を理解し、必要に応じて他の技術を検討するのが効率的な学習方法です。

NAT越えの仕組み:ICE/STUN/TURN

NATを理解しよう

多くの家庭や企業ネットワークでは、NAT(Network Address Translation)によってプライベートIPアドレスが使われています。これがP2P接続の大きな障害となります。

NATの4つの種類と特徴

// NATタイプの判定(概念的なコード)
function detectNATType() {
  // 実際の判定は複雑ですが、概念的には以下のような分類
  const natTypes = {
    'Full Cone NAT': {
      restriction: '制限なし',
      security: '低',
      p2pSuccess: '高',
      description: '一度外部に送信すると、どこからでも接続可能'
    },
    'Restricted Cone NAT': {
      restriction: 'IPアドレス制限',
      security: '中',
      p2pSuccess: '中',
      description: '送信先IPからのみ接続可能'
    },
    'Port Restricted Cone NAT': {
      restriction: 'IP+ポート制限',
      security: '高',
      p2pSuccess: '中',
      description: '送信先IP+ポートからのみ接続可能'
    },
    'Symmetric NAT': {
      restriction: '送信先ごとに異なるポート',
      security: '最高',
      p2pSuccess: '低',
      description: '接続先ごとに異なるポートを使用'
    }
  };
  
  return natTypes;
}

ICEプロトコル:最適な接続経路を見つける

ICE(Interactive Connectivity Establishment)は、様々な接続経路を収集し、最適なものを選択するプロトコルです。

ICEの5つの処理手順

ICEプロトコルは、P2P接続を確立するために以下の5つの手順を順番に実行します。各手順でWebRTCがどのような処理を行っているのか、実際のコードと合わせて理解していきましょう。
これらの手順は自動的に実行されますが、開発者は各段階でイベントを監視し、適切な処理を行う必要があります。

// 1. ローカルICE候補の収集
peerConnection.onicecandidate = (event) => {
  if (event.candidate) {
    console.log('ICE候補を発見:', {
      type: event.candidate.type,        // host, reflexive, relay
      protocol: event.candidate.protocol, // udp, tcp
      address: event.candidate.address,   // IPアドレス
      port: event.candidate.port,         // ポート番号
      priority: event.candidate.priority  // 優先度
    });
    
    // シグナリングサーバー経由で相手に送信
    socket.emit('ice-candidate', event.candidate, roomId);
  } else {
    console.log('すべてのICE候補の収集が完了');
  }
};

// 2. リモートICE候補の受信と追加
socket.on('ice-candidate', async (candidate) => {
  try {
    await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
    console.log('リモートICE候補を追加:', candidate.type);
  } catch (error) {
    console.error('ICE候補の追加に失敗:', error);
  }
});

// 3. 接続性チェックの実行
peerConnection.oniceconnectionstatechange = () => {
  const state = peerConnection.iceConnectionState;
  console.log('ICE接続状態:', state);
  
  switch(state) {
    case 'checking':
      console.log('各候補の接続性をテスト中...');
      displayStatus('接続経路を検索中...');
      break;
    case 'connected':
      console.log('ICE接続が確立されました!');
      displayStatus('接続完了');
      logSelectedCandidate();
      break;
    case 'failed':
      console.log('ICE接続に失敗');
      displayStatus('接続失敗');
      suggestTurnServer();
      break;
  }
};

// 4. 最適な経路の選択(統計情報で確認)
async function logSelectedCandidate() {
  const stats = await peerConnection.getStats();
  stats.forEach((report) => {
    if (report.type === 'candidate-pair' && report.selected) {
      console.log('選択された接続経路:', {
        localType: report.localCandidateType,    // host, reflexive, relay
        remoteType: report.remoteCandidateType,
        currentRTT: report.currentRoundTripTime, // 往復遅延時間
        priority: report.priority
      });
    }
  });
}

// 5. 接続の確立と監視
peerConnection.onconnectionstatechange = () => {
  const state = peerConnection.connectionState;
  console.log('P2P接続状態:', state);
  
  if (state === 'connected') {
    console.log('P2P接続が確立されました!');
    startQualityMonitoring(); // 品質監視を開始
  }
};

STUN/TURNサーバー:NATを突破する仕組み

NAT越えを実現するために、WebRTCは2種類の特別なサーバーを活用します。STUNサーバーとTURNサーバーは、それぞれ異なる方法でP2P接続を支援します。
どちらのサーバーも外部に設置され、クライアント同士が直接接続できるよう「手助け」する役割を果たします。まずはそれぞれの動作原理を理解しましょう。

STUNサーバー:「あなたのパブリックIPはこれです」

STUNサーバーは、クライアントに対して「あなたが外部からどのIPアドレスで見えているか」を教えてくれるサーバーです。

動作の流れ:

クライアントがSTUNサーバーに問い合わせ
STUNサーバーが「あなたのパブリックIPは○○です」と回答
この情報をICE候補として相手に送信
相手が直接そのIPアドレスに接続を試行

多くの場合、この仕組みだけでNAT越えが可能になります。

// STUNサーバーの設定例
const stunConfig = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },        // Google
    { urls: 'stun:stun1.l.google.com:19302' },       // Google予備
    { urls: 'stun:stun.cloudflare.com:3478' },       // Cloudflare
    { urls: 'stun:stun.nextcloud.com:443' }          // Nextcloud
  ]
};

const peerConnection = new RTCPeerConnection(stunConfig);

// STUNサーバーの応答で得られる情報
const reflexiveCandidate = {
  type: 'reflexive',           // STUNサーバー経由で得られたパブリックIP
  address: '203.0.113.5',     // パブリックIPアドレス
  port: 54321,                // パブリックポート
  relatedAddress: '192.168.1.10', // 元のプライベートIP
  relatedPort: 12345          // 元のプライベートポート
};

TURNサーバー:「つながらない時はTURNを経由する」

TURNサーバーは、STUNサーバーでも直接接続できない場合の「最後の手段」として機能します。

動作の流れ:

直接接続とSTUN経由の接続が両方失敗
TURNサーバーが「中継役」として動作
両方のクライアントがTURNサーバーに接続
TURNサーバーが通信を中継してP2P「風」の接続を実現

技術的にはP2Pではなくサーバー経由の通信になりますが、アプリケーションからは通常のP2P接続として扱えます。確実に接続できる反面、サーバーリソースを消費するためコストがかかります。

// TURNサーバーの設定例
const turnConfig = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    {
      urls: [
        'turn:your-turn-server.com:3478',
        'turns:your-turn-server.com:5349'  // TLS版
      ],
      username: 'your-username',
      credential: 'your-password',
      credentialType: 'password'
    }
  ]
};

// TURNサーバーの使用例
const relayCandidate = {
  type: 'relay',              // TURNサーバー経由
  address: '198.51.100.1',   // TURNサーバーのIP
  port: 9876,                // TURNサーバーのポート
  protocol: 'udp'           // 使用プロトコル
};

サーバー選定のガイドライン

実際のWebRTCアプリケーションを開発する際、どのSTUN/TURNサーバーを使うべきかは重要な判断になります。

考慮すべき要素:

コスト:STUNは安価、TURNは帯域使用量に応じて課金
成功率:STUNは70-80%、TURNは99%以上の接続成功率
パフォーマンス:直接接続が最速、リレー経由は遅延が増加
実装の複雑さ:STUNは設定のみ、TURNは認証設定が必要

以下の表に、まとめています。

要素 STUN TURN 備考
コスト 低(無料あり) 高(帯域課金) TURNは全通信がリレーされるため
成功率 70-80% 99%+ NATタイプに依存 vs 確実
遅延 直接接続 vs リレー経由
実装難易度 認証設定が必要

段階的なサーバー設定の例


function createPeerConnection(useTurn = false) {
  let iceServers = [
    { urls: 'stun:stun.l.google.com:19302' }
  ];
  
  if (useTurn) {
    iceServers.push({
      urls: 'turn:your-turn-server.com:3478',
      username: 'username',
      credential: 'password'
    });
  }
  
  return new RTCPeerConnection({ iceServers });
}

// 最初はSTUNのみで試行
let peerConnection = createPeerConnection(false);

// 接続失敗時はTURNを追加
peerConnection.oniceconnectionstatechange = () => {
  if (peerConnection.iceConnectionState === 'failed') {
    console.log('STUN接続失敗、TURNサーバーを試行...');
    peerConnection.close();
    peerConnection = createPeerConnection(true);
    // 接続を再試行
    restartConnection();
  }
};

オファー/アンサー交換:P2P接続の握手

SDP(Session Description Protocol)とは?

SDPは、メディアセッションの詳細を記述するプロトコルです。「どんなメディアを、どのコーデックで、どのポートで送受信するか」を定義します。

// SDPの例(簡略化)
const sdpExample = `
v=0                                    // SDPバージョン
o=- 123456789 123456789 IN IP4 0.0.0.0 // セッション情報
s=-                                    // セッション名
t=0 0                                  // 時間情報
a=group:BUNDLE 0 1                     // バンドル設定

m=audio 9 UDP/TLS/RTP/SAVPF 111 103    // 音声メディア
c=IN IP4 0.0.0.0                       // 接続情報
a=rtcp:9 IN IP4 0.0.0.0               // RTCP情報
a=rtpmap:111 opus/48000/2             // Opusコーデック
a=rtpmap:103 ISAC/16000               // ISACコーデック

m=video 9 UDP/TLS/RTP/SAVPF 96 97     // 映像メディア
a=rtpmap:96 VP8/90000                 // VP8コーデック
a=rtpmap:97 H264/90000                // H.264コーデック
`;

// SDPの解析例
function parseSDPInfo(sdp) {
  const audioCodecs = sdp.match(/a=rtpmap:\d+ (\w+)\/\d+/g)
    .filter(line => line.includes('audio'))
    .map(line => line.split(' ')[1].split('/')[0]);
    
  const videoCodecs = sdp.match(/a=rtpmap:\d+ (\w+)\/\d+/g)
    .filter(line => line.includes('video'))
    .map(line => line.split(' ')[1].split('/')[0]);
    
  return { audioCodecs, videoCodecs };
}

完全な接続確立フロー

ここまで学んだシグナリング、SDP、ICEの仕組みが、実際のWebRTCアプリケーションでどのように連携するかを見てみましょう。
以下のコードは、接続を開始する側(Caller)と接続を受ける側(Callee)の完全な流れを示しています。第2回で学んだ理論が、実際のコードでどのように実装されるかを確認してください。

重要なポイント:

オファー/アンサーの交換は非同期処理
各段階でエラーハンドリングが必要
実際のアプリではシグナリングサーバー経由で情報交換

// 接続を開始する側(Caller)
async function startCall() {
  try {
    // 1. PeerConnectionを作成
    setupPeerConnection();
    
    // 2. ローカルメディアストリームを取得・追加
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: true,
      video: true
    });
    
    stream.getTracks().forEach(track => {
      peerConnection.addTrack(track, stream);
    });
    
    // 3. オファーを作成
    const offer = await peerConnection.createOffer({
      offerToReceiveAudio: true,
      offerToReceiveVideo: true
    });
    
    // 4. ローカル記述子として設定
    await peerConnection.setLocalDescription(offer);
    console.log('オファーを作成し、ローカル記述子に設定');
    
    // 5. シグナリングサーバー経由でオファーを送信
    socket.emit('offer', offer, roomId);
    console.log('オファーを送信:', offer.type);
    
  } catch (error) {
    console.error('通話開始エラー:', error);
  }
}

// 接続を受ける側(Callee)
socket.on('offer', async (offer, senderId) => {
  try {
    console.log('オファーを受信:', offer.type);
    
    // 1. PeerConnectionを作成(まだなければ)
    if (!peerConnection) {
      setupPeerConnection();
    }
    
    // 2. リモート記述子として設定
    await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
    console.log('オファーをリモート記述子に設定');
    
    // 3. ローカルメディアストリームを取得・追加
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: true,
      video: true
    });
    
    stream.getTracks().forEach(track => {
      peerConnection.addTrack(track, stream);
    });
    
    // 4. アンサーを作成
    const answer = await peerConnection.createAnswer();
    
    // 5. ローカル記述子として設定
    await peerConnection.setLocalDescription(answer);
    console.log('アンサーを作成し、ローカル記述子に設定');
    
    // 6. シグナリングサーバー経由でアンサーを送信
    socket.emit('answer', answer, roomId);
    console.log('アンサーを送信:', answer.type);
    
  } catch (error) {
    console.error('オファー処理エラー:', error);
  }
});

// アンサーを受信(Caller側)
socket.on('answer', async (answer, senderId) => {
  try {
    console.log('アンサーを受信:', answer.type);
    
    // リモート記述子として設定
    await peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
    console.log('アンサーをリモート記述子に設定');
    console.log('P2P接続の準備が完了しました');
    
  } catch (error) {
    console.error('アンサー処理エラー:', error);
  }
});

状態変化の詳細監視

WebRTCの接続確立は複数の状態を経て進行します。開発者はこれらの状態変化を監視することで、接続の進行状況を把握し、問題が発生した場合の対処を行えます。

監視すべき主要な状態:

シグナリング状態:オファー/アンサー交換の進行状況
ICE接続状態:ネットワーク経路確立の進行状況
全体接続状態:P2P接続全体の状態

これらの状態を適切に監視することで、ユーザーに分かりやすい接続状況を表示したり、問題発生時の自動回復処理を実装できます。

function setupDetailedMonitoring() {
  // シグナリング状態の監視
  peerConnection.onsignalingstatechange = () => {
    console.log('シグナリング状態:', peerConnection.signalingState);
    /*
    stable: 安定状態(初期状態または接続完了後)
    have-local-offer: ローカルオファーを設定済み
    have-remote-offer: リモートオファーを受信済み
    have-local-pranswer: ローカル仮アンサーを設定済み
    have-remote-pranswer: リモート仮アンサーを受信済み
    closed: 接続が閉じられた
    */
  };
  
  // ICE収集状態の監視
  peerConnection.onicegatheringstatechange = () => {
    console.log('ICE収集状態:', peerConnection.iceGatheringState);
    /*
    new: ICE候補の収集を開始していない
    gathering: ICE候補を収集中
    complete: ICE候補の収集が完了
    */
  };
  
  // ICE接続状態の監視
  peerConnection.oniceconnectionstatechange = () => {
    console.log('ICE接続状態:', peerConnection.iceConnectionState);
    /*
    new: ICE候補の確認を開始していない
    checking: ICE候補の接続性をチェック中
    connected: 使用可能な接続が見つかったが、まだチェック中
    completed: すべてのチェックが完了し、接続が確立
    failed: 接続に失敗
    disconnected: 接続が切断されたが再接続を試行中
    closed: ICE接続が閉じられた
    */
  };
  
  // 全体的な接続状態の監視
  peerConnection.onconnectionstatechange = () => {
    console.log('接続状態:', peerConnection.connectionState);
    /*
    new: 接続が開始されていない
    connecting: 接続を確立中
    connected: 接続が確立された
    disconnected: 接続が切断された
    failed: 接続に失敗した
    closed: 接続が閉じられた
    */
    
    updateUIStatus(peerConnection.connectionState);
  };
}

// UI状態の更新
function updateUIStatus(state) {
  const statusElement = document.getElementById('connectionStatus');
  const statusMessages = {
    'new': '準備中...',
    'connecting': '接続中...',
    'connected': '接続完了!',
    'disconnected': '切断されました',
    'failed': '接続に失敗しました',
    'closed': '接続が終了しました'
  };
  
  statusElement.textContent = statusMessages[state] || state;
  statusElement.className = `status-${state}`;
}

setupDetailedMonitoring()関数の実際の使用方法

setupDetailedMonitoring()関数は、ブラウザ標準のRTCPeerConnectionオブジェクトを作成した直後に呼び出す必要があります。この関数は自動的に実行されるものではなく、開発者が明示的に呼び出すことで初めて状態監視が開始されます。

コード例:


async function startWebRTCConnection() {
  // 1. RTCPeerConnectionオブジェクトの作成
  const peerConnection = new RTCPeerConnection({
    iceServers: [
      { urls: 'stun:stun.l.google.com:19302' }
    ]
  });
  
  // 2. 状態監視の設定(前で定義した関数を使用)
  setupDetailedMonitoring(peerConnection);
  
  // 3. この後でメディアストリーム追加やオファー/アンサー交換を行う
  console.log('WebRTC接続オブジェクトと状態監視が準備完了');
  
  return peerConnection;
}

// 使用例
const myPeerConnection = await startWebRTCConnection();

注意:

RTCPeerConnection:ブラウザが提供するWebRTC標準API
※ 後ほど、WebRTCConnectionというクラスが登場しますが、これは本記事内で使用する開発者が作成する管理用のカスタムクラスなので、混同しないでください。

実践的な接続確立の実装

ここまでの理論的な内容を踏まえて、実際のプロダクション環境で使える接続確立の実装方法を学びましょう。
理論と実践の大きな違いは「エラーが発生すること」です。ネットワーク環境、デバイス性能、ユーザーの操作など、様々な要因で接続が失敗する可能性があります。
実用的なWebRTCアプリケーションでは、これらの問題に対処する仕組みが不可欠です。

接続確立で最も重要なのは、失敗した場合の適切な対処です。単純に「接続に失敗しました」と表示するだけでは、ユーザーは何をすればよいか分かりません。

実装すべき要素:

失敗原因の特定と分類
自動再試行のロジック
ユーザーへの分かりやすいフィードバック
代替手段の提案

以下のコードでは、これらの要素を含んだ実践的な実装例を示しています。

コード例:

class WebRTCConnection {
  constructor(roomId, socket) {
    this.roomId = roomId;
    this.socket = socket;
    this.peerConnection = null;
    this.retryCount = 0;
    this.maxRetries = 3;
  }
  
  async createConnection() {
    try {
      // タイムアウト付きで接続を試行
      const connectionPromise = this.establishConnection();
      const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => reject(new Error('接続タイムアウト')), 30000);
      });
      
      await Promise.race([connectionPromise, timeoutPromise]);
      console.log('WebRTC接続が正常に確立されました');
      
    } catch (error) {
      console.error('接続確立エラー:', error);
      await this.handleConnectionError(error);
    }
  }
  
  async handleConnectionError(error) {
    this.retryCount++;
    
    if (this.retryCount <= this.maxRetries) {
      console.log(`接続を再試行中... (${this.retryCount}/${this.maxRetries})`);
      
      // 少し待ってから再試行
      await new Promise(resolve => setTimeout(resolve, 2000));
      await this.createConnection();
    } else {
      console.error('最大再試行回数に達しました。接続を諦めます。');
      this.showErrorToUser('接続に失敗しました。ページを再読み込みして再試行してください。');
    }
  }
  
  showErrorToUser(message) {
    const errorDiv = document.createElement('div');
    errorDiv.className = 'error-message';
    errorDiv.textContent = message;
    document.body.appendChild(errorDiv);
  }
}

注意:

※ 上記サンプル内のWebRTCConnectionはカスタムクラスの例として本記事内で定義したクラスです。ブラウザが提供するWebRTC標準APIであるRTCPeerConnectionと混同しないようにしてください。

まとめ

この記事では、WebRTCの核心部分である接続確立の仕組みを詳しく学習しました。

今回学んだこと:

  • シグナリング:P2P接続確立のための情報交換の仲介役
    • 4つの役割(情報交換、セッション管理、認証、ルーム管理)
    • WebSocket、SIP、カスタムプロトコルの特徴と選択基準
  • NAT越えの仕組み:ICE/STUN/TURNによる接続経路の確保
    • NATの4種類と各特徴(Full Cone、Restricted Cone、Port Restricted、Symmetric)
    • ICEプロトコルの5段階処理(収集→受信→チェック→選択→確立)
    • STUN/TURNサーバーの役割分担とコスト・成功率のトレードオフ
  • オファー/アンサー交換:SDP情報を使った接続合意プロセス
    • SDPによるメディア情報の記述
    • 完全な接続確立フロー(オファー作成→送信→受信→アンサー作成→送信→受信)
    • 各状態の詳細監視と適切なエラーハンドリング

理解のポイント:

  • シグナリングは「仲人」、ICEは「探偵」、STUN/TURNは「案内人」の役割
  • 直接接続できない場合のフォールバック戦略が重要
  • 接続確立は非同期処理のため、適切な状態管理とエラーハンドリングが必須

試してみる:

  1. Chrome DevToolsでWebRTC内部を覗いてみる

    // 下記をブラウザのコンソールで実行し、どのようなオブジェクトがやり取りされるかチェックしてみてください。
    navigator.mediaDevices.getUserMedia({video: true, audio: true})
    .then(stream => {
      const pc = new RTCPeerConnection({
        iceServers: [{urls: 'stun:stun.l.google.com:19302'}]
      });
      stream.getTracks().forEach(track => pc.addTrack(track, stream));
      
      pc.onicecandidate = (event) => {
        if (event.candidate) {
          console.log('ICE候補:', event.candidate);
        }
      };
      
      return pc.createOffer();
    })
    .then(offer => {
      console.log('SDP Offer:', offer);
    });
    
  2. 公開WebRTCサンプルで接続プロセスを観察


次回予告:
次回は「実装実践編」として、今回学んだ仕組みを使って実際に動作するビデオ通話アプリケーションを一緒に作成します!

  • 完全なシグナリングサーバーの実装
  • クライアント側の1対1ビデオ通話機能
  • データチャネルを使ったテキストチャット
  • ファイル転送機能の実装
  • 実用的なエラーハンドリングとUI設計

連載記事一覧:

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

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

Discussion