Mirrativ tech blog

株式会社ミラティブの開発者(サーバサイドエンジニア,iOSエンジニア,Androidエンジニア,機械学習エンジニア,インフラエンジニア, etc...)によるブログです

Mirrativ のアバター活用事例を紹介します!

こんにちは、バックエンドエンジニアの平松です。今回は Mirrativ でのアバター(エモモ)を活用した機能開発事例を紹介します。

Mirrativ のエモモ

Mirrativ にはエモモと呼ばれるUnityで動く独自の3Dアバター機能が存在します。 エモモはユーザが自身の好みに合わせて自由にカスタマイズ可能なアバターであり、 Mirrativ では定期的に開催しているイベントに合わせて様々なパーツや衣装などのカスタマイズ用のアイテムを多数リリースしています。

カスタマイズ用のアイテムの追加のみではなく、エモモを活用した様々な機能の開発・リリースも行っています。 本記事ではこれまで開発してきた機能について紹介していきたいと思います。

なお、エモモがどのようにして動いているのかの詳細については今回は割愛しますので、興味のある方はこちらの記事をご覧ください。

tech.mirrativ.stream tech.mirrativ.stream

続きを読む

【iOS】ゲームアプリの音声設計とミラティブの配信について

 こんにちは。エンジニアのshogo4405です。ゲーム開発会社様より、iOSで画面収録またはミラティブで配信をすると、SEは鳴るが、BGMが消える場合がある。技術的な仕様について教えてくださいと連絡をいただく場合があります。

 本エントリーでは、ミラティブの配信中にゲームのBGMが鳴らない現象について、ゲームアプリ側での回避方法の例をご紹介したいと思います。

続きを読む

【iOS】Unity Framework とクラッシュ解析の取り組み

こんにちは、Mirrativ iOS エンジニアのちぎらです。クラッシュが発生して、その原因が分からないととてもかなしい気持ちになります。このブログでも以前から触れているように、Mirrativ のクライアントアプリではエモモなどの表示に Unity を使用しています。今回は、Unity の Framework とその内部で発生したクラッシュ解析の取り組みについて紹介をしたいと思います。

続きを読む

Androidアプリをリアーキテクチャした話

Mirrativ Androidエンジニアのmorizoooです。今回はAndroidアプリをFluxにリアーキテクチャした話をします。

背景

Mirrativは2015年春頃に開発が始まり、もうすぐリリースから6周年を迎えようとしています。以前はアーキテクチャについてのルールが決まっておらず、個々人が思うがままコードを書いており、開発しているメンバーでさえ処理の流れが追えなくなっているような状況でした。そこで、まずは既存のコードの改善を行いました。詳しくは以下の記事をご覧ください。 tech.mirrativ.stream

既存コードを改善した後に、開発の指針としてFluxアーキテクチャを選定しました。

Fluxを選定した理由

  • 状態がどこで更新されているのか分からなくなるという一番の課題が、Flux の導入によって解消できそうだった
  • iOSはFluxで実装していくという話が出ており知見をシェアできそうだった

Fluxとは

Facebookが提唱しているデータを単一方向で取り扱うアーキテクチャです。 詳細については以下をご覧ください。 facebook.github.io

Mirrativ内の実装

Androidにおける実装として、以下の資料を参考に実装を行いました。
speakerdeck.com

Mirrativ内でのFluxのフローとしては下記の図の流れで行っています。

f:id:morizo999:20210531003404p:plain
flow

具体例としてLive情報をAPIから取得して表示を行うサンプルを紹介します

続きを読む

【Unity】Mirrativのエモモ着せ替えの仕組みを解説する

こんにちは、Unityエンジニアの菅谷です。今回はMirrativのアバター(エモモ)の着せ替えについて解説します。Mirrativはゲーム配信のサービスではありますが、大きな特徴としてエモモがあります。エモモは2018年にリリースされ、ライブ配信のお供としてエモモが使われるようになりました。ユーザーは衣装やアクセサリーなどのエモモアイテムを組み合わせてエモモを着飾ることができます。エモモアイテムは主にイベントで追加されており、今では5000種類以上あります。

エモモの着せ替え

エモモアイテムには以下の種類があります。

  • 体型
    • 体格や身長
  • 顔のパーツ
    • 髪型や目、口など
  • 化粧や装飾
    • アイシャドウやフェイスペイントなど
  • 服装
    • 服や靴など
  • アクセサリーや置物

カテゴリーは全部で30種類以上あり、肌や目などのエモモアイテムは色が変えられます。ユーザーはエモモアイテムを組み合わせたり、色を変えたりして自分だけのエモモを自由に作ることができます。エモモアイテム以外にもモーションが設定でき、エモモが挨拶をしたり演出を伴った動きを行います。エモモで自分の個性を表現し、1万人いれば1万通りのエモモを表現できるよう日々クオリティアップを行っています。

着せ替えの仕組み

エモモには共通の素体となるモデルが1つだけ存在します。エモモのボーンなどの基礎構造を決め、エモモを動かすためのスクリプトの制御を行っています。この共通のモデルに対しそれぞれのパーツを入れ替えることでエモモを着せ替えます。それぞれのパーツの種類に応じて着せ替えの仕組みが異なり、大きく分けて5種類の仕組みがあります。それぞれの仕組みには長所・短所があるのでパーツの特性に応じて仕組みを使い分けています。

  1. ボーンスケールの変更
  2. ブレンドシェイプによる変形
  3. テクスチャの入れ替え
  4. メッシュとマテリアルの入れ替え
  5. プレハブの入れ替え

各仕組みについて順番に解説します。

1.ボーンスケールの変更

体型や身長など体全体に関わる項目はボーンのスケールを変更することで表現しています。ボーンスケールを変更することで例えば頭全体のサイズを小さくしたり、首だけ細くしたりと一括でサイズ変更ができます。ボーンスケールで表現するメリットとして、素体となるモデルを共通化しているため、体型や身長の違いでモデルを専用に作る必要がありません。あとから体型を追加する際も、各ボーンのサイズを表すプリセットファイルを用意するだけです。そのため見た目の調整や追加がしやすく、バリエーションを豊かにすることができます。ただし、ボーンのスケールが極端に大きい、または小さいと衣装との組み合わせによっては体に衣装がめり込むなど、見た目が破綻してしまうことがあります。デザイナーとともに表現を豊かにしながらも破綻しないようレギュレーションを決めたうえで衣装作成を行っています。また、以前は男性モデル専用の衣装や女性モデル専用の衣装などそれぞれの性別でしか着られない衣装もありましたが、現在は性別に問わず自由に衣装が着られるようにしています。

設定ファイルによりボーンスケールを変更しています。

2.ブレンドシェイプによる変形

ブレンドシェイプはボーンではなく、メッシュを変形させることによりモデルの表現の幅を広げる手法です。メッシュをどのように動かすかといった設定はモデル側であらかじめ決めておく必要があります。頭パーツや口パーツなどは素体のモデルにブレンドシェイプキーをもたせ、入れ替えるパーツによってブレンドシェイプ値を指定することで見た目を変えています。主にあご周辺の形や大きさを制御します。ブレンドシェイプによる変形はボーンスケールでの変更と同様にパーツごとに専用のモデルを作る必要がありません。

