T-CREATOR

WebRTC とは?仕組みと基本機能を徹底解説

WebRTC とは?仕組みと基本機能を徹底解説

WebRTC とは?仕組みと基本機能を徹底解説

リアルタイムコミュニケーション技術は、私たちの日常的な Web 体験を大きく変えてきました。ビデオ会議、オンライン学習、ライブ配信など、今では当たり前となっている機能の多くが、WebRTC(Web Real-Time Communication)という技術によって支えられています。

本記事では、WebRTC の仕組みや基本機能について、初心者の方にもわかりやすく解説いたします。複雑に見える技術も、段階的に理解することで、その魅力と可能性を実感していただけるはずです。

背景

Web コミュニケーションの進化

インターネットが普及し始めた頃、Web ブラウザでできることは主に静的なページの閲覧でした。しかし、技術の発展とともに、動的なコンテンツ、Ajax 通信、そしてリアルタイム通信へと進化を続けています。

特に 2010 年代に入ると、ビデオ会議やオンラインゲームなど、リアルタイムでの双方向コミュニケーションへの需要が急速に高まりました。従来は専用のソフトウェアやプラグインが必要だった機能を、ブラウザだけで実現したいというニーズが生まれたのです。

mermaidflowchart TD
    A[静的Webページ] --> B[動的コンテンツ]
    B --> C[Ajax通信]
    C --> D[WebSocket]
    D --> E[WebRTC]

    A -.-> A1[HTML表示のみ]
    B -.-> B1[JavaScript動作]
    C -.-> C1[サーバー通信]
    D -.-> D1[双方向通信]
    E -.-> E1[P2P通信]

上図は、Web 技術の発展段階を示しています。各段階で可能になった機能が、最終的に WebRTC でのリアルタイム通信を実現する基盤となっています。

WebRTC が生まれた背景

従来のリアルタイム通信では、以下のような課題がありました。

課題説明影響
プラグイン依存Flash や専用ソフトが必要インストール作業、セキュリティリスク
NAT 越えの困難ファイアウォール越えが複雑接続失敗率の高さ
開発コスト独自プロトコルの実装が必要開発期間の長期化

これらの課題を解決するため、Google を中心とした企業グループが WebRTC の仕様策定を開始しました。2011 年にオープンソース化され、現在では W3C と IETF による標準仕様として確立されています。

従来技術との比較

WebRTC 登場前後での技術比較を見てみましょう。

Flash 時代の課題

javascript// Flash時代のビデオ通信(ActionScript例)
// プラグインが必要で、セキュリティ制約が多い
var camera: Camera = Camera.getCamera();
var video: Video = new Video();
video.attachCamera(camera);

Flash では上記のようにカメラアクセスは可能でしたが、ブラウザ間での P2P 通信は非常に困難でした。

WebRTC 時代の解決策

javascript// WebRTCでのメディア取得
navigator.mediaDevices
  .getUserMedia({
    video: true,
    audio: true,
  })
  .then((stream) => {
    // ストリームを直接利用可能
    videoElement.srcObject = stream;
  });

WebRTC では、標準的な JavaScript API でメディアを取得し、プラグイン不要で P2P 通信を実現できます。

WebRTC の仕組み

P2P 通信の基本概念

WebRTC の最も重要な特徴は、Peer-to-Peer(P2P)通信です。従来のクライアント・サーバーモデルとは異なり、ブラウザ同士が直接データをやり取りします。

mermaidflowchart LR
    subgraph "従来のサーバー経由通信"
        A[ブラウザA] -->|アップロード| S[サーバー]
        S -->|ダウンロード| B[ブラウザB]
    end

    subgraph "WebRTCのP2P通信"
        C[ブラウザC] <-->|直接通信| D[ブラウザD]
    end

この図が示すように、WebRTC では中間サーバーを経由せずに直接通信を行うことで、低遅延と高品質な通信を実現しています。

