ImageFluxでClubhouse風サービスを作ってみた! 低コスト大規模配信システムの作り方(後編)

こんにちは、テリーです。Clubhouse使ってますか? 先日うちの娘がなんと「Clubhouseを使いたいからmacbook airを買って!」と言ってきました。そこはiPod touchにたどり着いてほしかったけれど、M1 macbookでリアルiPhoneアプリを動かす調査するという名目ができたので買ってみました。

Clubhouseは2月のブームが落ち着き、オワコンになったという説もありますが、一方で、Android版が5月中についにリリースされるという話や、大手SNSのFacebook、Twitter、Spotify、LinkedInなどが同様のサービスを始めるというウワサも出ています。2021年は第二次ラジオサービス元年と位置づけられるのか、これからの展開が楽しみになってきました。

今回は、話題沸騰中の音声SNS、Clubhouse(クラブハウス)と同等のWebサイトを、FirebaseImageFluxを使って短いコードで実現する方法を二部構成で紹介します。前編では、Firebaseを使ったWebサイトの開発手順を説明しました。本記事は後編で、ImageFlux LiveStreamingに接続し、WebRTCを用いた双方向音声通話と、多数のリスナーに向けたHLS視聴コードの書き方を紹介します。

3つのユーザー種別

Clubhouseは一度に一つの部屋にしか入れません。部屋に入っているユーザーを大きく分類すると、3つの種別にわけられます。

種別 できること
モデレーター 部屋を管理、話す、聞く、ミュート、昇格、降格、強制退室
スピーカー 話す、聞く、ミュート
リスナー 聞く、挙手

部屋を開設した人はモデレーターになります。その後に入室した人はリスナーになり、モデレーターがリスナーに対して昇格操作をするとスピーカーになります。
音声データの流れの観点で見ると、モデレーターとスピーカーは、自分の声が相手に届き、自分以外の全員の声が自分に届きます。この状態を「双方向」と言います。リスナーは、自分の声は誰にも届きませんが、モデレーターとスピーカー全員の声が自分に届き、聞くことができます。この状態を「片方向」と言います。

各種別の画面。モデレーターとスピーカーは右下にマイクのアイコンがあるがリスナーにはない。モデレーター画面にはリスナーからスピーカーに昇格/降格させるボタン(「↑」「↓」)がある

双方向=WebRTC、片方向=HLS

双方向か片方向かというのは大規模配信システムを構築する上でとても重要です。なぜなら双方向の場合は遅延が許されないからです。声が10秒後に相手に届く状態ではトランシーバーで話しているような感覚になり、相手が話し終わるまで待たなければならず、活発な会話ができません。遅延が0.2〜0.5秒程度だと違和感なく会話をすることができます。
では片方向の場合も双方向と同じようにデータを送ればよいではないか、と考えますが、片方向の場合は多少の遅延があっても視聴者には違和感がないため、コストの低い配信方式が選ばれます。YouTube Live、Abema、ニコニコ動画で採用されているHLSという方式です。遅延は10〜30秒程度です。リスナーが文字でコメントを打ち、配信者が声で返事を返すようなサービスではそれでもコミュニケーションが可能です。HLS方式では、配信サーバは動画・音声データをキャッシュサーバにキャッシュさせ、リスナーは連番のファイルをキャッシュサーバからダウンロードし順次再生します。リスナーの通信状況、通信速度、遅延状況を配信サーバが個別対応する必要がないため、サーバの負荷を分散できることが最大のメリットです。

双方向の場合はすべての主要WebブラウザがWebRTCという仕組みを持っています。片方向の場合によく使われるHLS方式はSafariブラウザだけが標準対応しており、それ以外のChrome、FirefoxはHLSデータをメディアとして認識しませんが、専用のJavaScriptのライブラリを読み込んで再生することができます。HLS方式のデータを再生させるためのライブラリで最も有名なものがhls.jsです。

ImageFlux LiveStreamingでは、双方向の通信をするためのWebRTC方式のデータを、HLS方式のデータに変換し、配信することができます。そのため、Clubhouseのような双方向・片方向併用の配信サービスを低コストで実現することができます。

サンプルコード

これから本サービスの各種機能の実装を紹介しますが、それに先立ち、サンプルコードを提供します。コードを見ながら読み進めてください。

部屋一覧画面

部屋一覧画面(下図)は、本連載の前編で作成しました。作成方法は前編の記事をご覧ください。

部屋を開設 = チャンネル作成とWebRTC接続

まず双方向通話の始め方を説明します。

