「WebRTC を使ってみたいが、シグナリングサーバーの実装で詰まってしまう」——そんな経験を持つエンジニアは少なくありません。本記事では、Next.js + WebRTC + Firebase を組み合わせてビデオ通話アプリを構築するアーキテクチャを解説します。Firebase Firestore をシグナリングサーバーとして活用することで、独自のバックエンドサーバーを用意せずに P2P 接続を実現できます。ビデオ通話・画面共有・チャット・録画・デバイス選択まで実装した実践的なサンプルアプリとあわせて、全体の設計を体系的に理解できる構成になっています。
WebRTC の仕組み:P2P 接続が確立するまでの流れ
WebRTC(Web Real-Time Communication)は、ブラウザ間でプラグイン不要・サーバー中継なしで音声・映像・データを直接やり取りできる技術標準です。低遅延でセキュアなリアルタイム通信が実現できる一方、接続確立までの仕組みが複雑なため、まずそこを整理します。
WebRTC の3つの核となる概念
WebRTC は大きく3つの要素で構成されています。
① getUserMedia / getDisplayMedia:ユーザーのカメラ・マイク・画面にアクセスして MediaStream を取得する API です。ビデオ通話では getUserMedia、画面共有では getDisplayMedia を使い分けます。
② RTCPeerConnection:ブラウザ間の P2P 接続を管理するインターフェースです。内部的には SDP(Session Description Protocol)でお互いのメディア情報を交換し、ICE(Interactive Connectivity Establishment)プロトコルで最適な通信経路(candidate)を探索します。NAT やファイアウォールを越えるために STUN / TURN サーバーも利用されます。
③ シグナリングサーバー:P2P 接続確立の「仲介役」です。WebRTC 自体にはシグナリング機能がないため、SDP や ICE candidate の交換に外部の仕組みが必要になります。本サンプルでは Firebase Firestore をシグナリングサーバーとして活用し、専用バックエンドサーバーを不要にしています。
P2P 接続が確立するまでの処理フロー
AさんとBさんが接続するまでの具体的な流れは以下のとおりです。Offer/Answer モデルと candidate 交換という2段階で接続が確立します。
- Join のブロードキャスト:A さんがルームに入ったら、Firestore 経由ですべてのメンバーに
joinを送信。 - Accept の返送:B さんが
joinを受信したら、A さんにacceptを返送し、A さんの情報をローカルに登録。 - Offer の送信(A → B):A さんが
acceptを受信したらRTCPeerConnectionを生成し、SDP(offer)を作成して B さんに送信。ICE candidate のイベントハンドラも登録。 - Answer の返送(B → A):B さんが offer を受信したら、自分の SDP(answer)を作成して A さんに送信。受信した offer は
setRemoteDescriptionで保存。 - Answer の受理(A):A さんが answer を受信したら
setRemoteDescriptionで保存。 - Candidate の交換:A さん・B さん双方が Firestore 経由で ICE candidate を交換し、
addIceCandidateで追加。P2P 接続が確立。
エンジニアFirebase Firestore をシグナリングサーバーに使う最大の利点は、リアルタイムリスナー(onSnapshot)を使って SDP や candidate の変化を即座に検知できる点です。WebSocket サーバーを自前で立てる必要がないので、インフラ管理のコストをゼロにできます。
デザイナービデオ通話だけでなく、チャットや録画まで同じアプリに入っているんですね。Firebase を使うことでフロントエンド側だけで完結しているのは、デプロイのシンプルさにもつながっていて良いなと思います。
Next.js との組み合わせが有効な理由
Next.js は React ベースのフレームワークで、今回の構成で特に有効なのは次の3点です。
1つ目はフロントエンドとシグナリングロジックの一元管理です。Firebase との連携コードを Next.js のコンポーネント・カスタムフック内にまとめることで、別途バックエンドリポジトリを持たずに済みます。2つ目はTypeScript との親和性です。RTCPeerConnection の複雑な状態管理も、型定義により安全に扱えます。3つ目はFirebase Hosting との相性です。Next.js の静的エクスポートまたは Firebase App Hosting と組み合わせることで、デプロイパイプラインをシンプルに保てます。
実装:コアとなる WebRTC の処理
1. メディアストリームの取得
ビデオ通話の起点となる、カメラ・マイクへのアクセスと映像表示の実装です。'use client' ディレクティブが必要な点(App Router 使用時)と、コンポーネントのアンマウント時にトラックを停止してリソースを解放する処理が実務上の重要ポイントです。
'use client';
import { useEffect, useRef } from 'react';
export const LocalVideo = () => {
const localVideoRef = useRef(null);
useEffect(() => {
let stream: MediaStream;
navigator.mediaDevices
.getUserMedia({ video: true, audio: true })
.then((s) => {
stream = s;
if (localVideoRef.current) {
localVideoRef.current.srcObject = stream;
}
})
.catch((error) => {
// NotAllowedError: ユーザーが権限を拒否した場合
// NotFoundError: カメラ・マイクが見つからない場合
console.error('メディアデバイスへのアクセスに失敗:', error.name, error.message);
});
// アンマウント時にトラックを停止してリソースを解放
return () => {
stream?.getTracks().forEach((track) => track.stop());
};
}, []);
return (
あなたの映像
{/* muted:自分の音声がエコーバックするのを防ぐ */}
);
};
2. RTCPeerConnection の生成と Offer/Answer 交換
P2P 接続の核心部分です。STUN サーバーの指定、ICE candidate のイベント登録、SDP の作成と Firestore への書き込みという流れになります。
// STUN サーバー設定(NAT を越えるための通信経路探索に使用)
const configuration: RTCConfiguration = {
iceServers: [
{ urls: 'stun:stun.stunprotocol.org' }, // パブリック STUN サーバー
],
};
// Offer 側(発信者)の処理
const startCall = async (roomId: string, localStream: MediaStream) => {
const peerConnection = new RTCPeerConnection(configuration);
// ローカルのメディアトラックを接続に追加
localStream.getTracks().forEach((track) => {
peerConnection.addTrack(track, localStream);
});
// ICE candidate が生成されたら Firestore に書き込む(シグナリング)
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
// Firestore の candidates コレクションに追加
addDoc(collection(db, `rooms/${roomId}/candidates`), event.candidate.toJSON());
}
};
// 相手のメディアストリームを受信したら video 要素に表示
peerConnection.ontrack = (event) => {
const remoteVideo = document.querySelector('#remote-video');
if (remoteVideo) remoteVideo.srcObject = event.streams[0];
};
// SDP (offer) を作成して Firestore に保存(Answer 側が読み取る)
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
await setDoc(doc(db, 'rooms', roomId), { offer: offer.toJSON() });
return peerConnection;
};
実装した機能の全体像
本サンプルアプリ(デモ)では、WebRTC の基本的なビデオ通話に加えて、実務で求められる機能を一通り実装しています。
- ビデオ通話:
getUserMedia+RTCPeerConnectionによる P2P 映像・音声通話。Firebase Firestore をシグナリングサーバーとして利用。 - 画面共有:
getDisplayMediaで取得した画面ストリームをreplaceTrackで送信トラックと差し替え、通話を継続したまま共有・解除できる設計。 - テキストチャット:
RTCDataChannelを使用した P2P テキスト送受信。サーバーを介さずブラウザ間で直接データを交換。 - 録画機能:
MediaRecorderAPI を使って映像・音声を録画し、ブラウザ上でダウンロード可能な形式に変換。 - デバイス選択:
enumerateDevicesで接続済みのカメラ・マイクを列挙し、通話中に動的に切り替えられる UX を実装。
エンジニア画面共有への切り替えは replaceTrack を使うのがポイントです。新しい RTCPeerConnection を作り直すと再度のネゴシエーションが必要になりますが、replaceTrack なら既存の接続を維持したままトラックだけ差し替えられるので、ユーザーには切れ目なく切り替わって見えます。
実装上の注意点と本番投入への課題
WebRTC アプリを本番運用する際に把握しておくべき課題も整理します。
STUN / TURN サーバーの選択:パブリック STUN サーバー(例:stun.stunprotocol.org)は NAT 越えの経路探索に使いますが、企業ネットワークや対称型 NAT 環境では P2P 接続が確立できないケースがあります。その場合は TURN サーバー(サーバー経由での中継)が必要になります。本番では Twilio や Cloudflare の TURN サービス、または coturn などのセルフホストを検討してください。
Firebase のコスト管理:Firestore のリアルタイムリスナーは接続数に応じた読み取りコストが発生します。シグナリング完了後は不要なドキュメントを削除し、接続終了時にリスナーを解除する実装を徹底することが重要です。
多人数通話のスケーラビリティ:P2P 接続は参加者が増えるほど接続数が二乗で増加します(3人なら3本、5人なら10本)。多人数会議には SFU(Selective Forwarding Unit)アーキテクチャの導入を検討してください。
まとめ
Next.js + WebRTC + Firebase の組み合わせは、「シグナリングサーバーを自前で持たずにビデオ通話を実現する」という目標に対して現実的な解です。改めてポイントを整理します。
- WebRTC の接続確立は、Join → Accept → Offer → Answer → Candidate 交換という6ステップで進む。この流れを理解することがデバッグの基本。
- Firebase Firestore をシグナリングサーバーとして使うことで、専用バックエンドを持たずに P2P 接続を実現できる。
- 画面共有・チャット・録画はそれぞれ
getDisplayMedia・RTCDataChannel・MediaRecorderという標準 API で実装可能。 - 本番投入には TURN サーバーの整備とFirestore のコスト管理が必須。多人数通話では SFU の検討も視野に入れる。
デモ・ソースコード
本記事で解説した構成を実際に動かして確認できるサンプルアプリを公開しています。
デモアプリ:https://nextjs-webrtc-firebase.web.app
ソースコード:https://github.com/isystk/nextjs-webrtc-firebase
Next.js(TypeScript)+ Firebase Firestore のシグナリング実装、ビデオ通話・画面共有・チャット・録画・デバイス選択までの一通りのコードがリポジトリに含まれています。Docker を使ったローカル開発環境と Firebase エミュレータの構成も整っているので、クローンしてすぐに動作確認できます。WebRTC の学習や、自社サービスへの通話機能追加の出発点として活用してみてください。