P2P 通信のメリットは以下の通りです。

  • 低遅延: サーバーを経由しないため、遅延が最小限
  • 帯域効率: サーバーの帯域を消費しない
  • スケーラビリティ: ユーザー数に比例してコストが増加しない
  • プライバシー: データが第三者サーバーを通らない

シグナリングサーバーの役割

P2P 通信と聞くと「サーバーは不要」と思われがちですが、実際には初期接続のためのシグナリングサーバーが必要です。

シグナリングサーバーは以下の役割を担います。

mermaidsequenceDiagram
    participant A as ブラウザA
    participant S as シグナリングサーバー
    participant B as ブラウザB

    A->>S: 通話開始リクエスト
    S->>B: 通話の通知
    B->>S: 接続OK応答
    A->>S: Offer(接続情報)
    S->>B: Offerを転送
    B->>S: Answer(応答情報)
    S->>A: Answerを転送

    Note over A,B: この後P2P通信開始
    A-->>B: 直接音声・映像データ(送信)
    B-->>A: 直接音声・映像データ(送信)

上記のシーケンス図は、シグナリングサーバーが仲介役となって、P2P 通信の開始に必要な情報交換を行う様子を表しています。

シグナリング実装例

javascript// WebSocketを使ったシグナリングサーバー(Node.js)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

// 接続されたクライアントを管理
const clients = new Set();

wss.on('connection', (ws) => {
  clients.add(ws);

  // メッセージを他のクライアントに転送
  ws.on('message', (message) => {
    const data = JSON.parse(message);

    // 送信者以外の全クライアントに転送
    clients.forEach((client) => {
      if (
        client !== ws &&
        client.readyState === WebSocket.OPEN
      ) {
        client.send(JSON.stringify(data));
      }
    });
  });
});

このコードは、簡単なシグナリングサーバーの実装例です。WebSocket を使って、接続情報を他のクライアントに転送する役割を果たします。

STUN サーバーと TURN サーバー

実際のインターネット環境では、多くのデバイスが NAT(Network Address Translation)やファイアウォールの内側に配置されています。P2P 通信を確立するために、STUN サーバーと TURN サーバーが重要な役割を果たします。

STUN サーバーの仕組み

STUN(Session Traversal Utilities for NAT)サーバーは、デバイスの「グローバル IP アドレスとポート番号」を教えてくれるサーバーです。

javascript// STUN設定の例
const iceServers = [
  {
    urls: 'stun:stun.l.google.com:19302',
  },
];

const peerConnection = new RTCPeerConnection({
  iceServers: iceServers,
});

STUN サーバーを使用することで、NAT 内のデバイスでも外部からアクセス可能なアドレス情報を取得できます。

TURN サーバーの必要性

一部のネットワーク環境では、厳格なファイアウォールによって P2P 通信が不可能な場合があります。そのような状況では、TURN サーバーがリレー(中継)の役割を果たします。

mermaidflowchart TD
    A[ブラウザA] -->|直接通信試行| B[ブラウザB]
    A -.->|失敗時| T[TURNサーバー]
    T -.-> B

    A -->|成功率約80%| A1[STUN経由P2P]
    A -.->|成功率約20%| A2[TURN経由中継]

この図は、WebRTC の接続確立における成功パターンを示しています。多くの場合は STUN 経由で P2P 通信が可能ですが、一部の環境では TURN サーバー経由の通信が必要になります。

ICE(Interactive Connectivity Establishment)

ICE は、最適な通信経路を自動的に見つける仕組みです。複数の接続候補を試し、最も良い接続方法を選択します。

ICE が試行する接続候補の優先順位は以下の通りです。

優先度接続タイプ説明
1Host同一 LAN 内での直接通信
2Server ReflexiveSTUN 経由での通信
3RelayTURN 経由での中継通信
javascript// ICE候補の生成イベント
peerConnection.onicecandidate = (event) => {
  if (event.candidate) {
    console.log('ICE候補:', event.candidate);

    // シグナリングサーバー経由で相手に送信
    signalingSocket.send(
      JSON.stringify({
        type: 'ice-candidate',
        candidate: event.candidate,
      })
    );
  }
};