部屋一覧画面にある「Start A Room」という緑のボタンを押すと、CreateRoomというAPIを呼び出します。Cloud FunctionsのcreateRoom関数(functions/index.js 273行目付近)を参照してください。Cloud Functionsの関数の中でImageFlux LiveStreamingのエンドポイントに向けて、CreateMultistreamChannelWithHLSというAPIを呼びます。APIを呼ぶためのAPIを作成した形になるのは、ImageFlux LiveStreamingのAPIを呼ぶためのACCESS_TOKENを、ユーザーに漏洩させないためです。ACCESS_TOKENが漏洩すると、配信中の部屋を閉じたり、割り込んだりなどの悪意のあるイタズラをされる可能性があります。

CreateMultistreamChannelWithHLS APIのオプションとして、event_webhook_urlを定義します。これはスピーカーの配信が開始・停止した場合に通知されるwebhookです。host.docker.internal:2002というホスト名とポート番号を使っていますが、ImageFlux LiveStreamingのサーバ群からインターネット越しでアクセス可能なホスト名に置き換えてください。
同様に、auth_webhook_urlというオプションも定義しています。これは配信開始直前にユーザー情報を識別し、不正アクセスでないか、権限のあるユーザーかなどをチェックし、場合によっては配信をお断りすることもできるwebhookです。こちらもImageFlux LiveStreamingのサーバ群からインターネット越しでアクセス可能なホスト名に置き換えてください。
functions/index.js 279行目付近に当該のコードが出てきます。以下に示します。

const options = {
  hls: [{ durationSeconds: 2, startTimeOffset: -2, audio: { bps: 32000 } }],
  auth_webhook_url:
    "http://host.docker.internal:2002/imagefluxhouse/us-central1/AuthWebhook",
  event_webhook_url:
    "http://host.docker.internal:2002/imagefluxhouse/us-central1/EventWebhook",
};
const channelInfo = await ImageFluxAPIInternal(
  "ImageFlux_20200316.CreateMultistreamChannelWithHLS",
  options
);

CreateMultistreamChannelWithHLS APIのレスポンスJSONを、スピーカーのデータベースに関連付けて保存し、クライアントはその値を使用してWebRTC接続をします。src/components/AppSora.vueのコードがWebRTC関連の処理を担当しています。

部屋にリスナーとして入室 = HLS再生

部屋一覧画面で、特定の部屋をクリックすると入室します。入室した直後はリスナーとなり、モデレーターとスピーカーに関連付けられたHLSのURLをhls.jsで再生します。src/components/AppHLS.vuesrc/components/HLS.vue のコードがHLS再生関連の処理を担当しています。

入室後の画面とボタン

入室後の画面は src/views/Room.vue のコードが担当しています。

画面に配置されている各種ボタン(上図参照)のうち、リスナーからスピーカーへの昇格ボタンおよび降格ボタンの動作は、Room.vueの後半(116-129行目)で記述しています。挙手・マイクミュート・退室ボタンのイベント処理は、src/components/AppFooter.vue で実装しています。

各ボタンが押されたときの処理のポイントを解説します。

リスナーからスピーカーへの昇格

モデレーターはリスナーをスピーカーに昇格することができます。音声の再生方法が片方向から双方向に変わるため、HLS再生を停止し、WebRTCで接続します。Clubhouseアプリでは別画面でこの操作を行っていますが、本サンプルではページ数節約のため、同画面内の矢印アイコンのクリックで昇格操作を行えるようにしています。Cloud FunctionsのUpgradeUser関数を呼んでいます。

スピーカーからリスナーへの降格

スピーカーは自分の意思でリスナーに降格できます。また、モデレーターはスピーカーをリスナーへ降格できます。Clubhouseアプリでは別画面でこの操作を行っていますが、本サンプルではページ数節約のため、同画面内の矢印アイコンのクリックで降格操作を行えるようにしています。Cloud FunctionsのUpgradeUser関数を呼んでいます。

挙手

リスナーが挙手ボタンを押すと、モデレーターに挙手アイコンでアピールできるようにします。この操作は音声再生と関係ないため、部屋にいる全員への表示のみです。Cloud FunctionsのSetHand関数を呼んでいます。

マイクミュート

モデレーターとスピーカーはマイクボタンを押すことで録音停止・再開を自分の意思でできるようにします。ミュートのオンオフ状態を部屋にいる全員に通知します。マイクをミュート後、Cloud FunctionsのSetMute関数を呼んでいます。

おわりに

ImageFlux LiveStreamingを使ってClubhouse風の音声SNSサービスを作ってみました。ImageFlux LiveStreamingを使ったサービスの提供を検討する場合の参考にしてください。