Genvidにおける動画視聴者からの送信データ「Event」を集計処理するjsonスキーマの読み方

#はじめに: Genvid SDKについて
Genvidは、動画ストリーミングとブラウザを介したインタラクティブな体験を組み合わせた「大規模インタラクティブ・ライブ・イベント」（Massive Interactive Live Events）を実現するSDKです。
https://www.genvidtech.com/ja/mile%E3%81%A8%E3%81%AF%EF%BC%9F/

ゲーム技術をベースに、リアルタイムで進行する動画番組に対して、動画視聴者が能動的に参加できるシステムを提供します。
コンテンツ内のキャラクターが次に何をするかを投票で決めたり、ミニゲームをプレイしてポイントをため、特定のキャラクターを応援するなどの活動を経て、物語が変化していきます。
MILEは、Unityを使いFacebook上で配信されている「Rival Peak」と、Unreal Engine 4を使った「Project Raven」があります。

『RIVAL PEAK』が示す次世代の視聴者参加型デジタルエンタテインメント
https://news.yahoo.co.jp/byline/onokenji/20210326-00229353

Genvid SDKの導入については、Genvidディベロッパーサイトの日本語マニュアルからご確認ください。
https://www.genvidtech.com/for-developers/

# Genvidの「Event」とは

Genvidを使用した動画配信を行う場合、ゲーム側の処理として「動画視聴者からのデータ送信を受け取る」という処理があります。
このインタラクティブ性を実現するには、たんにゲーム側にデータを受け取る口を作るだけではなく、大量の動画視聴者からのデータをどうまとめるかの設定が必要です。

Genvid SDKでは、動画を再生しているブラウザから、サーバー上で動作しているゲームインスタンスへのデータ送信を「Event」と呼びます。
たとえば、「次に召喚するボスを動画視聴者の投票で決める」というシステムをゲームに搭載するとしましょう。
視聴者が10万人いて、好きなタイミングで投票ボタンをタップすると考えると、何も考えずに入力をサーバーで受け付けてゲーム側に渡そうとすると、1フレームに何百ものデータ送信をしようとすることになり、ゲームは簡単に止まってしまいます。
このため、Genvidは「大規模インタラクティブ・ライブ・イベント」を実現させるために、何万人もの視聴者が1つのコンテンツにライブで参加できるシステムを提供しています。
「ステージ１に投票」という大量のデータを、「ステージ１に××人が投票」というデータに変換する集計作業を行います。
これをGenvidは「MapReduce アルゴリズム」と呼んでいます。

## データをまとめるための定義

Eventにおいて、どのようなデータが動画視聴者から送られてきて、どんなルールで「まとめる」作業をするかは「jsonスキーマ」で定義します。
jsonスキーマとは、サーバーで送受信するjsonデータには何という名前のフィールドが必要で、その中身の型はなんなのか、といった定義を記述するものです。
公式マニュアルでの説明は以下です。

Event MapReduce アルゴリズム
https://genvidtech.com/doc/ja/SDK-1.30.0/development_guide/architecture/events/schema.html?highlight=%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E

サンプルではconfig/events.jsonに書かれています。
Genvid Bastion（Genvidの基盤シスエム）起動後なら、Genvid Cluster UI（ブラウザ上の設定画面）から確認・変更できます。

jsonスキーマでの定義は、おそらくゲーム開発者には不慣れな概念だと思いますので、かみ砕いて紹介します。

こちらがマニュアルに記載のjsonスキーマの例です。

```json
{
  "version": "1.7.0",
  "event": {
      "game": {
          "maps": [
              {
                  "id": "changeColor",
                  "source": "userinput",
                  "where": {"key": ["changeColor", "<name>"], "name": "<color>", "type": "string"},
                  "key": ["changeColor", "<name>", "<color>"], "value": 1
              },
              {
                  "id": "cheer",
                  "source": "userinput",
                  "where": {"key": ["cheer"], "name": "<name>", "type": "string"},
                  "key": ["cheer", "<name>"], "value": 1
              }
          ],
          "reductions": [
              {
                  "id": "changeColor",
                  "where": {"key": ["changeColor", "<name>", "<color>"]},
                  "key": ["<name>", "<color>"],
                  "value": ["$count"],
                  "period": 250
              },
              {
                  "id": "cheer",
                  "where": {"key": ["cheer", "<name>"]},
                  "key": ["<name>"],
                  "value": ["$sum"],
                  "period": 250
              }
          ]
      }
  }
}
```