上記コードは、ICE 候補が生成された際の処理例です。生成された候補を相手のブラウザに送信し、接続テストを行います。

WebRTC の基本機能

MediaStream の取得

WebRTC でリアルタイム通信を行うには、まずユーザーのカメラやマイクからメディアストリームを取得する必要があります。

基本的なメディア取得

javascript// カメラとマイクへのアクセス許可を要求
async function getLocalStream() {
  try {
    const stream =
      await navigator.mediaDevices.getUserMedia({
        video: true,
        audio: true,
      });

    return stream;
  } catch (error) {
    console.error('メディアアクセスエラー:', error);
    throw error;
  }
}

このコードは、ユーザーのカメラとマイクにアクセスし、MediaStream オブジェクトを取得します。初回実行時には、ブラウザがユーザーに許可を求めるダイアログを表示します。

詳細な制約指定

より細かい設定でメディアストリームを取得することも可能です。

javascript// 高解像度ビデオとノイズキャンセリング音声の取得
const constraints = {
  video: {
    width: { min: 640, ideal: 1280, max: 1920 },
    height: { min: 480, ideal: 720, max: 1080 },
    frameRate: { ideal: 30, max: 60 },
  },
  audio: {
    echoCancellation: true, // エコーキャンセレーション
    noiseSuppression: true, // ノイズ除去
    autoGainControl: true, // 自動音量調整
  },
};

const stream = await navigator.mediaDevices.getUserMedia(
  constraints
);

これらの制約設定により、通信品質を大幅に向上させることができます。

画面共有の実装

javascript// 画面共有用のストリーム取得
async function getScreenStream() {
  try {
    const stream =
      await navigator.mediaDevices.getDisplayMedia({
        video: {
          cursor: 'always', // マウスカーソルも含める
        },
        audio: true, // システム音声も共有
      });

    return stream;
  } catch (error) {
    console.error('画面共有エラー:', error);
    throw error;
  }
}

getDisplayMedia API を使用することで、画面全体やアプリケーションウィンドウの共有が可能になります。

RTCPeerConnection の確立

RTCPeerConnection は、WebRTC の P2P 通信を管理する中核となるクラスです。

PeerConnection 初期化

javascript// ICE サーバー設定
const iceServers = [
  { urls: 'stun:stun.l.google.com:19302' },
  {
    urls: 'turn:example.com:3478',
    username: 'user',
    credential: 'pass',
  },
];

// RTCPeerConnection インスタンス作成
const peerConnection = new RTCPeerConnection({
  iceServers: iceServers,
});

初期化時に、STUN/TURN サーバーの設定を行います。これにより、様々なネットワーク環境での接続が可能になります。

オファー・アンサー交換

WebRTC の接続確立では、「オファー」と「アンサー」という 2 つのメッセージを交換します。

javascript// 発信者側: オファー作成
async function createOffer() {
  // ローカルストリームを追加
  localStream.getTracks().forEach((track) => {
    peerConnection.addTrack(track, localStream);
  });

  // オファー作成
  const offer = await peerConnection.createOffer();

  // ローカルディスクリプションとして設定
  await peerConnection.setLocalDescription(offer);

  // シグナリングサーバー経由で相手に送信
  signalingSocket.send(
    JSON.stringify({
      type: 'offer',
      offer: offer,
    })
  );
}

オファーには、音声・映像の形式、暗号化キーなど、通信に必要な情報が含まれています。

javascript// 着信者側: アンサー作成
async function createAnswer(offer) {
  // リモートディスクリプションとして設定
  await peerConnection.setRemoteDescription(offer);

  // ローカルストリームを追加
  localStream.getTracks().forEach((track) => {
    peerConnection.addTrack(track, localStream);
  });

  // アンサー作成
  const answer = await peerConnection.createAnswer();

  // ローカルディスクリプションとして設定
  await peerConnection.setLocalDescription(answer);

  // シグナリングサーバー経由で相手に送信
  signalingSocket.send(
    JSON.stringify({
      type: 'answer',
      answer: answer,
    })
  );
}

