T-CREATOR

WebRTC の内部プロトコル徹底解剖:DTLS-SRTP・RTP/RTCP・ICE-Trickle を一気に理解

WebRTC の内部プロトコル徹底解剖:DTLS-SRTP・RTP/RTCP・ICE-Trickle を一気に理解

WebRTC は現代のリアルタイムコミュニケーションの基盤技術として、ブラウザ間でのビデオ・音声通話を可能にしています。しかし、その内部で動作している複数のプロトコルがどのように連携し、セキュアで高品質なメディア伝送を実現しているかは、多くの開発者にとって複雑で理解しづらい領域でしょう。

今回は、WebRTC の核となる DTLS-SRTP、RTP/RTCP、ICE-Trickle という 3 つの主要プロトコルを深く掘り下げ、それぞれの役割と相互作用を図解とともに詳しく解説いたします。この記事を読むことで、WebRTC の内部アーキテクチャを体系的に理解できるようになるでしょう。

WebRTC プロトコルスタックの全体像

WebRTC のプロトコルスタックは、OSI 参照モデルのように階層化された構造を持っています。各層が特定の責任を担い、上位層のサービスを支える設計になっています。

WebRTC プロトコルスタックの階層構造を図で確認してみましょう。

mermaidflowchart TB
    app[アプリケーション層<br/>JavaScript API]

    subgraph webrtc[WebRTC プロトコルスタック]
        signaling[シグナリング層<br/>SDP/WebSocket]
        security[セキュリティ層<br/>DTLS-SRTP]
        media[メディア伝送層<br/>RTP/RTCP]
        transport[トランスポート層<br/>ICE/STUN/TURN]
        network[ネットワーク層<br/>UDP/TCP]
    end

    app --> signaling
    signaling --> security
    security --> media
    media --> transport
    transport --> network

この図では、WebRTC が多層構造で構成されていることがわかります。各層は独立した機能を持ちながら、上下の層と密接に連携してリアルタイム通信を実現しています。

プロトコル連携の仕組み

各プロトコルは以下のような役割分担で動作します:

  • ICE-Trickle: 最適な通信経路を確立
  • DTLS-SRTP: セキュアなチャネルを構築
  • RTP/RTCP: 効率的なメディア伝送を実行

これらのプロトコルが段階的に動作することで、NAT やファイアウォールを越えたセキュアなリアルタイム通信が可能になります。

セキュリティ層:DTLS-SRTP の役割

WebRTC におけるセキュリティは、DTLS(Datagram Transport Layer Security)と SRTP(Secure Real-time Transport Protocol)の組み合わせによって実現されています。この DTLS-SRTP は、WebRTC 通信の機密性と完全性を保証する重要な役割を担っています。

DTLS によるハンドシェイク

DTLS は、UDP 上で動作する TLS の拡張版です。WebRTC では、メディアチャネルの暗号化に必要な鍵交換と認証を DTLS ハンドシェイクで行います。

DTLS ハンドシェイクの流れを詳しく見てみましょう。

mermaidsequenceDiagram
    participant Client_A as クライアントA
    participant Client_B as クライアントB

    Client_A->>Client_B: ClientHello<br/>(暗号スイート提案)
    Client_B->>Client_A: ServerHello + Certificate<br/>(証明書送信)
    Client_A->>Client_B: Certificate Verify<br/>(証明書検証)
    Client_B->>Client_A: ServerKeyExchange<br/>(公開鍵交換)
    Client_A->>Client_B: ClientKeyExchange<br/>(プリマスターシークレット)
    Client_A->>Client_B: ChangeCipherSpec + Finished
    Client_B->>Client_A: ChangeCipherSpec + Finished

    Note over Client_A, Client_B: SRTP マスターキー導出完了

このハンドシェイクプロセスでは、両方のクライアントが相互に証明書を検証し、共通の暗号化鍵を安全に生成します。WebRTC では自己署名証明書が使用され、フィンガープリント値をシグナリングサーバー経由で交換することで認証を行います。

DTLS ハンドシェイクの実装詳細

DTLS ハンドシェイクでは以下の処理が段階的に実行されます。

javascript// DTLS 設定の初期化
const dtlsTransport = new RTCDtlsTransport(iceTransport);

// 証明書の生成(自己署名)
const certificate =
  await RTCPeerConnection.generateCertificate({
    name: 'ECDSA',
    namedCurve: 'P-256',
  });

// フィンガープリント値の取得
const fingerprint = certificate.getFingerprints()[0];
console.log('証明書フィンガープリント:', fingerprint.value);

WebRTC では、各ピアが独自の証明書を生成し、そのフィンガープリント値をシグナリング経由で相手に送信します。これにより、中間者攻撃を防ぐことができます。

SRTP による暗号化メカニズム

