🐚

Twilio Programmable Videoを用いたビデオ通話の実装 with React(UX編)

2022/10/10に公開

はじめに

これは今さらな2021年振り返りカレンダーの8日目の記事です.

前回はTwilio Programmable VideoとReactを用いてビデオ通話を実装するという話でした.

https://zenn.dev/sheep96/articles/cb585dc3982190

今回は,実装したTwilioのビデオ通話の質をどんな感じで調整するかについて書いていきます.

仕様

前回同様,フロントエンドはReact (typescript),バックエンドはgoとします.
また,ビデオ通話は最大でも十数人程度で行うとします(ライブ配信みたいに
数百,数千人とかは想定していない).

参考

https://www.twilio.com/docs/video/tutorials/developing-high-quality-video-applications
https://qiita.com/mobilebiz/items/c779d8f93ac597eafdff

パフォーマンスチューニング

パフォーマンスチューニングについては,Twilioエバンジェリストの方が書いてくださっている以下の記事が参考になります.
一応自分がやった部分とかを書いていこうと思います.

https://qiita.com/mobilebiz/items/c779d8f93ac597eafdff

Default Connection Optionsの設定

Twilio Video GroupRoomでは,room作成の際に接続に関するパラメータの設定を行います.
以下のような感じです.

const defaultConnectionOptions: ConnectOptions = {
  bandwidthProfile: {
    video: {
      mode: "collaboration",
      trackSwitchOffMode: "detected",
      dominantSpeakerPriority: "standard",
      maxTracks: 10,
    },
  },
  dominantSpeaker: true,
  networkQuality: { local: 1, remote: 1 },
  maxAudioBitrate: 16000,
  preferredVideoCodecs: [{ codec: "VP8", simulcast: true }],
  region: "jp1",
};

Regionの設定

https://www.twilio.com/docs/video/tutorials/video-regions-and-global-low-latency

GroupRoom方式では,Twilio側のサーバを経由してビデオ通話を行います.
その際,サーバのロケーションをユーザに最も近い場所に設定しておくことで通話の品質を向上することができます.

Regionには,ユーザ個別に関わるSignaling Regionと,ルームのユーザ全体に関わるMedia Regionの2つがあります.

connection optionではSignaling Regionを設定します.

例えば,日本のユーザの場合はregionを"jp1"にすることでレイテンシが上がり,通話の速度や品質が向上します.

"gll"を設定した場合は,ユーザにとって最もレイテンシが低いregionが選択されるとのことなので,複数国にわたってサービスを展開している場合などはそうするのが良さそうです.

また,Media RegionはTwilioのコンソールより設定できます.

こちらのRegionは通話に参加しているユーザからの平均レイテンシが最も低くなるように設定します.
日本国内のみならば,Signalingと同様に"jp1"にするのが良さそうです.

また,グローバルサービスではユーザのRegionが複数あるので,全てに対して
平均的に近い場所を選ぶのが難しいです.そのような場合は"gll"に設定します.
ただ,"gll"と言ってもSignalingの時とは挙動が違い,最初にRoomに接続したユーザのRegionに設定されるようです(SFUサーバは全ユーザに共通するので).

Bandwidth Profile

https://www.twilio.com/docs/video/tutorials/using-bandwidth-profile-api

mode

mode にはcollaboration, grid, presentationがあり,デフォルトはcollaborationになっています.

それぞれを簡単に解説すると,

  • collaborationは一部の人のトラックを他より優先して表示するものです.優先表示する人については,喋っている人(dominant speaker)を自動認識させたり,プログラム側で設定することもできます(優先側はpriorityをhigh, 他の人はlowにする).授業や,小・中規模のミーティングなど,全員いて欲しいが喋る人を優先したい場合に向いているでしょう.
  • gridは全員のトラックを均等に配信するものです(全員のpriorityをstandardにする).向いてるケースは友達同士のお話や,ミーティングなどみんなおんなじ感じで出てて欲しい場合でしょうか.
  • presentationは一部の人のトラックに帯域を全て割り振るというものです.帯域が余った場合は他の参加者のトラックにも使われます.collaborationとの違いは,優先されている人以外が表示されるかどうかです.公演のような聴衆がたくさんいるケースに向いてると思います.