ブレンドシェイプによりあごや口周りの形を変えています。2つめの例はあご部分に丸みがあります。

3.テクスチャの入れ替え

鼻や唇といった顔のパーツやフェイスペイントやアイシャドウといった化粧はテクスチャの入れ替えで実現しています。頭パーツで使用しているシェーダーにアイテムと対応したテクスチャおよびブレンドカラーを渡し、フラグメントシェーダーで表示する色を決めています。

4.メッシュとマテリアルの入れ替え

目や衣装、靴下などはメッシュとマテリアルを素体のモデル側と入れ替えることで実現しています。また、各パーツの種類ごとに独自処理を追加しています。例えば目のパーツではブレンドシェイプの変換が行われます。素体側に存在するブレンドシェイプと作成した目のパーツに設定したブレンドシェイプの名前が異なっている場合があるので対応付けを行っています。

5.プレハブの入れ替え

アクセサリーや置物などのアイテムはプレハブから生成することで追加できるようにしています。アクセサリーはカテゴリーが一番多く、複数同時に組み合わせることもできます。プレハブから生成するため既存のエモモ構造に縛られず、一番自由な表現方法になっています。例えばアクセサリーごとにアニメーションを持たせたり、パーティクルにより演出を加えることができます。また、カテゴリーごとにプレハブ生成の基準となる位置をエモモの素体側に持たせています。それにより手持ちアイテムや翼アイテムを背中につけると、エモモの動きに合わせて装着したアクセサリーも追従します。置物アイテムはエモモの動きとは連動しない位置に生成することで固定位置に表示できるようにしています。

手持ちアイテムや翼アイテムはエモモの動きに追従します。

置物はエモモの動きとは独立しており固定の位置に表示されます。

エモモのクオリティを上げる手法

エモモを魅力的に見せるため、より華やかで自由度の高い表現とそのためのを様々な工夫が必要です。エモモは細部までよく動き、複数のアイテムを装着しても見た目が破綻せず整っていることが求められます。よく動かすための方法としての揺れものの追加や、複数のアイテムの組み合わせにおける細かな制御について紹介します。

揺れものの追加

揺れものとは髪やスカートなどのアイテムにおいて、エモモの動きと連動して物理運動するものを指します。髪や衣装には素体のボーン以外にも各アイテム専用でボーンを追加できるようにしています。メッシュとマテリアルの入れ替えの際に、アイテム側のボーンをもとの素体側にも一時的に追加することで、アイテムごとの揺れもの表現ができるようになっています。揺れものが多いほどリッチな表現ができますが、数が多いと負荷も高くなるため効果的な見せ方やボーン設定の仕方を日々工夫しています。特に髪型はディテールが細かいとボーン数も多くなりやすいので、大きく動く部分を優先するようにしています。また、揺れものはDynamicBoneを利用していますが、最近では軽量な揺れもの表現のアセットも登場しているため導入の検討を行っています。

髪と衣装がエモモの動きに合わせて華やかに動きます。

異なるアイテムの組み合わせ

トップスやボトムス、靴などの衣装ではアイテム間での組み合わせの制御を行います。例えばワンピースなどの全身がセットになった衣装を装着した際はトップスアイテムとボトムスアイテムを外すなど、排他制御を行っています。また、一緒に装着できるアイテム間で表示が重ならないよう対策を行っています。例えば髪はフード付きの衣装や帽子と干渉するため、髪にブレンドシェイプを設定しておき、衣装やアイテム側から指定できるようにしています。また、ヒールなどかかとの高い靴には高さを設定しておき、靴下側のブレンドシェイプを変更することで足のめり込みを防いでいます。

フードがなければ髪のブレンドシェイプは0です。

フードと組み合わせる際は髪のブレンドシェイプを変えます。実際の値は組み合わせるアイテム側で指定します。

日々エモモが魅力的に見えるように議論と研究をしながら改善を積み重ねています。

We are hiring!

エモモの魅力やクオリティは日々上昇しており、どこにも負けません。 ミラティブでは最高のアバター文化を一緒に実現するUnityエンジニアを募集しています。

www.mirrativ.co.jp

ミラティブ おすすめ配信の仕組みについて解説

こんにちは、エンジニアのタテノです。

ミラティブアプリを起動するとおすすめ配信の一覧が表示されます。 今回はこのおすすめ配信の仕組みについて解説しつつ、おすすめ配信の運用・改善を行う上でのポイントなどをまとめてみました。 システム面では機械学習などのトピックも含みますが、今回はサーバサイドの設計周り、特にパフォーマンスを考慮している部分にフォーカスしています。

f:id:hirota982:20210514105335p:plain
おすすめ配信一覧

おすすめ配信の役割

ユーザはすでにお気に入りの配信があれば、フォロータブから配信に入室しますが、ミラティブをはじめて初期の段階やお気に入りの配信がない場合は、そのほとんどはおすすめ配信から配信に入室します。おすすめ配信はユーザが配信に興味を持つきっかけとしてその役割を果たしており、とても重要です。

ミラティブでは常時、多数の配信が行われており一度に全ての配信を表示することはできません。たくさんある配信の中からそのユーザにおすすめの配信を表示しています。

おすすめ配信のポイントは大きく2点あります。1つ目はどのユーザにどの配信を表示するのか、すなわちレコメンデーションの精度、2つ目はその配信の魅力をいかに伝えるか、すなわち配信の表現力です。クラロワAPIを使ったイベントの記事では、おすすめ配信にプレイヤー情報を表示した取り組みを紹介しています。本記事では主に1つ目の話をしていきます。

tech.mirrativ.stream

おすすめ配信の処理概要

おすすめ配信を作成するにあたり、次のことを行っています。

f:id:hirota982:20210514091754p:plain
おすすめ配信作成の流れ

  1. ユーザ毎の視聴傾向や配信傾向を示す特徴量の算出
  2. 配信毎の特徴量の算出と、特徴量に基づく配信リストの作成
  3. 独自のレコメンドモデルを用いて、ユーザの特徴量と配信リストから、ユーザ毎のおすすめ配信リストを作成
  4. おすすめ配信を表示する際に、APIレスポンスとしてこれを返す

上記を実装するにあたりポイントとなるのは次の内容です。

  • ユーザ毎に異なるおすすめ配信を作成するには計算量が膨大となること
  • アプリトップで表示されるおすすめ配信はもっともよくアクセスされるAPIの一つであること

都度計算する内容が多くなるほどAPIの応答時間は伸びます。APIの応答時間が長いと、ユーザ体験が悪くなったりウェブサーバ全体が不安定になりやすくなります。

おすすめ配信は、次のように負荷を分散する、計算量を減らすことで、安定して動作しています。

  • デーモンプロセスによる非同期処理、事前計算
  • 中間データの生成とDB負荷を下げるためのメモリキャッシュ
  • 機械学習サーバなどで大規模解析することで一括でユーザの特徴量を算出

システムにおいて各種サーバがどのような処理を行っているか、下記にて説明します。

