🧑‍💻

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

に公開

はじめに

「ブラウザだけでビデオ通話やリアルタイム通信ができる」と聞いて興味を持ったものの、WebRTCって結局何ができるの?どんな仕組みなの?という疑問をお持ちの方も多いのではないでしょうか。

この連載では、筆者がひょんなことからWebRTC使ったプロダクトを開発することになり、完全初心者の状態から実際にプロダクトを実装し、様々な問題に対処できるようになるまでの過程で理解したことを、順を追ってお伝えしていこうと思います。

連載全体の流れ:

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

今回の記事で学べること:

  • WebRTCとは何か、何ができるのか
  • 従来技術との違いとWebRTCの優位性
  • WebRTC実装に必要な背景知識
  • 主要なAPI群の基本概念

前提知識:

  • HTML/CSS/JavaScriptの基本
  • HTTPでのWebページ表示の仕組み
  • ブラウザの基本的な動作原理

対象:

  • WebRTCについてよくわかっていないけど基本的なWebRTCのアプリが実装できるようになりたい人
  • 公式の(基本的な)サンプルアプリで何をやっているのか理解したい人

今回は基本原理理解し公式ドキュメントが読み解ける様になることをゴールにしています。
それでは、WebRTCの世界に一緒に飛び込んでみましょう!

WebRTCって結局何なの?

まずは身近な例から理解しよう

想像してみてください。あなたが友人とビデオ通話をするとき、従来は以下のような仕組みでした:

あなた → 通話サーバー → 友人
       (映像・音声を中継)

これに対してWebRTCは、以下のような直接通信を可能にします:

あなた ←→ 友人
    (直接通信)

この**「ブラウザ同士が直接やり取りする通信方式」を「P2P(Peer to Peer)通信」**と呼びます。PeerとPeerが直接つながるという意味です。

WebRTCの正体を理解しよう

WebRTC(Web Real-Time Communication) は、ブラウザ間でリアルタイムの音声・映像・データ通信を可能にする技術です。2011年にGoogleが開発を開始し、現在はW3CとIETFによって標準化されています。

「リアルタイム」とは?
一般的なWebページの表示(HTTP通信)では、クリックしてから表示まで数百ミリ秒〜数秒かかります。WebRTCでは、音声や映像を**100ミリ秒以下(0.1秒以下)**の遅延で送受信できます。これにより、自然な会話やライブ感のある体験が実現できます。

一言で表現するなら「ブラウザが標準で持っている、ブラウザ同士の直接通信を実現する機能群」です。

WebRTCの4つの特徴

1. ブラウザ標準の機能

追加のソフトウェアインストールが不要で、Chrome、Firefox、Safari、Edgeなどのモダンブラウザであればそのまま使用できます。

2. P2P(Peer to Peer)直接通信

サーバーを経由せず、ブラウザ同士が直接通信します。これにより低遅延でプライベートな通信が実現できます。

3. 自動暗号化

通信内容は自動的に暗号化されるため、第三者に盗聴される心配がありません。

4. プラグイン不要

HTMLとJavaScriptだけで実装でき、追加のプラグインやソフトウェアが不要です。

なぜWebRTCが画期的なのか?

従来の動画通話を実現するには、以下のような方法が必要でした:

2010年頃の方法(Flash Player使用)

  1. Adobe Flash Playerをインストール
  2. 専用の動画通話アプリをダウンロード
  3. セキュリティリスクを承知で使用

現在のWebRTC

  1. ブラウザでWebページにアクセス
  2. カメラ・マイクの使用を許可
  3. すぐに通話開始

この劇的な簡素化により、動画通話が誰でも簡単に利用できるようになりました。

WebRTCで何ができるの?

1. ビデオ通話・会議システム

身近な例:Zoom、Google Meet、Microsoft Teams

技術的な仕組み:

  • ユーザーのカメラ・マイクから映像・音声を取得
  • 相手のブラウザに直接送信
  • 遅延を最小限に抑えた自然な会話を実現

実際の利用場面:

  • オンライン会議
  • 家族・友人との通話
  • オンライン授業
  • 遠隔医療での診察

2. ライブストリーミング

身近な例:YouTube Live、Twitch

技術的な仕組み:

  • 配信者のカメラ・画面を取得
  • 多数の視聴者に同時配信
  • チャット機能で双方向コミュニケーション

