GenvidでUnityからストリーミング動画と同期したデータ配信を行う

#はじめに: 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 SDK for Unityを使っていますが、Unreal Engineでも利用可能です。

# ゲームの動画配信で、ブラウザ上でゲームの動画に合わせた演出を行う
Genvidは、UnityやUnreal Engine、その他リアルタイムで動作するコンテンツをクラウドサーバー上で動作させ、そこから動画を生成してTwitchやYouTubeで配信します。
動画の視聴者は、ブラウザ上に描画される視聴者専用UIからコンテンツに介入できます。
コメント欄で視聴者と遊ぶタイプのゲームもありますが、Genvidのシステムでは動画の上に視聴者専用のUIを表示するため、コメント欄は使用しません。

動画視聴者→動画コンテンツの通信をGenvidでは「Genvid Event」と呼んでいます。
Eventの基礎と実装については次の記事をご参考ください。

Genvidにおける動画視聴者からの送信データ「Event」を集計処理するjsonスキーマの読み方
https://qiita.com/Takaaki_Ichijo/items/33e46d7849107f0bcfa7

Ｇenvidにはもうひとつ、クラウドサーバー上で動作しているリアルタイムコンテンツから動画と同期したデータ配信ができます。
これを「Genvid Stream」と呼んでいます。テレビのデータ放送にイメージが近いです。

たとえば、動画視聴者専用のUIの表示・非表示をコンテンツ側の進行に合わせてオンオフしたり、動画側の要素をクリックして選択できる、といった演出に使えます。

![genvidstreammatrix2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/a9c1b78c-af87-15f7-0d71-1cdef275b4a9.gif)

こちらのgifはGenvidシステムが動作している様子です。Unityで描画されている白い板と黒い球があります。これらはクラウドサーバー上でレンダリングされ、ブラウザには動画としてストリーミングされています。ブラウザ上ではいっさいゲームの描画はしておらず、ただ動画を再生しているだけです。
そこに、「黒い球の位置」情報を同時にデータ配信しています。黄色いラベル「BallPosition」はゲーム内で描画されておらず、このブラウザ上で合成しています。

この「同期」がGenvidの重要なポイントです。同期したデータをもとにインタラクションを作ることで、「触れる動画コンテンツ」を実現できます。

本投稿では、ブラウザ側でどのように情報を受け取るか、Unityからどうやって情報を送信するかを紹介します。
なお、今回はボールの2D座標の計算をUnity側で実行していますが、変換行列（Matrix4x4）をそのままブラウザ側に送って、ブラウザで2D座標の計算をするアプローチもあります。

Genvid mathを利用したマトリックス変換行列による位置情報のデータ配信と動画同期
https://qiita.com/Takaaki_Ichijo/items/8a79cfa630238b22591c


# ブラウザ側(js)の実装

### index.html
htmlファイルは動画再生部分と、ラベルを定義するのみになります。
ソースとしてGenvid動作ライブラリであるgenvid.umd.jsをインクルードします。
（Genvidインストールフォルダのapi\web\distにあります）
overlay.jsでGenvidサーバーからの情報を受け取ったり、その情報を加工してラベルの位置情報を更新する処理を行います。

```index.html
<!doctype html>
<html>
<head>
    <title>Genvid Overlay</title>
    <link rel="stylesheet" href="style.css">
</head>
<body style="background:black">
    <div id="video_player"></div>
    <div class="label" id="ballPosition">ballPosition</div>
    <script src="genvid.umd.js"></script>
    <script src="overlay.js"></script>
</body>
</html>

```

### Genvidの初期化とGenvid Stream受け取りイベントの設定
genvidClientの初期化後、onStreamsReceivedでGenvidStreamが来た時にJSONとしてパースする設定と、onDrawで描画時に指定の名前のGenvidStreamが来ていたら描画関数を実行する手続きを設定します。
drawの実装はこの後説明します。


