WebRTCとは何か

WebRTC(Web Real-Time Communication)は、ブラウザやアプリの間で音声・映像・任意のデータを低遅延でやり取りするための技術だ。 Google MeetやDiscordのブラウザ版、画面共有ツール、ブラウザで完結するファイル転送など、身近なところで広く使われている。 プラグインなしで、JavaScriptのAPIから扱える。

WebRTCは「ブラウザ同士を直接つなぐ(P2P)」技術として紹介されることが多い。 ところが実際にサービスを作ると、シグナリングサーバー・STUNサーバー・TURNサーバーの3種類のサーバーが必要になる。 本記事では、この「P2Pなのにサーバーが要る」理由を軸に、接続が確立するまでの流れと、実務で選ぶ構成を整理する。

GoogleがWebRTCをオープンソース化

買収したGIPSの音声・映像エンジンをもとに公開

Safari 11が対応

主要ブラウザがそろい、モバイルSafariでも使えるようになる

W3C勧告とIETFのRFC群が公開

WebRTC 1.0がW3C勧告に。RFC 8825をはじめプロトコル側の仕様もまとめて発行

WHIPがRFC 9725に

配信ソフトからWebRTCで映像を送り込む手順がHTTPで標準化

WHEPはdraft-04

視聴側の対になるプロトコルは、執筆時点でまだInternet-Draft

ブラウザから見える3つのAPI

開発者が触るAPIは、大きく次の3つだ。

API役割補足
getUserMedia()カメラ・マイクの取得HTTPSなどのセキュアコンテキストでしか呼べない。画面共有は getDisplayMedia()
RTCPeerConnection相手との接続と、音声・映像の送受信接続確立・暗号化・コーデック交渉・帯域制御をまとめて受け持つ
RTCDataChannel任意のデータの送受信順序保証や再送の有無を選べる。ゲームの位置同期やファイル転送に使う

このうち RTCPeerConnection が本体で、残りの2つはそこへ流すメディアやデータの入口にあたる。 ただし RTCPeerConnection は、相手の場所を自分で探しに行かない。そこで次のシグナリングが必要になる。

シグナリング:接続の前に「話し合い」がいる

2台の端末がメディアを送り合うには、事前に次の2種類の情報を交換しておく必要がある。

  • セッション記述(SDP):使えるコーデック、送る映像・音声のトラック、暗号化鍵の指紋(DTLSフィンガープリント)など
  • ICE候補:自分に届くための「住所」の候補(IPアドレスとポートの組)

SDPは、発信側が作る offer と、受信側が返す answer の1往復で交換する(offer/answerモデル)。 ここで大事なのは、この交換をどう運ぶかがWebRTCの仕様に含まれていない点だ。 WebSocketでもHTTPのポーリングでも、極端に言えばコピー&ペーストでもよい。 多くのサービスはWebSocketでシグナリングサーバーを立て、ルーム管理や認証もそこで行う。

sequenceDiagram
    participant A as 発信側ブラウザ
    participant S as シグナリングサーバー
    participant B as 受信側ブラウザ
    A->>A: createOffer / setLocalDescription
    A->>S: offer(SDP)
    S->>B: offer(SDP)
    B->>B: setRemoteDescription / createAnswer / setLocalDescription
    B->>S: answer(SDP)
    S->>A: answer(SDP)
    A-->>B: ICE候補を随時交換(Trickle ICE)
    A->>B: 疎通確認後、メディアを直接送信(DTLS-SRTP)
offer/answerとICE候補の交換はシグナリングサーバー経由、メディアは端末間で直接流れる

最小のコード

発信側の処理を最小限に書くと次のようになる。signaling は自前のWebSocketなどを包んだオブジェクトとする。

const pc = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.example.com:3478' },
    { urls: 'turn:turn.example.com:3478', username: 'user', credential: 'pass' },
  ],
});

// カメラとマイクを取得してトラックを追加
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
stream.getTracks().forEach((track) => pc.addTrack(track, stream));

// 見つかったICE候補を、その都度相手へ送る(Trickle ICE)
pc.addEventListener('icecandidate', (event) => {
  if (event.candidate) signaling.send({ type: 'candidate', candidate: event.candidate });
});