実際の利用場面:

  • ゲーム配信
  • イベントのライブ中継
  • ウェビナーやオンラインセミナー
  • 商品説明会やデモンストレーション

3. リアルタイムデータ通信

身近な例:Google Docs、Figma

技術的な仕組み:

  • 文字入力やマウス操作をリアルタイム送信
  • 複数ユーザーの操作を同期
  • サーバーを経由しない高速データ交換

実際の利用場面:

  • 複数人での文書同時編集
  • オンラインホワイトボード
  • リアルタイムチャット
  • ファイル共有(P2P)

4. IoT・デバイス制御

身近な例:家庭用監視カメラ、スマートホーム

技術的な仕組み:

  • デバイスからのセンサーデータを取得
  • Webブラウザで直接制御
  • 遠隔地からのリアルタイム監視

実際の利用場面:

  • 外出先からの家庭内監視
  • 工場設備の遠隔監視
  • ロボットの遠隔操作
  • スマートホームの制御

WebRTC実装に必要な背景知識

実際にWebRTCを使った開発を始める前に、押さえておきたい背景知識を整理しましょう。

ネットワークの基礎知識

TCP vs UDP - なぜWebRTCは「速度重視」を選ぶのか?

Webページを見るとき(HTTP通信)とWebRTCでの通信では、異なるネットワークプロトコルを使用します。

従来のWebページ表示(TCP使用)

あなた: 「このページを見せて」
サーバー: 「わかりました。まず1つ目のデータを送ります」
あなた: 「1つ目、受信しました」
サーバー: 「では2つ目を送ります」
あなた: 「2つ目、受信しました」
...(すべてのデータが確実に届くまで繰り返し)

WebRTCでの通信(UDP使用)

あなた: 「今の映像を送ります!」(送信)
あなた: 「次の映像を送ります!」(前の確認を待たずに送信)
あなた: 「さらに次の映像を送ります!」(どんどん送信)

なぜWebRTCは速度を重視するのか?

ビデオ通話で考えてみましょう:

  • TCP方式: 「1秒前の完璧な映像」が遅れて届く
  • UDP方式: 「今の少し乱れた映像」がすぐ届く

リアルタイム通信では、多少の品質低下があっても「今の情報」の方が価値があります。

インターネットの「住所問題」- なぜ直接通信が難しいのか?

一般的なインターネット環境では、以下のような問題があります:

【家庭のネットワーク環境】
インターネット --- ルーター --- あなたのPC
                    ↑
                 「門番」の役割

問題の詳細:

  1. あなたのPCの住所(IPアドレス): 192.168.1.10(家庭内でのみ有効)
  2. インターネット上での住所: ルーターの住所(例:203.0.113.1
  3. 他の人があなたに直接連絡しようとしても: ルーターが「知らない人はお断り」

この問題を**「NAT(Network Address Translation:ネットワークアドレス変換)問題」**と呼びます。

WebRTCの解決方法(詳細は次回で説明):

  • STUN(スタン)サーバー: 「あなたのインターネット上の住所を教えてくれるサービス」
  • TURN(ターン)サーバー: 「直接つながらない場合の中継サービス」
  • ICE(アイス)プロトコル: 「最適な接続方法を自動選択する仕組み」

注:これらの詳細な仕組みは次回の記事で詳しく説明します。今は「住所問題を解決する仕組みがある」ということを理解していただければ十分です。

WebSocketとWebRTCの使い分け

どちらもリアルタイム通信を実現しますが、用途と仕組みが大きく異なります

WebSocket - サーバー経由のリアルタイム通信

仕組み:

ユーザーA --- サーバー --- ユーザーB
           チャット中継

特徴:

  • サーバーがメッセージを中継
  • テキストデータが中心
  • 実装がシンプル
  • サーバーリソースが必要

適用例:チャットアプリの実装

ここでは、WebSocketによるチャット機能の基本的な考え方を理解することが目的です。以下のコード例で、サーバー経由でのメッセージ交換の流れを確認してください:

// WebSocketによるチャット実装の考え方
// ポイント:すべてのメッセージがサーバーを経由する

// サーバーへの接続
const ws = new WebSocket('wss://example.com/chat');

// メッセージ送信(サーバー経由で他のユーザーに届く)
function sendMessage(text) {
  const message = {
    type: 'chat',
    text: text,
    timestamp: Date.now()
  };
  
  // サーバーに送信(サーバーが他のユーザーに転送)
  ws.send(JSON.stringify(message));
}

// メッセージ受信(サーバーから他のユーザーのメッセージを受信)
ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  console.log('受信:', message.text);
  
  // 画面に表示する処理
  displayMessage(message);
};

