100日チャレンジ day5 (WebRTC 動画チャットアプリ)

昨日
100日チャレンジに感化されたので、アレンジして自分でもやってみます。
やりたいこと
- 世の中のさまざまなドメインの簡易実装をつくり、バックエンドの実装に慣れる(dbスキーマ設計や、関数の分割、使いやすいインターフェイスの切り方に慣れる
- 設計力(これはシステムのオーバービューを先に自分で作ってaiに依頼できるようにする
- 生成aiをつかったバイブコーティングになれる
- 実際にやったことはzennのスクラップにまとめ、成果はzennのブログにまとめる(アプリ自体の公開は必須ではないかコードはgithubにおく)
できたもの
GitHub: https://github.com/lirlia/100day_challenge_backend/tree/main/day5_webrtc_chat

前回の振り返り
- Cursor Proでもずっとつかってると Slow になる
- Cursor FAQ は LLM 入っててつかいやすい
- タイムラインを SSE と get リクエストで取得したものをうまく統合しようとした時に、結構時間がかかった。そもそもどのようにやるべきなのか?を会話しなかったため時間がかかったのだと思う。フロントとバクエンドでそれぞれ考える必要がある内容については事前に設計をしっかりする必要がある
- フロントエンドで複雑なことをやろうとするとそっちにかかりっきりになってしまって、バックエンドでやりたいことが全然できない。
- 極力シンプルにした方がいいな。。。
- SSE が結構簡単に実装できる
- 今までN:Nのリレーションの表現のことを中間テーブルっていうのは初めて聞いた

今日作るのは WebRTC を使った動画チャット
はい、承知いたしました。day5 の WebRTC を使ったリアルタイム動画チャットアプリの仕様案を作成します。
day5: WebRTC 動画チャットアプリ 仕様案
1. アプリ概要
ユーザー同士が 1 対 1 でリアルタイムにビデオ通話を行えるシンプルな Web アプリケーションです。
2. 機能要件
- 1対1ビデオ通話: 2人のユーザー間で映像と音声を送受信できます。
- ユーザー識別: 各ユーザーはシンプルなユーザーIDで識別されます(ログイン機能は不要)。
- 接続開始: ユーザーは通話したい相手のユーザーIDを指定して接続を開始します。
- リアルタイム通信: WebRTC (RTCPeerConnection) を利用して、P2P (Peer-to-Peer) でメディアストリームを交換します。
- シグナリング: 通話の接続確立に必要な情報 (SDP Offer/Answer, ICE Candidate) を交換するために、サーバー (Next.js Route Handler) を介したシグナリングを行います。
3. 技術要素
- フロントエンド: Next.js (App Router), TypeScript, Tailwind CSS
- リアルタイム通信: WebRTC (RTCPeerConnection API)
- シグナリング: Next.js Route Handlers を利用した Server-Sent Events (SSE)
-
STUNサーバー: Google の公開 STUN サーバー (
stun:stun.l.google.com:19302
) を利用します(NAT越えのため)。TURN サーバーは今回実装しません。 - DB: 不要
4. 画面構成
-
トップページ (
/
):- 自分のユーザーIDを入力する欄。
- 通話したい相手のユーザーIDを入力する欄。
- 「通話開始」ボタン。
-
チャットページ (
/chat?userId=[自分のID]&peerId=[相手のID]
):- 自分のビデオ映像を表示する
<video>
要素。 - 相手のビデオ映像を表示する
<video>
要素。 - 接続状態を示すシンプルな表示。
- 「通話終了」ボタン。
- 自分のビデオ映像を表示する
5. WebRTC 通信フロー (概要)
- ユーザーAがユーザーBとの通話を開始。
- ユーザーAが
RTCPeerConnection
を作成し、メディア(カメラ・マイク)を取得してトラックを追加。 - ユーザーAが Offer (SDP) を作成し、シグナリングサーバー経由でユーザーBに送信。
- ユーザーBが Offer を受信し、自身の
RTCPeerConnection
にリモート情報として設定。メディアを取得してトラックを追加。 - ユーザーBが Answer (SDP) を作成し、シグナリングサーバー経由でユーザーAに送信。
- ユーザーAが Answer を受信し、自身の
RTCPeerConnection
にリモート情報として設定。 - 両者がネットワーク経路情報である ICE Candidate を生成し、シグナリングサーバー経由で交換。
- ICE Candidate の交換が完了すると、P2P 接続が確立され、互いの映像・音声が
<video>
要素に表示される (ontrack
イベント)。
6. シグナリング実装方針
- Server-Sent Events (SSE) を利用した単方向通信で実装します。
-
app/api/signaling/route.ts
に以下のエンドポイントを実装します。-
GET /api/signaling?userId=[自分のID]
: SSE 接続を確立し、サーバーからのメッセージ(相手からの Offer/Answer/Candidate)を受信します。 -
POST /api/signaling?targetUserId=[送信先ID]
: 相手にメッセージ (Offer/Answer/Candidate) を送信します。サーバー側で受信し、対象ユーザーの SSE 接続にメッセージを流します。
-
- 接続中のクライアント情報はサーバーのメモリ上で簡易的に管理します。
7. 考慮事項
- エラーハンドリングは
console.error
でのログ出力など、最小限に留めます。 - 複数人通話、画面共有、テキストチャットなどの機能は含みません。
- 切断処理や再接続処理もシンプルにします。
上記仕様で進めてよろしいでしょうか? 問題なければ、以下の作業順序で実装を開始します。
-
プロジェクト初期化: テンプレートコピー、
package.json
更新、README.md
作成。 -
シグナリングサーバー (SSE) 実装: Route Handler (
app/api/signaling/route.ts
) の作成。 -
フロントエンド実装 (トップページ): ユーザーID入力と接続開始ボタン (
app/page.tsx
)。 -
フロントエンド実装 (チャットページ): ビデオ表示と WebRTC ロジック (
app/(pages)/chat/page.tsx
)。 - 動作確認: ローカルでの接続テスト。

なんかめちゃくちゃ順調にいった(モザイク機能もつけた)

動いているところ

振り返り
- 20分ぐらいでできた
- まだ WebRTC 1mm も理解してない
- Google が公開 STUN サーバを持っている
- SSE が登場した
- 動画とか音声をブラウザに許可する処理もめちゃくちゃ簡単

WebRTC (Web Real-Time Communication) は、そのどちらか一方というより、両方の側面を持つ技術 と言えます。
簡単に言うと、WebRTC は:
- API 群 (インターフェース) である: Web ブラウザ (JavaScript) やモバイルアプリが、特別なプラグインなしにリアルタイム通信 (音声、映像、任意のデータ) を行うための 標準的な API (命令セット) を提供します。開発者はこれらの API を使って通信機能を実装します。
-
複数のプロトコルを利用する: その API の裏側では、リアルタイム通信を実現するために、いくつかの 既存の標準プロトコル を組み合わせて利用しています。主なものとしては:
- SDP (Session Description Protocol): 通話の形式 (どんなコーデックを使うか、解像度はどれかなど) を記述し交換するためのプロトコル。
- ICE (Interactive Connectivity Establishment): ファイアウォールや NAT を越えて P2P ( Peer-to-Peer ) 接続を確立するための最適な経路を見つけるためのプロトコル (STUN や TURN を利用)。
- RTP/RTCP (Real-time Transport Protocol / RTP Control Protocol): 実際の音声や映像データを送受信するためのプロトコル。
- DTLS (Datagram Transport Layer Security): 送受信されるメディアデータを暗号化するためのプロトコル。
- (オプション) SCTP (Stream Control Transmission Protocol): 映像や音声以外の任意のデータを送受信する「データチャネル」で使われるプロトコル。
-
特定のプロセス (流れ) を伴う: これらのプロトコルと API を使って、実際に通信を確立するには、決まった 一連の流れ (プロセス) が必要になります。
- シグナリング: まず、通話相手と接続情報を交換する必要があります (SDP Offer/Answer や ICE Candidate の交換)。WebRTC 自体はこのシグナリングの方法を定義していないため、今回私たちが実装した SSE や、WebSocket など、別の方法で実装する必要があります。
- 接続確立: 交換した情報をもとに、ICE プロセスを通じて P2P 接続を試みます。
- メディア交換: 接続が確立したら、RTP を使ってリアルタイムに音声・映像データを送受信します。
まとめ:
WebRTC は、「リアルタイム通信を実現するための標準的な API 群であり、その内部では複数の標準プロトコルを利用し、接続のためには特定のプロセス(シグナリングを含む)を必要とする技術」 と理解するのが最も近いかと思います。
単なる「プロトコル」ではなく、それらを組み合わせた「フレームワーク」や「技術スタック」と捉えると分かりやすいかもしれません。

応用について
-
NAT越え(特にSymmetric NAT)と接続性
• STUN/TURNの適切な運用が必須
• TURNサーバーは帯域食うのでコストも高い
• P2Pできないケースも多く、TURN必須になる -
スケーラビリティ
• グループ通話・多人数通話(3人以上)ではP2Pが破綻しがち
• SFU(Selective Forwarding Unit)やMCU(Multipoint Control Unit)の導入が必要
• SFU構築やメンテは難易度高い(例:mediasoup, Janus, Jitsi) -
低遅延と音質のバランス
• コーデックの選定(Opusなど)やビットレート調整
• ネットワーク状態による適応(帯域が狭い・遅いと音が途切れる)
• 自動ゲインコントロール、ノイズキャンセル、エコー除去の調整 -
モバイル・ブラウザ間の非互換性
• iOS Safari、Android Chromeで挙動が違う
• WebRTCのAPIバージョンや制限の違い(iOSのバックグラウンド制限など) -
認証・認可の仕組み
• 誰が誰と話せるのか?セキュリティ確保
• 認証付きでTURNを使う仕組み(TURNのauthの実装) -
通信の安定性と再接続
• 一時的なネットワーク切断時の再接続処理
• ICE接続の再ネゴシエーション
• 接続監視と状態遷移管理(ICEConnectionState, SignalingState) -
通話制御とUIの複雑さ
• ミュート、退出、入室通知、タイマー、話者表示など
• UIと状態管理の整合性を保つ必要がある(状態の同期) -
セキュリティ
• SRTP(Secure Real-time Transport Protocol)の扱い
• TURN/STUNのDoS対策(無認証利用されると高額請求リスク)
• 悪意のあるユーザーの音声乱入やスパム対策 -
録音・モデレーション・ログ
• サーバー録音 or クライアント側録音(どちらも難しい)
• 不適切発言の通報と証拠保全 -
インフラ・コスト
• TURN/SFUサーバーは帯域・CPUを食うためインフラコスト大
• 同時接続ユーザー数が増えると一気に運用が厳しくなる -
トラブル対応・モニタリング
• 接続できない/音が聞こえないときのトラブルシューティングが超難しい
• WebRTCの通信は基本的にP2Pなのでログが取れないことも多い
• RTCPeerConnectionのstatsやSIPトレースを活用する必要がある

Google Meets
Google Meet の正確な内部アーキテクチャは公開されていませんが、一般的に大規模なビデオ会議サービスで採用されている技術や Google の技術力から推測すると、主に SFU (Selective Forwarding Unit) 型 のアーキテクチャを採用している可能性が最も高いと考えられます。
理由:
- スケーラビリティ: Google Meet は数十人、時には数百人が参加する会議をサポートします。完全な P2P (Mesh) 型では参加者数の増加に伴いクライアント側の負荷が爆発的に増えるため、現実的ではありません。SFU 型であれば、各クライアントは SFU サーバーとの接続だけを管理すればよいため、クライアントの負荷を抑えつつ多数の参加者に対応できます。
- 柔軟性: SFU は各参加者からのメディアストリームをそのまま他の参加者に転送します。これにより、クライアント側で誰を表示するか、どの解像度で受信するかなどを柔軟に制御できます。また、Simulcast(複数の品質のストリームを同時に送信)や SVC(単一ストリーム内で品質階層を分ける)といった技術と組み合わせることで、各参加者のネットワーク状況に応じて最適な品質の映像を配信しやすくなります。MCU のようにサーバーで映像を合成する方式よりも、この点で柔軟性があります。
- サーバー負荷: MCU はサーバー側で映像・音声のデコード、ミキシング、再エンコードを行うため、非常に高い CPU パワーを必要とします。SFU は基本的に受信したパケットを転送するだけなので、MCU に比べてサーバー側の負荷は低くなります(もちろん、大量のトラフィックを捌くための帯域や基本的な処理能力は必要です)。
補足:
- 1 対 1 の通話や非常に少人数の場合は、効率の良い P2P で接続を開始し、参加者が増えた段階で SFU サーバー経由に切り替える、といった ハイブリッドなアプローチ を採用している可能性もあります。
- Google は独自の高度なインフラと技術を持っているため、標準的な SFU に独自の最適化や改良を加えたカスタム実装を使用している可能性も十分に考えられます。
したがって、Google Meet が大規模会議で利用している主な方式は SFU 型 であると考えるのが、現在の一般的な見解です。

STUN と TURN
STUN と TURN は、どちらも WebRTC で P2P 接続を確立するために NAT (Network Address Translation) やファイアウォールを越える (NAT トラバーサル) ための技術ですが、役割が異なります。
簡単に言うと:
- STUN (Session Traversal Utilities for NAT): 自分の「外から見える IP アドレスとポート番号」を発見 (Discover) するためのサーバーです。
- TURN (Traversal Using Relays around NAT): どうしても直接 P2P 接続できない場合に、通信を中継 (Relay) してくれるサーバーです。
それぞれの詳細です。
STUN サーバー
- 目的: クライアント (あなたの PC など) が NAT の内側にいる場合、通常はプライベート IP アドレスしか知りません。外部の別のクライアントと直接通信するには、NAT が割り当てたグローバル IP アドレスとポート番号 (公開アドレス) を知る必要があります。STUN サーバーはこの公開アドレスをクライアントに教えてくれます。
-
仕組み:
- クライアントは STUN サーバーにリクエストを送ります。
- STUN サーバーは、そのリクエストが「どの IP アドレスとポートから来たか」を見て、それをクライアントに返信します。
- クライアントは返信された情報を見て、自身の公開アドレスを知ることができます。
- 役割: この発見した公開アドレス情報 (ICE Candidate の一部) をシグナリング経由で相手に伝えることで、直接 P2P 接続を試みるための情報を提供します。
- 限界: STUN は、ほとんどの種類の NAT で機能しますが、一部の制限の厳しい NAT (特に Symmetric NAT) やファイアウォール環境下では、発見したアドレスを使っても直接接続できない場合があります。STUN サーバー自体はメディアデータを中継しません。
TURN サーバー
- 目的: STUN を使っても直接 P2P 接続を確立できない場合の最後の手段として、通信を中継することです。
-
仕組み:
- クライアントは TURN サーバーに接続し、通信の中継を依頼します。
- TURN サーバーは、そのクライアントのために一時的な中継用アドレスを確保します。
- クライアントは、この中継用アドレス情報 (ICE Candidate の一部) を相手に伝えます。
- 相手のクライアントも同様に TURN サーバーを利用します。
- 両クライアントは、直接通信する代わりに、すべての音声・映像データを TURN サーバー経由で送受信します。TURN サーバーが両者間の「郵便局」のように振る舞います。
- 役割: 直接接続が不可能な場合のフォールバック (代替手段) を提供し、ほぼ確実に接続を確立させることができます (ただし、両者が TURN サーバーに到達できる必要があります)。
-
デメリット:
- サーバー負荷: すべてのメディアデータを中継するため、TURN サーバーには高いネットワーク帯域と処理能力が要求され、運用コストがかかります。
- 遅延: サーバーを経由するため、直接 P2P 接続よりも遅延が大きくなる可能性があります。
関係性 (ICE フレームワーク内)
WebRTC の ICE (Interactive Connectivity Establishment) というフレームワークが、STUN と TURN を利用して最適な接続経路を見つけようとします。
- まず、ローカル IP アドレスと、STUN サーバーを使って発見した公開アドレスを使って、直接 P2P 接続を試みます。
- これが成功すれば、最も効率的な P2P 通信が開始されます。
- 直接接続が失敗した場合、フォールバックとして TURN サーバー経由での接続を試みます。
まとめ
項目 | STUN | TURN |
---|---|---|
主な役割 | 公開アドレスの発見 | メディアデータの中継 (リレー) |
メディア転送 | しない | する (直接接続失敗時) |
サーバー負荷 | 低い | 高い (帯域、CPU) |
成功率 | 高いが、Symmetric NAT などで失敗あり | 非常に高い (サーバーに接続できれば) |
ICE での位置づけ | P2P 接続のための情報収集 | P2P 接続失敗時の代替手段 (フォールバック) |
したがって、WebRTC アプリケーションで接続成功率を高めるためには、STUN サーバーに加えて TURN サーバーも設定することが一般的です。

家だと基本TURNになるの?
いいえ、必ずしもそういうわけではありません。基本はまず STUN を使って P2P 接続を試み、それが失敗した場合のフォールバックとして TURN を使います。
一般家庭の環境についてもう少し詳しく見てみましょう。
- NAT の存在: ほとんどの家庭用ルーターは NAT (Network Address Translation) を使用しており、複数のデバイスが 1 つのグローバル IP アドレスを共有しています。
- ポート: おっしゃる通り、外部からの不特定の通信に対しては、通常ポートは閉じています(ルーターのファイアウォール機能)。
-
NAT トラバーサル (STUN とホールパンチング):
- WebRTC (ICE) は、この状況を打破するために STUN と UDP ホールパンチング (UDP Hole Punching) という技術を使います。
- まず STUN サーバーを使って、ルーターが割り当てた自分の 公開 IP アドレスとポート を知ります。
- 次に、お互いにその公開アドレス宛に 同時に UDP パケットを送信しようとします。
- 多くの家庭用ルーター (特に Cone NAT と呼ばれるタイプ) は、内側から外側への通信があった場合、その通信に対する戻りの通信は一時的に許可するという動作をします。
- お互いが同時に外向きにパケットを送ることで、ルーターに一時的な「穴 (hole)」が開き、相手からのパケットがその穴を通って届く可能性があります。これが成功すれば、直接 P2P 接続が確立します。
- STUN が成功する確率: 多くの一般的な家庭用ルーターでは、この STUN とホールパンチングによって P2P 接続が成功します。体感的には、かなりの割合 (例えば 70%〜90% 以上とも言われますが、環境によります) で直接 P2P 接続が可能です。
-
TURN の役割:
- しかし、一部の制限の厳しい NAT (Symmetric NAT など) や、特定のファイアウォール設定下では、ホールパンチングが成功しません。
- このような、STUN を使った直接接続の試みがすべて失敗した場合にのみ、最後の手段として TURN サーバー が登場し、通信を中継します。
結論:
一般家庭環境でも、まずは STUN による直接 P2P 接続が試みられ、それが成功することが多いです。TURN はあくまでも「直接接続できなかった場合」の保険・代替手段であり、常に TURN が使われるわけではありません。理想は常に遅延の少ない直接 P2P 接続です。
え、でもUDPって戻りないのに ConeNATって穴開くの?
鋭いご指摘ですね!確かに UDP は TCP のように「接続 (コネクション)」を確立してセッションを維持するプロトコルではありません。送信元と宛先の情報 (IP アドレスとポート番号) をヘッダーに付けてパケットを送り出すだけです。
ではなぜ「穴」が開くのかというと、それは NAT デバイス (ルーター) が「ステートフル (stateful)」だからです。
- NAT の状態管理: 現代のほとんどの家庭用ルーターは、単にアドレスを変換するだけでなく、通過する通信の状態を一時的に記憶しています。これを「ステートフルファイアウォール」や「ステートフル NAT」と呼びます。
-
外向き通信の記録: あなたの PC (内部 IP:
192.168.1.10
, ポート:50000
) が、外部のサーバー (例: STUN サーバー) に UDP パケットを送るとします。ルーターはこれを自身のグローバル IP (YourPublicIP
) と適当なポート (RouterPort
) に変換して (YourPublicIP:RouterPort
) 送信します。このとき、ルーターは**「192.168.1.10:50000
からSTUNサーバーIP:STUNポート
への通信があった。その際にYourPublicIP:RouterPort
を使った」という情報を一時的に記録**します。 -
戻りの通信の許可: この記録がある間、ルーターは**「
STUNサーバーIP:STUNポート
からYourPublicIP:RouterPort
宛に来た UDP パケット」は、先ほどの通信に対する「戻り」**だと判断し、特別に許可して192.168.1.10:50000
へ転送します。これが「穴」が開いた状態です。外部からの不特定の通信は拒否しますが、自分から通信を開始した相手からの戻りは一時的に受け入れるわけです。 -
UDP ホールパンチング: WebRTC のホールパンチングでは、お互いが相手の公開アドレス (STUN で知る) に向けて同時に UDP パケットを送信します。
- あなたが相手 (
PeerPublicIP:PeerPort
) に送る -> あなたのルーターが記録を作る & 相手からの戻り (PeerPublicIP:PeerPort
から) を待つ状態になる。 - 相手があなた (
YourPublicIP:RouterPort
) に送る -> 相手のルーターが記録を作る & あなたからの戻り (YourPublicIP:RouterPort
から) を待つ状態になる。 - あなたが送ったパケットが相手のルーターに届くと、相手のルーターは「これはさっき内側から通信開始した相手からの戻りだ」と判断して通します。
- 相手が送ったパケットがあなたのルーターに届くと、あなたのルーターも同様に「これはさっき内側から通信開始した相手からの戻りだ」と判断して通します。
- こうしてお互いのパケットが相手に届き、P2P 経路が確立します。
- あなたが相手 (
Cone NAT との関係:
このホールパンチングがうまく機能しやすいのが Cone NAT です。Cone NAT は、同じ内部 IP アドレス・ポートからの通信であれば、宛先が異なっても同じ外部ポートマッピングを再利用する傾向があります (あるいは、少なくとも特定の外部ホストからの戻りを受け入れる)。これにより、STUN サーバーで確認したポートが、実際の相手との通信でも使える可能性が高くなります。
一方で、Symmetric NAT は、同じ内部 IP・ポートからでも、宛先が違うと外部ポートマッピングを変えてしまうことが多いです。この場合、STUN サーバーで確認したポートマッピングと、相手と通信しようとするときのポートマッピングが異なってしまうため、ホールパンチングが失敗しやすくなります。
まとめ:
UDP自体はコネクションレスですが、中継する NAT ルーターが通信状態を記憶している (ステートフル) ため、「自分から通信を始めた相手」からの UDP パケットは一時的に受け入れるようになります。これを利用して、お互いが同時にパケットを送り合うことで「穴」を開け、P2P 接続を試みるのが UDP ホールパンチングの仕組みです。
ちなみに、お互いが同時に送るといいましたが、それってどのようにタイミングを合わせてるの?
良い質問ですね! 実は、WebRTC の UDP ホールパンチングでは、厳密にミリ秒単位でタイミングを合わせているわけではありません。
「同時」というのは、少し比喩的な表現で、**「お互いが相手からのパケットを受け入れる準備(穴が開いている状態)ができているであろう時間帯に、パケットを送り合う」**という意味合いが近いです。
そのタイミングは、以下の ICE (Interactive Connectivity Establishment) のプロセスによって、ある程度自然に(しかし意図的に)作られます。
-
候補 (Candidate) の交換:
- まず、シグナリングサーバー経由で、お互いの接続候補となるアドレス情報 (ローカル IP、STUN で取得した公開 IP、場合によっては TURN の中継 IP) を交換します。
- ピア A はピア B の候補リストを、ピア B はピア A の候補リストを受け取ります。
-
接続性チェック (Connectivity Checks) の開始:
- 候補リストを受け取ったら、各ピアはリストにある相手の候補アドレスに向けて、積極的に「接続性チェック」と呼ばれる軽量な STUN パケット (BINDING リクエスト) を送信し始めます。
- ピア A は、ピア B の候補アドレス (例:
PeerPublicIP:PeerPort
) に向けて STUN チェックパケットを送信します。 - ピア B も、ピア A の候補アドレス (例:
YourPublicIP:RouterPort
) に向けて STUN チェックパケットを送信します。
-
タイミングの「ずれ」と「許容範囲」:
- シグナリングサーバー経由での候補交換には当然遅延がありますし、ネットワークの状況によってパケットが届く時間も異なります。そのため、両者がチェックパケットを送信し始めるタイミングは完全に同時ではありません。
- しかし、これは問題ありません。重要なのは、NAT ルーターが外向きのパケット送信を検知して「穴(状態)」を開けている持続時間です。この持続時間は NAT の実装によりますが、通常は数十秒から数分程度あります。
- ピア A がパケットを送ってから、ピア B のパケットが届くまでの間に、ピア A のルーターの「穴」が閉じなければ OK です(逆も同様)。
-
結果的な「ほぼ同時」:
- お互いが相手の候補アドレスを知り次第、待つことなく接続性チェックを開始するため、結果的に比較的短い時間差でパケットの送受信が試みられることになります。
- これにより、NAT に穴が開いている時間帯が重なる可能性が高まり、ホールパンチングが成功しやすくなります。
まとめ:
特定の同期メカニズムがあるわけではなく、**「お互いが接続に必要な情報を得たら、すぐに接続確認のパケットを積極的に送り始める」**という ICE フレームワークの動作によって、NAT の状態が一時的に開くタイミングが重なることを期待している、という仕組みです。完璧な同時性を目指すのではなく、NAT が状態を保持している短い時間内に、お互いのパケットが通過できれば成功、という考え方ですね。

スマホの場合
はい、その傾向は非常に強いと言えます。一般家庭の Wi-Fi 環境に比べて、スマホ (モバイルネットワーク、例: 4G/LTE, 5G) で WebRTC 接続を行う場合は、TURN サーバーによる中継が必要になる可能性がかなり高くなります。
主な理由:
- キャリアグレード NAT (CGN/LSN): モバイルネットワーク事業者は、多数のユーザーを少数のパブリック IP アドレスで共有するために、大規模な NAT (Carrier-Grade NAT または Large-Scale NAT) を運用しています。
-
Symmetric NAT の多用: この CGN は、セキュリティやアドレス管理の観点から、Symmetric NAT として実装されている場合が多いです。
- 前述の通り、Symmetric NAT は同じ内部 IP・ポートからであっても、通信宛先が異なると異なる外部ポートマッピングを割り当てます。
- これは、STUN サーバーで確認したポートマッピング (
自分のPC -> STUNサーバー
) と、実際に相手のピアと通信しようとする際のポートマッピング (自分のPC -> 相手のピア
) が異なってしまうことを意味します。 - 結果として、UDP ホールパンチングが成功する確率が大幅に低下します。
- 予測不能なネットワーク環境: モバイルネットワークは、Wi-Fi に比べてネットワーク環境が動的に変化しやすく、ファイアウォールの設定などもより厳しい場合があります。
結果として:
- モバイルネットワーク上のデバイスが WebRTC で通信しようとすると、ICE フレームワークはまず STUN を使って P2P 接続を試みますが、Symmetric NAT の影響で失敗する可能性が高くなります。
- P2P 接続の試みが失敗すると、ICE は自動的にフォールバックし、設定されている TURN サーバー経由での接続を試みます。
- TURN サーバーはメディアを中継するため、NAT の種類に関わらず(サーバーに接続できれば)接続を確立できます。
ただし、注意点もあります:
- スマホが Wi-Fi に接続している場合は、その Wi-Fi 環境の NAT タイプに依存します(一般家庭なら P2P 成功率が高い)。
- すべてのモバイルネットワークが Symmetric NAT とは限りませんし、状況によってはモバイルネットワーク間でも P2P が成功することもありえます(例えば、偶然同じキャリアの比較的緩い CGN 下にいる場合など)。
- ICE は常に効率の良い P2P を優先して試みます。
結論:
モバイルデバイスを含む WebRTC アプリケーションで高い接続成功率を確保するためには、TURN サーバーの準備と設定が事実上必須となります。モバイルネットワークでは、STUN だけでは接続できないケースが Wi-Fi 環境よりも格段に多くなるためです。