f:id:hirota982:20210517103321p:plain
システム構成図とそれぞれの処理内容

  1. ウェブサーバ、デーモンサーバは定常的にシステムログをログサーバに送っている
  2. 機械学習サーバは定期的にログサーバのシステムログを参照して、ユーザの特徴量を算出
  3. デーモンサーバは定期的に機械学習サーバからユーザの特徴量を取得し、DBに保存
  4. デーモンサーバは定期的に配信の特徴量を算出し、特徴量に基づく配信リストを作成、それらをDB、Cacheに保存
  5. ウェブサーバはミラティブアプリからのおすすめ配信API呼び出し時にDB、Cacheからおすすめ配信リストを生成しつつCacheに保存
  6. ウェブサーバはおすすめ配信リストをもとにDB、Cacheから必要な配信情報を取得し、応答を返す

おおよその処理内容は上記の通りで、ほかにもウェブサーバでのおすすめ配信レスポンス作成時には、配信に固有な値を配信ごとにキャッシュしてDB負荷を下げるなどしています。一方で、おすすめ配信に表示されている情報の中でもフォローしているかなど、ユーザ毎に変わるもの、変化しやすいものがあり、それらはキャッシュせずに都度計算しています。

また、先日 Envoy導入 の記事を公開しましたが、おすすめ配信のエンドポイントへのアクセスはEnvoy経由で専用クラスタにアクセスを振り分けており、負荷分散しています。

tech.mirrativ.stream

おすすめ配信の開発サイクル

おすすめ配信の開発サイクルは次の通りです。

  1. ユーザの行動観察や大規模なデータ分析から示唆を抽出する
  2. 抽出した示唆からユーザの好ましい行動を再現する施策を導き出す
  3. 施策を実装可能な仕様に落とし込み、実装する
  4. 結果を評価し、1. に戻る

おすすめ配信の開発はユーザに表示する配信を決定するパラメータを導き出す部分、すなわち 1, 4 の示唆の抽出と結果の評価の部分に時間と工数がかかります。レコメンデーションの精度向上は特にそうです。

たとえば、おすすめ配信の評価指標として単に配信にユーザが入室すればいいというKPIをおいてしまうと、施策を実施しても入室はするけどすぐ離脱してしまう、といったことがおきます。このような場合、長くミラティブを遊んでいるユーザに見られる相互に配信を行き来するような関係までは至らない、長期で定着しないなど、局所最適なものとなりがちです。

おすすめ配信はあくまでユーザが配信に入室するまでをサポートするものにすぎません。おすすめ配信の施策を実施する際には、ユーザが相互に配信を行き来するか、ギフトを贈り合う関係になるかといったミラティブ体験の深いファネルのKPIまで改善するかまで見る必要があります。

私は主に実装担当として関わっていますが、1, 4 が可能な限りスムーズに行えるよう可用性を維持しつつすばやく実装したり、適切に分析ログを仕込むといったところを意識して開発に取り組んでいます。

おすすめ配信の開発で注意するポイント

施策や評価について

おすすめ配信はミラティブ体験の出発点であり、配信視聴の体験の前段でその役割を果たすものです。そのためちょっとしたレコメンドロジックの修正でも、実施した施策の効果を適切に測定するのに数日程度の時間が必要な場合が多いです。施策を段階的に展開していく場合にはサービス全体に展開するのに1ヶ月かかることもあり、修正量の割に時間がかかります。

また、実感として少し変更したくらいでは何も変化が起きないことが多いです。深いKPIの改善につながった施策は、大規模なデータ分析や本質的な示唆に基づいたものでした。分析や示唆だしが不十分だったり、施策が局所最適なものだったりすると、施策を実施しても効果が出ないといったことが起きてしまいます。

要件管理、技術負債管理について

各種KPIはサービス全体の各断面を表すものであり、要件個々の良し悪しを表したものではなく複合的に捉える必要があります。そのため、時間の経過とともに現在のKPIを実現している要件がなんなのかが曖昧になっていきます。そんななか、新しい要件を追加していく際に、優先されたり捨てられたりする要件が出てくるのですが、それらを刷新するか改めて判断したい場合、新しい要件の評価と同じくらい工数がかかります。

新しい要件を追加で実装する際に、競合する古い要件を維持しつつ新しい要件を足していくのか、古い要件を効用が少ないと判断してばっさり捨て去って新しい要件を足すのか、判断が難しい場合があります。古い要件を維持するか、新しい要件をどの程度のスピードで展開していくかで全体の開発期間がかなり伸び縮みします。

古い要件を捨てるとKPIが大きく変化する懸念があるので、それらを維持しつつ新しい要件を足すのが正しそうなものの、それだとユーザへの影響が小さく意味のある効果となるまで至らないといったことが起きます。新しい要件を試した結果、優位な結果が得られることがわかったら、古い要件はばっさり捨て去ってしまうことで予定期間内に大きくKPIを改善させられたといったこともありました。

おわりに

おすすめ配信の開発においては、データ解析やユーザ観察からレコメンドロジックを考え、それを実装することでKPIが変化していくとき、作り手としてとてもやりがいを感じます。効果がなかったり時間がかかったりしてなかなか結果がでないときもある分、結果が出たときはなおさら嬉しいですね。今回、詳細に紹介していませんが、おすすめ配信に機械学習で算出した特徴量を利用するにあたり分析チームが様々な取り組みを行っています。機会があればそのような内容の記事をアップロードするかもしれません。

We are hiring!

おすすめ配信は、ミラティブでユーザが配信に興味を持つきっかけとして大きな役割を果たしており、今後も継続しておすすめ配信の改善を行っていきます。興味を持っていただけた方は、ぜひ下の採用サイトもチェックしてみてください。

  • 大規模なデータ解析やユーザ観察を通してユーザに楽しんでもらえる機能を提供したい!
  • パフォーマンスを考慮して要件をシステムに反映するのが得意!
  • 機械学習を使って新しい価値を創造し、実用例を世に出していきたい!

といった方、ご応募お待ちしております!

www.mirrativ.co.jp

speakerdeck.com

【インフラ】 Envoy の導入と xDS API で Consul 連携やってみた話

f:id:octu0:20210427120345p:plain こんにちは ハタ です 今回はインフラ/基盤開発で導入している Envoy について紹介したいなと思っています

現在ミラティブでは Go移行 を進めているところで、
既存のWebアプリケーション(Perl で実装されてます)と、新たに Go で実装された Web アプリケーションをシームレスに導入/切り替えするために Envoy の導入を行いました

Envoy xDSとConsul によるインスタンス管理

NGINXHAProxy または Apache HTTP Server など使い慣れた && ノウハウもあるミドルウェアではなくなぜ Envoy を選定したかというと、
なんといっても魅力的なのが xDS と呼ばれる Discovery Service 群があることで柔軟に既存コンポーネントと連携を取りやすいことやAPI操作によるコントロールの柔軟性が高いことが決め手となっています
また後述しますが go-control-plane による API連携の実装が容易だったことも今後バックエンドのミドルウェアが変更した際にも連携しやすくなるというのも決め手の一つです

ミラティブでは従来より Consul による 仮想マシンインスタンスの管理およびクラスタの管理を行っており ほぼ全てのインフラ監視やアプリケーションの連携は consul 経由にて行われています