WebRTC - ブラウザ間の直接通信

仕組み:

ユーザーA ←直接通信→ ユーザーB
    ビデオ・音声・データ

特徴:

  • ブラウザ同士が直接通信
  • 音声・映像・データすべて対応
  • 低遅延・高品質
  • サーバーリソース節約

適用例:ビデオ通話の実装

ここでは、WebRTCによる直接通信の基本的な考え方を理解することが目的です。以下のコード例で、ブラウザ間での直接的な映像・音声交換の流れを確認してください:

// WebRTCによるビデオ通話実装の考え方
// ポイント:ブラウザ同士が直接映像・音声をやり取りする

// P2P接続の準備
const peerConnection = new RTCPeerConnection();

// 自分のカメラ・マイクを取得して相手に送信する準備
navigator.mediaDevices.getUserMedia({video: true, audio: true})
  .then(localStream => {
    // 取得した映像・音声を相手に送信する設定
    localStream.getTracks().forEach(track => {
      peerConnection.addTrack(track, localStream);
    });
    
    // 自分の映像を自分の画面に表示
    document.getElementById('localVideo').srcObject = localStream;
  });

// 相手からの映像・音声を直接受信
peerConnection.ontrack = (event) => {
  // 相手の映像を画面に表示
  document.getElementById('remoteVideo').srcObject = event.streams[0];
  console.log('相手の映像を直接受信しました');
};

どちらを選ぶべきか?

WebSocketを選ぶべき場面:

  • テキストチャット
  • 通知システム
  • ゲームのスコア更新
  • シンプルなリアルタイム機能

WebRTCを選ぶべき場面:

  • ビデオ通話・音声通話
  • 画面共有
  • 大容量ファイル転送
  • 低遅延が重要なアプリケーション

WebRTCの主要API群を理解しよう

WebRTCは3つの主要なAPIで構成されています。料理に例えると、「材料を準備」→「調理」→「盛り付け」のような段階的な役割分担があります。

1. MediaStream API - メディアデバイスへのアクセス(材料準備)

役割: カメラ、マイク、画面共有などのメディアデバイスにアクセスして、送信する「材料」を準備する

なぜカメラ・マイクアクセスが必要なのか?

Webページが勝手にカメラやマイクを使えてしまったら、プライバシーの大問題になります。そのため、ブラウザは**「ユーザーの明示的な許可」**を求めます。

カメラとマイクへのアクセスの実装

以下のコードでは、ユーザーのカメラとマイクにアクセスする基本的な流れを理解してください。特に、ユーザーの許可が必要な点と、エラーハンドリングの重要性に注目してください:

// ユーザーのカメラとマイクにアクセスする基本的な実装
// 実行すると、ブラウザに「カメラとマイクの使用を許可しますか?」ダイアログが表示される

async function startCamera() {
  try {
    // ユーザーに許可を求め、カメラ・マイクにアクセス
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: true,  // マイクの使用を要求
      video: {      // カメラの詳細設定
        width: { ideal: 1280 },    // できれば1280pxの横幅
        height: { ideal: 720 },    // できれば720pxの縦幅
        frameRate: { ideal: 30 }   // できれば秒間30フレーム
      }
    });
    
    console.log('カメラとマイクのアクセス許可を取得しました');
    
    // 取得した映像・音声ストリームを画面に表示
    const videoElement = document.getElementById('localVideo');
    videoElement.srcObject = stream;
    
    // この時点で、ユーザーは自分のカメラ映像を画面で確認できる
    // 次のステップ:このストリームを相手に送信する処理へ進む
    
    return stream; // 他の処理で使用するためstreamを返す
    
  } catch (error) {
    console.error('メディアアクセスエラー:', error);
    
    // エラーの種類によって、ユーザーに分かりやすいメッセージを表示
    switch(error.name) {
      case 'NotAllowedError':
        alert('カメラとマイクの使用許可が拒否されました。ブラウザの設定を確認してください。');
        break;
      case 'NotFoundError':
        alert('カメラまたはマイクが見つかりません。デバイスが接続されているか確認してください。');
        break;
      case 'NotReadableError':
        alert('カメラまたはマイクが他のアプリケーションで使用中です。');
        break;
      default:
        alert('メディアアクセスでエラーが発生しました: ' + error.message);
    }
  }
}

