😽

Cloudflare のデモアプリを組み合わせ、サーバレスなライブ配信 ‐ Realtime SFU と仲間たち

に公開

はじめに

Cloudflare の提供するデモアプリを組み合わせ、サーバレスなライブ配信を味見します。
目指すのは下記です。

  • 1秒までの時間差
  • チャット機能
  • ログイン認証・認可

素材

デモアプリ

Cloudflare が公開しているデモアプリを使います。

送受信のツール

  • ライブ配信は OBS Studio かブラウザー
  • 視聴はブラウザー

Cloudflare のプロダクト

利用する Cloudflare のプロダクトを列記します。
いずれも Free Plan で利用可能なものです。

Free Plan 枠の情報(利用上限などご確認ください)

Realtime SFU There is a free tier of 1,000 GB before any charges start.
Workers
Durable Objects If you exceed any one of the free tier limits, further operations of that type will fail with an error.
Pages
Access
WAF Custom Rules
Bulk Redirects
DNS Available on all plans plans and features
SSL/TLS Available on all plans plans and features

最低限必要なもの(ローカル環境)

デモアプリが最低限必要とするものです。
アプリを実行すると裏で自動配備されるものも含みます。

インターネット公開で使うもの

デモアプリをインターネットに公開する際は、認証・認可などセキュリティプロダクトをつけます。
アプリ自体を動かすのに必須ではありません。プライベートな配信環境を想定し、組み込みます。

また、プロキシーの基盤となる DNSSSL/TLS が必要です。
これらは Workers で Custom Domains を作成するなどにあわせ、基本的に自動配備され、簡単です。

手順

まずローカルでつなげ、その後公開します。

ローカル環境

簡単です。
ローカル稼働にはほとんどダッシュボードを触る必要はないです。

WHIP-WHEP Server

サーバーのセットアップ

WHIP-WHEP Server を立てます。

  1. Realtime SFU の作成


    App IDAPI Token を得ます

  2. サーバーのセットアップ

    whip-whep-server
    npm create cloudflare@latest -- --template https://github.com/cloudflare/calls-examples/tree/main/whip-whep-server
    
  3. App IDAPI Token の登録
    wrangler.toml に書くようなサンプルになってますが、TBD と書いてある部分は .dev.vars書きます

    wrangler.toml
    [vars]
    CALLS_API = "https://rtc.live.cloudflare.com"
    #下記 2 つは削除し、.dev.vars に移動
    #CALLS_APP_ID = "<TBD>"
    #CALLS_APP_SECRET = "<TBD>"
    

    CALLS_APP_IDApp IDCALLS_APP_SECRETAPI Token を入れます。

    .dev.vars
    CALLS_APP_ID="..."
    CALLS_APP_SECRET="..."
    
  4. 起動(ローカルサーバー)

    npx wrangler dev
    

    http://localhost:8787 で立ち上がりました。
    WHIP サーバーWHEP サーバーの両方をサービス提供する状態です。

    log
    npx wrangler dev
    :
    Your worker has access to the following bindings:
    - Durable Objects: 
     - LIVE_STORE: LiveStore
    - Vars:
      - CALLS_API: "https://rtc.live.cloudflare.com"
      - CALLS_APP_ID: "(hidden)"
      - CALLS_APP_SECRET: "(hidden)"
    ⎔ Starting local server...
    [wrangler:inf] Ready on http://localhost:8787
    
WHIP 接続(OBS ➜ ローカルサーバー)

OBS から WHIP で接続します。

  1. OBS Settings > Stream の設定
    Server は WHIP サーバー Ingest <deployed-domain>/ingest/<stream-name> に合わせます。
    例: http://localhost:8787/ingest/my-live
  2. 接続
    ソースを指定して Start Streaming
  3. ログ
    WHIP-WHEP Server のコンソールに WHIP サーバーへの接続が現れます。
    [wrangler:inf] POST /ingest/my-live 201 Created (2090ms)
    
WHEP 接続(ブラウザー ➜ ローカルサーバー)