DTLS ハンドシェイクが完了すると、導出された鍵を使用して SRTP によるメディアパケットの暗号化が開始されます。

javascript// SRTP パラメータの設定
const srtpParameters = {
  cryptoSuite: 'AES_CM_128_HMAC_SHA1_80',
  keyLen: 128,
  saltLen: 112,
  authTagLen: 80,
};

// RTP パケットの暗号化処理
function encryptRTPPacket(
  rtpPacket,
  masterKey,
  masterSalt
) {
  // セッション鍵の導出
  const sessionKey = deriveSessionKey(
    masterKey,
    masterSalt,
    rtpPacket.ssrc
  );

  // ペイロードの暗号化
  const encryptedPayload = aes128Encrypt(
    rtpPacket.payload,
    sessionKey
  );

  // 認証タグの生成
  const authTag = hmacSha1(
    rtpPacket.header + encryptedPayload,
    sessionKey
  );

  return {
    header: rtpPacket.header,
    payload: encryptedPayload,
    authTag: authTag,
  };
}

SRTP では、各 RTP パケットに対して個別のセッション鍵を導出し、AES-128 暗号化と HMAC-SHA1 認証を適用します。これにより、パケットレベルでの機密性と完全性が保証されます。

鍵交換と証明書検証

WebRTC における鍵交換は、DTLS-SRTP の核心的な機能です。安全な鍵配布により、第三者によるメディアの盗聴や改ざんを防止します。

javascript// 証明書検証のプロセス
async function verifyCertificate(
  remoteCertificate,
  expectedFingerprint
) {
  // 証明書のフィンガープリント計算
  const actualFingerprint = await calculateFingerprint(
    remoteCertificate
  );

  // シグナリングで受信したフィンガープリントと比較
  if (actualFingerprint !== expectedFingerprint) {
    throw new Error(
      '証明書フィンガープリントが一致しません'
    );
  }

  // 証明書の有効期限チェック
  const now = new Date();
  if (
    now < remoteCertificate.notBefore ||
    now > remoteCertificate.notAfter
  ) {
    throw new Error('証明書の有効期限が切れています');
  }

  return true;
}

マスターキーの導出プロセス

DTLS ハンドシェイク完了後、SRTP で使用するマスターキーを導出します。

javascript// SRTP マスターキーの導出
function deriveSRTPKeys(
  dtlsMasterSecret,
  clientRandom,
  serverRandom
) {
  const keyMaterial = prf(
    dtlsMasterSecret,
    'EXTRACTOR-dtls_srtp',
    clientRandom + serverRandom,
    60 // SRTP_AES_CM_128_HMAC_SHA1_80 の鍵長
  );

  return {
    clientWriteKey: keyMaterial.slice(0, 16),
    serverWriteKey: keyMaterial.slice(16, 32),
    clientWriteSalt: keyMaterial.slice(32, 46),
    serverWriteSalt: keyMaterial.slice(46, 60),
  };
}

このプロセスにより、送信用と受信用の異なる鍵ペアが生成され、双方向の安全な通信が実現されます。

メディア伝送層:RTP/RTCP の仕組み

RTP(Real-time Transport Protocol)と RTCP(RTP Control Protocol)は、WebRTC におけるメディアデータの効率的な伝送と品質制御を担当します。リアルタイム性が求められる音声・映像通信において、これらのプロトコルは欠かせない役割を果たしています。

RTP パケット構造の詳細

RTP パケットは、ヘッダー部とペイロード部から構成され、メディアデータの配信に必要な情報を含んでいます。

RTP パケットの内部構造を詳しく見てみましょう。

mermaidflowchart TB
    subgraph rtp["RTP パケット構造"]
        subgraph header["RTP ヘッダー (12バイト)"]
            v["V:バージョン<br/>2bit"]
            p["P:パディング<br/>1bit"]
            x["X:拡張<br/>1bit"]
            cc["CC:CSRC数<br/>4bit"]
            m["M:マーカー<br/>1bit"]
            pt["PT:ペイロードタイプ<br/>7bit"]
            seq["シーケンス番号<br/>16bit"]
            timestamp["タイムスタンプ<br/>32bit"]
            ssrc["SSRC識別子<br/>32bit"]
        end

        subgraph optional["オプション部"]
            csrc["CSRC リスト<br/>可変長"]
            ext["ヘッダー拡張<br/>可変長"]
        end

        payload["ペイロード<br/>音声/映像データ"]
    end

    v --> p --> x --> cc --> m --> pt
    pt --> seq --> timestamp --> ssrc
    ssrc --> csrc --> ext --> payload

RTP ヘッダーの各フィールドは、メディアストリームの管理と同期に重要な役割を果たします。特にタイムスタンプとシーケンス番号は、パケットの順序制御と同期再生に使用されます。