// 関数の使用例
// この関数を呼ぶと、カメラ・マイクへのアクセスが開始される
startCamera().then(stream => {
  if (stream) {
    console.log('ストリーム取得成功、次の処理に進めます');
    // ここで次のステップ(P2P接続)の処理を呼び出す
  }
});

画面共有機能の実装

画面共有は、カメラ映像の代わりにデスクトップ画面を相手に送信する機能です。以下のコードで、画面共有の基本的な仕組みを理解してください:

// 画面共有機能の実装
// オンライン会議でプレゼンテーション資料を共有する際に使用される機能

async function startScreenShare() {
  try {
    // ユーザーに画面共有の許可を求める
    // 実行すると「共有する画面を選択してください」ダイアログが表示される
    const screenStream = await navigator.mediaDevices.getDisplayMedia({
      video: true,      // 画面の映像を取得
      audio: true       // システム音声も一緒に共有(オプション)
    });
    
    console.log('画面共有を開始しました');
    
    // 画面共有の映像を表示
    const screenVideo = document.getElementById('screenVideo');
    screenVideo.srcObject = screenStream;
    
    // 画面共有が終了した時の処理
    screenStream.getVideoTracks()[0].onended = () => {
      console.log('画面共有が終了されました');
      // カメラ映像に戻す処理などを実行
      switchBackToCamera();
    };
    
    return screenStream;
    
  } catch (error) {
    console.error('画面共有エラー:', error);
    
    if (error.name === 'NotAllowedError') {
      alert('画面共有がキャンセルされました。');
    } else {
      alert('画面共有でエラーが発生しました: ' + error.message);
    }
  }
}

重要なポイント:

  • ユーザーの明示的な許可が必要
  • HTTPS環境でないとアクセスできない(セキュリティ要件)
  • デバイスの制約(解像度、フレームレート)を考慮する必要がある

2. RTCPeerConnection API - P2P接続の確立と管理(調理)

役割: ブラウザ間のP2P接続を確立し、取得したメディアストリームを実際に送受信する

これは、WebRTCの最も重要で複雑な部分です。「あなたのブラウザ」と「相手のブラウザ」を直接つなげる橋渡しの役割を担います。

P2P接続の基本設定

以下のコードでは、P2P接続を確立するための基本的な準備を理解してください。特に、STUN サーバーの役割と、イベントハンドラーの設定方法に注目してください:

// P2P接続を確立するための基本設定
// この設定により、世界中のどこにいる相手とも直接通信できる準備が整う

// P2P接続オブジェクトを作成
const peerConnection = new RTCPeerConnection({
  iceServers: [
    // STUNサーバー:「あなたのインターネット上の住所」を教えてくれるサービス
    // Googleが無料で提供している公開STUNサーバーを使用
    { urls: 'stun:stun.l.google.com:19302' }
    
    // 注:本格的なサービスでは独自のSTUN/TURNサーバーを用意することが推奨
  ]
});

// 先ほど取得したカメラ・マイクのストリームをP2P接続に追加
// この処理により、相手に自分の映像・音声を送信する準備が完了
async function addLocalStreamToConnection(localStream) {
  localStream.getTracks().forEach(track => {
    peerConnection.addTrack(track, localStream);
    console.log(`${track.kind}トラックを接続に追加しました`); // 'video' または 'audio'
  });
}

// 相手からの映像・音声ストリームを受信する処理
// P2P接続が確立されると、この関数が自動的に呼ばれる
peerConnection.ontrack = (event) => {
  console.log('相手からのストリームを受信しました');
  
  // 受信した相手の映像を画面に表示
  const remoteVideo = document.getElementById('remoteVideo');
  remoteVideo.srcObject = event.streams[0];
  
  // この時点で、双方向のビデオ通話が実現される
};

// 接続状態の変化を監視
// ネットワークの状況や相手の状態に応じて接続状態が変化する
peerConnection.onconnectionstatechange = () => {
  const state = peerConnection.connectionState;
  console.log('P2P接続状態:', state);
  
  // 状態の変化例:
  // 'new' → 'connecting' → 'connected' → 'disconnected'
  
  // ユーザーに接続状況を表示
  document.getElementById('connectionStatus').textContent = 
    state === 'connected' ? '接続中' : 
    state === 'connecting' ? '接続準備中...' : 
    state === 'disconnected' ? '切断されました' : state;
};