f:id:octu0:20210426211019p:plain
参加するconsulのクラスタを分けてマシンの管理をしてます

HAProxyやNGINXなどでは 設定ファイルをベースに構成を管理するため、例えば consul-template を使うことで
conuslのクラスタに応じた 設定ファイルの生成と動的なreloadを実装することができるのですが
ミラティブのオートスケーリングなどのワークフローと連携させるにはもっと細かく制御可能な方法を導入する必要がありました

Envoy xDS は下記のような種類があり、既存の consul クラスタとのマッピングを行うことで、既存機能に大きな変更することなく導入することができました

  • CDS (Cluster Discovery Service)
    • consul service のクラスタ群を定義
    • クラスタに流れるロードバランサーとヘルスチェックの定義
  • EDS (Endpoint Discovery Service)
    • エンドポイントの定義としてインスタンスのアドレス(IP/Port)の定義
    • リージョン/ゾーンの定義(Zone aware routingに使いたいため)
    • ステータスが active (内部チェックが完了したもの) になったインスタンスの定義 (LbEndpointのHealthStatusとの連携のため)
  • RDS (Route Discovery Service)
    • リクエストとクラスタのマッピング(ドメイン名とPath Prefixに応じて振り分け先クラスタ(consul service)を定義してます)
    • リクエストの振り分けは重み付けを定義(WeightedClustersを使っています)

他にも LDS だったり ALS の定義もあります

実際どのように Consul を連携を行っているのか

Envoy は 静的な設定ファイル ベースで設定することも可能なのですが v3 API を使って動的に設定を行うようにしています

Envoy 自体の冗長化も行うため Consul KV に構成の設定(後述)を記録しておき、xDS 連携するサーバが KV の内容を読み出して envoy に動的に設定するようにしています
具体的には go-control-plane による実装を行った xds-server を sidecar として、 Envoy と共に冗長化を行っています(consul kv自体は consul cluster による冗長化が行われています)

f:id:octu0:20210426191631p:plain
構成情報はConsul KVに永続化してます

ミラティブでは、サービスイン可能なインスタンス群のクラスタやインスタンスに不具合が起きた場合はエラー用のクラスタなどがあり、適切なクラスタに参加するインスタンスをEDSに設定しリクエストの振り分け対象にしています

EDS 以外のリクエスト振り分けの設定値やListenerの定義などは、次のような yaml の形式になっていてこれを consul kv に登録し、xds-server がconsul kvから読み出しながらCDSやRDS を更新しています

listener:
  listen:
    protocol: "tcp"
    address: "0.0.0.0"
    port: 8000
  ... # 他にもtimeout定義とか
route:
  domain: ["mirrativ.com", "www.mirrativ.com"]
  host-header: "www.mirrativ.com"
  cluster:
    web-api-perl:
      prefix: ["/api"]
      weight: 90
    web-api-go:
      prefix: ["/api"]
      weight: 10
    web-asset:
      prefix: ["/asset"]
      weight: 100
cluster:
  loadbalancer:
    web-api-perl:
      lb-policy: "round-robin"
      discovery: "eds"
      health-check:
        type: "http"
        host: "www.mirrativ.com"
        value: "/health_check"
        expected: ["200"]
        timeout: 3
        interval: 1
        healthy: 3
        unhealthy: 4
    web-api-go:
      lb-policy: "round-robin"
      discovery: "eds"
      health-check:
        ... # ヘルスチェックの定義
    web-asset:
      lb-policy: "round-robin"
      discovery: "eds"
      health-check:
        ... # ヘルスチェックの定義
endpoint:
  eds:
    web-api-perl:
      protocol: "tcp"
      port: 80
      balancing-policy: "locality"
    web-api-go:
      protocol: "tcp"
      port: 8080
      balancing-policy: "locality"
    web-asset:
      protocol: "tcp"
      port: 8080
      balancing-policy: "locality"

Yaml ファイルの定義なので分かりづらいのですが、CDSおよびEDSとRDSの連携は 既存の consul クラスタ(Service名)でマッピングするようにして(web-api-perlやweb-api-go という ServiceName のクラスタが存在します)、なるべく複雑にならないように Consul で使用しているものをそのまま利用するように定義にしています

このあたりの xDS 連携方法は、何か特別なミドルウェアを入れる必要はなく、既存のインフラ構成に合わせて実装しやすいのも envoy の良いところだと思います

ヘルスチェックとアクセスログ

envoy と sidecar で動作する xds-server は CLB などの LoadBalancer 配下に配置するようにしているのですが、
administration api の /ready を直接ヘルスチェックに使うのではなく、xds-server 側でヘルスチェックのエンドポイントを用意しています

これは何かしらの理由で envoy を CLB から外したい場合(多くの場合はインスタンスそのもののメンテナンスや特定ゾーンの抜き差しになると思います)や安全弁として特定の状態になった場合に、安全にリクエストを停止させる方法として用意しているエンドポイントになります

ミラティブのほぼ全てのアプリケーションレイヤでは、 メンテナンスファイル という形式で、特定のパスにファイルが存在する場合はアプリケーションを安全に停止させるように実装していて、今回の xds-server にも同じように実装しています

例えば Go の http server で実装すると下記のような実装です

http.HandleFunc("/api/foo", handleAPIFoo) // 各種 API
http.HandleFunc("/api/bar", handleAPIBar) //
...
// health_check エンドポイント
http.HandleFunc("/_my_health_check", func(w http.ResponseWriter, r *http.Request){
  if _, err := os.Stat("/opt/mirrativ/my-app/maintenance"); err == nil { // ファイルが存在する場合
    w.WriteHeader(599)              // status:200 以外を返す
    w.Write([]byte(`MAINTENANCE`))  // もちろんLBのhealth checkは 200-399 を healthy として定義する必要があります(L7の場合)
    return
  }

  // 通常時は status:200
  w.WriteHeader(http.StatusOK)
  w.Write([]byte(`OK`))
})

xds-server のヘルスチェックも同じように実装していて、メンテナンスファイルがある場合または envoy の cluster 構成が正常ではない場合にリクエストを受けないように status:200 以外を返すように実装し、CLBのヘルスチェック先に指定しています

また、アクセスログについても envoy が直接出力するのではなく、grpc によって xds-server に転送されるようにしています
これは今後リアルタイムなログ集計などを行えるるようにしたり、アクセスログの出力先を柔軟に切り替えれるようにするためです

ALS の実装は xDS を指定する時と同じように grpc server を指定することでアクセスログを受け取ることができます
例えば次のように定義して実装します

# bootstrap.yaml に追加
static_resources:
  clusters:
  # 通常のxdsのgrpc
  - name: xds_cluster
    ...
    type: STATIC
    load_assignment:
      cluster_name: xds_cluster
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address: { protocol: TCP, address: 127.0.0.1, port_value: 9000 }
  # alsのgrpc
  - name: als_cluster
      ...
      type: STATIC
      load_assignment:
        cluster_name: als_cluster
        endpoints:
        - lb_endpoints:
          - endpoint:
              address:
                socket_address: {protocol: TCP, address: 127.0.0.1, port_value: 9001 }

