WebRTC 最新動向 2025:AV1・SVC・WHIP/WHEP が変えるリアルタイム配信設計
WebRTC がリアルタイム通信の標準として定着してから数年が経ち、2025 年現在、新しい技術仕様が次々と実用段階に入っています。
特に注目すべきは、次世代コーデック AV1 の本格採用、スケーラビリティを実現する SVC(Scalable Video Coding)、そして配信ワークフローを劇的に簡素化する新プロトコル WHIP/WHEP です。これらの技術は、ビデオ会議から大規模ライブ配信まで、あらゆるリアルタイム配信システムの設計思想を根本から変えようとしています。
本記事では、これら最新技術の仕組みと実装方法、そして実際の配信設計にどう活かすべきかを、初心者の方にもわかりやすく解説します。
背景
WebRTC のこれまでの進化
WebRTC(Web Real-Time Communication)は、2011 年に Google が提案し、2021 年に W3C および IETF で正式に標準化されました。ブラウザ間で直接リアルタイム通信が可能になったことで、ビデオ会議、オンライン授業、ライブ配信など、多様なサービスが生まれました。
しかし、従来の WebRTC には以下のような課題がありました。
従来の WebRTC の主な制約
| # | 項目 | 課題内容 |
|---|---|---|
| 1 | コーデック | VP8/VP9/H.264 が中心で、帯域効率に限界 |
| 2 | スケーラビリティ | 複数解像度の同時配信が困難 |
| 3 | シグナリング | 標準化されておらず、実装が複雑 |
| 4 | 大規模配信 | P2P ベースのため、視聴者数に応じてサーバー負荷が増大 |
これらの課題を解決するために、新しい技術仕様が登場してきました。
新技術が求められる背景
リアルタイム配信のニーズは多様化しています。
たとえば、企業のオンライン会議では 低遅延と安定性 が求められますし、ライブコマースやスポーツ中継では 数千〜数万人への同時配信 が必要です。また、モバイル環境では 帯域の変動に強い配信 が不可欠でしょう。
以下の図は、WebRTC を取り巻く技術進化の全体像を示しています。
mermaidflowchart TB
legacy["従来の WebRTC<br/>(VP8/VP9/H.264)"] --> needs["多様化するニーズ"]
needs --> n1["帯域効率の向上"]
needs --> n2["スケーラブルな配信"]
needs --> n3["シグナリングの簡素化"]
n1 --> av1["AV1 コーデック"]
n2 --> svc["SVC 技術"]
n3 --> whip["WHIP/WHEP プロトコル"]
av1 --> future["2025年の<br/>リアルタイム配信"]
svc --> future
whip --> future
図で理解できる要点
- 従来の WebRTC の制約が、新しい技術仕様を生み出す原動力になった
- AV1・SVC・WHIP/WHEP は、それぞれ異なる課題を解決する
- これらを組み合わせることで、次世代のリアルタイム配信が実現する
課題
従来のコーデックの限界
VP8 や VP9、H.264 といった従来のコーデックは、リアルタイム通信において十分な性能を発揮してきました。しかし、4K・8K 映像や、モバイル回線での安定配信といった現代の要求には、帯域効率の面で限界があります。
たとえば、1080p の映像を VP9 で配信する場合、2〜3 Mbps 程度の帯域が必要です。これが 4K になると 10 Mbps 以上が必要になり、モバイル環境では不安定になりがちでした。
マルチビットレート配信の複雑さ
従来の WebRTC では、異なるネットワーク環境に対応するために、配信者が複数のビットレートで映像をエンコードする Simulcast という手法が使われてきました。
しかし、Simulcast には以下のような課題がありました。
mermaidflowchart LR
source["配信者"] -->|高画質| enc1["エンコーダー1<br/>(1080p)"]
source -->|中画質| enc2["エンコーダー2<br/>(720p)"]
source -->|低画質| enc3["エンコーダー3<br/>(480p)"]
enc1 --> sfu["SFU サーバー"]
enc2 --> sfu
enc3 --> sfu
sfu -->|適切な品質を選択| viewer["視聴者"]
この図が示すように、Simulcast では配信者側で 複数のエンコーダーを同時に動かす 必要があり、CPU 負荷が高くなります。また、すべての解像度のストリームをネットワークに送信するため、上り帯域も大量に消費してしまいます。
Simulcast の主な課題
| # | 課題 | 影響 |
|---|---|---|
| 1 | CPU 負荷が高い | 配信者のデバイスが発熱・バッテリー消費 |
| 2 | 上り帯域を圧迫 | モバイル環境では配信が不安定に |
| 3 | 実装が複雑 | 開発コストが増大 |
シグナリングプロトコルの乱立
WebRTC の標準仕様には、シグナリング(接続確立のための情報交換)の方法が定義されていません。そのため、各サービスが独自のシグナリング方式を実装しており、相互運用性が低く、実装コストが高いという問題がありました。
たとえば、ある WebRTC サーバーに接続するには、そのサーバー専用のクライアントライブラリが必要になることが多く、異なるサービス間での映像のやり取りが困難でした。
mermaidsequenceDiagram
participant Client as クライアント
participant Server as WebRTC サーバー
Note over Client,Server: 独自プロトコルでの接続例
Client->>Server: WebSocket 接続
Server->>Client: セッション ID 発行
Client->>Server: カスタム JSON (Offer)
Server->>Client: カスタム JSON (Answer)
Client->>Server: カスタム JSON (ICE Candidate)
Note over Client,Server: プロトコルが標準化されていないため<br/>サーバーごとに実装が異なる
このように、シグナリングの標準化がなされていないことで、開発者は毎回独自の実装を行う必要があり、エコシステムの成長を妨げていました。
解決策
AV1 コーデック:次世代の帯域効率
AV1(AOMedia Video 1)は、Google、Netflix、Amazon などが参加する Alliance for Open Media が開発した、ロイヤリティフリーの次世代動画コーデックです。
AV1 の最大の特徴は、VP9 や H.264 と比較して 30〜50% の帯域削減 を実現できることです。同じ画質であれば、より少ない帯域で配信できるため、モバイル環境や低速回線でも高画質な映像を届けられます。
主要コーデックの比較
| # | コーデック | 帯域効率 | ブラウザ対応 | ロイヤリティ |
|---|---|---|---|---|
| 1 | H.264 | ★ | すべて対応 | 有料 |
| 2 | VP9 | ★★ | 主要ブラウザ対応 | 無料 |
| 3 | AV1 | ★★★ | Chrome/Firefox/Edge 対応 | 無料 |
2025 年現在、Chrome、Firefox、Edge などの主要ブラウザが AV1 に対応しており、Safari も対応を進めています。
WebRTC での AV1 利用方法
WebRTC で AV1 を利用するには、RTCPeerConnection の設定で AV1 コーデックを優先指定します。
以下は、AV1 を優先的に使用するための設定例です。
typescript// RTCPeerConnection の設定
const configuration: RTCConfiguration = {
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
};
次に、Peer Connection を作成し、コーデックの優先順位を設定します。
typescriptconst peerConnection = new RTCPeerConnection(configuration);
// メディアストリームの取得
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 1920, height: 1080 },
});
// トラックを追加
stream.getTracks().forEach((track) => {
peerConnection.addTrack(track, stream);
});
次に、SDP(Session Description Protocol)を操作して、AV1 を優先するように設定します。
typescript// Offer の作成
const offer = await peerConnection.createOffer();
// SDP を操作して AV1 を優先
const modifiedOffer = {
type: offer.type,
sdp: preferCodec(offer.sdp, 'video', 'AV1'),
};
await peerConnection.setLocalDescription(modifiedOffer);
コーデックの優先順位を変更する関数は以下のようになります。
typescript/**
* SDP 内のコーデック優先順位を変更する関数
* @param sdp - 元の SDP 文字列
* @param type - 'audio' または 'video'
* @param codec - 優先したいコーデック名(例: 'AV1')
* @returns 変更後の SDP 文字列
*/
function preferCodec(
sdp: string,
type: 'audio' | 'video',
codec: string
): string {
const lines = sdp.split('\r\n');
const mLineIndex = lines.findIndex((line) =>
line.startsWith(`m=${type}`)
);
if (mLineIndex === -1) return sdp;
// コーデックの payload type を検索
const codecPattern = new RegExp(
`a=rtpmap:(\\d+) ${codec}`,
'i'
);
const codecLine = lines.find((line) =>
codecPattern.test(line)
);
if (!codecLine) return sdp;
const match = codecLine.match(codecPattern);
if (!match) return sdp;
const payloadType = match[1];
// m= 行の payload type リストを並び替え
const mLine = lines[mLineIndex];
const parts = mLine.split(' ');
const payloadTypes = parts.slice(3);
// 指定コーデックを先頭に
const reordered = [
payloadType,
...payloadTypes.filter((pt) => pt !== payloadType),
];
lines[mLineIndex] = `${parts
.slice(0, 3)
.join(' ')} ${reordered.join(' ')}`;
return lines.join('\r\n');
}
この関数は、SDP の m= 行(メディア記述行)を解析し、指定したコーデックの payload type を先頭に移動させることで、優先順位を変更します。
SVC:1 つのエンコーダーで複数品質を実現
SVC(Scalable Video Coding)は、1 つのエンコードストリームから複数の解像度やフレームレートを取り出せる技術です。
Simulcast が複数のエンコーダーを並列稼働させるのに対し、SVC は 1 つのエンコーダーで階層的にエンコード し、受信側が必要な品質だけを取り出します。
mermaidflowchart TB
source["配信者"] --> encoder["SVC エンコーダー<br/>(1回のエンコード)"]
encoder --> layer["階層化されたストリーム"]
layer --> l1["ベースレイヤー<br/>(480p)"]
layer --> l2["拡張レイヤー1<br/>(720p)"]
layer --> l3["拡張レイヤー2<br/>(1080p)"]
l1 --> sfu["SFU サーバー"]
l2 --> sfu
l3 --> sfu
sfu -->|低速回線| v1["視聴者A<br/>(480p受信)"]
sfu -->|中速回線| v2["視聴者B<br/>(720p受信)"]
sfu -->|高速回線| v3["視聴者C<br/>(1080p受信)"]
図で理解できる要点
- SVC では 1 回のエンコードで複数の品質レイヤーを生成
- SFU サーバーが視聴者の回線状況に応じて、適切なレイヤーを配信
- 配信者側の CPU 負荷と上り帯域が大幅に削減される
SVC と Simulcast の比較
| # | 項目 | Simulcast | SVC |
|---|---|---|---|
| 1 | エンコード回数 | 複数回(品質ごと) | 1 回 |
| 2 | CPU 負荷 | 高い | 低い |
| 3 | 上り帯域 | 大量消費 | 効率的 |
| 4 | ブラウザ対応 | 広い | Chrome/Edge(拡大中) |
WebRTC での SVC 実装
WebRTC で SVC を利用するには、RTCRtpSender の setParameters メソッドでエンコーディングパラメータを設定します。
以下は、AV1 SVC を利用した実装例です。
typescript// Peer Connection の作成
const peerConnection = new RTCPeerConnection(configuration);
// ビデオトラックの取得
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 1920, height: 1080 },
});
const videoTrack = stream.getVideoTracks()[0];
const sender = peerConnection.addTrack(videoTrack, stream);
次に、SVC のエンコーディングパラメータを設定します。
typescript// SVC のエンコーディング設定
const parameters = sender.getParameters();
// AV1 の場合、scalabilityMode で SVC レイヤーを指定
parameters.encodings = [
{
scalabilityMode: 'L3T3', // 3つの空間レイヤー、3つの時間レイヤー
maxBitrate: 3000000, // 3 Mbps
},
];
await sender.setParameters(parameters);
scalabilityMode の指定方法は以下のとおりです。
typescript/**
* scalabilityMode の形式
* L{空間レイヤー数}T{時間レイヤー数}
*
* 例:
* - L1T3: 1つの解像度、3つのフレームレート
* - L3T3: 3つの解像度、3つのフレームレート
*/
// 一般的な設定例
const scalabilityModes = {
// フルHD 配信(3つの解像度レイヤー)
highQuality: 'L3T3',
// HD 配信(2つの解像度レイヤー)
mediumQuality: 'L2T3',
// SD 配信(1つの解像度のみ)
lowQuality: 'L1T2',
};
SFU サーバー側では、視聴者の帯域状況に応じて適切なレイヤーを選択して配信します。
typescript// SFU サーバー側での実装例(概念コード)
class SVCRouter {
/**
* 視聴者の帯域に応じて適切なレイヤーを選択
* @param availableBandwidth - 利用可能な帯域(bps)
* @returns 配信すべきレイヤー情報
*/
selectLayer(availableBandwidth: number) {
if (availableBandwidth > 2000000) {
// 2Mbps 以上: フル品質
return { spatialLayer: 2, temporalLayer: 2 };
} else if (availableBandwidth > 1000000) {
// 1Mbps 以上: 中品質
return { spatialLayer: 1, temporalLayer: 2 };
} else {
// 1Mbps 未満: 低品質
return { spatialLayer: 0, temporalLayer: 1 };
}
}
}
WHIP/WHEP:標準化されたシグナリング
WHIP(WebRTC-HTTP Ingestion Protocol)と WHEP(WebRTC-HTTP Egress Protocol)は、WebRTC のシグナリングを HTTP ベースで標準化した新しいプロトコルです。
- WHIP: 配信者からサーバーへの映像送信プロトコル
- WHEP: サーバーから視聴者への映像配信プロトコル
従来は WebSocket や独自 API でシグナリングを実装していましたが、WHIP/WHEP を使えば HTTP POST リクエスト 1 回 で接続が確立できます。
mermaidsequenceDiagram
participant Pub as 配信者
participant Server as WHIP サーバー
participant Sub as 視聴者
Note over Pub,Server: WHIP による配信開始
Pub->>Pub: SDP Offer 生成
Pub->>Server: POST /whip<br/>(SDP Offer)
Server->>Pub: 201 Created<br/>(SDP Answer)
Note over Pub,Server: 1回の HTTP リクエストで接続完了
Note over Server,Sub: WHEP による視聴開始
Sub->>Sub: SDP Offer 生成
Sub->>Server: POST /whep<br/>(SDP Offer)
Server->>Sub: 201 Created<br/>(SDP Answer)
Note over Server,Sub: 1回の HTTP リクエストで接続完了
WHIP/WHEP の利点
| # | 利点 | 詳細 |
|---|---|---|
| 1 | シンプル | HTTP POST のみで接続確立 |
| 2 | 標準化 | IETF で標準化済み(RFC 9414, 9418) |
| 3 | 相互運用性 | 異なるサーバー間でも動作 |
| 4 | 実装容易 | 既存の HTTP インフラを活用 |
WHIP での配信実装
WHIP を使った配信者側の実装例を見ていきましょう。
まず、メディアストリームを取得し、Offer を生成します。
typescript// メディアストリームの取得
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 1920, height: 1080 },
audio: true,
});
// Peer Connection の作成
const peerConnection = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
});
// トラックの追加
stream.getTracks().forEach((track) => {
peerConnection.addTrack(track, stream);
});
次に、SDP Offer を生成し、WHIP サーバーに送信します。
typescript// SDP Offer の生成
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
// WHIP サーバーへの送信
const whipUrl = 'https://example.com/whip/stream-id';
const response = await fetch(whipUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/sdp',
Authorization: 'Bearer YOUR_TOKEN', // 認証が必要な場合
},
body: offer.sdp,
});
サーバーからの応答(SDP Answer)を受け取り、接続を確立します。
typescriptif (response.status === 201) {
// SDP Answer の取得
const answerSdp = await response.text();
// リソース URL の取得(切断時に使用)
const resourceUrl = response.headers.get('Location');
// Answer の設定
await peerConnection.setRemoteDescription({
type: 'answer',
sdp: answerSdp,
});
console.log('WHIP 接続確立完了');
// 切断時のクリーンアップ用に URL を保存
return resourceUrl;
} else {
throw new Error(`WHIP 接続失敗: ${response.status}`);
}
配信を停止する際は、取得した Resource URL に DELETE リクエストを送信します。
typescript/**
* WHIP 配信の停止
* @param resourceUrl - 接続時に取得した Resource URL
*/
async function stopWhipStream(resourceUrl: string) {
await fetch(resourceUrl, {
method: 'DELETE',
headers: {
Authorization: 'Bearer YOUR_TOKEN',
},
});
console.log('WHIP 配信停止');
}
WHEP での視聴実装
視聴者側では WHEP を使って映像を受信します。
typescript// Peer Connection の作成(受信専用)
const peerConnection = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
});
// トランシーバーの追加(受信専用)
peerConnection.addTransceiver('video', {
direction: 'recvonly',
});
peerConnection.addTransceiver('audio', {
direction: 'recvonly',
});
次に、SDP Offer を生成し、WHEP サーバーに送信します。
typescript// SDP Offer の生成
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
// WHEP サーバーへの送信
const whepUrl = 'https://example.com/whep/stream-id';
const response = await fetch(whepUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/sdp',
},
body: offer.sdp,
});
サーバーからの応答を受け取り、受信ストリームを video 要素にバインドします。
typescriptif (response.status === 201) {
const answerSdp = await response.text();
const resourceUrl = response.headers.get('Location');
await peerConnection.setRemoteDescription({
type: 'answer',
sdp: answerSdp,
});
// 受信ストリームの処理
peerConnection.ontrack = (event) => {
const videoElement = document.querySelector('video');
if (videoElement) {
videoElement.srcObject = event.streams[0];
}
};
return resourceUrl;
}
WHEP でも、視聴を停止する際は Resource URL に DELETE リクエストを送信します。
typescriptasync function stopWhepStream(resourceUrl: string) {
await fetch(resourceUrl, { method: 'DELETE' });
console.log('WHEP 視聴停止');
}
具体例
AV1 + SVC + WHIP/WHEP を組み合わせた配信システム
ここでは、これまで解説した技術をすべて組み合わせた、最新の WebRTC 配信システムを構築してみます。
システム全体の構成は以下のとおりです。
mermaidflowchart TB
pub["配信者<br/>(ブラウザ)"] -->|WHIP<br/>AV1 SVC| sfu["SFU サーバー<br/>(mediasoup/Janus)"]
sfu -->|WHEP<br/>高品質レイヤー| sub1["視聴者A<br/>(高速回線)"]
sfu -->|WHEP<br/>中品質レイヤー| sub2["視聴者B<br/>(中速回線)"]
sfu -->|WHEP<br/>低品質レイヤー| sub3["視聴者C<br/>(低速回線)"]
sfu -.->|統計情報| monitor["モニタリング<br/>システム"]
図で理解できる要点
- 配信者は WHIP で AV1 SVC エンコードされた映像を送信
- SFU サーバーが視聴者ごとに最適なレイヤーを選択
- 視聴者は WHEP でシンプルに受信
- モニタリングシステムで配信品質を監視
配信者側の実装(統合版)
すべての技術を統合した配信クライアントを実装します。
まず、設定情報とインターフェースを定義します。
typescript/**
* WHIP 配信の設定
*/
interface WhipPublisherConfig {
whipUrl: string; // WHIP エンドポイント URL
authToken?: string; // 認証トークン(オプション)
videoConstraints: MediaTrackConstraints; // 映像制約
scalabilityMode: string; // SVC モード(例: 'L3T3')
}
/**
* 配信状態
*/
interface PublishState {
isPublishing: boolean;
resourceUrl?: string;
peerConnection?: RTCPeerConnection;
stream?: MediaStream;
}
次に、配信クラスを実装します。
typescriptclass WhipAV1Publisher {
private state: PublishState = {
isPublishing: false
};
constructor(private config: WhipPublisherConfig) {}
/**
* 配信開始
*/
async startPublish(): Promise<void> {
// メディアストリームの取得
this.state.stream = await navigator.mediaDevices.getUserMedia({
video: this.config.videoConstraints,
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 48000
}
});
// Peer Connection の作成
this.state.peerConnection = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
});
// トラックの追加とSVC設定
await this.addTracksWithSVC();
// WHIP接続の確立
await this.establishWhipConnection();
this.state.isPublishing = true;
}
/**
* トラック追加と SVC 設定
*/
private async addTracksWithSVC(): Promise<void> {
const pc = this.state.peerConnection!;
const stream = this.state.stream!;
// ビデオトラックの追加
const videoTrack = stream.getVideoTracks()[0];
const videoSender = pc.addTrack(videoTrack, stream);
// AV1 SVC の設定
const params = videoSender.getParameters();
params.encodings = [
{
scalabilityMode: this.config.scalabilityMode,
maxBitrate: 3000000 // 3 Mbps
}
];
await videoSender.setParameters(params);
// オーディオトラックの追加
const audioTrack = stream.getAudioTracks()[0];
pc.addTrack(audioTrack, stream);
}
WHIP による接続確立処理を実装します。
typescript /**
* WHIP 接続の確立
*/
private async establishWhipConnection(): Promise<void> {
const pc = this.state.peerConnection!;
// Offer の生成
const offer = await pc.createOffer();
// AV1 を優先
const modifiedOffer = {
type: offer.type as RTCSdpType,
sdp: this.preferCodec(offer.sdp!, 'video', 'AV1')
};
await pc.setLocalDescription(modifiedOffer);
// WHIP サーバーへ送信
const response = await fetch(this.config.whipUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/sdp',
...(this.config.authToken && {
'Authorization': `Bearer ${this.config.authToken}`
})
},
body: modifiedOffer.sdp
});
if (response.status !== 201) {
throw new Error(`WHIP 接続失敗: ${response.status}`);
}
// Answer の取得と設定
const answerSdp = await response.text();
this.state.resourceUrl = response.headers.get('Location')!;
await pc.setRemoteDescription({
type: 'answer',
sdp: answerSdp
});
}
コーデック優先順位の変更と配信停止処理を実装します。
typescript /**
* コーデック優先順位の変更
*/
private preferCodec(sdp: string, type: string, codec: string): string {
const lines = sdp.split('\r\n');
const mLineIndex = lines.findIndex(line => line.startsWith(`m=${type}`));
if (mLineIndex === -1) return sdp;
const codecPattern = new RegExp(`a=rtpmap:(\\d+) ${codec}`, 'i');
const codecLine = lines.find(line => codecPattern.test(line));
if (!codecLine) return sdp;
const match = codecLine.match(codecPattern);
if (!match) return sdp;
const payloadType = match[1];
const mLine = lines[mLineIndex];
const parts = mLine.split(' ');
const payloadTypes = parts.slice(3);
const reordered = [
payloadType,
...payloadTypes.filter(pt => pt !== payloadType)
];
lines[mLineIndex] = `${parts.slice(0, 3).join(' ')} ${reordered.join(' ')}`;
return lines.join('\r\n');
}
/**
* 配信停止
*/
async stopPublish(): Promise<void> {
if (!this.state.isPublishing || !this.state.resourceUrl) {
return;
}
// WHIP リソースの削除
await fetch(this.state.resourceUrl, {
method: 'DELETE',
headers: this.config.authToken ? {
'Authorization': `Bearer ${this.config.authToken}`
} : {}
});
// Peer Connection のクローズ
this.state.peerConnection?.close();
// ストリームの停止
this.state.stream?.getTracks().forEach(track => track.stop());
this.state.isPublishing = false;
}
}
視聴者側の実装(統合版)
視聴者側も同様に、WHEP を使った実装を行います。
typescript/**
* WHEP 視聴の設定
*/
interface WhepSubscriberConfig {
whepUrl: string; // WHEP エンドポイント URL
videoElement: HTMLVideoElement; // 再生用 video 要素
}
class WhepAV1Subscriber {
private resourceUrl?: string;
private peerConnection?: RTCPeerConnection;
constructor(private config: WhepSubscriberConfig) {}
/**
* 視聴開始
*/
async startSubscribe(): Promise<void> {
// Peer Connection の作成
this.peerConnection = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
});
// トランシーバーの追加(受信専用)
this.peerConnection.addTransceiver('video', {
direction: 'recvonly'
});
this.peerConnection.addTransceiver('audio', {
direction: 'recvonly'
});
// 受信ストリームの処理
this.peerConnection.ontrack = (event) => {
this.config.videoElement.srcObject = event.streams[0];
};
// WHEP 接続の確立
await this.establishWhepConnection();
}
WHEP による接続確立と停止処理を実装します。
typescript /**
* WHEP 接続の確立
*/
private async establishWhepConnection(): Promise<void> {
const pc = this.peerConnection!;
// Offer の生成
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// WHEP サーバーへ送信
const response = await fetch(this.config.whepUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/sdp'
},
body: offer.sdp
});
if (response.status !== 201) {
throw new Error(`WHEP 接続失敗: ${response.status}`);
}
// Answer の取得と設定
const answerSdp = await response.text();
this.resourceUrl = response.headers.get('Location')!;
await pc.setRemoteDescription({
type: 'answer',
sdp: answerSdp
});
}
/**
* 視聴停止
*/
async stopSubscribe(): Promise<void> {
if (!this.resourceUrl) return;
// WHEP リソースの削除
await fetch(this.resourceUrl, { method: 'DELETE' });
// Peer Connection のクローズ
this.peerConnection?.close();
// video 要素のクリア
this.config.videoElement.srcObject = null;
}
}
実際の利用例
実装したクラスを使って、配信と視聴を開始する例です。
typescript// 配信者側
const publisher = new WhipAV1Publisher({
whipUrl: 'https://sfu.example.com/whip/my-stream',
authToken: 'your-auth-token',
videoConstraints: {
width: { ideal: 1920 },
height: { ideal: 1080 },
frameRate: { ideal: 30 },
},
scalabilityMode: 'L3T3', // 3つの空間レイヤー、3つの時間レイヤー
});
// 配信開始
await publisher.startPublish();
// 配信停止(必要に応じて)
// await publisher.stopPublish();
視聴者側の実装例です。
typescript// 視聴者側
const videoElement = document.querySelector('video')!;
const subscriber = new WhepAV1Subscriber({
whepUrl: 'https://sfu.example.com/whep/my-stream',
videoElement: videoElement,
});
// 視聴開始
await subscriber.startSubscribe();
// 視聴停止(必要に応じて)
// await subscriber.stopSubscribe();
期待される効果
この統合システムを導入することで、以下のような効果が期待できます。
システム改善効果
| # | 項目 | 従来方式 | 新方式(AV1+SVC+WHIP/WHEP) | 改善率 |
|---|---|---|---|---|
| 1 | 帯域消費量 | 3.0 Mbps (VP9) | 2.0 Mbps (AV1) | ▲33% |
| 2 | 配信者 CPU 負荷 | 80% (Simulcast) | 40% (SVC) | ▲50% |
| 3 | 実装コード量 | 500 行 (独自) | 200 行 (WHIP/WHEP) | ▲60% |
| 4 | 接続確立時間 | 2-3 秒 (WebSocket) | 0.5-1 秒 (HTTP) | ▲70% |
特にモバイル環境では、帯域削減と CPU 負荷軽減により、バッテリー持続時間が大幅に改善されます。また、WHIP/WHEP によるシンプルな実装は、開発期間の短縮とメンテナンスコストの削減につながります。
まとめ
WebRTC の最新技術である AV1 コーデック、SVC、WHIP/WHEP プロトコル は、それぞれが異なる課題を解決し、組み合わせることで強力なリアルタイム配信システムを構築できることがわかりました。
各技術の役割と効果
- AV1: 従来比 30〜50% の帯域削減で、同じ画質をより少ない帯域で配信
- SVC: 1 回のエンコードで複数品質を実現し、配信者の CPU 負荷と帯域を削減
- WHIP/WHEP: HTTP ベースの標準化により、シグナリング実装を大幅に簡素化
これらの技術は、2025 年現在、主要ブラウザや SFU サーバー(mediasoup、Janus など)で実用段階に入っています。特に、以下のようなユースケースで大きな効果を発揮するでしょう。
推奨ユースケース
- 大規模ライブ配信: 数千〜数万人への同時配信でも帯域コストを抑制
- モバイル配信: バッテリー消費を抑えながら高画質配信を実現
- グローバル配信: 多様なネットワーク環境に対応した品質最適化
- 低遅延配信: 従来の HLS/DASH より圧倒的に低い遅延(1 秒以下)
実装にあたっては、まず WHIP/WHEP で基本的な配信フローを構築し、次に SVC で品質の最適化を行い、最後に AV1 で帯域効率を高めるという段階的なアプローチが効果的です。
WebRTC の進化は今も続いており、今後も新しい技術仕様が登場するでしょう。しかし、本記事で紹介した技術は、今後数年間のリアルタイム配信設計の基盤となる重要な要素です。ぜひ、実際のプロジェクトに取り入れてみてください。
関連リンク
articleWebRTC 最新動向 2025:AV1・SVC・WHIP/WHEP が変えるリアルタイム配信設計
articleWebRTC 本番運用の SLO 設計:接続成功率・初画出し時間・通話継続率の基準値
articleWebRTC が「connecting」のまま進まない:ICE 失敗を 5 分で切り分ける手順
articleWebRTC AV1/VP9/H.264 ベンチ比較 2025:画質・CPU/GPU 負荷・互換性を実測
articleWebRTC で遠隔支援:画面注釈・ポインタ共有・低遅延音声の実装事例
articleWebRTC で E2EE ビデオ会議:Insertable Streams と鍵交換を実装する手順
articleStorybook 代替ツール比較:Ladle/Histoire/Pattern Lab と何が違う?
articleAnsible Inventory 初期構築:静的/動的の基本とベストプラクティス
articleSolidJS で無限ループが止まらない!createEffect/onCleanup の正しい書き方
article伝搬方式を比較:Zustand の selector/derived-middleware/外部 reselect の使い分け
articleShell Script 設計 7 原則:可読性・再利用・堅牢性を高める実践ガイド
articleRuby 基本文法から学ぶ 90 分速習:変数・制御構文・ブロックを一気に理解
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来