はじめに

CAMでは、あらゆる表現活動をする方を全力で応援するサービスとして、Fensiの運営・開発を行っており、2021年1月には1on1トーク機能をリリースしました。1on1トーク機能は根幹となるトーク部分にAmazon Chimeを使用しています。 今回は、Amazon Chimeを使用してどのように1on1トーク機能を実現しているかを紹介します。

目次

  • Fensiについて
  • Fensi Platformについて
  • 1on1トークについて
  • ネイティブアプリの実装
  • 監視録画機能
  • まとめ

Fensiについて

Fensiは記事・写真・動画などのコンテンツを投稿することで、簡単に自身の公式サイトやファンクラブを開設することができるサービスです。 ページの仕様やデザインを自由に組み合わせることでその方らしいサイトを制作できるほか、オリジナルグッズの販売機能やオンラインサロン、1on1トークやレッスンなどの販売も行うことができます。

Fensi Platformについて

Fensi PlatformはCAM, Inc. が2020年から始めたサービスレベルでのサービス基盤です。 以前からメール配信やユニークID払い出し等に関するAPI基盤はありましたが、あくまでコアな機能を切り出しているだけで「共通ライブラリ」に留まるものでした。Fensi Platformの特徴は、従来のフルスクラッチ型の開発Styleではなく、Packaged business capabilities を基本概念においた積み上げ式の開発Styleを目指しているところです。 どのサービスにも類似する「月額料金を支払う」「会員登録をする」「会員に通知をする」などの機能をプラットフォームがノーコードで提供することで、乗り入れる各サービスは独自の機能開発に集中することができます。

1on1トークについて

1on1トークはサイト開設者であるオーナーとファンであるユーザーがFensiアプリ上で1対1でのコミュニケーションを取ることのできる機能です。 オーナーは、「開始日時、1枠あたりのトーク時間、販売件数、販売価格」を決めて、従量販売コンテンツとして投稿することで1on1トークを開催できます。   ユーザーは投稿されたコンテンツを購入することで参加できます。 1on1トークには、

  1. 「開始日時、1枠あたりのトーク時間、販売件数、販売価格」を設定するだけで開催できる
  2. トークの様子はすべて録画されている
  3. 事前にユーザーから「トーク中に話したい内容」をヒアリングできる

といった特徴があります。

1on1全体のアーキテクチャ

1on1トーク機能は、SolaというAmazon Chime SDKを使った、内製のコミュニケーションサービス基盤と連携することで実装しました。 以下の画像を元に、1on1トークについて、簡単に全体の流れを解説します。 前提として、事前にサイトオーナーによりトーク(以下 Meeting)の実施が予約されている状態とします。

  1. FensiのAWS Batchが開始時刻の近い予約枠を監視して、SolaへMeeting作成をリクエスト
  2. Solaから基盤APIへ接続しストリーム情報を作成
  3. 録画用のヘッドレスブラウザをAWS Fargateタスクで起動
  4. ヘッドレスブラウザがストリーム情報をWowzaサーバーに送信開始
  5. FensiAPIに対してMeeting参加をリクエスト
  6. Chime SDKでMeetingSessionを作成

※ WowzaサーバーにはWowza Streaming Engineをインストールしています。 Wowzaは、ライブ配信/オンデマンド配信/マルチデバイスへの高品質ビデオ/オーディオをストリーミングを行えるプロダクトです。 詳しくはこちらを参照ください。

Amazon Chimeについて

Amazon Chimeは、ビデオ、音声、テキストチャット、画面共有などのオンラインミーティング機能を提供するサービスで、iOS、Android、Mac OS、Windowsに対応したアプリケーションが用意されています。 また、Amazon Chime SDKを使用することで、自身のアプリケーションにこれらの機能を追加できます。 今回は、iOSアプリ用のAmazon Chime SDK for iOS、Androidアプリ用のAmazon Chime SDK for Android、監視録画機能でAmazon Chime SDK for JavaScriptを使用しました。

Solaについて

Solaは、Amazon Chime SDKを使った社内向けのコミュニケーションサービス基盤です。 Fensiなどの社内で開発しているプロダクトにビデオ通話機能を導入する際に必要なAPI群を提供しています。 提供しているAPIは大きく分類すると、

  • Meeting操作
  • Attendee操作
  • Archive機能

の3種類です。

Meeting操作

