ゲームの実況動画内にブラウザ側で広告画像を合成 with Genvid

# ゲーム実況動画内広告を動画視聴者ごとにカスタマイズする

ゲーム内に広告を表示する仕組みは以前からあり、ゲームプレイヤーに向けた広告手法としてさまざまなサービスやSDKがありました。
ゲームプレイヤーが対象の広告であれば、セグメンテーションは比較的わかりやすいです（昨今はEUの法律により厳しくなりましたが）。
ゲームのアカウントを持つプレイヤーがどんなセグメントに属しているかを取得し、表示する広告を出し分けるといったことができます。

それでは、ゲームプレイヤーだけではなく、ゲームの動画配信を見るする視聴者にも広告を届けたい、と考えてみます。
ストリーミング動画はたくさんの動画視聴者に見てもらえます。しかし、ゲームの動画内で広告を表示するということは単一の広告になりますから、動画視聴者ごとに内容をカスタマイズすることはできません。
そこで、ストリーミング動画の中に広告を表示するのではなく、ブラウザ上でゲーム動画に広告を出すことをもできます。
単純なバナー表示であれば、動画の下などに表示できますが、動画サイズが狭くなるのでちょっと邪魔です。
そこで発想を変えて、ゲーム動画内の看板エリアなどに広告画像を上から被せて出す形を考えます。
ゲーム内のカメラ位置が変わらないならば、ただその座標に広告を出しておけます。しかし、カメラの位置が変わるゲームはどうなるでしょうか？

この記事ではその一つの方法として「Genvid SDK」によるインタラクティブなライブストリーミング技術を使って解決を図りたいと思います。

# ゲーム動画の内容に同期して、広告画像の位置が移動する
この仕組みは単純に言えば、ゲームから広告を表示する位置情報を、配信動画と同期した形でデータを送信します。
ブラウザ側は、そのデータを受け取って広告を表示する位置を更新します。

たとえば横視点の対戦ゲームがあったとして、ゲームの背景のビルボードに広告が表示される実装を考えてみます。
プレイヤーの操作に応じてカメラの位置は上下左右に動きますが、ゲーム内のビルボードの位置をブラウザに配信するため、ブラウザ側で重ねる画像の位置を同期させることができます。

Unityで作ったデモは次の通りです。

![admoving2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/8e413fe7-feac-1d07-b4b1-5b3f7e1f0341.gif)

青い画像はブラウザ側で描画しています。その他の要素はストリーミング動画です。

![app 2021_12_29 20_46_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/44ce6b8f-fd50-418f-3326-42c2cdd8d24b.png)

Unityで描画されているものは以上で、これがゲームから動画として配信されます。

![ad.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19010/d2429fd7-5d9c-beff-f7c8-077c05096aeb.png)

その動画に、ブラウザ上でこの広告画像を重ねつつ、位置情報を使って同期させます。

## 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 SDKは広告SDKではなく、上記のシステムを実現するサーバーミドルウェアです。
表示する広告データを取得する仕組みは別途必要になりますので、ご留意ください。

# 下準備
## Genvid SDKのインストールとデモ実行

Genvid SDKの基礎知識とインストール、デモ実行については公式ドキュメントと、以下の記事をご参考ください

Genvid Cube サンプル
https://www.genvidtech.com/doc/ja/SDK-1.32.0/samples/cube/cube.html

GenvidでUnityからストリーミング動画と同期したデータ配信を行う
https://qiita.com/Takaaki_Ichijo/items/4fbc70e7efdbdef85459

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

##style.css

今回のデモではCanvasを描画して、その位置を動画の上に重ねます。
overlay-layer、overlay-layer canvasをそれぞれ定義します。
canvas要素はpositionプロパティabsoluteでウィンドウ左上からの位置を指定します。

```style.css
.overlay-layer {
    position: relative; 
}
.overlay-layer canvas { 
   position: absolute;
   top: 0;
   left: 0;
}
```

## index.html
Genvidの動作に必要なgenvid.umd.js,genvid-math.umd.jsのほか、
今回は画像の描画・移動・拡大縮小のため、createJSフレームワークを使用します。スクリプトにはcreatejs.min.jsを指定します。Overlay.jsについてはこの後説明します。

```index.html
<!doctype html>
<html>
<head>
    <title>Genvid Overlay</title>
    <link rel="stylesheet" href="style.css">
</head>
<body style="background:black">
    <div class="overlay-layer">
        <canvas id="adCanvas" width="500" height="300"></canvas>
    </div>
    <div id="video_player"></div>
    <script src="genvid.umd.js"></script>
    <script src="genvid-math.umd.js"></script>
    <script src="https://code.createjs.com/1.0.0/createjs.min.js"></script>
    <script src="overlay.js"></script>
</body>
</html>

```

## overlay.js
オーバーレイ処理を実際に行うjsです。

### 画像の描画
まずcreateJSでステージ（描画エリア）と画像の読み込み、親子関係の設定を行います。

```overlay.js
var stage = new createjs.Stage("adCanvas");
var adBitmap = new createjs.Bitmap("ad.png");
stage.addChild(adBitmap);
```