RTP ヘッダーの処理実装

RTP パケットの生成と解析を行うコードを見てみましょう。

javascript// RTP ヘッダーの構造定義
class RTPHeader {
  constructor() {
    this.version = 2; // RTP バージョン
    this.padding = 0; // パディングフラグ
    this.extension = 0; // 拡張フラグ
    this.csrcCount = 0; // CSRC 数
    this.marker = 0; // マーカービット
    this.payloadType = 0; // ペイロードタイプ
    this.sequenceNumber = 0; // シーケンス番号
    this.timestamp = 0; // タイムスタンプ
    this.ssrc = 0; // 同期ソース識別子
  }

  // ヘッダーのエンコード
  encode() {
    const buffer = new ArrayBuffer(12);
    const view = new DataView(buffer);

    // 最初の1バイト: V(2) + P(1) + X(1) + CC(4)
    view.setUint8(
      0,
      (this.version << 6) |
        (this.padding << 5) |
        (this.extension << 4) |
        this.csrcCount
    );

    // 2バイト目: M(1) + PT(7)
    view.setUint8(1, (this.marker << 7) | this.payloadType);

    // シーケンス番号 (2バイト)
    view.setUint16(2, this.sequenceNumber);

    // タイムスタンプ (4バイト)
    view.setUint32(4, this.timestamp);

    // SSRC (4バイト)
    view.setUint32(8, this.ssrc);

    return buffer;
  }
}

RTCP フィードバック制御

RTCP は RTP と組み合わせて使用され、通信品質の監視とフィードバックを提供します。WebRTC では特に、パケットロス率や遅延の情報を交換して適応的な品質制御を行います。

javascript// RTCP レポートの生成
class RTCPSenderReport {
  constructor(ssrc) {
    this.packetType = 200; // SR パケットタイプ
    this.ssrc = ssrc;
    this.ntpTimestamp = 0; // NTP タイムスタンプ
    this.rtpTimestamp = 0; // RTP タイムスタンプ
    this.packetCount = 0; // 送信パケット数
    this.octetCount = 0; // 送信オクテット数
    this.receptionReports = []; // 受信レポート
  }

  // 送信統計の更新
  updateStats(rtpPacket) {
    this.packetCount++;
    this.octetCount += rtpPacket.payload.length;
    this.rtpTimestamp = rtpPacket.timestamp;
    this.ntpTimestamp = Date.now() * 1000; // マイクロ秒
  }
}

RTCP フィードバックメカニズム

RTCP は複数のパケットタイプを使用して、詳細な品質情報を交換します。

mermaidflowchart LR
    subgraph rtcp_types[RTCP パケットタイプ]
        sr[SR<br/>送信者レポート]
        rr[RR<br/>受信者レポート]
        sdes[SDES<br/>ソース記述]
        bye[BYE<br/>終了通知]
        app[APP<br/>アプリ固有]
    end

    subgraph feedback[フィードバック情報]
        loss[パケットロス率]
        jitter[ジッター統計]
        delay[往復遅延]
        bandwidth[帯域使用率]
    end

    sr --> loss
    rr --> jitter
    sdes --> delay
    app --> bandwidth

    feedback --> adaptation[適応制御]
    adaptation --> bitrate[ビットレート調整]
    adaptation --> resolution[解像度変更]

RTCP フィードバックにより、ネットワーク状況に応じたリアルタイムな品質調整が可能になります。

ペイロード形式とコーデック連携

RTP ペイロードの形式は、使用するコーデックによって決定されます。WebRTC では、音声・映像それぞれに最適化されたペイロード形式が定義されています。

javascript// 音声ペイロード(Opus コーデック)の処理
class OpusRTPPayload {
  constructor() {
    this.payloadType = 111; // Opus の標準 PT
    this.clockRate = 48000; // サンプリングレート
    this.channels = 2; // ステレオ
  }

  // Opus フレームのパッケージング
  packageFrame(opusFrame, timestamp) {
    const rtpPacket = new RTPPacket();
    rtpPacket.header.payloadType = this.payloadType;
    rtpPacket.header.timestamp = timestamp;
    rtpPacket.payload = opusFrame;

    // Opus 固有の設定
    if (opusFrame.length > 1200) {
      // フラグメンテーションが必要
      return this.fragmentFrame(opusFrame, timestamp);
    }

    return [rtpPacket];
  }
}

映像ペイロード(VP8/VP9)の処理

映像コーデックでは、フレームタイプやパーティション情報を含む詳細なペイロード記述子が使用されます。

javascript// VP8 ペイロード記述子の実装
class VP8PayloadDescriptor {
  constructor() {
    this.extendedControlBits = 0; // X ビット
    this.nonReferenceFrame = 0; // N ビット
    this.startOfPartition = 0; // S ビット
    this.partitionIndex = 0; // PID フィールド
  }