// ICE候補(接続経路の候補)が見つかった時の処理
// この処理により、最適な接続経路が自動的に選択される
peerConnection.onicecandidate = (event) => {
  if (event.candidate) {
    console.log('新しい接続経路候補を発見:', event.candidate.type);
    
    // 実際のアプリでは、ここで相手にこの候補を送信する
    // (詳細は次回の「シグナリング」で説明)
    sendToRemotePeer({
      type: 'ice-candidate',
      candidate: event.candidate
    });
  }
};

注意:上記のコードはP2P接続の「準備」段階です。実際に接続を確立するには「シグナリング」という仕組みが必要で、これは次回の記事で詳しく説明します。

3. RTCDataChannel API - P2P上でのデータ通信(盛り付け)

役割: 確立されたP2P接続上で、映像・音声以外のデータ(テキスト、ファイルなど)を送受信する

映像・音声以外にも、チャットメッセージやファイル、ゲームのデータなど、様々な情報をP2P接続で直接やり取りできます。

テキストチャット機能の実装

以下のコードでは、P2P接続上でのチャット機能を理解してください。特に、メッセージの信頼性設定と、データの構造化方法に注目してください:

// P2P接続上でのリアルタイムチャット機能
// WebSocketとは異なり、サーバーを経由せず直接相手とメッセージ交換

// データチャネルの作成(P2P接続確立後に実行)
const dataChannel = peerConnection.createDataChannel('chat', {
  ordered: true,        // メッセージの順序を保証(重要なメッセージには必須)
  maxRetransmits: 3     // 送信失敗時の最大再送回数
});

// データチャネルが使用可能になった時の処理
dataChannel.onopen = () => {
  console.log('チャット機能が利用可能になりました');
  
  // UIでチャット入力欄を有効化
  const chatInput = document.getElementById('chatInput');
  const sendButton = document.getElementById('sendButton');
  
  chatInput.disabled = false;
  sendButton.disabled = false;
  sendButton.textContent = 'メッセージ送信';
};

// チャットメッセージを送信する関数
function sendChatMessage(messageText) {
  // データチャネルが使用可能かチェック
  if (dataChannel.readyState === 'open') {
    // メッセージを構造化(JSON形式で送信)
    const messageData = {
      type: 'chat',           // メッセージの種類
      content: messageText,   // メッセージ内容
      timestamp: Date.now(),  // 送信時刻
      sender: 'me'           // 送信者識別
    };
    
    // JSON文字列として送信
    dataChannel.send(JSON.stringify(messageData));
    
    // 自分の画面にメッセージを表示
    displayMessage(messageData, 'sent');
    
    console.log('メッセージ送信:', messageText);
  } else {
    console.warn('チャット機能が利用できません(接続状態:' + dataChannel.readyState + ')');
    alert('まだチャット機能が利用できません。しばらくお待ちください。');
  }
}

// 相手からのメッセージを受信する処理
dataChannel.onmessage = (event) => {
  try {
    // 受信したJSON文字列を解析
    const messageData = JSON.parse(event.data);
    
    // メッセージの種類に応じて処理を分岐
    switch(messageData.type) {
      case 'chat':
        console.log('チャットメッセージ受信:', messageData.content);
        displayMessage(messageData, 'received');
        break;
        
      case 'file-info':
        console.log('ファイル情報受信:', messageData.filename);
        prepareFileReceive(messageData);
        break;
        
      default:
        console.log('未知のメッセージタイプ:', messageData.type);
    }
  } catch (error) {
    console.error('メッセージ解析エラー:', error);
    console.warn('不正な形式のデータを受信しました');
  }
};

// メッセージを画面に表示する関数
function displayMessage(messageData, direction) {
  const chatArea = document.getElementById('chatArea');
  const messageElement = document.createElement('div');
  
  messageElement.className = direction === 'sent' ? 'message-sent' : 'message-received';
  messageElement.innerHTML = `
    <div class="message-content">${messageData.content}</div>
    <div class="message-time">${new Date(messageData.timestamp).toLocaleTimeString()}</div>
  `;
  
  chatArea.appendChild(messageElement);
  chatArea.scrollTop = chatArea.scrollHeight; // 最新メッセージまでスクロール
}

ファイル転送機能の実装