Amazon Chime SDKのCreateMeetingAPIをコールしてMeetingIdおよび、MediaPlacementと呼ばれる映像と音声を送受信するためのURLセットを発行します。 また、Meetingを終了するためのAPIや開催中のMeeting一覧を取得するAPIを提供しています。

Attendee操作

ユーザーがMeetingに参加する際はAmazon Chime SDKのCreateAttendeeAPIをコールしてAttendeeIdとJoinTokenを発行します。Amazon ChimeではユーザーがMediaPlacementに接続する際に、JoinTokenを用いて認証します。 また、MeetingからAttendeeを退室させるAPIやMeetingに参加しているAttendeeの一覧を取得するAPIを提供しています。

archive機能

Solaではセキュリティの観点から開催されたMeetingをすべて録画しています。録画したMeetingをサービス側で検閲するためのAPIを提供しています。

トークシステムの概要

1on1トーク機能は、1回の開催で複数の参加枠を作成できるようになっています。 Amazon ChimeのMeetingは参加枠ごとに作成しており、オーナーは初回枠の開始時刻に合わせてMeetingに参加し、終了時刻になると自動的に退室となります。 また、次の枠にはMeeting開始時刻になると自動的に参加するようになっています。

それぞれのMeetingは、AWS Batchを使用して開始時刻が近い参加枠を監視し、条件に一致する枠がある場合はSolaへMeeting作成を依頼するようにしています。 参加時にMeeting作成するのではなく開始前に作成しておくことで、開始時刻から実際に入室してトークが始まるまでのラグを減らしています。

NativeアプリがMeetingへ参加、退室をする際には、Fensiサーバーを経由することで、1on1トークとして必要な独自処理などを施すようにしています。

ネイティブアプリの実装

システム構成

Amazon Chime SDKを使用すれば、ミーティングの作成から参加まですべて行えます。 しかし、1on1トークでは参加者の制御が必要だったため、Amazon Chimeのミーティングへの参加はAmazon Chime SDK経由ではなく、Fensiサーバー経由で行っています。 FensiサーバーのAPIに対して参加をリクエストし、サーバー側はそれに応じてAmazon Chimeのミーティング情報を返します。アプリ側はミーティング情報に対してMeetingSessionを作成し、Amazon Chime SDKを介して音声、映像の送受信やカメラの切り替えなどを行っています。 1on1トーク機能はすべてのやり取りをAmazon Chime SDKで完結させているわけではなく、参加者の追加、ミーティングを生成、終了の通知などをコントロールするために、Fensiのサーバーを経由して行っています。

iOSの画面構成

1on1トークはオーナーとユーザーで別のViewControllerを使用し、ベースとなるViewControllerに各状態のViewControllerをaddChildする構成になっています。 ベースのViewControllerは状態によって各VCをaddChild / removeFromParentすることで画面を切り替えています。 作成当初は、1つのViewController上に各状態のViewを用意し、isHiddenを切り替えることによって画面の切り替えを行っていました。 しかし、実装が進むにつれて処理が増え、Reactorが肥大化してしまい、どの処理がどの状態で使われるものなのかがわかりづらく、メンテナンス性が損なわれてしまいました。 そのため、各状態でViewControllerとそのViewControllerに対応した Reactorを用意し、その状態で必要となる処理(残り時間の計算やトークルームへの入退室等)はそのReactorに実装することで、各ViewControllerを独立させ、画面ごとに必要な処理がわかるようにしました。

 

Androidの画面構成

Androidアプリでのユーザー側機能について説明します。 この3つの状態をUI側では画面を分けて実装し、NavigationComponentを使って各状態の画面を切り替えています。 本来であれば1つの画面で完結するのがシンプルなのですが、ただ通話するだけでなく、上記のような3つの状態を管理することや、各状態の中で複数のレイアウトパーツに対して表示非表示、更新などを行う必要がありました。 また、今後機能を追加していくにあたり、1画面だとコードの見通しも複雑になると思い、3つの状態を画面として切り分ける形にしました。 各状態でFragmentとViewModelを用意し、状態ごとで切り分けることのできる処理はそれぞれのViewModelで行い、更新情報を扱うポーリング処理やAmazon Chime SDKを扱うクラスのインスタンス管理などは、状態ごとではなく横断的なスコープのViewModelを用意して管理しています。

Androidで注意すべき点