  // VP8 フレームのRTPパッケージング
  packageVP8Frame(vp8Frame, isKeyFrame) {
    const packets = [];
    const maxPayloadSize = 1200; // MTU を考慮

    let offset = 0;
    let partitionIndex = 0;

    while (offset < vp8Frame.length) {
      const remainingBytes = vp8Frame.length - offset;
      const payloadSize = Math.min(
        maxPayloadSize,
        remainingBytes
      );

      const rtpPacket = new RTPPacket();

      // VP8 ペイロード記述子の設定
      const descriptor = new VP8PayloadDescriptor();
      descriptor.startOfPartition = offset === 0 ? 1 : 0;
      descriptor.partitionIndex = partitionIndex;

      rtpPacket.payload = this.createVP8Payload(
        descriptor,
        vp8Frame.slice(offset, offset + payloadSize)
      );

      packets.push(rtpPacket);
      offset += payloadSize;
    }

    return packets;
  }
}

これらのペイロード形式により、各コーデックの特性を活かした効率的なメディア伝送が実現されています。

接続確立層:ICE-Trickle の動作原理

ICE(Interactive Connectivity Establishment)は、NAT やファイアウォールが存在する環境でも確実に通信経路を確立するためのプロトコルです。Trickle ICE は、この ICE プロセスを段階的に実行することで、接続確立の高速化を実現します。

ICE 候補収集プロセス

ICE では、利用可能な通信経路の候補(ICE candidate)を収集し、それらをテストして最適な経路を選択します。

ICE 候補収集の全体的な流れを図で確認しましょう。

mermaidflowchart TB
    start[ICE エージェント開始]

    subgraph gathering[候補収集フェーズ]
        host[ホスト候補<br/>ローカルIP]
        reflexive[Server Reflexive候補<br/>STUN経由]
        relay[Relay候補<br/>TURN経由]
    end

    subgraph connectivity[コネクティビティチェック]
        pairing[候補ペア生成]
        priority[優先度計算]
        check[接続性テスト]
        nomination[候補指名]
    end

    start --> gathering
    host --> reflexive --> relay
    gathering --> connectivity
    pairing --> priority --> check --> nomination
    nomination --> established[接続確立完了]

この図では、ICE が段階的に候補を収集し、最終的に最適な通信経路を選択するプロセスが示されています。

ICE 候補の種類と収集

ICE では、3 つの主要な候補タイプを収集します。

javascript// ICE 候補の基本構造
class ICECandidate {
  constructor(type, ip, port, protocol = 'udp') {
    this.foundation = this.generateFoundation(type, ip);
    this.component = 1; // RTP=1, RTCP=2
    this.protocol = protocol;
    this.priority = this.calculatePriority(type);
    this.ip = ip;
    this.port = port;
    this.type = type; // host, srflx, relay
    this.relatedAddress = null;
    this.relatedPort = null;
  }

  // 候補の優先度計算
  calculatePriority(type) {
    const typePreference = {
      host: 126, // ホスト候補が最優先
      srflx: 100, // Server Reflexive 候補
      relay: 0, // Relay 候補は最後の手段
    };

    return (
      (typePreference[type] << 24) +
      (65535 << 8) +
      (256 - this.component)
    );
  }
}

ホスト候補の収集

ローカルネットワークインターフェースから直接取得される候補です。

javascript// ホスト候補の収集
async function gatherHostCandidates() {
  const candidates = [];

  // ネットワークインターフェースの取得
  const interfaces = await getNetworkInterfaces();

  for (const iface of interfaces) {
    if (iface.family === 'IPv4' && !iface.internal) {
      const candidate = new ICECandidate(
        'host',
        iface.address,
        0
      );

      // 動的ポートの割り当て
      const socket = createUDPSocket();
      candidate.port = socket.localPort;

      candidates.push(candidate);
    }
  }

  return candidates;
}

Server Reflexive 候補の収集

STUN サーバーを使用して、NAT 外部から見えるパブリック IP アドレスを取得します。

javascript// STUN による Server Reflexive 候補の収集
async function gatherServerReflexiveCandidates(
  stunServers,
  hostCandidates
) {
  const candidates = [];

  for (const stunServer of stunServers) {
    for (const hostCandidate of hostCandidates) {
      try {
        // STUN バインディングリクエストの送信
        const response = await sendSTUNBindingRequest(
          stunServer,
          hostCandidate.ip,
          hostCandidate.port
        );

        // XOR-MAPPED-ADDRESS の取得
        if (response.xorMappedAddress) {
          const candidate = new ICECandidate(
            'srflx',
            response.xorMappedAddress.ip,
            response.xorMappedAddress.port
          );

          candidate.relatedAddress = hostCandidate.ip;
          candidate.relatedPort = hostCandidate.port;

          candidates.push(candidate);
        }
      } catch (error) {
        console.error('STUN リクエストエラー:', error);
      }
    }
  }

  return candidates;
}