P2P接続では大容量ファイルも直接転送できます。以下のコードで、ファイルを小さなチャンクに分割して送信する基本的な仕組みを理解してください:

// P2P接続上でのファイル転送機能
// 大きなファイルを小さな断片に分けて、順番に送信する仕組み

async function sendFile(file) {
  // ファイルサイズの制限チェック(例:100MB以下)
  const maxFileSize = 100 * 1024 * 1024; // 100MB
  if (file.size > maxFileSize) {
    alert('ファイルサイズが大きすぎます(100MB以下にしてください)');
    return;
  }
  
  console.log(`ファイル送信開始: ${file.name} (${(file.size / 1024 / 1024).toFixed(2)}MB)`);
  
  // 送信設定
  const chunkSize = 16384; // 16KBずつ送信(ネットワーク負荷を考慮)
  let offset = 0; // 現在の送信位置
  
  // まず、ファイルの基本情報を送信
  const fileInfo = {
    type: 'file-info',
    filename: file.name,
    filesize: file.size,
    mimetype: file.type,
    chunks: Math.ceil(file.size / chunkSize) // 送信するチャンク数
  };
  
  dataChannel.send(JSON.stringify(fileInfo));
  
  // ファイルを読み込んで送信する処理
  const fileReader = new FileReader();
  
  fileReader.onload = (event) => {
    // 読み込んだデータを送信
    dataChannel.send(event.target.result);
    
    offset += chunkSize;
    const progress = Math.min(100, (offset / file.size) * 100);
    
    console.log(`送信進捗: ${progress.toFixed(1)}%`);
    updateProgressBar(progress);
    
    // まだ送信すべきデータがある場合は続行
    if (offset < file.size) {
      sendNextChunk();
    } else {
      console.log('ファイル送信完了');
      // 送信完了を通知
      dataChannel.send(JSON.stringify({type: 'file-complete'}));
    }
  };
  
  // エラーハンドリング
  fileReader.onerror = (error) => {
    console.error('ファイル読み込みエラー:', error);
    alert('ファイルの読み込みでエラーが発生しました');
  };
  
  // 次のチャンクを読み込む関数
  function sendNextChunk() {
    const slice = file.slice(offset, offset + chunkSize);
    fileReader.readAsArrayBuffer(slice); // バイナリデータとして読み込み
  }
  
  // 最初のチャンクから送信開始
  sendNextChunk();
}

// ファイル受信の準備
let receivingFile = null;
let receivedChunks = [];

function prepareFileReceive(fileInfo) {
  console.log(`ファイル受信準備: ${fileInfo.filename} (${(fileInfo.filesize / 1024 / 1024).toFixed(2)}MB)`);
  
  receivingFile = fileInfo;
  receivedChunks = [];
  
  // UIにファイル受信状況を表示
  document.getElementById('fileReceiveStatus').textContent = 
    `受信中: ${fileInfo.filename}`;
}

// バイナリデータ(ファイルの一部)を受信
function appendFileChunk(chunkData) {
  if (receivingFile) {
    receivedChunks.push(chunkData);
    
    const receivedSize = receivedChunks.reduce((total, chunk) => total + chunk.byteLength, 0);
    const progress = (receivedSize / receivingFile.filesize) * 100;
    
    console.log(`受信進捗: ${progress.toFixed(1)}%`);
    updateProgressBar(progress);
  }
}

// ファイル受信完了処理
function finalizeFileReceive() {
  if (receivingFile && receivedChunks.length > 0) {
    // 受信したチャンクを結合してファイルを復元
    const completeFile = new Blob(receivedChunks, {type: receivingFile.mimetype});
    
    // ダウンロードリンクを作成
    const downloadUrl = URL.createObjectURL(completeFile);
    const downloadLink = document.createElement('a');
    downloadLink.href = downloadUrl;
    downloadLink.download = receivingFile.filename;
    downloadLink.textContent = `${receivingFile.filename} をダウンロード`;
    
    document.getElementById('downloadArea').appendChild(downloadLink);
    
    console.log('ファイル受信完了:', receivingFile.filename);
    
    // クリーンアップ
    receivingFile = null;
    receivedChunks = [];
  }
}

データチャネルの特徴:

  • WebSocketに似たAPI設計で使いやすい
  • P2P通信による低遅延(サーバー経由なし)
  • 信頼性と順序保証の細かい設定が可能
  • テキストとバイナリデータの両方に対応

セキュリティの基礎知識