Amazon Chime SDKでは、映像を映し出すViewとしてSurfaceViewを継承したDefaultVideoRenderViewが用意されています。 SurfaceViewは一見普通のViewと同じように扱うことができるように見えますが、映し出すコンテンツ(今回で言うところのカメラ映像)は通常のレイアウト階層ではなく、別で配置されます。 そのため、今回のトーク画面のようにDefaultVideoRenderViewを重ねるような画面構成にした場合、レイアウト階層上は上に配置したはずのViewが表示されないといった問題が発生してしまいます。 この問題は必ず発生するというわけではなく、OSバージョンが6系と7系の端末で確認できました。 解決策としては、SurfaceViewにはsetZOrderMediaOverlayというメソッドが用意されており、これを上に配置したいSurfaceViewに対して設定すると重なりの順序を制御できます。

トークルームへの入室

Amazon Chimeのミーティングへの参加はFensiサーバー経由で行い、そのレスポンスで受け取った情報をもとにMeetingSessionを作成しています。 そのため、トーク画面に切り替わってからミーティングへの参加を行うと、相手とトークが開始できるまでに15秒程かかってしまいます。このラグは通信状況等により前後しますが、ユーザー体験としてはよくないため、短縮する必要がありました。 1on1トークではこの問題を解決するために、AWS Batchを使って開始時間より前にミーティングを作成し、トーク開始時間までの待機中、事前にミーティングへの参加を行えるようにすることで、相手とトークが開始できるまでのラグを2秒程に短縮しました。 事前にミーティングへ参加することで、トークが開始できるまでのラグは短縮できましたが、今度は開始時間まではトークができないようにする必要がありました。 そのため、MeetingSession作成時には音声をミュートにしておき、映像の送受信は開始しないようにしています。 そして、開始時間になり、トーク画面に切り替わったらミュートを解除し、映像の送受信も開始するようにしています。

カメラ、音声の制御

カメラの制御

待機(インターバル)画面とトーク画面とで自分の映像を映し出す仕組みが違います。 トーク画面以外はAmazon Chimeと接続していないので、Amazon Chime SDKの機能ではなく、端末のカメラを起動してそのプレビューを表示しています。 そのため、各画面でのカメラの状態(オン / オフ、インカメラ / アウトカメラ)を保持しておき、例えば待機画面でアウトカメラにしていた場合は、トーク画面もアウトカメラの状態で始めるように制御しています。

音声の制御

トーク中に使用するオーディオデバイスは、

  1. 端末で使用可能なオーディオデバイス一覧を取得する
  2. 取得した中から出力したいオーディオデバイスを選び、設定する

という流れで設定します。 端末で使用可能なオーディオデバイスの一覧はMeetingSession内にあるlistAudioDevicesから取得できます。 取得したlistAudioDevicesの中から設定する値を選び、chooseAudioDeviceを呼ぶことで任意のオーディオデバイスに切り替えることができます。 注意点としては、取得したオーディオデバイスの型であるMediaDeviceはオーディオデバイス以外にもカメラの状態も管理しているため、オーディオデバイスを設定するときはlistAudioDevices経由で取得したMediaDeviceを使うようにすると安全です。 使用可能なオーディオデバイスが変更された場合、DeviceChangeObserverのonAudioDeviceChangedが呼ばれるので、ここで変更を検知し、毎回上記の処理を行うことでオーディオデバイスを自動で切り替える事ができます。

ユーザー間の同期

1on1トークの待機画面は、

  •  オフラインのイベントでユーザーが並んで順番待ちをしている様子を表す
  •  自分の番まであと何人というのが視覚的にわかるようにする

という理由から、2枠目以降のユーザーの待機画面は、待機ユーザーのアイコンが順番に並んでいるUIとなっています。 順番待ちの様子を再現するために、ユーザー間でトークの進行状況を同期し、前のユーザーのトークが終了したら1枠ずつ順番を進めています。

このトークの進行状況の同期にはポーリングを使用しています。   データの取得間隔は短いほど実際の進行状況との差異が少なくなりますが、間隔が短すぎるとAPI通信の回数が増えてしまい、サーバーに負荷をかけてしまいます。 そのため、進行状況のリアルタイム性とサーバー負荷のバランスを考慮して現在は5秒間隔でデータを取得しています。

監視録画機能

1on1トーク機能では、

  • トラブル対策
  • 安心感
  • サービス責任

の観点から、監視録画機能を導入しています。

システム構成