Trickle ICE による段階的接続

従来の ICE では、すべての候補を収集してからコネクティビティチェックを開始していましたが、Trickle ICE では候補が見つかり次第、順次チェックを開始します。

javascript// Trickle ICE の実装
class TrickleICEAgent {
  constructor() {
    this.candidates = [];
    this.remoteCandidates = [];
    this.candidatePairs = [];
    this.state = 'gathering';
  }

  // 候補が見つかり次第、即座に処理
  onLocalCandidateFound(candidate) {
    this.candidates.push(candidate);

    // シグナリングで相手に送信
    this.signaling.sendCandidate(candidate);

    // 既知のリモート候補とペアリング
    this.createCandidatePairs(candidate);

    // 即座にコネクティビティチェック開始
    this.startConnectivityChecks();
  }

  // 相手からの候補受信
  onRemoteCandidateReceived(candidate) {
    this.remoteCandidates.push(candidate);

    // ローカル候補とペアリング
    for (const localCandidate of this.candidates) {
      this.createCandidatePair(localCandidate, candidate);
    }

    this.startConnectivityChecks();
  }
}

STUN/TURN サーバーとの連携

ICE では、NAT 越えとリレー通信のために STUN と TURN サーバーを使用します。

STUN/TURN サーバーとの連携プロセスを詳しく見てみましょう。

mermaidsequenceDiagram
    participant Client as クライアント
    participant STUN as STUN サーバー
    participant TURN as TURN サーバー
    participant Remote as リモートピア

    Note over Client: ホスト候補収集
    Client->>STUN: Binding Request
    STUN->>Client: Binding Response<br/>(パブリックIP/ポート)
    Note over Client: Server Reflexive 候補取得

    Client->>TURN: Allocate Request<br/>(認証情報付き)
    TURN->>Client: Allocate Response<br/>(リレーアドレス)
    Note over Client: Relay 候補取得

    Client->>Remote: ICE 候補交換<br/>(シグナリング経由)

    Note over Client, Remote: コネクティビティチェック
    Client->>Remote: STUN Binding Request<br/>(各候補ペアで)
    Remote->>Client: STUN Binding Response

    Note over Client, Remote: 最適経路決定・通信開始

この図では、STUN サーバーでパブリックアドレスを発見し、TURN サーバーでリレーアドレスを確保する流れが示されています。

TURN サーバーによるリレー通信

直接通信が不可能な場合の最後の手段として、TURN サーバーを経由したリレー通信を行います。

javascript// TURN による Relay 候補の取得
async function allocateTURNRelay(turnServer, credentials) {
  const turnClient = new TURNClient(turnServer);

  try {
    // TURN サーバーでの認証
    await turnClient.authenticate(
      credentials.username,
      credentials.password
    );

    // アドレス割り当てリクエスト
    const allocation = await turnClient.allocate({
      requestedTransport: 'UDP',
      lifetime: 600, // 10分間
    });

    const relayCandidate = new ICECandidate(
      'relay',
      allocation.relayAddress.ip,
      allocation.relayAddress.port
    );

    // 定期的なリフレッシュを設定
    setInterval(() => {
      turnClient.refresh(allocation.allocationId);
    }, 300000); // 5分間隔

    return relayCandidate;
  } catch (error) {
    console.error('TURN アロケーションエラー:', error);
    throw error;
  }
}

コネクティビティチェックの実装

収集された候補ペアに対して、実際の接続性をテストします。

javascript// コネクティビティチェックの実行
class ConnectivityChecker {
  constructor() {
    this.checkList = [];
    this.validatedPairs = [];
    this.nominatedPair = null;
  }

  // 候補ペアのチェック実行
  async performConnectivityCheck(candidatePair) {
    const stunRequest = this.createSTUNBindingRequest();

    try {
      // ICE-CONTROLLING/ICE-CONTROLLED 属性の設定
      stunRequest.addAttribute(
        'ICE-CONTROLLING',
        this.tieBreaker
      );
      stunRequest.addAttribute(
        'PRIORITY',
        candidatePair.priority
      );
      stunRequest.addAttribute(
        'USE-CANDIDATE',
        candidatePair.useCandidate
      );

      // STUN リクエストの送信
      const response = await this.sendSTUNRequest(
        stunRequest,
        candidatePair.remote.ip,
        candidatePair.remote.port,
        candidatePair.local
      );

      // 成功レスポンスの場合、ペアを有効とマーク
      if (response.isSuccess()) {
        candidatePair.state = 'succeeded';
        this.validatedPairs.push(candidatePair);

        // USE-CANDIDATE フラグがある場合、指名
        if (stunRequest.hasAttribute('USE-CANDIDATE')) {
          this.nominatedPair = candidatePair;
          this.concludeICE();
        }
      }
    } catch (error) {
      candidatePair.state = 'failed';
      console.error('コネクティビティチェック失敗:', error);
    }
  }
}