WebRTCには強力なセキュリティ機能が標準で組み込まれており、開発者が特別な設定をしなくても安全な通信が実現されます。

自動で適用される暗号化機能

WebRTCでは、以下の暗号化技術が自動的に適用されます:

DTLS(Datagram Transport Layer Security)

  • HTTPSのSSL/TLSのUDP版
  • データチャネル(テキスト・ファイル)の暗号化に使用
  • 第三者による盗聴・改ざんを防止

SRTP(Secure Real-time Transport Protocol)

  • 音声・映像データ専用の暗号化プロトコル
  • リアルタイム通信に最適化された暗号化
  • ビデオ通話の内容を完全に保護
// 暗号化は自動的に行われる(開発者の追加作業は不要)
const peerConnection = new RTCPeerConnection({
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});

// ↑ この時点で、以下のセキュリティ機能が自動的に有効になる:
// ✓ すべての映像・音声データがSRTPで暗号化
// ✓ すべてのチャット・ファイルデータがDTLSで暗号化
// ✓ 暗号化キーの自動生成・交換
// ✓ 通信相手の身元確認

アプリケーションレベルで実装すべきセキュリティ

標準暗号化とは別に、実際のサービス開発では以下のセキュリティ対策が必要です:

以下のコード例は、実用的なWebRTCアプリケーションで実装すべきセキュリティ対策の考え方を示しています。これらの詳細実装は今後の記事で扱う予定ですが、今は「このようなセキュリティ配慮が必要」ということを理解してください:

// 1. ユーザー認証(シグナリングサーバー側で実装)
// WebRTCの接続確立前に、正当なユーザーかどうかを確認

// 接続時に認証トークンを送信
const socket = io('wss://your-signaling-server.com', {
  auth: {
    token: userAuthToken  // JWT(JSON Web Token)などを使用
  }
});

// サーバー側で認証トークンを検証
socket.on('connect', () => {
  console.log('認証済みユーザーとしてサーバーに接続');
});

socket.on('auth-error', (error) => {
  console.error('認証エラー:', error);
  alert('ログインが必要です');
  redirectToLogin();
});

// 2. ルームアクセス制御
// 特定の会議室やチャットルームへの参加権限を制御

function joinRoom(roomId, accessToken) {
  // ルーム参加前に権限チェック
  socket.emit('join-room', {
    roomId: roomId,
    accessToken: accessToken  // ルーム固有のアクセストークン
  });
}

socket.on('room-access-denied', (reason) => {
  alert('この会議室への参加権限がありません: ' + reason);
});

// 3. 受信データの検証
// P2P通信で受信したデータが悪意のあるものでないかチェック

dataChannel.onmessage = (event) => {
  try {
    const data = JSON.parse(event.data);
    
    // 受信データの安全性チェック
    if (isValidAndSafeMessage(data)) {
      processMessage(data);
    } else {
      console.warn('安全でないデータを受信したため、処理をスキップしました');
    }
  } catch (error) {
    console.warn('不正な形式のデータを受信:', error);
    // 悪意のあるデータや壊れたデータの場合、処理を中断
  }
};

// メッセージの安全性を確認する関数
function isValidAndSafeMessage(data) {
  // 1. 必要なフィールドの存在確認
  if (!data.type || !data.timestamp) {
    return false;
  }
  
  // 2. タイムスタンプの妥当性チェック(リプレイ攻撃の防止)
  const now = Date.now();
  const messageAge = now - data.timestamp;
  if (messageAge > 60000 || messageAge < -5000) { 
    // 1分以上古い、または未来のメッセージは拒否
    return false;
  }
  
  // 3. メッセージサイズの制限
  if (data.content && data.content.length > 1000) { 
    // 1000文字以上のメッセージは拒否
    return false;
  }
  
  // 4. HTMLタグの除去(XSS攻撃の防止)
  if (data.content && containsHTMLTags(data.content)) {
    data.content = stripHTMLTags(data.content);
  }
  
  return true;
}

// HTMLタグを検出する関数
function containsHTMLTags(text) {
  return /<[^>]*>/g.test(text);
}

// HTMLタグを除去する関数
function stripHTMLTags(text) {
  return text.replace(/<[^>]*>/g, '');
}

セキュリティのポイント:

  • 通信内容の暗号化: WebRTCが自動で実施
  • ユーザー認証: アプリケーション側で実装が必要
  • アクセス制御: アプリケーション側で実装が必要
  • データ検証: 受信データの安全性確認が重要