1on1トークの監視録画機能は、ヘッドレスブラウザを視聴者としてMeetingに参加させ、そのストリームをffmpegを使用して社内基盤システムに対して送信することで実現しています。 実際の処理の流れは、

  1. SolaのAPIサーバーから基盤APIにストリーミング配信できるサーバーを問い合わせ、複数構築されている配信サーバーから1台を選定
  2. 基盤APIはWowzaサーバと通信し、ストリーミングサーバーを選定
  3. ストリーム情報を作成
  4. 基盤APIは、作成したストリーム情報をSolaのAPIサーバに送付
  5. 作成されたストリーム情報をSolaのAPIサーバからAWS Fargateに送りイメージを起動
  6. AWS Fargateのイメージでヘッドレスブラウザの立ち上げと、Wowzaに対してストリーム情報を送付して録画を実施

となっています。

録画について

録画には、起動したAWS FargateタスクでffmpegとReal Time Messaging Protocol (RTMP)を使用しています。 実際には、FirefoxでレンダリングされたWebページをキャプチャし、そのデータを社内基盤のWowzaに送信することで録画しています。 その後、社内基盤側でトランスコードを実施し、HLS形式でS3に動画を保存しています。

ffmpegのリクエスト例

$ ffmpeg -hide_banner -loglevel error -nostdin -s 1080x1920 -r 30 -draw_mouse 0 -f x11grab -i :2.0 -f pulse -ac 2 -i default -c:v libx264 -pix_fmt yuv420p -profile:v main -preset veryfast -x264opts nal-hrd=cbr:no-scenecut -minrate 3000 -maxrate 3000 -g 60 -filter_complex 'adelay=delays=1000|1000' -c:a aac -b:a 160k -ac 2 -ar 44100 -f flv 'rtmp://[URL]:[port]/[service名]?token=xxxxxx&authmod=adobe&user=[user name]/[streamId]'

開発中はある程度バッファを持ったスケジュールで配信実行順序を設定していました。 しかし、本番を想定した場合にそれでは遅延が許容できませんでした。 そのため、個々のサブシステム同士の実行順序をシビアに設定することで、できるだけ遅延を少なくした配信を可能にしました。

ヘッドレスブラウザについて

視聴者としてMeetingに参加させるヘッドレスブラウザの立ち上げには、

  • 起動までの時間が短い (約40sec)
  • 常時起動させておくものではないので、スポットで利用したい

といった理由で、ECSのタスクスケジュールではなくAWS Fargateを採用しました。 また、使用するブラウザについては、AWS のdemoを参考にFirefoxを立ち上げています。 Fensiの1on1トーク機能はiOSおよびAndroidのネイティブアプリで通話が行われます。 そのため、録画画面も実際のネイティブアプリの画面と合わせてヘッドレスブラウザの画角を9:16の1080×1920で起動します。

録画画面では最低限、オーナーとユーザーの画面があればいいので、Browser Meeting Demoの画面を参考に最適化しました。 オーナーの画面を背景にするため、Attendeeがオーナーかユーザーかを識別する必要があります。 そのため、Attendeeに識別タイプタグを付与しオーナーかどうかを判定しています。

// attendeeIdから参加者情報を取得するAPI 
const attendee = await this.getAttendee(attendeeId);
const isOwner = attendee.tags[AttendeeType].Value === 'owner'; 

そして、videoTileDidUpdateでオーナーは背景のvideoタグにレンダリングします。

this.audioVideo.bindVideoElement(ownerVideoTileId, videoElement);

まとめ

Fensiの1on1機能について紹介させていただきました。Packaged business capabilitiesを意識して、1on1機能ではSolaという内製の基盤サービスやマイクロサービスアーキテクチャを採用することで、積み上げ型で再利用可能なサービス開発をできるようにしています。 今回1on1トーク機能を実現するにあたって採用したAmazon Chimeは、本来オンラインミーティング機能を提供するサービスのため、仕様を満たすには様々な工夫が必要でした。中でも、相手とトークを開始するまでのラグをいかになくすかがユーザー体験向上の鍵だったので、最後まで粘って調整しラグを最小限に抑えました。 また、サイトオーナーとユーザーが直接コミュニケーションを取る機能なので、より安心、安全に使っていただけるように監視録画機能の実装を行いました。 今後も1on1機能の改善・新機能の追加などを行い、オーナー、ユーザー双方にとってより満足度の高いサービスへと仕上げていきますので、よろしくお願いします。