ICE-Trickle により、WebRTC は様々なネットワーク環境でも確実に通信経路を確立し、最適なパフォーマンスを実現しています。

プロトコル間の相互作用とフロー

WebRTC における DTLS-SRTP、RTP/RTCP、ICE-Trickle の 3 つのプロトコルは、単独で動作するのではなく、密接に連携してリアルタイム通信を実現しています。これらのプロトコル間の相互作用を理解することで、WebRTC の全体的な動作メカニズムが明確になります。

全体的な通信フロー

WebRTC 通信の開始から終了までの全体的なフローを詳しく見てみましょう。

mermaidsequenceDiagram
    participant App_A as アプリA
    participant ICE_A as ICE Agent A
    participant DTLS_A as DTLS A
    participant Signaling as シグナリング<br/>サーバー
    participant DTLS_B as DTLS B
    participant ICE_B as ICE Agent B
    participant App_B as アプリB

    Note over App_A, App_B: 1. シグナリングフェーズ
    App_A->>ICE_A: 候補収集開始
    App_B->>ICE_B: 候補収集開始
    ICE_A->>Signaling: ICE 候補送信
    ICE_B->>Signaling: ICE 候補送信
    Signaling->>ICE_B: ICE 候補転送
    Signaling->>ICE_A: ICE 候補転送

    Note over App_A, App_B: 2. 接続確立フェーズ
    ICE_A->>ICE_B: コネクティビティチェック
    ICE_B->>ICE_A: チェック応答
    Note over ICE_A, ICE_B: ICE 接続確立完了

    Note over App_A, App_B: 3. セキュリティフェーズ
    DTLS_A->>DTLS_B: DTLS ハンドシェイク開始
    DTLS_B->>DTLS_A: 証明書交換・検証
    Note over DTLS_A, DTLS_B: SRTP 鍵導出完了

    Note over App_A, App_B: 4. メディア伝送フェーズ
    App_A->>App_B: RTP/SRTP パケット送信
    App_B->>App_A: RTCP フィードバック
    App_A->>App_B: 品質適応済みメディア

この図では、各プロトコルが段階的に動作し、最終的にセキュアなメディア通信が確立される様子が示されています。

プロトコル間のデータ共有

各プロトコルは、他のプロトコルから必要な情報を取得して動作します。この相互依存関係により、効率的で安全な通信が実現されています。

javascript// プロトコル間の情報共有を管理するクラス
class WebRTCContext {
  constructor() {
    this.iceAgent = new ICEAgent();
    this.dtlsTransport = null;
    this.rtpTransceiver = null;
    this.securityContext = {
      localFingerprint: null,
      remoteFingerprint: null,
      srtpKeys: null,
    };
  }

  // ICE 完了後の DTLS 開始
  onICEConnectionEstablished(selectedPair) {
    console.log('ICE 接続確立完了:', selectedPair);

    // ICE で確立された経路で DTLS トランスポートを初期化
    this.dtlsTransport = new DTLSTransport(
      selectedPair.localCandidate
    );

    // DTLS ハンドシェイク開始
    this.dtlsTransport.start({
      role: this.determineRole(),
      remoteFingerprint:
        this.securityContext.remoteFingerprint,
    });
  }

  // DTLS 完了後の RTP 開始
  onDTLSHandshakeComplete(srtpKeys) {
    console.log('DTLS ハンドシェイク完了');

    this.securityContext.srtpKeys = srtpKeys;

    // SRTP コンテキストの初期化
    this.rtpTransceiver = new RTPTransceiver({
      transport: this.dtlsTransport,
      encryptionKeys: srtpKeys,
    });

    // メディア送信開始
    this.rtpTransceiver.startSending();
  }
}

ICE と DTLS の連携

ICE で確立された通信経路上で、DTLS ハンドシェイクが実行されます。この連携により、最適な経路でのセキュア通信が実現されます。

javascript// ICE-DTLS 連携の実装
class ICEDTLSCoordinator {
  constructor() {
    this.iceState = 'new';
    this.dtlsState = 'new';
    this.mediaFlowEnabled = false;
  }

  // ICE 状態変化の処理
  onICEStateChange(newState, connectionPair) {
    this.iceState = newState;

    switch (newState) {
      case 'connected':
        console.log('ICE 接続確立、DTLS 開始');
        this.startDTLS(connectionPair);
        break;

      case 'disconnected':
        console.log('ICE 切断、メディアフロー停止');
        this.stopMediaFlow();
        break;

      case 'failed':
        console.log('ICE 失敗、再接続試行');
        this.restartICE();
        break;
    }
  }