grpc.Server の定義(抜粋)

import (
  acclogv3 "github.com/envoyproxy/go-control-plane/envoy/service/accesslog/v3"
  ...
)

var (
  _ acclogv3.AccessLogServiceServer = (*myHandler)(nil)
)

type myHandler struct {}
func (h *myHandler) StreamAccessLogs(stream alsv3.AccessLogService_StreamAccessLogsServer){
  msg, err := stream.Recv()
  if err != nil {
    panic(err.Error())
  }
  for _, e := range msg.GetHttpLogs().GetLogEntry() {
    ... // いい感じにログを保存
  }
}

func main() {
  alsHandler := &myHandler{}

  svr := grpc.NewServer()
  acclogv3.RegisterAccessLogServiceServer(svr, alsHandler)
  
  listener, err := net.Listen("tcp", "[0.0.0.0]:9001")
  if err != nil {
    panic(err.Error())
  }
  svr.Serve(listener)
}

LDS の HttpConnectionManager の定義(抜粋)

import(
  "github.com/golang/protobuf/ptypes"

  accesslogv3 "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3"
  alsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/grpc/v3"
  corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
  httpconnmgrv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
  wellknownv3 "github.com/envoyproxy/go-control-plane/pkg/wellknown"
  ...
)

func createHttpConnectionManager() *httpconnmgrv3.HttpConnectionManager {
  configDef := &alsv3.HttpGrpcAccessLogConfig{
    CommonConfig: &alsv3.CommonGrpcAccessLogConfig{
      ...
      GrpcService: &corev3.GrpcService{
        TargetSpecifier: &corev3.GrpcService_EnvoyGrpc_{
          EnvoyGrpc: &corev3.GrpcService_EnvoyGrpc{ClusterName: "als_cluster"}
        },
      },
    },
  }

  config, err := ptypes.MarshalAny(configDef)
  if err != nil {
    panic(err.Error())
  }

  return &httpconnmgrv3.HttpConnectionManager{
    ....
    AccessLog: []*accesslogv3.AccessLog{
      &accesslogv3.AccessLog{
        Name: wellknownv3.HTTPGRPCAccessLog,
        ConfigType: &accesslogv3.AccessLog_TypedConfig{
          TypedConfig: config,
        },
      },
    }
    ....
  }
}

ここまでの実装例として YAML 管理となってしまいますが実装例を用意してみました

github.com

v3 xDSの実装で、yamlで読み込んでいる箇所が consul のインスタンスに置き換わるとイメージがしやすいかもしれません

Envoy の導入に際して

f:id:octu0:20210426185233p:plain
既存のCLB構成

ミラティブでは既存のオートスケーリングが動作しており、既存のオートスケーリングではCLBを直接制御してサービスイン・アウトの実装がされていたり、カナリアリリースなどが実装されているため
既存の動作を保ちつつEnvoyを共存させるため、リリース順序を作りつつ導入に至りました

release.v1 CLB backend の perl 構成と同居

f:id:octu0:20210426185451p:plain
最初のreleaseではenvoyとperlの既存環境がCLB内で混在するところからスタート

LB の backend group は既存のリクエストを直接受けるものと、 envoy 経由で proxy させるものを共存させました
オートスケーリングは従来どおり CLB の backend group に直接追加する形のままとしてます
envoyからのリクエストの振り分けが正しく consul のクラスタ経由で連携できるようにしました

release.v2 CLB backend を envoy のみにする

f:id:octu0:20210426190111p:plain
CLB内で混在しているよりもだいぶすっきりしました

既存のオートスケーリングで CLB の backend group に直接追加せずに、 consul のクラスタ経由で行うように変更 ここで既存機能の一部修正が行われました

release.v3 envoy loadbalancingy/routing でアクセスの振り分けを行う

f:id:octu0:20210426205531p:plain
既存のperlのAPIの中から一部だけ別クラスタとして定義し、問題なく捌けるかを試すための構成

この段階で初めて envoy によるリクエストの振り分け(load balancing/routing)を行うようにしました
既存の perl 構成のものから、特定のエンドポイントだけを受け付ける クラスタを構成しPathベース負荷分散を行うようにしました

その後(Next Step)

f:id:octu0:20210426194141p:plain
consul 連携イメージ

