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 アプリケーションの開発に取り組んでいただければと思います。
関連リンク
- WebRTC 1.0: Real-time Communication Between Browsers (W3C)
- RFC 5245: Interactive Connectivity Establishment (ICE)
- RFC 8829: JavaScript Session Establishment Protocol (JSEP)
- RFC 5764: Datagram Transport Layer Security (DTLS) Extension
- RFC 3550: RTP: A Transport Protocol for Real-Time Applications
- MDN Web Docs: WebRTC API
- webrtc.org: WebRTC Home
- article
WebRTC の内部プロトコル徹底解剖:DTLS-SRTP・RTP/RTCP・ICE-Trickle を一気に理解
- article
WebRTC 技術設計:SFU vs MCU vs P2P の選定基準と費用対効果
- article
WebRTC でビデオチャットアプリを作る手順【初心者向け】
- article
WebRTC API の基本:getUserMedia, RTCPeerConnection, RTCDataChannel
- article
WebRTC と WebSocket の違いをわかりやすく比較
- article
WebRTC 入門:ブラウザだけで始めるリアルタイム通信
- article
Zustand × Next.js の Hydration Mismatch を根絶する:原因別チェックリスト
- article
NestJS 依存循環(circular dependency)を断ち切る:ModuleRef と forwardRef の実戦対処
- article
MySQL ロック待ち・タイムアウトの解決:SHOW ENGINE INNODB STATUS の読み解き方
- article
WordPress を Docker で最速構築:開発/本番の環境差分をなくす手順
- article
Motion(旧 Framer Motion)でカクつき・ちらつきを潰す:レイアウトシフトと FLIP の落とし穴
- article
WebSocket 導入判断ガイド:SSE・WebTransport・長輪講ポーリングとの適材適所を徹底解説
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来