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)
最小のコード
発信側の処理を最小限に書くと次のようになる。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が穴開け(ホールパンチング)を許す場合 |
| relay | TURNサーバーに中継用アドレスを確保してもらう | TURNサーバー上のアドレス | ほぼ常につながる(その代わり全トラフィックが中継される) |
flowchart TD
Start["ICE候補を収集"] --> H{"host候補で<br/>届くか"}
H -->|"届く"| D1["直接通信"]
H -->|"届かない"| R{"STUNで得た<br/>srflx候補で届くか"}
R -->|"届く"| D2["NAT越しに直接通信"]
R -->|"届かない"| T["TURNのrelay候補で中継"]
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を検討するのが一般的 |
実務での使い方:作る前に決めること
- マネージドかセルフホストか:まずはLiveKit Cloud、Cloudflare Realtime、Twilioなどのマネージドサービスで十分かを検討する。TURNとSFUの運用、とくに帯域コストと障害対応は想像以上に重い
- TURNを用意する:セルフホストならcoturnが定番。認証情報は固定値にせず、有効期限付きの一時クレデンシャルをシグナリングサーバーから払い出す
- 構成を人数で決める:1対1はP2P(TURNフォールバック付き)、それ以上はSFU。配信の取り込みはWHIPを第一候補にする
- 状態を観測する:
pc.getStats()で往復遅延(RTT)・パケットロス・ジッター、採用された候補の種類(host・srflx・relay)を定期的に取り、relay率の高さや品質劣化を追えるようにする。開発中はChromeのchrome://webrtc-internalsが便利だ - 再接続を設計する:Wi-Fiからモバイル回線への切り替えなどで経路が切れたら、
pc.restartIce()でICEだけをやり直せる。接続全体を作り直す前にまずこれを試す
理解度チェック
NATの内側の端末が、自分がNATの外側からどのIPアドレスとポートで見えているかを知るために問い合わせるサーバーを___サーバーという