その後いくつかのエンドポイントの切り出しを行いました

  • 例えば /asset/* だけを切り出した web-asset クラスタを構成し画像などを配信するだけの軽量なサーバに切り替えたり
  • 例えば 比較的アクセス数の多い /api/foobar のエンドポイントだけを切り出し、クラスタ内の構成は同じまま、特定のエンドポイントだけの負荷分散を構成したりしました
  • 他にも複数のQA環境の切り替えなども envoy と xds-server によって切り替えを行っています

また現在では、当初の目的であったGoのエンドポイントのリリースも行われており、徐々に Perl から Go の実装に切り替わっていっています

今回Envoyの導入によって全ての上りのトラフィックが Envoy経由でコントロールできるようになったため、次なるステップとして下記のようなものを実施できたらなと考えています(できるかどうかは別として)

  • 動的WeightedRoundRobin
    • 現在は同一Pathの場合にWeightedClusterによる振り分けを行っていますが、新規リリースの場合に小さなWeightからスタートさせリクエストを反映できたらいいなとか
    • カナリアリリースや暖気にも応用が効きそうです
  • バージョニングルーティング
    • IngressだけでなくEgressのトラフィックコントロールを行うことで同一のリリースバージョンは同一のリソースを参照させたりできないかなとか
    • 後方互換を伴うリクエストを受けたりするときに役立ちそう
  • GCPのメンテナンスイベントとの連携
    • ライブマイグレーション中は一時的にインスタンスのパフォーマンス劣化することが見えているため、振り分け対象から外したりできないかなとか
    • Outlierで細かく制御したりとか

この他にもMySQL Filter の活用もできたりしないかなと考えたりしています
RBAC(Role Based Access Control)が使えるので、フェイルオーバ時の挙動制御に使えたりしないかなとか、今後調べつつトライしてみたいです

We are hiring!

Goによる大規模サービスの開発が進む中で、インフラ基盤開発は常に次の一手を考えつつ新しい基盤となる技術を作っていっています 一緒に基盤開発をしてくれるエンジニアを募集しています!

www.mirrativ.co.jp

ミラティブサーバチームで行っている障害振り返りを紹介します!!

こんにちは、サーバエンジニアの夏(なつ)です。今回はミラティブのサーバチームで行っている障害振り返りを紹介したいと思います。

ミラティブのサーバチームではサービスに障害が発生した場合、その後、担当者を決めて障害の振り返りのたたき台を作成し、チーム内で振り返りを行って、今後の改善に活かす努力を続けています。

今回はその振り返りの目的やフォーマット・注意点についてお話したいと思います。

目的

システムを運用していれば障害はつきものです。ましてや改善を続けるならば、その代償として不確実性が障害として表面化し、放置していけば徐々にユーザの信頼を失っていくことになります。かといって、障害の防止にコストをかければかけるほど、費用対効果は見合わなくなり、障害を絶対に起こしてはならないという心理的圧力はメンバーのメンタルを擦り減らしていきます。そのため障害の振り返りでは障害の詳細や原因をチームメンバーと共有しつつ、本来価値提供したいことに対して、障害への対応コストを妥当な範囲に収めるためにはどうしたらいいかが議論されます。

f:id:hottestseason:20210402035914p:plain

また、チームで振り返ることにより、個人のミスをチームの経験へと昇華することができます。とくに障害の引き金を引いてしまったのが自分であれば、不注意や確認不足を悔やみ冷静に振り返り辛いのは当然ですし、現実から目を背けてしまいたくなる場合も多々あります。チームに共有することで、異なるバックグラウンドをもつメンバーから建設的なアドバイスを聞けるかもしれません。組織全体を1つのシステム、個人のミスもシステム内で一定の頻度で発生する事象として捉えることで、障害振り返りを通して組織の強化を続ける努力をしましょう。

振り返りフォーマット

ミラティブのサーバチームでは障害の振り返りとして「影響」「背景」「発生・復旧フロー」「原因」「被害の最小化・再発防止策」をまとめてConfluenceに蓄積しています。

どの情報も他の人が再現確認できるよう、エビデンスとセットで書くように意識しています。 (Slackのリンク、調査時のコマンドや出力結果、監視画面のリンクやスクリーンショットなど)

影響

発生日時や影響を受けた機能・対象者を書いておきます。

背景

サーバチームを対象に、障害を理解する上で必要なドメイン知識を共有します。エンジニア以外にも伝わるように書く必要はありませんが、他事業部や歴史的経緯を知らないエンジニアにもこの後の章を理解できるように説明する必要があります。

発生・復旧フロー

何がどう作用して障害を引き起こし、その後どう検知され、どう作業して障害が復旧したのかを時系列に沿って書きます。 障害を検知したチャネルを明確にすることで、そのチャネルの透明性を評価できます。

  • 前兆が見つかってから、調査開始されるまでのチャネルに不要な伝言ゲームが起きていないだろうか
  • 有用そうなチャネル上のノイズが多く、無意識にミュートされていないだろうか
  • 今後そのチャネルの優先順位を上げるべきだろうか

また、復旧フローを時系列に沿って書くことで、どこの対応に時間がかかったかが把握でき、支配的かを洗い出すことで復旧時間短縮に向けて優先順位を決めることができます。

原因

どういう因果関係で障害が発生したのかを詳細に書きます。

被害の最小化・再発防止策

発生・復旧フローや原因をもとに、今後組織として再発防止できるような仕組み化や、早期発見や原因の解像度を向上できるような仕組みを検討します。

障害の芽はできるだけ初期段階で潰せれば、それだけ復旧コストが安く済みますので、以下のようにフェーズごとに何かできないかを考える必要があります。

  • 開発時
    • 危険なコードは型やアーキテクチャレベルで書けないようにする
    • CI上の自動テスト時に検知する
    • コードレビューで指摘する
    • オンボーディングの資料に盛り込む
  • 機能検証時
    • 過去に何度か障害が発生したケースをQAチームに共有し、検証項目に盛り込んでもらう
  • 本番リリース後
    • Canaryリリース時に気付ける体制にする
    • エラーログを充実させる
      • ユーザからのお問い合わせで気付くよりも、エラーログで早めに気付ける方が、影響範囲や復旧コストが少なくて済む
    • リリースしてから数年後にデータ量の増加に伴って、計算量が爆発する場合もあるので、レスポンス速度の劣化やslow query、explainの実行結果などを監視する

逆に以下のようなことが再発防止策に入っていないか注意が必要です。

  • 場当たり的な対応になっていないか
    • HTTP 5xxの監視に引っかかったからといって、エラーを握りつぶすような解法になっていないか
      • エラーを握りつぶしたからといってユーザ影響が解消するとは限らないし、むしろ不具合の検知が遅くなる可能性がある
  • 開発速度を過剰に制限していないか
    • 罰則的な対応になっていないか
    • 本来価値提供したいことに影響が出ていないか
  • 細心の注意で確認を入念に行うなどのような対応になっていないか
    • 漠然としており、障害が再発した場合でも、確認が足りていませんでしたと同じ振り返りにしかならない
    • 確認やダブルチェックが必要ならば、手順書やチェック項目まで落とし込む必要がある
      • 確認箇所が曖昧だと、どこかの現場猫よろしく「後の人がちゃんとみてくれてるだろうからヨシ!」「前の人がレビューをとおしてるんだから絶対ヨシ!」みたいなことになりかねない
      • 自動化できるのがベストだが、工数や優先順位の都合上すべてが自動化できるとは限らない
  • 重厚長大な再発防止になっていないか
    • 解決できるスコープに対して労力を多大にかけていないか
    • 一見良さそうに見えるが、銀の弾丸などないので、導入した場合でも別の問題が出る可能性がある
    • また、実際にサービスに組み込んで、既存のシステムを刷新するためには相当なコストがかかる
    • 但し、重厚長大な再発防止を否定しているわけではなく、短期で結果が出ないし、多大なコストを払うかもしれないが、中長期的な戦略に立った時に改善に繋がるのであれば、コスパを意識した上で導入するのはあり

再発防止策の具体例

ミラティブサーバチームがここ1年半ぐらいの障害振り返りを通して行ってきた代表的な再発防止策としては以下のようなものが挙げられます。
(詳細に関してはまたいつかテックブログに書いたり書かなかったりする予定です)

  • Partition Pruningの効いていないクエリの洗い出し・修正
  • DDLのマイグレーションツール上のノイズを減らし、人間が確認すべき項目に集中できるように改善
  • 特定の時刻に発火する処理は、ゆらぎを自動で付与して負荷が重ならないように改善
  • Pull Requestのテンプレートに新規で発行されるクエリのEXPLAINを記入する項目を追加
  • Webの負荷がスパイクしないように、全体へのPush通知をゆっくり送る修正を追加
  • 開発環境で時刻を変更できる機能を追加
  • INSERT IGNOREを非推奨に
  • チーム内のレビュー方針に「その機能で障害があった時に自分で調査・復旧できるか」を追加
  • OpenAPIのadditionalPropertiesを必須に
  • アプリケーションサーバ側でのSlow Queryの通知
  • ユーザがいる環境での検証作業を最小限に
  • アーキテクチャの再設計・Goへの移行
  • マスタデータの運用フロー改善

特に最後3つの「ユーザがいる環境での検証作業を最小限に」「アーキテクチャの再設計・Goへの移行」「マスタデータの運用フロー改善」はミラティブサーバチームにおける中長期での最重要技術戦略だと位置づけており、障害振り返りで培った経験を元に、現在もプロジェクト化してチーム全体で取り組み続けております。

tech.mirrativ.stream

おわりに

新しい挑戦を続けている以上、障害をゼロにすることはできません。また、ときにはチーム内で話しあっても、上手い解決策が見つからない場合も多々あると思います。しかし、個人のミスとして片付けるのではなく、チームの経験として獲得し、向き合い続けることができれば、障害を通して反脆い組織へと成長できると信じています。

We are hiring!

ミラティブでは新機能によるユーザへの価値提供と同じくらい基盤の安定化に向き合えるエンジニアを募集しています。

  • Goで大規模サービスの開発をしたい
  • サーバーシステムの基盤の整備をしたい
  • ゲーム×ライブ配信サービスの開発をしたい

www.mirrativ.co.jp

speakerdeck.com

Mirrativのコラボ通話&配信のクライアント/サーバー間の仕組みを徹底解説

こんにちは。サーバーエンジニアのユンです。

今回Mirrativは「コラボ配信」という機能を開発しました。他の配信者(最大3人)と音声でつながり、視聴者とも同時にコミュニケーションを楽しむことができる機能です。

コラボ配信機能の紹介記事 | Mirrativ公式ブログ

f:id:canto87:20210225102438p:plain
コラボ配信機能

現在のところ、一部の配信者さんにご利用いただき、さらなる機能改善を進めています。

もともと、Mirrativでは「コラボ通話」という配信者と視聴者が通話する機能があります。コラボ通話はWebRTCの機能を使っていて、iOS/Android側はlibwebrtcにパッチを当てたものを利用しています。またSTUN/TURNはTwilioを利用しています。

コラボ通話をしているときユーザーは配信者、コラボ通話者、視聴者の3種類に区別され、配信者とコラボ通話者はWebRTCで音声が双方向になり、お互いの声をWebRTCを通して送受信しています。コラボ通話者が2人以上の場合メッシュ状にP2P接続させています。視聴者はRTMPで配信者の画面映像、配信者のマイク入力により合成されたゲーム音&配信者の声&コラボ通話者の声を受信して視聴することができます。

f:id:canto87:20210319110527p:plain
コラボ通話での配信者とコラボ通話者と視聴者

今回はそのコラボ通話機能と配信機能を組み合わせてコラボ配信の機能を作りました。 通常の配信では配信者と視聴者が1対Nの関係性を持ち、映像と音声は配信者から視聴者への一方通行になっており、視聴者はコメントを通じてコミュニケーションを行っています。コラボ配信は自分の配信をしながら、他の配信者と(最大3人)音声でつながり、その配信者&視聴をしている視聴者とコミュニケーションができます。 配信者同士&その視聴者とコミュニケーションを取ることで配信中のコミュニケーションをもっと楽しむ、配信内容によっては一緒にゲームをプレイすることも可能になります。

f:id:canto87:20210319110750p:plain
通常配信とコラボ配信での配信者とコラボ配信者と視聴者

この記事ではそのコラボ通話とコラボ配信の技術の中でクライアント/サーバー間の仕組み、そしてコラボ配信の実装時の工夫したところを中心にお話します。

ここから使う用語の定義

  • 配信者 : 配信を行っているユーザー
  • コラボ通話者 : 配信者とコラボ通話でつながっているユーザー
  • コラボ配信者 : 配信者とコラボ配信でつながっているユーザー
  • API Server : WebAPIを提供するサーバー
  • Pub-Sub Server: リアルタイムメッセージ配信を提供するサーバー

コラボ通話がつながるまでのクライアント/サーバー間の仕組み

f:id:canto87:20210321160953p:plain
視聴者のコラボ通話申請からコラボ通話接続まで

コラボを開始する際にコラボ通話者は配信者に対してAPI Serverにコラボ申請を送ります。API Server側はコラボ申請を受け取った時点で配信者↔コラボ通話者の関係性とコラボ状況(コラボ申請中)とコラボ通話用のPub-Sub Server情報(Pub-Sub Server接続用のキー)を作成してコラボ通話用のデータベースに保存します。

保存後API ServerではPub-Sub Serverを通してコラボ申請が届いたことを配信者に伝えて、配信者のアプリケーション側ではWebRTCClientの作成と承認したことをAPI Serverに送ります。API Serverでは承認したことをデータベースのコラボ通話用テーブル上にあるデータからコラボ状況を更新(承認)して、Pub-Sub Serverを通してコラボ通話者に承認情報を送り、Pub-Subからの承認情報を受信したコラボ通話者は同じくWebRTCClientを作成します。

また、配信者↔コラボ通話者でのWebRTCによるP2Pの接続が確立されたら、配信者側はAPI Serverにコラボ接続が完了してコラボが開始したことを送信します。

コラボ通話者を追加する場合

コラボ通話では配信者を含めて最大4人の視聴者までつなげることができます。それぞれがWebRTCで接続され、音声データのやりとりをしています。 それに備えてサーバー側は「コラボ通話がつながるまでのクライアント/サーバー間の仕組み」を行った後、既存につながってるコラボ通話者ともコラボをWebRTCで接続する処理を追加で行います。

f:id:canto87:20210321161652p:plain
コラボ通話者の追加

コラボ通話者Aがもともと配信者とコラボ通話を行っていて、後からコラボ通話者Bがそこに加わるようなことになり、コラボ通話者Bは上のフローと同様に配信者とコラボを接続した後、その時点でコラボに接続しているユーザーの一覧(この場合はコラボ通話者Aのみ)をサーバーから取得し、コラボ通話者Aとの接続を行います。

コラボ配信がつながるまでのクライアント/サーバー間の仕組み

f:id:canto87:20210321162209p:plain
視聴者のコラボ配信申請からコラボ配信接続まで

コラボ配信はコラボ通話機能と配信機能を組み合わせて作りました。その背景もあり、ユーザーがコラボ配信を申請するタイミングでは配信前の視聴者の可能性があるので、その場合は既存の配信処理もコラボ配信がつながるまで同時に進行する必要と、既存のコラボ通話と配信を区別する必要もありました。

それで、今回は視聴者がAPI Serverにコラボ申請を送る際にサーバー側でその申請がコラボ通話かコラボ配信かを区別するコラボタイプをパラメータに追加して、コラボタイプのパラメータがコラボ配信の場合に配信者↔コラボ通話者の関係性とコラボ状況(コラボ申請中)と情報(Pub-Subのキー)を作成してコラボ通話用に作られたデータベースに保存するとともに、配信を開始するための配信情報を作成およびデータベースに保存するようにしました。 また、レスポンスにはコラボ配信時に使用する配信情報を渡してWebRTCの接続処理までに配信開始処理を行うようにしています。

配信者とコラボ配信者のWebRTC接続の切断がしばしば発生

コラボ配信の実装後、動作確認時に配信者とコラボ配信者の間でWebRTCの接続が切断され、配信は続いているが、コラボ配信をしている配信者間の声が聞こえない問題が発生しました。 原因はコラボ配信を行ってる際にクラッシュなどでアプリケーションが異常終了をしたり通信環境の問題などでWebRTCの接続がなくなるが、データベース上ではコラボ開始状態から変化がないことが問題でした。

コラボ通話の場合、通話者は切断が起きた際にアプリケーション側でコラボ通話を終了させ、ユーザーがもう一回コラボ通話を申請することで再度開始するようにしていました。しかし、コラボ配信の場合はコラボ配信者側に視聴者が入っていることもあるので、コラボ通話のようにもう一回コラボ配信を行うように案内をすることは難しい状況でした。そのため、コラボ配信を終了させずにWebRTCの再接続をするフローを追加することにしました。 データベース上ではコラボ配信が開始している状態になっていたので、該当のユーザーのリストをクライアントに送り、そのリストの上にいるユーザーの中でWebRTCでの接続がされてないユーザーに対してWebRTCでの再接続処理&クライアント/サーバー間での再接続処理を行うようにしました。

f:id:canto87:20210321163204p:plain
コラボ配信者からの再接続
再接続処理の場合にはコラボ配信申請にパラメータ is_reconnect=1 が付与されており、これによってコラボ申請が再接続かを確認するようにしました。 また、コラボ配信者のアプリケーションが異常終了したのちに再接続処理を行う場合、コラボ用Pub-Sub Serverの接続を再度行う必要がありましたので、コラボ配信申請のレスポンスにコラボ配信用Pub-Sub Serverの情報ももう一回渡して接続を行うようにしました。

おわりに

今回はMirrativのコラボ通話とコラボ配信機能のクライアント/サーバー間の仕組みについてお話しました。

特にコラボ配信については、新しい体験を与える大きな規模の機能を0から実装するのではなく既存のユーザーが日頃よく使ってる機能の組み合わせで開発を行うことで、約2ヶ月で開発終了することができ、新しい体験をユーザーに提供することができました。

しかしながら、今のコラボ配信やコラボ通話をメッシュ状にP2P接続させる仕組みだと端末の負荷的にこれ以上参加者を増やせないため、さらなる品質の改善に向けてSFUの導入なども検討しています。

We are hiring!

このように新しい体験をユーザー提供し続けるミラティブではサーバーエンジニアを募集中です!

  • ゲーム×ライブ配信サービスの開発をしたい
  • サーバーシステムの基盤の整備をしたい といった方のご応募をお待ちしております!

www.mirrativ.co.jp

【Unity】MirrativのギフトとUnityを活用したリッチな演出の紹介

こんにちは、Unityエンジニアの菅谷です。 今回はMirrativのギフト機能について紹介します。特にMirrativ内のアバター(エモモ)を利用したMirrativ特有のギフトについて解説します。

Mirrativのギフト

ギフトは配信を盛り上げるための機能の一つで、視聴者がギフトを贈ることで配信者の画面に演出が表示されます。 代表的なギフト演出を仕組みごとに紹介します。 スタンプギフトやシンプルなアニメーションギフトでは、Lottieライブラリ(iOS/Android)を利用してギフトの画像をアニメーションさせています。 LottieはAfter Effectsから出力したJsonファイルをそのまま利用することができます。

スタンプギフトは画面上にアイコンやアニメーションを表示します。 ギフトは30種類以上あり、様々なシチュエーションに合わせて贈られています。

また、エモモと連動したギフトではUnityを活用しており、配信者のエモモにアイテムを渡すギフトや季節限定のギフトなどがあります。 Lottieでのアニメーションと比べると、3Dでの演出が可能になります。 演出のために専用のモーションや3Dモデルを作成する必要があり、手間はかかりますが表現に大きな幅を持たせることができます。

ギフトにより贈られたアイテムを使ってエモモがリアクションを返します。

季節限定ギフトではギフトを贈った視聴者のエモモが登場し配信者と一緒になって配信を盛り上げます。

今回はUnityの機能をフルに活用した季節限定ギフトの作り方について紹介します。

エモモの着せ替えとUnityでの演出の作り方

エモモは顔や衣装、帽子などのアイテムを組み合わせることで自分だけのアバターを作ることができます。 アイテムはイベントで毎週追加され、すでに5000種類以上あるため組み合わせは無限大です。 季節限定ギフトでは視聴者も自身のエモモをギフトの演出として配信に登場させることができます。 そのため配信者と衣装を合わせたり、演出のシチュエーションによって衣装を変えたりしてエモモを通じたコミュニケーションが行われています。

季節限定ギフトは独自のエモモの着せ替えの仕組みに加えて、UnityのTimelineとCinemachineを活用することで実現しています。 Timelineはオブジェクトのオンオフやアニメーションの再生をシーケンス形式で管理することができるため一連のカットシーンの利用に向いています。季節限定ギフトの演出もカットシーンなためTimelineとCinemachineを採用しました。

Timelineでのシーケンス制御

Timelineでは主に以下を制御しています。

  • エモモのアニメーション
  • エモモ以外に演出中に登場する小物、パーティクル、背景
  • 小物に紐付けたAnimator用のTrigger
  • カメラの切り替えとブレンディング

Timelineは内容が変わらないアニメーションを再生することに長けていますが、登場するエモモは衣装やアクセサリーなどユーザーによって様々です。そのためエモモは既存の衣装着せ替えの仕組みを使って表示し、Timelineのトラックに後から紐付けることで着せ替え機能と専用のアニメーションとを両立させています。

具体的な紐付けの処理は以下となっています。

  1. PlayableDirectorのトラック名を元に名前検索によりエモモに対応するトラックを取得する。
  2. PlayableDirectorのSetGenericBindingによりエモモのAnimatorとトラックのAnimatorを紐付ける。
  3. エモモがTimeline上で操作できるようになるため、アニメーションはトラック内に配置したAnimationClipで指定する。
var binding = playableDirector
                  .playableAsset
                  .outputs
                  .First(c => c.streamName == avatarTimeline.animationTrackName);
playableDirector.SetGenericBinding(binding.sourceObject, avatar.GetComponent<Animator>());

トラックに名前を設定しておく(streamName)

インスペクタで取得したいトラックを指定する(avatarTimeline.animationTrackName)

Timeline上のAnimatorとエモモ(Avatar_40)のAnimatorを紐付ける
AnimationClipはTimeline上で指定する

Cinemachineでのカメラ制御

Cinemachineは複数のカメラの切り替えを行う仕組みです。カットシーンにおけるカメラのブレンディングがスムーズに行えます。10個以上のカメラをTimelineにより切り替えることで演出に広がりをもたせています。また、Timelineのレコーディングモードを利用しUnity上で実際にカメラを動かしながらアニメーションをつけることもあります。Unityでカメラワークが作成できるためトライアンドエラーや細かい調整が高速に行えます。

運用の効率化のために

演出データはアセットバンドル化することでアプリを更新せずに追加できます。また、デザイナーだけで演出が作れるように基盤を整えることで運用の効率化を行っています。ただし、Timelineでの演出作成は自由度が高いため、アセットサイズが大きくなり端末の負荷も高くなりやすいです。そのためエンジニアは演出のデータ構造や負荷に問題がないかをチェックしてからリリースしています。また、アセットバンドル内に不要なアセットがないかや、アセットバンドル間の依存関係をチェックするツールを作成しより効率良く運用できるよう日々改善しています。

まとめ

3Dを利用したリッチな演出はUnityの得意領域です。Timeline+Cinemachineの導入により演出の幅も上がりました。ギフトの演出により配信者と視聴者が一緒になって楽しんでもらえると嬉しいですね。今後もエモモを活かしたコミュニケーションが進むようなギフトを作っていきます。

We are hiring!

ミラティブではUnityエンジニアを募集しています。 Unityをフル活用してリッチな演出を一緒に作りましょう!

www.mirrativ.co.jp