  // DTLS 状態変化の処理
  onDTLSStateChange(newState, keyMaterial) {
    this.dtlsState = newState;

    if (
      newState === 'connected' &&
      this.iceState === 'connected'
    ) {
      console.log('ICE + DTLS 完了、メディアフロー開始');
      this.enableMediaFlow(keyMaterial);
    }
  }

  // メディアフロー制御
  enableMediaFlow(srtpKeys) {
    this.mediaFlowEnabled = true;

    // RTP エンドポイントの設定
    this.configureRTPEndpoint(srtpKeys);

    // RTCP フィードバックループの開始
    this.startRTCPFeedback();
  }
}

DTLS-SRTP と RTP/RTCP の連携

DTLS ハンドシェイクで生成された暗号化鍵を使用して、RTP パケットの SRTP 暗号化が行われます。

javascript// DTLS-SRTP と RTP の連携実装
class SRTPMediaProcessor {
  constructor() {
    this.srtpContext = null;
    this.rtpStats = {
      packetsSent: 0,
      packetsReceived: 0,
      bytesTransferred: 0,
    };
  }

  // DTLS 鍵導出完了時の初期化
  initializeSRTP(keyMaterial) {
    this.srtpContext = new SRTPContext({
      sendKey: keyMaterial.localKey,
      sendSalt: keyMaterial.localSalt,
      receiveKey: keyMaterial.remoteKey,
      receiveSalt: keyMaterial.remoteSalt,
      cryptoSuite: 'AES_CM_128_HMAC_SHA1_80',
    });

    console.log('SRTP コンテキスト初期化完了');
  }

  // RTP パケットの送信処理
  sendRTPPacket(mediaData, payloadType, timestamp) {
    // RTP パケットの構築
    const rtpPacket = this.buildRTPPacket(
      mediaData,
      payloadType,
      timestamp
    );

    // SRTP 暗号化
    const srtpPacket = this.srtpContext.encrypt(rtpPacket);

    // ICE で確立された経路で送信
    this.sendOverICEConnection(srtpPacket);

    // 統計情報の更新
    this.updateSendStats(srtpPacket);

    return srtpPacket;
  }

  // SRTP パケットの受信処理
  onSRTPPacketReceived(encryptedPacket) {
    try {
      // SRTP 復号化
      const rtpPacket =
        this.srtpContext.decrypt(encryptedPacket);

      // RTP パケットの処理
      this.processRTPPacket(rtpPacket);

      // 統計情報の更新
      this.updateReceiveStats(rtpPacket);

      // RTCP フィードバックの生成
      this.generateRTCPFeedback(rtpPacket);
    } catch (error) {
      console.error('SRTP 復号化エラー:', error);
      this.handleDecryptionError(error);
    }
  }
}

適応的品質制御の実装

RTCP フィードバック情報を基に、ネットワーク状況に応じた動的な品質調整を行います。

javascript// 適応的品質制御システム
class AdaptiveQualityController {
  constructor() {
    this.networkMetrics = {
      rtt: 0, // 往復遅延
      packetLoss: 0, // パケットロス率
      jitter: 0, // ジッター
      bandwidth: 0, // 利用可能帯域
    };

    this.mediaSettings = {
      videoBitrate: 1000000, // 1Mbps
      videoResolution: '720p',
      audioCodec: 'opus',
      videoCodec: 'vp8',
    };
  }

  // RTCP フィードバック処理
  processRTCPFeedback(rtcpReport) {
    // ネットワークメトリクスの更新
    this.updateNetworkMetrics(rtcpReport);

    // 品質調整の判定
    const adaptationDecision = this.decideAdaptation();

    // メディア設定の調整
    if (adaptationDecision.shouldAdapt) {
      this.applyAdaptation(adaptationDecision);
    }
  }

  // 適応判定ロジック
  decideAdaptation() {
    const decision = {
      shouldAdapt: false,
      newBitrate: this.mediaSettings.videoBitrate,
      newResolution: this.mediaSettings.videoResolution,
    };

    // パケットロス率に基づく判定
    if (this.networkMetrics.packetLoss > 0.05) {
      // 5%以上
      decision.shouldAdapt = true;
      decision.newBitrate = Math.max(
        this.mediaSettings.videoBitrate * 0.8,
        200000 // 最低200kbps
      );
    }

    // RTT に基づく判定
    if (this.networkMetrics.rtt > 200) {
      // 200ms以上
      decision.shouldAdapt = true;
      decision.newResolution = this.downgradeResolution(
        this.mediaSettings.videoResolution
      );
    }

    return decision;
  }