まず、第二階層までの（  "version": "1.7.0" "event": "game":  ）はフォーマットとして決まっているものなので、変更しません。
実際に書く必要があるのは「maps」定義と「reduce」定義です。これはjavascriptにおけるmap(), reduce()と近い挙動です。
他のクエリシステム同様に入力値をフィルタします。

### Mapステップ(maps:)
動画視聴者から送られてきたjsonデータの配列から、新しい配列を生成する手続きを定義します。
マニュアルの例では「changeColor」「cheer」という２種類のイベントが定義されています。
動画視聴サイトからは、GenvidClient.sendEvent関数でイベントを送信します。


```javascript

 onColorChange(cube, color) {
    let evt = {
      key: ["changeColor", cube],
      value: color,
    };
    this.genvidClient.sendEvent([evt]);
  }

  onCheer(cubeName) {
    this.genvidClient.sendEventObject({
      key: cheer,
      value: cubeName,
    });
  }
```

このGenvidClient.sendEvent関数によって送信されるjsonデータは以下のような感じです。

`````json
  {"key": ["changeColor", "Aramis"], "value": "red"}

  {"key": ["cheer"], "value": "Porthos"}
````

このKeyValueが超大量に送られてくるので、ルールに従ってデータをまとめましょうというのがjsonスキーマで定義されていることです。
まずはイベントchangeColorのjsonスキーマでの定義を見てみましょう。

```json
"where": {"key": ["changeColor", "<name>"], "name": "<color>", "type": "string"},
"key": ["changeColor", "<name>", "<color>"],"value": 1
```

1行目のwhere句では、処理を行う対象のデータはこれ、というフィルタリングを行います。
この例ではKeyが文字列changeColorと何らかのフィールドを持つ配列で、value（なぜかここではname）のデータタイプは文字列です。

Keyの2つ目のオブジェクトを仮に<name>、nameのオブジェクトを<color>と名付けます。
2行目で、このデータをどう変換するかを定義します。キーは文字列changeColorと<name>と<color>の配列、valueは1のデータを生成します。

つまり、このデータ

```json
  {"key": ["changeColor", "Aramis"], "value": "red"}
```

が、次のように変換されます。

```json
  {"key": ["changeColor", "Aramis", "red"], "value": 1}
```

### Reduceステップ (reductions:)

mapsステップで変換したデータを集計します。
reduce 処理には値を合計する$sum処理の他、$min、$max、$countなどの操作ができます。

```json
"reductions": [
  {
          "id": "changeColor",
          "where": {"key": ["changeColor", "<name>", "<color>"]},
          "key": ["<name>", "<color>"],
          "value": ["$count"],
          "period": 250
  }
]
```
id:changeColorのデータに対し、where句でフォーマットを確認します。
Mapの時に作成した["changeColor", "<name>", "<color>"]の構造を持つデータをもとに、
Keyを ["<name>", "<color>"],Valueをそのデータの個数（$count）としたデータを生成します。
Periodは実行間隔（ミリ秒）です。この例では250ミリ秒に一回集計処理を行います。

たとえば動画視聴者がキャラクター「Porthos」を「緑色」に変える、という投票を250msec中に200人が行った場合、MapReduce処理を経て、最終的に以下のデータがゲームインスタンスに渡されるようになります。

```json
  {"key": ["Porthos", "green"], "value": 200}
```

Unityの場合はGenvid EventインスタンスでEventのIDを指定しておくことで、まとめられたデータが指定した間隔で送信されてきます。

# MapReduceがGenvidの強みのひとつ

今回紹介したjsonスキーマを使った定義とその処理を有するため、大量の動画視聴者が投票や応援などの入力を行った場合でも、ゲームインスタンス側に爆発的なデータが流れ込まないようになっています。
ただ、集計をしにくいデータ（個別の文字列など）を扱う場合は、そもそも送信側でデータを減らすなどの工夫が必要です。