### Genvidの初期化とStreamデータの受け取り
次にGenvidの初期化行います。
genvidClient.createGenvidClient後、onStreamsReceivedでGenvidStreamが来た時にJSONとしてパースする設定と、onDrawで描画時に指定の名前のGenvidStreamが来ていたら描画関数を実行する手続きを設定します。

```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["adPosition"];
            if (gameDataFrame && gameDataFrame.user) {
                draw(adBitmap, gameDataFrame.user);
            }
        });
        
        genvidClient.start();

        window.addEventListener("resize", this.resizeStage);
        this.resizeStage();
    })
    .catch(function (e) { console.log(e) });

```

genvidClient.start();のあと、ウィンドウのサイズが更新されるたびにresizeStageという関数を呼んでいます。これはウィンドウサイズに合わせてstageと画像のサイズを調整するための処理です。

### 画像位置のアップデート

genvidClient.onDrawの中で呼んでいる処理です。基本的には[前回記事]("https://qiita.com/Takaaki_Ichijo/items/4fbc70e7efdbdef85459")と同じで、ゲームから送信されてきたマトリックスをベースに平面位置の座標を計算します。
ただし、前回はleft,bottomの値から座標設定していましたが、createJSを使っているためleft,topからの座標が必要です。そのためy軸を反転しています。

```overlay.js
function draw(canvas_element, gameData) {

    //calculate 2d position of builboard
    let mat = this.convertMatrix(gameData.matProjView);
    let b = gameData.adBillboardMatrix;
    let ap = genvidMath.vec3(b.e03, b.e13, b.e23);
    let pos_ad_2d = genvidMath.projectPosition(mat, ap);

    //invert y position
    pos_ad_2d.y = -pos_ad_2d.y;

    // Convert from [-1, 1] range to [0, 1].
    let vh = genvidMath.vec2(0.5, 0.5);
    let pos_2d_n = genvidMath.mad2D(pos_ad_2d, vh, vh);

    // Convert from [0, 1] range to [0, w].
    let stage_size = genvidMath.vec2(stage.canvas.width, stage.canvas.height);
    let pos_in_stage = genvidMath.mul2D(pos_2d_n, stage_size);

    // Adjust for centering element.
    let e_size = genvidMath.vec2(canvas_element.image.width * canvas_element.scaleX, canvas_element.image.height * canvas_element.scaleY);
    let e_offset = genvidMath.muls2D(e_size, -0.5);
    let pos_centered = genvidMath.add2D(pos_in_stage, e_offset);

    canvas_element.x = pos_centered.x;
    canvas_element.y = pos_centered.y;
    stage.update();
}

function convertMatrix(rawmat) {
    return genvidMath.mat4(genvidMath.vec4(rawmat.e00, rawmat.e01, rawmat.e02, rawmat.e03),
        genvidMath.vec4(rawmat.e10, rawmat.e11, rawmat.e12, rawmat.e13),
        genvidMath.vec4(rawmat.e20, rawmat.e21, rawmat.e22, rawmat.e23),
        genvidMath.vec4(rawmat.e30, rawmat.e31, rawmat.e32, rawmat.e33));
}

```
センタリングを行った座標を画像のcanvasに渡した後、stage.update()を呼んで描画更新を行います。

### ウィンドウサイズに合わせて画像を拡大縮小する

今回デモとして用意した画像は720pの解像度でちょうどいい大きさ（400 x 300）にしています。
マジックナンバーになってしまっています、このサイズ設定を元にウィンドウ幅に対して画像を何パーセントに拡縮すればよいかを指定しています。

```overlay.js

function resizeStage(event) {

    let videoHeight = document.documentElement.clientWidth / genvidClient.videoAspectRatio;
    stage.canvas.width = document.documentElement.clientWidth ;
    stage.canvas.height = videoHeight;
    
    console.log("stage " + stage.canvas.width + stage.canvas.height);

    //広告画像は動画720pでちょうどいい大きさ（400 x 300）なので、この係数で広告画像のスケールを調整する
    let adScale = stage.canvas.width * 0.00078125;

    adBitmap.scaleX = adScale;
    adBitmap.scaleY = adScale;

    stage.update();
}

```

ちゃんとやるならばUnity側から広告表示エリアの現在の高さ・幅を配信しながら合わせる形ができると思います。

## Unity側実装

[前回記事]("https://qiita.com/Takaaki_Ichijo/items/4fbc70e7efdbdef85459")とほぼ同一ですので、実装はこちらをご覧ください。

前回記事はボールの位置を送信していましたが、今回はQuadをゲーム内の看板立てに見立てて表示し、その位置をMatrix4x4で送信しています。
ゲームから広告表示位置を送信するデータのプロパティ名は「adBillboardMatrix」として定義しています。

# 応用
Genvidを使ったインタラクティブ・ストリーミングは、ゲームからの情報を配信するだけではなく、動画を見ているブラウザ側からデータを取得し、ゲームへ送信できます。
たとえば動画視聴者に対して、ゲームのロード中などにアンケートを表示し、その内容を公告配信に反映させるといった仕組みも構築が可能と言えます。（個人情報に関する法的な部分は別問題ですが）