  // 品質調整の適用
  applyAdaptation(decision) {
    console.log('品質調整実行:', decision);

    // エンコーダー設定の更新
    this.updateEncoderSettings({
      bitrate: decision.newBitrate,
      resolution: decision.newResolution,
    });

    // 新しい設定でメディア送信継続
    this.mediaSettings.videoBitrate = decision.newBitrate;
    this.mediaSettings.videoResolution =
      decision.newResolution;
  }
}

エラー処理と復旧メカニズム

プロトコル間の連携において、エラーが発生した場合の復旧処理も重要です。

javascript// 統合エラー処理システム
class WebRTCErrorHandler {
  constructor(webrtcContext) {
    this.context = webrtcContext;
    this.errorCounts = {
      ice: 0,
      dtls: 0,
      rtp: 0,
    };
  }

  // ICE エラーの処理
  handleICEError(error) {
    this.errorCounts.ice++;

    console.error('ICE エラー:', error);

    switch (error.type) {
      case 'connectivity_timeout':
        // ICE 再開始
        this.context.iceAgent.restart();
        break;

      case 'stun_server_failure':
        // 代替 STUN サーバーへ切り替え
        this.context.iceAgent.switchSTUNServer();
        break;

      default:
        // TURN サーバー経由へフォールバック
        this.context.iceAgent.forceRelay();
    }
  }

  // DTLS エラーの処理
  handleDTLSError(error) {
    this.errorCounts.dtls++;

    console.error('DTLS エラー:', error);

    if (error.type === 'handshake_failure') {
      // 証明書の再生成とハンドシェイク再試行
      this.context.regenerateCertificate();
      this.context.restartDTLS();
    }
  }

  // RTP/RTCP エラーの処理
  handleRTPError(error) {
    this.errorCounts.rtp++;

    console.error('RTP エラー:', error);

    if (error.type === 'decryption_failure') {
      // SRTP キーの再ネゴシエーション
      this.context.renegotiateSRTPKeys();
    }
  }
}

これらのプロトコル間連携により、WebRTC は複雑なネットワーク環境でも安定したリアルタイム通信を実現しています。各プロトコルが独立しながらも協調して動作することで、高品質な音声・映像通信が可能になっているのです。

まとめ

本記事では、WebRTC の内部で動作する 3 つの主要プロトコル(DTLS-SRTP、RTP/RTCP、ICE-Trickle)について、その役割と相互作用を詳しく解説いたしました。

各プロトコルの重要なポイント

**DTLS-SRTP(セキュリティ層)**では、WebRTC 通信の機密性と完全性を保証します。DTLS ハンドシェイクによる安全な鍵交換と、SRTP によるパケットレベルの暗号化により、第三者からの盗聴や改ざんを防止しています。自己署名証明書とフィンガープリント検証の組み合わせにより、証明書インフラに依存しない認証を実現していることも特徴的です。

**RTP/RTCP(メディア伝送層)**では、リアルタイム性を重視したメディアデータの効率的な配信を担当します。RTP パケットのヘッダー情報により順序制御と同期再生を実現し、RTCP フィードバックによってネットワーク状況に応じた適応的品質制御を行います。各コーデックに最適化されたペイロード形式により、音声・映像それぞれの特性を活かした伝送が可能です。

**ICE-Trickle(接続確立層)**では、NAT やファイアウォールが存在する複雑なネットワーク環境でも確実に通信経路を確立します。段階的な候補収集とコネクティビティチェックにより、最適な通信経路を動的に選択し、STUN/TURN サーバーとの連携で様々なネットワーク制約を克服しています。

プロトコル連携の価値

これらのプロトコルは単独で動作するのではなく、密接に連携することで WebRTC の価値を創出しています。ICE で確立された経路上で DTLS ハンドシェイクを実行し、生成された鍵で RTP パケットを暗号化するという段階的な処理により、セキュアで効率的なリアルタイム通信が実現されています。

また、RTCP フィードバックによる適応的品質制御や、エラー発生時の協調的な復旧処理など、プロトコル間の情報共有により、動的なネットワーク環境でも安定した通信品質を維持できます。

今後の展望

WebRTC は現在も活発に発展を続けており、新しいコーデックの採用や、より効率的な候補収集アルゴリズムの実装など、継続的な改善が行われています。これらの内部プロトコルの理解により、より高度な WebRTC アプリケーションの開発や、パフォーマンスの最適化、トラブルシューティングが可能になるでしょう。

WebRTC の内部アーキテクチャは複雑ですが、各プロトコルの役割と連携メカニズムを理解することで、リアルタイム通信技術の本質的な仕組みを把握できます。この知識を基に、より質の高い WebRTC アプリケーションの開発に取り組んでいただければと思います。

関連リンク