このチュートリアルでは、WHIP ↗ を使ってブラウザーから Cloudflare Stream へ超低遅延のライブ動画を配信し、WHEP ↗ でブラウザー再生する方法を説明します。配信側と再生側の両方で、ブラウザー組み込みの WebRTC API だけを使います。インストールするライブラリも、外部アプリケーションも不要です。
完了すると、カメラとマイクを取得してライブ入力へ配信し、同じストリームをサブ秒の遅延で再生する、基本的な HTML ページができます。所要時間は 15 分未満です。
WHIP と WHEP は、WebRTC 向けのシンプルな HTTP ベースのシグナリングプロトコルです。どちらも、このコードは RTCPeerConnection ↗ を作り、ローカルのセッション記述(SDP offer)を生成し、HTTP の POST 1 回で Cloudflare の URL へ送り、返ってきた SDP answer を適用します。やり取りはリクエストと応答の 1 往復だけなので、独自のシグナリングサーバーは不要です。
このチュートリアルには次が必要です。
- ライブ入力を作成できる、次のいずれか。
- 有料の Stream サブスクリプション。
- Pro または Business のゾーンプラン。動画ストレージ 100 分と、動画配信 10,000 分が含まれます。
- Stream が有効な Enterprise 契約。
- カメラとマイク付きの、現行のブラウザー。
- ページを
httpsまたはlocalhostで配信すること。- 理由: ブラウザーは セキュアコンテキスト ↗ でのみ
getUserMedia()↗ を許可します。file://URL で HTML ファイルを直接開いても動作しません。 - すぐに始めるには、Cloudflare の Workers または Pages へ無料でデプロイできます。
- 理由: ブラウザーは セキュアコンテキスト ↗ でのみ
配信先は、必ずライブ入力です。次のいずれかで作成します。
-
Cloudflare ダッシュボードの Live inputs ページを使い、Broadcast タブと Playback タブで WebRTC URL を確認します。
Live inputs を開く ↗ -
/live_inputsAPI エンドポイント へPOSTリクエストを送ります。
応答には、このチュートリアルで使う 2 つの URL が含まれます。
{
"uid": "1a553f11a88915d093d45eda660d2f8c",
...
"webRTC": {
"url": "https://customer-<CODE>.cloudflarestream.com/<SECRET>/webRTC/publish"
},
"webRTCPlayback": {
"url": "https://customer-<CODE>.cloudflarestream.com/<INPUT_UID>/webRTC/play"
},
...
}webRTC.urlは配信先の WHIP エンドポイントです。配信用シークレットはこの URL に含まれます。 ストリームキーと同様に扱い、配信する人にだけ共有してください。webRTCPlayback.urlは視聴者が再生する WHEP エンドポイントです。ただし、入力で署名付き URL を有効にしている場合は除きます(ここでは扱いません)。
両方の URL をコピーし、次のコードへ貼り付けます。
この配信スクリプトはローカルメディアを取得し、RTCPeerConnection に send-only トラックとして追加し、できた SDP offer を WHIP URL へ POST します。
基本的な HTML ページから始め、ローカルカメラのプレビュー用に <video> 要素を追加します。
<video id="broadcast-preview" autoplay muted playsinline></video>次に、配信用のスクリプトを追加します。
// Paste the webRTC.url value from your live input.
const WHIP_URL = "<WHIP_URL_FROM_YOUR_LIVE_INPUT>";
async function startBroadcast() {
// 1. Capture the camera and microphone.
const media = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
document.getElementById("broadcast-preview").srcObject = media;
// 2. Create the peer connection and add each track as send-only.
const pc = new RTCPeerConnection();
media.getTracks().forEach((track) => {
pc.addTransceiver(track, { direction: "sendonly" });
});
// 3. Create the SDP offer and set it as the local description.
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// 4. POST the offer to the WHIP endpoint.
const response = await fetch(WHIP_URL, {
method: "POST",
headers: { "Content-Type": "application/sdp" },
body: offer.sdp,
});
if (!response.ok) {
throw new Error(`WHIP request failed: ${response.status}`);
}
// 5. Apply the SDP answer returned by Cloudflare.
const answer = await response.text();
await pc.setRemoteDescription({ type: "answer", sdp: answer });
// The Location header identifies this session, used to stop it later.
const sessionUrl = new URL(
response.headers.get("Location"),
WHIP_URL,
).toString();
return { pc, sessionUrl };
}
startBroadcast().catch(console.error);startBroadcast() を呼び出し、カメラとマイクの許可を与えると、ブラウザーは接続をネゴシエートし、WebRTC でライブの動画と音声を Cloudflare へ送り始めます。コーデックを選ぶ必要はありません。ブラウザーが 対応コーデック を自動でネゴシエートします。
このスクリプトは、複数のカメラや音声ソースの切り替えは扱いません。ブラウザーが提供するデフォルトを使います。
再生スクリプトは配信側の逆です。ローカルトラックを追加する代わりに、receive-only の transceiver を追加し、WHEP URL へ offer を POST し、受信メディアを <video> 要素へ接続します。
基本的な HTML ページから始め、再生用の <video> 要素を追加します。
<video id="playback-video" autoplay playsinline controls></video>次に、再生用のスクリプトを追加します。
// Paste the webRTCPlayback.url value from your live input.
const WHEP_URL = "<WHEP_URL_FROM_YOUR_LIVE_INPUT>";
async function startPlayback() {
const pc = new RTCPeerConnection();
// 1. Ask to receive one audio track and one video track.
pc.addTransceiver("video", { direction: "recvonly" });
pc.addTransceiver("audio", { direction: "recvonly" });
// 2. Attach incoming media to the video element as it arrives.
const stream = new MediaStream();
document.getElementById("playback-video").srcObject = stream;
pc.ontrack = (event) => stream.addTrack(event.track);
// 3. Create the SDP offer and set it as the local description.
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// 4. POST the offer to the WHEP endpoint.
const response = await fetch(WHEP_URL, {
method: "POST",
headers: { "Content-Type": "application/sdp" },
body: offer.sdp,
});
if (!response.ok) {
throw new Error(`WHEP request failed: ${response.status}`);
}
// 5. Apply the SDP answer returned by Cloudflare.
const answer = await response.text();
await pc.setRemoteDescription({ type: "answer", sdp: answer });
const sessionUrl = new URL(
response.headers.get("Location"),
WHEP_URL,
).toString();
return { pc, sessionUrl };
}
startPlayback().catch(console.error);配信側がライブのあいだ、再生側は接続し、遅延 500 ミリ秒未満でストリームを表示します。
WebRTC セッションは、ページを閉じるか接続が切れると自動で終了します。ただし、ユーザーが終了したら明示的に止めてください。Location ヘッダーのセッション URL へ HTTP DELETE を送り、そのあとピア接続を閉じます。
async function stop({ pc, sessionUrl }) {
if (sessionUrl) {
await fetch(sessionUrl, { method: "DELETE" });
}
pc.close();
}これは WHIP と WHEP の両方のセッションに当てはまります。startBroadcast() または startPlayback() が返すオブジェクトを渡してください。
次の 1 ファイルは、ここまでの内容をまとめたものです。2 つのプレースホルダー URL を、ライブ入力の webRTC.url と webRTCPlayback.url に置き換えます。そのあと、Workers または Pages で https 配信するか、localhost で配信してブラウザーで開きます。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Cloudflare Stream WHIP/WHEP example</title>
</head>
<body>
<h2>Broadcast (WHIP)</h2>
<video id="broadcast-preview" autoplay muted playsinline></video>
<button id="broadcast-btn">Start broadcasting</button>
<h2>Playback (WHEP)</h2>
<video id="playback-video" autoplay playsinline controls></video>
<button id="playback-btn">Start playback</button>
<script type="module">
const WHIP_URL = "<WHIP_URL_FROM_YOUR_LIVE_INPUT>";
const WHEP_URL = "<WHEP_URL_FROM_YOUR_LIVE_INPUT>";
async function negotiate(pc, url) {
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/sdp" },
body: offer.sdp,
});
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
const answer = await response.text();
await pc.setRemoteDescription({ type: "answer", sdp: answer });
return new URL(response.headers.get("Location"), url).toString();
}
document
.getElementById("broadcast-btn")
.addEventListener("click", async () => {
const media = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
document.getElementById("broadcast-preview").srcObject = media;
const pc = new RTCPeerConnection();
media
.getTracks()
.forEach((track) =>
pc.addTransceiver(track, { direction: "sendonly" }),
);
await negotiate(pc, WHIP_URL);
});
document
.getElementById("playback-btn")
.addEventListener("click", async () => {
const pc = new RTCPeerConnection();
pc.addTransceiver("video", { direction: "recvonly" });
pc.addTransceiver("audio", { direction: "recvonly" });
const stream = new MediaStream();
document.getElementById("playback-video").srcObject = stream;
pc.ontrack = (event) => stream.addTrack(event.track);
await negotiate(pc, WHEP_URL);
});
</script>
</body>
</html>配信または再生のセッションが接続しない場合は、ブラウザー組み込みの WebRTC ツールで SDP のやり取りと ICE 接続状態を確認できます。
- Chrome:
chrome://webrtc-internalsを開き、詳細ログとグラフを確認します。 - Firefox:
about:webrtcを開き、WebRTC セッションの情報を確認します。 - Safari: インスペクターから設定タブ(歯車アイコン)を開き、ドロップダウンで WebRTC ログを "Verbose" にします。
よくある問題:
getUserMediaがエラーになる、または何も返さない — ページがセキュアに配信されていることと、カメラとマイクの許可を与えたことを確認します。POSTが失敗する — 正しい URL を貼り付けたことを確認します。配信にはwebRTC.url、再生にはwebRTCPlayback.urlを使います。- 再生が黒いまま — 同じ入力で配信側がライブであることと、署名付き URL が有効になっていないことを確認します。
- 対応コーデック、プロトコル準拠、制限は WebRTC リファレンス を参照してください。
- 他のソフトウェアから配信する場合は、OBS と FFmpeg の手順 を参照してください。
- シグナリングを自分で書かず、メンテナンスされている WHIP または WHEP のクライアントライブラリ を使うこともできます。