```overlay.js

var genvidClient;

fetch("/api/public/channels/join", { method: "post" })
    .then(function (data) { return data.json() })
    .then(function (response) {
        genvidClient = genvid.createGenvidClient(response.info, response.uri, response.token, "video_player");

        genvidClient.onStreamsReceived(function (dataStreams) {
            for (let stream of [...dataStreams.streams, ...dataStreams.annotations]) {
                for (let frame of stream.frames) {
                    try {
                        frame.user = JSON.parse(frame.data);
                    }
                    catch (e) {
                        console.log(e, frame.data);
                    }
                }
            }
        });

        genvidClient.onDraw(function (frame) {
            let gameDataFrame = frame.streams["ball"];
            if (gameDataFrame && gameDataFrame.user) {
                draw(gameDataFrame.user);
            }
        });

        genvidClient.start();
    })
    .catch(function (e) { console.log(e) });

```


### 描画実行

draw関数では、Genvid Streamによって送られてきたデータgameDataから、ボールの位置情報posXとposYを取り出し、ラベル（ballPosition）に直接指定します。
後ほどUnity側の実装を説明しますが、位置情報はスクリーン座標に対する0~1の値で正規化されていますので、まずはGenvidで描画されている動画のアス比とスクリーンのサイズを調べ、座標を割り出します。

```draw.js
function draw(gameData) {

	let videoHeight = visualViewport.width / this.genvidClient.videoAspectRatio;
	let heightRaito = videoHeight / visualViewport.height;

	ballPosition.style.left = Math.floor(gameData.posX * 100) + "vw";
	ballPosition.style.top = Math.floor((1 - gameData.posY)* heightRaito * 100) + "vh";
}
```

cssファイルでラベルの定義をします。left,bottom要素をjsから操作します。

```style.css
.label {
    position: absolute;
    left:0vw;
    bottom:0vw;
    opacity: 0.7;
    border-radius: 8px;
    background-color:rgb(255, 200, 47);
}

```

# Unity側の実装

Unity側の実装を見ていきましょう。本投稿ではGenvidの初期化関連は省略します。
Genvidの初期化を行うプレハブの配置については、Genvid Cube Sampleを参照していただければと思います。

RectTransformUtility.WorldToScreenPoint関数を使ってスクリーンのカメラから2Dの座標を割り出し、スクリーンサイズで割って正規化します。
その後、SubmitGameDataJSONでブラウザ側に送信します。

##

```BallPositionBroadcaster.cs

public class BallPositionBroadcaster : MonoBehaviour
{
    public GameObject currentBallGameObject;    // translation matrix used to display tank position
    private Camera mainCam;
    
    private void Awake () 
    {
        mainCam = Camera.main;
    }
    
    public void SubmitBallPosition(string streamId) 
    {
        if (GenvidSessionManager.IsInitialized && GenvidSessionManager.Instance.enabled) 
        {
            
            var pos = RectTransformUtility.WorldToScreenPoint (mainCam, currentBallGameObject.transform.position);

            GameData gameData = new GameData () {
                posX = pos.x / Screen.width,
                posY = pos.y / Screen.height
            };

            GenvidSessionManager.Instance.Session.Streams.SubmitGameDataJSON (streamId, gameData);
        }
    }
    
    [System.Serializable]
    public struct GameData
    {
        [SerializeField]
        public float posX, posY;
    }
}
```


### Genvid StreamプレハブからSubmitBallPosition関数を呼ぶ

シーン内でGenvid StreamプレハブをGenvidSessionプレハブ下に配置し、先ほど用意したSubmitBallPositionを呼びます。このとき、インスペクターで指定するIDの名前とブラウザ側で受け取るIDの名前が一致するようにしてください。

![スクリーンショット 2021-12-02 030922.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/11f08fd5-482a-752b-fcbb-1476aa9214a5.jpeg)

この例ではID「ball」を指定し、ブラウザ側でlet gameDataFrame = frame.streams["ball"];としてデータを取り出しています。


# 動画内の要素と位置同期するHTML要素で動画をインタラクティブに

今回はシンプルに位置同期の手順を開設しましたが、「動画内のオブジェクトをクリック選択可能にする」「位置同期した要素を奥行き計算をして他のHTML要素の後ろに隠す」といった活用が可能です。
これによって、動画視聴者をコンテンツに能動的に参加し、よりのめり込めるコンテンツを作ることができます。