WHIP-WHEP Server に WHEP player が同梱されているので、それを使います。
Pages を使っています。

  1. ディレクトリを移動

    mv whip-whep-server/wish-whep-00-player ./
    cd wish-whep-00-player
    
  2. wrangler.toml 編集
    wrangler.tomlcompability-date を追加

    wrangler.toml
    name = "whep-00-player"
    pages_build_output_dir = "./static"
    compatibility_date = "2025-04-01"
    
  3. 起動(ローカルサーバー)
    Pages です。

    npx wrangler pages dev
    

    http://localhost:8788 で立ち上がりました。

    log
    npx wrangler pages dev
    :
    No Functions. Shimming...
    No bindings found.
    ⎔ Starting local server...
    ⎔ Reloading local server...
    [wrangler:inf] Ready on http://localhost:8788
    
  4. 接続

    • ブラウザーで WHEP player の URL(この場合 http://localhost:8788)に接続
    • WHEP サーバー play URL( http://localhost:8787/play/my-live)を入力、 Load

      動画と音声が配信されました。1秒くらいの時間差が見られました。
  5. ログ
    WHIP-WHEP Server のコンソールに WHEP サーバーへの接続が現れます。

    [wrangler:inf] POST /play/my-live 201 Created (1533ms)
    [wrangler:inf] OPTIONS /play/my-live/cf5753324a74f8ad9c6df7ad7b3dd077 204 No Content (5ms)
    [wrangler:inf] PATCH /play/my-live/cf5753324a74f8ad9c6df7ad7b3dd077 200 OK (726ms)
    
結果

この時点で下記を確認できました。

  • 1秒までの時間差 ✅️
  • チャット機能 ✘
  • ログイン認証 ✘

Edge Chat Demo

サーバーのセットアップ

Edge Chat Demo を立てます

  1. サーバーのセットアップ
    workers-chat-demo
    npm create cloudflare@latest -- --template https://github.com/cloudflare/workers-chat-demo
    
  2. 起動(ローカルサーバー)
    npx wrangler dev
    
    http://localhost:62257 で立ち上がりました。
    log
    npx wrangler dev
    :
    Your worker has access to the following bindings:
    - Durable Objects:
      - rooms: ChatRoom
      - limiters: RateLimiter
    ⎔ Starting local server...
    [wrangler:inf] Ready on http://localhost:62257
    
接続
  1. 接続(ローカルサーバー)
    your name 任意の名前を入力
    room name 任意の部屋の名前を指定 my-live

    部屋の名前をアンカーでつけると、当該の Chat に参加できます。
    http://localhost:62257/#my-live
    [wrangler:inf] GET /api/room/my-live/websocket 101 Switching Protocols (17ms)
    
    Create a Private Room の方

    Create a Private Room はローカルサーバーを HTTPS で起動する必要があるようでした。
    一意の部屋 ID が発行されるだけなので、今回の場合は任意に指定したものを利用することにします。
    もしローカルサーバーを HTTPS で起動する場合は
    wrangler dev --local-protocol https
    オプションで俺俺が立ちます。

  2. WHEP player からの呼び出し
    生成された URL を WHEP player の index.html から iframe で呼んでみると追加されました。
    src のリッスンポートはローカルサーバー起動時の状況によりけりです。
    <iframe
      id="chatroom"
      title="chatroom"
      src="http://localhost:62257/#my-live"
    </iframe>
    
    Safari と Firefox から呼んでみます。
    お互いにチャットが出来ています。
結果

この時点で下記を確認できました。

  • 1秒までの時間差 ✅️
  • チャット機能 ✅️
  • ログイン認証 ✘

インターネット公開

ここからちょっと手間かかります。

セキュリティ適用の方針

あらかじめ打てる手は打ち、最低限の安心を確保したうえで公開を進めます。

  1. デモアプリの公開点(=攻撃点)を減らす
    • 自前ドメインでのみ公開
      Workers・Pages の公開で自動付与されるドメイン名は利用しない
      Workers/Pages Custom Domains
      Bulk Redirects
    • 公開する URL 以外は受け付けない
      WAF Custom Rules
  2. デモアプリの公開点は全て認証・認可する(利便性は損なわず)
    • WHEP player・WHEP サーバー・Chat へのログインとシングルサインオン
      Access
    • WHIP サーバーの保護(OBS からの接続)
      Access または WAF Custom Rules

公開の方針

セキュリティ関連項目を適用してから公開デプロイするか、その逆か、を考えます。
前者が望ましいですが、動作確認を考えると、後者がやりやすいです。

今回は下記の手順にします。
➜ 誰が参加しても問題のないダミーを公開し、動作確認
➜ セキュリティ適用を確認
➜ セキュリティ土台の上で、本番配信の公開

ドメイン

自前ドメインで提供します。
自動付与されるドメイン名は利用せず、管理すべき点を減らします。

単一ホストも考えましたが、デモアプリを書き直すところがありそうなため、アプリごとの独立にしました。
プロトコルは HTTPHTTPS で。

サーバー ローカル デプロイ カスタム
WHEP localhost:8787 whip-whep-server.<your>.workers.dev
DISABLE
wh.live.oymk.work
WHIP localhost:8787 whip-whep-server.<your>.workers.dev
DISABLE
wh.live.oymk.work
Player localhost:8788 <your-app>.pages.dev
REDIRECT
live.oymk.work
Chat localhost:62257 chat-demo.<your>.workers.dev
DISABLE
chat.live.oymk.work

デモアプリの公開

WHIP-WHEP Server
  1. Route 追加 wrangler.toml
    workers.dev での公開を止め、自分ドメインのみにします。

    wrangler.toml
    name = "whip-whep-server"
    main = "src/index.ts"
    compatibility_date = "2024-04-03"
    
    workers_dev = false
    routes = [
      { pattern = "wh.live.oymk.work", custom_domain = true }
    ]
    
    npx wrangler deploy
    
    code: 10097 エラーが出たら(free plan)

    free plan は SQLite storage backend のみ利用可能なので、

    In order to use Durable Objects with a free plan, you must create a namespace using a `new_sqlite_classes` migration. [code: 10097]
    

    のエラーが出たら wrangler.toml を編集し、デプロイし直します。

    [[migrations]]
    tag = "v1"
    new_sqlite_classes = [ "LiveStore" ]
    #new_classes = [ "LiveStore" ]
    
  2. App IDAPI Tokensecret で配備

    whip-whep-server
    npx wrangler secret put CALLS_APP_ID
    Enter a secret value:
    npx wrangler secret put CALLS_APP_SECRET
    Enter a secret value:
    
  3. CORES 関連の調整 index.ts

    変更箇所

    optionsResponse()access-control-allow-origin 指定

    function optionsResponse(): Response {
    	return new Response(null, {
    		status: 204,
    		headers: {
        		"accept-post": "application/sdp",
    			"access-control-allow-credentials": "true",
    			"access-control-allow-headers": "content-type,authorization,if-match",
    			"access-control-allow-methods": "PATCH,POST,PUT,DELETE,OPTIONS",
    			//"access-control-allow-origin": "*",
    			"access-control-allow-origin": "https://live.oymk.work",
    			"access-control-expose-headers": "x-thunderclap,location,link,accept-post,accept-patch,etag",
        		"link": "<stun:stun.cloudflare.com:3478>; rel=\"ice-server\""
    		}
    	})
    }
    

    coresHeaders を変更

    async function whepHandler(request: Request, env: Env, ctx: ExecutionContext, parsedURL: URL): Promise<Response> 
    {
    	//const corsHeaders = {"access-control-allow-origin": "*"}
    	const corsHeaders = {
    		"access-control-allow-origin": "https://live.oymk.work",
    		"access-control-allow-methods": "POST,PATCH,OPTIONS,DELETE",
    		"access-Control-allow-Credentials": "true",
    	}
    

    再デプロイ

    npx wrangler deploy
    
Edge Chat Demo
  1. Route 追加 wrangler.toml
    workers.dev での公開を止め、自分ドメインのみにします。
    wrangler.toml
    name = "chat-demo"
    compatibility_date = "2024-01-01"
    main = "src/chat.mjs"
    
    workers_dev = false
    routes = [
      { pattern = "chat.live.oymk.work", custom_domain = true }
    ]
    
    npx wrangler deploy
    
    code: 10097 エラーが出た場合は WHIP-WHEP Server 同様に wrangler.toml を編集し、再デプロイします。
    code: 10097 エラーが出たら(free plan)
    [[migrations]]
    tag = "v1"
    new_sqlite_classes = [ "ChatRoom", "RateLimiter" ]
    #new_classes = [ "ChatRoom", "RateLimiter" ]
    
WHEP player
  1. Chat リソースの更新 index.html
    src="http://localhost:62257/#my-live" を公開 URL に置換します。
    <iframe
     id="chatroom"
     title="chatroom"
     src="https://chat.live.oymk.work/#my-live">
    </iframe>
    
  2. デプロイ
    npx wrangler pages deploy
    
    log
    :
    ✔ The project you specified does not exist: "whep-00-player". Would you like to create it? › Create a new project
    ✔ Enter the production branch name: … production
    ✨ Successfully created the 'whep-00-player' project.
    ✨ Success! Uploaded 1 files (1.57 sec)
    
    🌎 Deploying...
    ✨ Deployment complete! Take a peek over at https://642b14b1.whep-00-player-3a2.pages.dev
    
  3. Pages Custom Domains の追加
    ダッシュボードで実施します。
    Workers & Pages > whep-00-player > カスタムドメイン
    DNS レコードの自動作成の手順も出てきます。
  4. CORES 関連の調整 index.html
    変更箇所

    資格情報連携のため
    credentials: 'include' 追加

            const offer = await fetch(resource, {method: "POST", credentials: 'include'})
    
            await fetch(sessionUrl.href, {method: "PATCH", credentials: 'include', body: answer.sdp})
    

    再デプロイ

    npx wrangler pages deploy
    

セキュリティの適用

WHEP player・WHEP サーバー・Chat の認証認可とシングルサイオン 👉️Access

  1. アカウントホーム > Zero Trust > Access > ポリシー > 再利用可能なポリシー
    認可ポリシーを作ります。
    例: @cloudflare.com のメールアドレスを Allow
  2. Access > アプリケーション
    アプリケーションを作ります。

    セルフホストを選択します。
    • パブリックホスト名(基本情報
      3 つのページ起点で同一の認証トークンによるアクセス制限をかけます。
      live.oymk.work/ wh.live.oymk.work/play/ chat.live.oymk.work/
      WHIP-WHEP server は WHEP サーバー(/play/)のみ対象にしています。
    WHIP サーバー(/ingest/)は WAF で保護

    WHIP サーバー(/ingest/)については、OBS を Access のポリシー(Service token含め)に対応させる方法がわからなかったため、WAF Custom Rules で保護することにします。
    ブラウザーでの配信なら Access で一括できそうです。

    • ポリシー
      1 で作成した認可ポリシーを適用します。
    • CORS(詳細設定 > クロスオリジン リソース共有 (CORS) 設定
      オリジンへのオプション リクエストをバイパスする を有効にします。
      CORES 関連はデモアプリ側で仕込んであるのでそちらにまかせます。
      プリフライトリクエストが通るようにするためです。
    • アプリケーションを保存
      一度作成したアプリケーションを編集した際には押し忘れないようにします。

WHEP player(pages.dev ドメインの回避) 👉️Bulk Redirects

Pages は Custom Domains を利用しても pages.dev を無効にすることができません
デプロイのたびにドメイン名が発行される特性もあるので、自分ドメインに一括リダイレクトします。

  1. Pages ドメインの確認
    デプロイしたときに表示されます。

    展開時の Pages ドメイン
    npx wrangler pages deploy
    
    ? Select an account › - Use arrow-keys. Return to submit.
    :
    :
    🌎 Deploying...
    ✨ Deployment complete! Take a peek over at https://7bc1e776.whep-00-player-3a2.pages.dev
    

    ビルドのたびに頭のラベルが変わります。

  2. 一括リダイレクト
    共通部分 whep-00-player-3a2.pages.dev/https://live.oymk.work/ にリダイレクトします。

    アカウントホーム > 一括リダイレクト

WHIP の認証 👉️WAF Custom Rules

手元の OBS からだけ接続できるようにします。

OBS では Bearer Token をつけれるようです。

設定すると HTTP ヘッダー Authorization: Barer hogehoge でつけてきます。
これは WAF Custom Rules で検証できるので、適用します。

  1. セキュリティ > WAF > Custom Rules
    WHEP サーバー /ingest/*authorization ヘッダー hogehoge を必須にします。

全体の保護 👉️WAF Custom Rules

最後に、デモアプリで全体を保護します。
利用しないパスへのアクセスを落とします。
(cdn-cgi は Cloudflare の RUM を有効にしているので入れました)

  1. セキュリティ > WAF > Custom Rules

結果

全体の保護含め、下記の目的を達成できました。

  • 1秒までの時間差 ✅️
  • チャット機能 ✅️
  • ログイン認証 ✅️

観測

それぞれにアナリティクスやメトリクスがあります。参考に一部抜粋します。
上位プランになるとログや API も拡充されてきます。

Realtime SFU Realtime > サーバーレス SFU > Analytics

WHIP-WHEP Server Workers & Pages > メトリクス

Edge Chat Demo Workers & Pages > メトリクス

Access Analytics > Access


WAF セキュリティ > Analytics

Durable Objects Workers & Pages > Durable Objects
chat-demo_ChatRoom

chat-demo_RateLimiter

whip-whep-server_LiveStore

おまけ

  1. 認証情報の再利用
    JWT で認証情報を連携できるので、WHEP player でログインしたメールアドレスを Chat のログインに使います。

    JWT decode by claude
    chat.html
    // === JWT Email Auto-fill Logic ===
    function getCookie(name) {
      return document.cookie
        .split('; ')
        .find(row => row.startsWith(name + '='))
        ?.split('=')[1];
    }
    
    function parseJwt(token) {
      try {
        const base64Url = token.split('.')[1];
        const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
        const jsonPayload = decodeURIComponent(
          atob(base64).split('').map(c =>
            '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
          ).join('')
        );
        return JSON.parse(jsonPayload);
      } catch (e) {
        console.error("Failed to decode JWT:", e);
        return null;
      }
    }
    
    const jwt = getCookie("CF_Authorization");
    const decoded = parseJwt(jwt);
    const emailFromJwt = decoded?.email;
    
    if (emailFromJwt && nameInput) {
      nameInput.placeholder = emailFromJwt;
    }
    
    //get account from email
    function getAccountPart(email) {
      if (!email || typeof email !== 'string') {
        return null;
      }
      
      const atIndex = email.indexOf('@');
      if (atIndex === -1) {
        return null; // Not a valid email format
      }
      
      return email.substring(0, atIndex);
    }
    //
    
    const accountPart = getAccountPart(emailFromJwt)
    
    // Replace original startNameChooser with email fallback
    function startNameChooser() {
      nameForm.addEventListener("submit", event => {
        event.preventDefault();
        const inputVal = nameInput.value.trim();
        username = inputVal || accountPart;
        if (username?.length > 0) {
          startRoomChooser();
        }
      });
    
      nameInput.addEventListener("input", event => {
        if (event.currentTarget.value.length > 32) {
          event.currentTarget.value = event.currentTarget.value.slice(0, 32);
        }
      });
    
      nameInput.focus();
    }
    //
    
    startNameChooser();
    
  2. 生成 AI の組み込み
    Workers AI も組み込んでみます。
    ライブ配信者として一言もらうようにしました。
    無理矢理でいいアイデアが浮かばず、、

  3. Chat も Realtime で
    今回、既存のデモアプリを使いましたが、DataChannles の利用も素直そうです。

    DataChannles でのデータ転送デモを追加すると、動画・音声ブロードキャストの脇で双方向のデータのやり取りが行われました。

    ライブ用の 1 チャンネルに加え、双方向 2 つのチャンネルが開設されていました。

まとめ

やりたいことは一通りできました。

WebRTC WHIP・WHEP のリアルタイム通信を中心に Cloudflare のその他の機能をレイテンシーなく簡単に追加できることも確認しました。

と思っていたら Developer Week 2025 中に RealtimeKit の発表が出ていましたね。

なんか凄そうです。
こちらも期待です。

Discussion