自分の場合は,先生と生徒がいる授業サービスをやっていたので,collaborationに設定しました.

trackSwitchOffMode

https://www.twilio.com/docs/video/tutorials/using-bandwidth-profile-api#understanding-trackSwitchOffMode

trackSwitchOffModeでは,参加者の下り帯域幅が足りなくなった時の挙動をpredicted, detected, disabledから指定できます(デフォルトはpredicted).
それぞれ以下のような挙動をします.

  • predictedでは,実際に帯域が足りなくなるのを自動で予測し,一部のトラックをオフにします(audioとpriorityがhighのもの以外).喋ってる人以外は最悪見えなくても問題ないケースで用います.

  • detectedでは,実際に帯域が足りなくなった時にのみトラックをオフにします.不必要なトラックオフを防げる一方で,ビデオが止まったり,音声が途切れる現象がユーザ側に出てしまいます.継続して表示することが重要になるケース(授業など通話に課金が関わるケース)で有用なんじゃないかと思います.

  • disabledでは,トラックオフを行いません.そのため,停止したビデオがずっと表示されることが起こり得ます.公式的には,よっぽどの理由がなければ使わない方がいいとのこと.

自分の場合は通話に対して課金が発生するサービスをやっていたので,お金を払っているユーザに不利益が出ないよう,detectedにしていました.

dominantSpeaker & dominantSpeakerPriority

https://www.twilio.com/docs/video/detecting-dominant-speaker

dominant speakerとは,先ほども少し出てきましたが,Room中で声を発しているユーザのことを指します.
この設定をtrueにしておくと,trackに動的に変化するdominant speakerかの情報が付与されるので,それを用いて優先表示したりできます.ZoomやMeetなどのように,話してる人を大きく表示したい時に使えます.

dominantSpeakerPriorityは,名前の通りdominant speakerのpriorityを設定するものであり,例えばこれをhighにしておけば,喋ってる人のトラックは絶対オフにならないようにできます.

自分の場合は,生徒と教師のように必ずしも喋ってる人が重要ではないという感じだったので,dominantSpeakerPriorityはstandardにしてました(そもそもこの機能onにしなくてよかったかも...).

networkQuality

Twilioではユーザのネットワーク品質を得るための,Network Quality APIが用意されています.
通話サービスだと通信強度を表すバーなどが出ていますが,あれのために利用します.

https://www.twilio.com/docs/video/using-network-quality-api

optionsにおけるnetworkQualityでは,ローカルとリモートのネットワーク状況をどの程度詳細に取るかを指定します.

0〜3(none, minimal, moderate, detailed)の範囲でレベルがあるようですが,自分は公式で推奨されていた1 (minimal) にしていて,高いレベルにしたときに取れるデータをどう使うかはよくわかっていないです.

一応まとめておくと,

  • minimalでは参加者にNQレベルをレポートする.
  • moderateではNQレベル以外にNetworkQualityStatsを提供する.NQSは音声,動画の受信・送信状況より構成される.
  • detailedでは,NQSを,音声・動画の送受信状況に加え,NetworkQualityMediaStatsを持っている.NQMSは帯域やレイテンシなどの情報を含む.

minimalで,一般的な通信状況を表すバーは出せるのですが,検証を行う場合などにより詳細なメトリックが欲しい場合はこのレベルを上げたりするのかも?

preferredVideoCodecs

こちらにおいては,通信を行う際のコーデックと,Simulcastを有効かするかの指定をします.

今回はcollaborationモードがSimulcastでしか使えないということで,VP8 Simulcastとしましたが,前述の記事によると,コーデックは以下のような基準で選ぶと良いそうです.