まとめ

この記事では、WebRTCの基本概念と実装に必要な背景知識を段階的に学習しました。

今回学んだこと:

1. WebRTCの本質理解

  • WebRTCは「ブラウザ同士の直接通信」を実現する技術
  • 従来のサーバー経由通信と比べて低遅延・高品質を実現
  • プラグイン不要で、ブラウザ標準機能として利用可能

2. 技術的な背景知識

  • TCP vs UDP: リアルタイム性を重視してUDPを採用
  • NAT問題: 家庭・企業ネットワークでの直接通信の課題
  • STUN/TURN/ICE: NAT問題を解決する仕組み(詳細は次回)

3. 主要API群の役割分担

  • MediaStream API: カメラ・マイクへのアクセス(材料準備)
  • RTCPeerConnection API: P2P接続の確立・管理(調理)
  • RTCDataChannel API: データ通信の実現(盛り付け)

4. セキュリティの基本

  • 通信内容の自動暗号化(DTLS/SRTP)
  • アプリケーションレベルでの追加セキュリティ対策の必要性

知識定着へのアクション

1. ブラウザでの動作確認

まずは、あなたのブラウザでWebRTCが動作するか確認してみましょう:

// ブラウザのコンソール(F12キー → Console)に貼り付けてエンターで実行→ ログを確認
navigator.mediaDevices.getUserMedia({video: true, audio: true})
  .then(stream => {
    console.log('✅ WebRTC対応ブラウザです!');
    console.log('取得したストリーム:', stream);
    
    // ストリームの詳細情報を確認
    stream.getTracks().forEach((track, index) => {
      console.log(`トラック${index + 1}: ${track.kind} - ${track.label}`);
    });
    
    // ストリームを停止(カメラのライトが消える)
    stream.getTracks().forEach(track => track.stop());
  })
  .catch(error => {
    console.error('❌ エラー:', error.name, '-', error.message);
    
    if (error.name === 'NotAllowedError') {
      console.log('💡 カメラ・マイクの使用許可が必要です');
    }
  });

2. WebRTC公式サンプルの体験

  • WebRTC samples にアクセス
  • 「getUserMedia」「Basic peer connection」などのサンプルを試す
  • 各サンプルのソースコードを確認して、今回学んだAPIの使われ方を観察

3. 開発環境の準備

  • Node.js: 公式サイトから最新LTS版をダウンロード
  • テキストエディタ: VS Code、WebStorm、Atomなど、お好みのエディタを準備
  • HTTPS環境: 本格的な開発では必要(次回以降で詳しく説明)

4. 理解度チェック

以下の質問に答えられるか確認してみてください:

  1. WebRTCとWebSocketの違いは何ですか?
  2. なぜWebRTCはUDPを主に使用するのですか?
  3. MediaStream、RTCPeerConnection、RTCDataChannelの役割は何ですか?
  4. NAT問題とは何で、WebRTCはどう解決しますか?

これらに答えられたら、次回の内容を理解する準備が整っています!

最後に

WebRTCは確かに複雑な技術ですが、基本的な概念と仕組みを理解すれば、驚くほど強力で柔軟なアプリケーションを作ることができます。

今回は「材料の準備方法」を学びました。次回は「材料をどう組み合わせて料理するか」、つまり実際の接続確立プロセスを詳しく学んでいきましょう!

次回予告:
次回は「シグナリングとP2P接続確立」について詳しく解説します。今回学んだAPI群が実際にどのように連携して接続を確立するのか、その詳細なメカニズムを学びます。

次回で学ぶ内容:

  • シグナリング: ブラウザ同士が「どうやって最初に知り合うか」の仕組み
  • オファー/アンサー: 接続確立のための「交渉」プロセス
  • ICE候補: 最適な接続経路を見つける方法
  • 実際のコード例: 完全なP2P接続確立の実装

連載記事一覧:

  1. 【今回】基礎理解編 - WebRTCの概要と仕組み
  2. シグナリングとP2P接続確立編(次回)
  3. 実装実践編 - ビデオ通話アプリ作成
  4. トラブルシューティングと実用化編

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

質問・感想お待ちしています!
WebRTCについてわからないことや、実際にコードを試してみた結果など、コメント欄でお気軽にお聞かせください。皆さんのフィードバックを今後の記事にも活かしていきます!

Discussion