// 相手の映像が届いたら<video>に表示
pc.addEventListener('track', (event) => {
  remoteVideo.srcObject = event.streams[0];
});

// offerを作って送る
await pc.setLocalDescription(await pc.createOffer());
signaling.send({ type: 'offer', sdp: pc.localDescription });

// 相手からのanswerとICE候補を受け取る
signaling.onmessage = async (msg) => {
  if (msg.type === 'answer') await pc.setRemoteDescription(msg.sdp);
  if (msg.type === 'candidate') await pc.addIceCandidate(msg.candidate);
};

pc.addEventListener('connectionstatechange', () => {
  console.log('connectionState:', pc.connectionState); // connected になれば成功
});

NAT越え:ICE・STUN・TURN

シグナリングで「住所」を交換しても、そのまま届くとは限らない。 家庭や会社の端末の多くはNAT(ルーター)の内側にあり、自分のプライベートIP(192.168.x.x など)しか知らないからだ。 これを解決する仕組みが ICE(Interactive Connectivity Establishment、RFC 8445)で、次の3種類の候補を集めて、つながる組み合わせを総当たりで試す。

候補の種類取得方法中身つながる条件
host端末自身のNICプライベートIPとポート同じLAN内など、直接届く場合
srflx(server reflexive)STUNサーバーに問い合わせるNATの外側から見えたグローバルIPとポートNATが穴開け(ホールパンチング)を許す場合
relayTURNサーバーに中継用アドレスを確保してもらうTURNサーバー上のアドレスほぼ常につながる(その代わり全トラフィックが中継される)
flowchart TD
    Start["ICE候補を収集"] --> H{"host候補で<br/>届くか"}
    H -->|"届く"| D1["直接通信"]
    H -->|"届かない"| R{"STUNで得た<br/>srflx候補で届くか"}
    R -->|"届く"| D2["NAT越しに直接通信"]
    R -->|"届かない"| T["TURNのrelay候補で中継"]
ICEは候補を並行して試し、優先度の高い経路から採用する(図は優先順位を単純化したもの)

STUNは「自分が外からどう見えるか」を教えるだけの軽いサーバーで、メディアは通らない。 一方TURNはメディアをすべて中継するため、帯域コストがかかる。 それでもTURNを省けないのは、対称型NAT(宛先ごとに外側のポートが変わるNAT)や、UDPを通さない企業ファイアウォールの内側では、穴開けが成立しないからだ。 TURNはTCPやTLS(443番ポート)でも中継できるので、厳しいネットワークでも最後の手段になる。

暗号化とトランスポート

WebRTCでは暗号化を切ることができない(RFC 8827)。 ICEで経路が決まると、まずその上でDTLS(UDP版のTLS)のハンドシェイクを行い、そこで得た鍵で音声・映像をSRTPで暗号化する。これを DTLS-SRTP と呼ぶ。 SDPに書かれたDTLSフィンガープリントと、実際のハンドシェイクで提示された証明書を照合するので、シグナリングが改ざんされていなければ途中で盗聴できない。

データチャネルは、DTLSの上にSCTPを載せて実現している(RFC 8831)。 SCTPのおかげで、チャネルごとに「順序保証あり・なし」「再送する・しない(回数や時間で打ち切る)」を選べる。 リアルタイム性が大事な位置情報は「順序なし・再送なし」、ファイル転送は「順序あり・再送あり」と使い分ける。

// 最新の値だけ届けばよいデータ:遅れて届いた古い値は不要なので再送しない
const positions = pc.createDataChannel('positions', { ordered: false, maxRetransmits: 0 });

// 欠けてはいけないデータ:TCPに近い挙動(デフォルト)
const files = pc.createDataChannel('files');

参加者が増えたら:Mesh・SFU・MCU

1対1ならP2Pで十分だが、参加者が増えると話が変わる。 全員が全員とP2Pでつなぐ Mesh では、N人の会議で各端末が自分の映像をN−1本エンコードして送り出す必要があり、上り帯域とCPUがすぐ足りなくなる。 そこでサーバーを間に置く構成が使われる。