パフォーマンスチューニングの視点でみると、利用するデバイスの多くがモバイル端末であり、H.264による恩恵が受けられそうな場合や、H.264端末しか利用できないデバイスが必須の場合ではH.264を積極的に採用し、それ以外のケースではVP8で通信できるようにしておくとよいでしょう。特にVP8には、オプションとしてVP8 Simulcastが利用できるため、複数の参加者が利用するビデオ会議でパフォーマンスを出しやすくなります。 by https://qiita.com/mobilebiz/items/c779d8f93ac597eafdff

コーデックはデバイスやブラウザによって対応・非対応があるため,詳細は以下を参照.

https://www.twilio.com/docs/video/managing-codecs

Simulcastは,ネットワークの下りが遅いユーザがいた時,それに合わせてpublishする
データのクオリティを下げなくても良いよう,各ユーザが複数の品質のトラックをpublish
し,最適なものをsubscribeさせる仕組みです.詳細は以下など.

https://www.twilio.com/docs/video/tutorials/working-with-vp8-simulcast

https://gist.github.com/voluntas/dd3af733825c7ae64505a1fd1bd0d684

maxAudioBitrate

maxAudioBitrateについては,公式によると,音声として人の話し声が主の場合は,帯域制限のために16kbps程度にし,それ以外の場合は設定しない(制限なし)のが推奨らしいです.
音楽などを流す場合などは特に制限をかけない方がいいそうです.

https://www.twilio.com/docs/video/tutorials/developing-high-quality-video-applications#general-recommendations

動画のサイズ

動画の品質と通信品質はトレードオフなので,用途に合わせて適宜調整します.
サンプルコードとしては以下あたりで設定します.

https://github.com/twilio/twilio-video-app-react/blob/master/src/constants.ts

調整のための考え方は以下が参考になります.Twilio側で,コーデックと解像度に応じて必要な帯域を示してくれているので,ユーザのネットワーク環境に応じて適切な動画解像度を設定する感じになります.

https://qiita.com/mobilebiz/items/c779d8f93ac597eafdff#twilio-video-が利用する最低通信帯域

また,VP8 Simulcastを用いる場合は,以下のリンク先の表のように,3段階で異なる解像度の映像を生成し,ユーザによって最適なものを流してくれるので,あまりネット環境が悪いユーザに合わせて解像度を決めなくてよくなります.

https://www.twilio.com/docs/video/tutorials/working-with-vp8-simulcast#resolution-and-layers

ネットワーク状況の表示

ユーザのネットワーク状況が悪いと,アプリが正常だったとしてもユーザ側に不具合が出てるように見え,クレームが入ったりします.
そのため,前述したNetwork Quality APIを使って,適切なネットワーク状況かをユーザ側に常に提示するようにします.
例えば,接続前のチュートリアルなどで,ネットワーク状況のバーが2本以下だと動作が不安定になります,などの注意書きをしておきす.

サンプルの実装の以下が参考になります.

https://github.com/twilio/twilio-video-app-react/blob/923fa2c799b39fc6191c5ea0892c2f448b59a5ab/src/components/NetworkQualityLevel/NetworkQualityLevel.tsx

エラーハンドリング

ビデオ通話においては,以下のような様々なエラーが発生し,それに応じてユーザに適切な対応をしてもらう必要があります.

  • カメラなどのデバイスにアクセスできない
    • 権限がない -> 許可してもらう
    • 他のアプリケーションに使われている -> 他のアプリ停止をお願いします
    • そもそもカメラついてない -> 使えへんよ!
  • ネットワーク環境が悪い
    • ユーザ側のネットワークが重い -> 場所移動してもらう
    • アプリケーション側が死亡 -> ごめんなさい
  • トークンの有効期限が切れている

こちら側に非がないエラーもあるのが大変です.そのため,吐かれたエラーに応じて,ユーザにしてもらうべき動作を提示する通知を行うのが良いです.

自分は以下のようなutil関数を作り,エラー通知を行なっていました.