アンサーは、オファーに対する応答として、着信者の通信設定を相手に伝えます。

データチャンネル通信

WebRTC では、音声・映像だけでなく、任意のデータを送受信できるデータチャンネル機能があります。

データチャンネル作成

javascript// データチャンネル作成(信頼性重視)
const reliableChannel = peerConnection.createDataChannel(
  'reliable',
  {
    ordered: true, // 順序保証あり
    maxRetransmits: 3, // 最大3回再送
  }
);

// データチャンネル作成(速度重視)
const unreliableChannel = peerConnection.createDataChannel(
  'unreliable',
  {
    ordered: false, // 順序保証なし
    maxPacketLifeTime: 100, // 100ms以内に配信
  }
);

用途に応じて、信頼性と速度のバランスを調整できます。

データ送受信の実装

javascript// データ送信
function sendMessage(message) {
  if (reliableChannel.readyState === 'open') {
    reliableChannel.send(
      JSON.stringify({
        type: 'chat',
        message: message,
        timestamp: Date.now(),
      })
    );
  }
}

// データ受信
reliableChannel.onmessage = (event) => {
  const data = JSON.parse(event.data);

  switch (data.type) {
    case 'chat':
      displayChatMessage(data.message);
      break;
    case 'file':
      handleFileTransfer(data);
      break;
  }
};

データチャンネルを活用することで、チャット機能やファイル転送など、豊富な機能を実現できます。

具体例

簡単なビデオ通話アプリの実装

ここでは、基本的なビデオ通話アプリを段階的に実装していきます。

HTML 構造