構成仕組み端末の上りサーバー負荷向いている用途
Mesh全員が全員と直接接続N−1本なし(シグナリング・TURNのみ)1対1、2〜4人程度の小規模
SFUサーバーが受け取ったストリームを復号・再エンコードせずに転送1本(Simulcastなら解像度違いを数本)中(転送のみ)ビデオ会議、多人数配信。現在の標準構成
MCUサーバーが全員の映像を1枚に合成して配る1本大(デコード・合成・再エンコード)古い会議端末との相互接続、録画用の合成

SFU(Selective Forwarding Unit)は、受信者の回線状況に合わせて「どの解像度のストリームを流すか」を選べる。 送信側が高・中・低の3段階を同時に送るSimulcastと組み合わせると、回線の細い参加者には低解像度だけを送れる。 OSSではmediasoup・LiveKit・Janus・Pion(Go実装、SFUを組むためのライブラリ)などが定番だ。

配信の世界へ:WHIPとWHEP

WebRTCはシグナリングを規定しないため、配信ソフトとメディアサーバーをつなぐたびに独自のやり方が必要だった。 これを統一したのが WHIP(WebRTC-HTTP Ingestion Protocol)で、2025年3月に RFC 9725 として標準化された。 手順は単純で、クライアントがSDPのofferをHTTP POSTで送り、サーバーが 201 Created でanswerを返すだけだ。セッションの終了は、返されたリソースURLへのHTTP DELETEで行う。

POST /whip/endpoint HTTP/1.1
Host: media.example.com
Content-Type: application/sdp
Authorization: Bearer <token>

v=0
o=- 5228595038118931041 2 IN IP4 127.0.0.1
...(SDP offer)

HTTP/1.1 201 Created
Content-Type: application/sdp
Location: https://media.example.com/whip/resource/abc123

v=0
...(SDP answer)

OBS Studio(30以降)がWHIP出力に対応したこともあり、RTMPの代わりにWHIPでサブ秒遅延の配信を取り込む構成が増えている。 視聴側の対になる WHEP(WebRTC-HTTP Egress Protocol)は、執筆時点(2026年10月)でまだInternet-Draft(draft-ietf-wish-whep-04)の段階だ。 実装は先行して出回っているが、RFCになるまでは細部が変わりうる前提で扱う。

よくある誤解

誤解実際
P2Pだからサーバーは要らないシグナリングは必須。NAT越えにはSTUN、つながらない環境のためにTURNも要る
STUNがあればたいていつながる対称型NATやUDP遮断の環境では穴開けできない。本番ではTURNを前提に設計する
WebSocketの高速版として置き換えられるクライアントとサーバーの単純なメッセージ交換なら、WebSocketのほうが実装も運用も軽い。WebRTCが効くのは、低遅延のメディアや、損失を許容してでも遅延を減らしたいデータ
WebRTCなら必ずエンドツーエンド暗号化暗号化は常に有効だが、SFUを挟むとSFUで一度復号される。E2EEには追加の仕組みが要る
多人数でもP2Pのほうが安いMeshでは参加者の上り帯域が足りなくなる。5人を超えるあたりからSFUを検討するのが一般的

実務での使い方:作る前に決めること

  1. マネージドかセルフホストか:まずはLiveKit Cloud、Cloudflare Realtime、Twilioなどのマネージドサービスで十分かを検討する。TURNとSFUの運用、とくに帯域コストと障害対応は想像以上に重い
  2. TURNを用意する:セルフホストならcoturnが定番。認証情報は固定値にせず、有効期限付きの一時クレデンシャルをシグナリングサーバーから払い出す
  3. 構成を人数で決める:1対1はP2P(TURNフォールバック付き)、それ以上はSFU。配信の取り込みはWHIPを第一候補にする
  4. 状態を観測する:pc.getStats() で往復遅延(RTT)・パケットロス・ジッター、採用された候補の種類(host・srflx・relay)を定期的に取り、relay率の高さや品質劣化を追えるようにする。開発中はChromeのchrome://webrtc-internalsが便利だ
  5. 再接続を設計する:Wi-Fiからモバイル回線への切り替えなどで経路が切れたら、pc.restartIce() でICEだけをやり直せる。接続全体を作り直す前にまずこれを試す

理解度チェック

問題 0 / 50%
Q1

NATの内側の端末が、自分がNATの外側からどのIPアドレスとポートで見えているかを知るために問い合わせるサーバーを___サーバーという