const getErrorContent = (
  hasAudio: boolean,
  hasVideo: boolean,
  error?: Error
): string => {
  switch (true) {
    // This error is emitted when the user or the user's system has denied permission to use the media devices
    case error?.name === "NotAllowedError":
      if (error?.message === "Permission denied by system") {
        // Chrome only
        return "OSによりマイク、カメラへのアクセスがブロックされました。設定の見直しを行ってください。";
      } else {
        return "ユーザによってデバイスへのアクセスが拒否されました。ブラウザにデバイスへのアクセス権限を与えてください。";
      }

    // This error is emitted when input devices are not connected or disabled in the OS settings
    case error?.name === "NotFoundError":
      return "マイク、カメラが見つかりませんでした。デバイスが接続,有効化されていることを確認してください。";

    case error?.name === "NotReadableError":
      return "マイク、カメラを取得できませんでした。デバイスを他のアプリケーションで利用している場合は終了させてください。";

    case error?.name === "AbortError":
      return "マイク、カメラを取得できませんでした。デバイスを他のアプリケーションで利用している場合は終了させてください。また、それでも起動しない場合は、ブラウザの再起動を行ってください。";

    case !hasAudio && !hasVideo:
      return "カメラ及びマイクが検出されませんでした";

    case !hasVideo:
      return "カメラが検出されませんでした";

    case !hasAudio:
      return "マイクが検出されませんでした";
  }
};

export default getErrorContent;

以下のように呼び出します.

const { hasAudioInputDevices, hasVideoInputDevices } = useDevices();
getAudioAndVideoTracks().catch((error) => {
    notify.error(
      `${getErrorContent(
        hasAudioInputDevices,
        hasVideoInputDevices,
        error
      )}`
);

自分は特に,Windows,Linuxで発生する,他のアプリがカメラを使っていると出るエラーに
苦しめられました(Macだと起きないので).できたと思って一旦先方に出したら,
エラー出て動きませんと来て,なんで!?となり,実はZoom繋ぎながら触ってたからだったという
ことがありました.

参考はサンプルのこの辺です.

https://github.com/twilio/twilio-video-app-react/blob/master/src/components/PreJoinScreens/MediaErrorSnackbar/MediaErrorSnackbar.tsx

その他細かい知見など

  • ネットワーク状況とかのテストは,ブラウザの検証機能についてるのでやっていた.
  • パフォーマンスチューニングをちゃんとやるならばユーザタイプごとにpublishする動画サイズを変えた方がいいのだろうが,そこまでできなかった.
  • Twilioのjs sdkだと,VP8 Simulcastはfirefoxに対応していない.
  • Twilioでは,コンソールから,過去のRoomの参加者の通話・切断履歴やデバイス,OSなどがみれるので
    それをもとにエラーが起きてないか,ネットワーク状況の下限はどのぐらいかとかを
    観察することができます(切断を繰り返してるケースとかがたまにある).
  • また,ビデオの録画もできるので,エラーの詳細やユーザ調査も可能です.
    https://www.twilio.com/docs/video/tutorials/understanding-video-recordings-and-compositions

Roomの履歴の実例

最後に

Twilioでビデオ通話を作るときに,パフォーマンスやUXのために気をつけたことについて書きました.
パフォーマンス周りはほとんど記事まんま(劣化版)みたいな感じですが,体験記としてみてもらえると嬉しいです.あとWebRTCとか動画まわりについて知らないことばかりだったので記事を読んでいてとても勉強になりました.

その他,ネットワーク周りやエラーハンドリング周りについては,開発側の非とユーザ側の環境要因を
分離するのが難しく,結構悩まされました.例えユーザ起因であっても,エラーが出た時の利用者側ストレスは半端ないと思いますし,クレームも受けたりしたので,そこら辺の対応処理は今後も気をつけていきたいと思いました(原因と対応方法を詳細に表示するだけで全然違うことを理解した).

次回は,Twilioが提供する,複数ブラウザ間でリアルタイムでの状態同期(jsオブジェクトなど)のためのSDKであるTwilio Syncの利用方法について書いていこうと思います.

Discussion