html<!DOCTYPE html>
<html>
  <head>
    <title>WebRTC ビデオ通話</title>
    <style>
      video {
        width: 300px;
        height: 200px;
        border: 1px solid #ccc;
      }
      .container {
        display: flex;
        gap: 20px;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div>
        <h3>ローカル映像</h3>
        <video id="localVideo" autoplay muted></video>
      </div>
      <div>
        <h3>リモート映像</h3>
        <video id="remoteVideo" autoplay></video>
      </div>
    </div>

    <div>
      <button id="startCall">通話開始</button>
      <button id="endCall">通話終了</button>
    </div>
  </body>
</html>

シンプルな構造で、ローカルとリモートの映像を表示できるようになっています。

JavaScript 実装

javascriptclass SimpleVideoCall {
  constructor() {
    this.localVideo = document.getElementById('localVideo');
    this.remoteVideo =
      document.getElementById('remoteVideo');
    this.startButton = document.getElementById('startCall');
    this.endButton = document.getElementById('endCall');

    this.localStream = null;
    this.peerConnection = null;
    this.signalingSocket = null;

    this.setupEventListeners();
    this.connectSignalingServer();
  }

  setupEventListeners() {
    this.startButton.onclick = () => this.startCall();
    this.endButton.onclick = () => this.endCall();
  }
}

基本的なクラス構造を定義し、UI 要素への参照を保持します。

通話開始処理

javascriptasync startCall() {
    try {
        // ローカルストリーム取得
        this.localStream = await navigator.mediaDevices.getUserMedia({
            video: true,
            audio: true
        });

        this.localVideo.srcObject = this.localStream;

        // PeerConnection初期化
        this.setupPeerConnection();

        // ストリームをPeerConnectionに追加
        this.localStream.getTracks().forEach(track => {
            this.peerConnection.addTrack(track, this.localStream);
        });

        console.log('通話準備完了');

    } catch (error) {
        console.error('通話開始エラー:', error);
        alert('カメラ・マイクアクセスに失敗しました');
    }
}

ユーザーのメディアにアクセスし、PeerConnection を準備します。

PeerConnection 設定

javascriptsetupPeerConnection() {
    this.peerConnection = new RTCPeerConnection({
        iceServers: [
            { urls: 'stun:stun.l.google.com:19302' }
        ]
    });

    // リモートストリーム受信時の処理
    this.peerConnection.ontrack = (event) => {
        console.log('リモートストリーム受信');
        this.remoteVideo.srcObject = event.streams[0];
    };

    // ICE候補生成時の処理
    this.peerConnection.onicecandidate = (event) => {
        if (event.candidate) {
            this.sendSignalingMessage({
                type: 'ice-candidate',
                candidate: event.candidate
            });
        }
    };

    // 接続状態変更の監視
    this.peerConnection.onconnectionstatechange = () => {
        console.log('接続状態:', this.peerConnection.connectionState);
    };
}

PeerConnection の各種イベントハンドラを設定し、通信状態を監視します。

画面共有機能の実装

ビデオ通話に画面共有機能を追加してみましょう。

画面共有ボタンの追加

html<div>
  <button id="startCall">通話開始</button>
  <button id="shareScreen">画面共有</button>
  <button id="stopShare">共有停止</button>
  <button id="endCall">通話終了</button>
</div>

画面共有用のボタンを追加します。

画面共有実装

javascriptasync shareScreen() {
    try {
        // 画面共有ストリーム取得
        const screenStream = await navigator.mediaDevices.getDisplayMedia({
            video: true,
            audio: true
        });

        // 映像トラックを置き換え
        const videoSender = this.peerConnection.getSenders().find(
            sender => sender.track && sender.track.kind === 'video'
        );

        if (videoSender) {
            await videoSender.replaceTrack(screenStream.getVideoTracks()[0]);
        }

        // ローカル表示も更新
        this.localVideo.srcObject = screenStream;

        // 画面共有終了時の処理
        screenStream.getVideoTracks()[0].onended = () => {
            this.stopScreenShare();
        };

        console.log('画面共有開始');

    } catch (error) {
        console.error('画面共有エラー:', error);
        alert('画面共有に失敗しました');
    }
}

getDisplayMedia API を使用して画面を取得し、既存のビデオトラックと置き換えます。

共有停止処理

javascriptasync stopScreenShare() {
    try {
        // カメラストリームに戻す
        const cameraStream = await navigator.mediaDevices.getUserMedia({
            video: true,
            audio: true
        });

        // 映像トラックを元に戻す
        const videoSender = this.peerConnection.getSenders().find(
            sender => sender.track && sender.track.kind === 'video'
        );

        if (videoSender) {
            await videoSender.replaceTrack(cameraStream.getVideoTracks()[0]);
        }

        this.localVideo.srcObject = cameraStream;
        console.log('カメラに戻しました');

    } catch (error) {
        console.error('共有停止エラー:', error);
    }
}

画面共有を停止し、通常のカメラ映像に戻します。

まとめ

WebRTC は、現代の Web アプリケーションにおいて欠かせない技術となっています。本記事では、その仕組みと基本機能について詳しく解説してきました。

主要なポイント

WebRTC の理解において重要な要点をまとめますと以下の通りです。

  • P2P 通信によりサーバーを経由しない直接通信を実現
  • シグナリングサーバーで初期接続情報を交換
  • STUN/TURN サーバーで NAT 越えを解決
  • MediaStream APIでカメラ・マイクにアクセス
  • RTCPeerConnectionで通信を管理
  • データチャンネルで任意のデータ送受信が可能

実装時の注意点

実際に WebRTC アプリケーションを開発する際は、以下の点にご注意ください。

  1. エラーハンドリングの充実
  2. ブラウザ互換性の確認
  3. セキュリティの考慮(HTTPS 必須)
  4. 通信品質の監視と調整

WebRTC の技術は日々進歩しており、新しい API や機能が継続的に追加されています。基本概念を理解することで、より高度な機能の実装や、最新技術の活用も可能になるでしょう。

この記事が、WebRTC を使った素晴らしいアプリケーション開発の第一歩となれば幸いです。

関連リンク