チュートリアル用サンプル Web サイト

チュートリアル用 Web サイトは、サーバーに Node.js を使用し、フロントエンドに AngularJS 1.5 を使用するように最適化しています。型の確認がしやすいよう、コード全般に TypeScript を使用しています。

このセクションでは、各コンポーネントを Genvid Web API とリンクさせるコードについてのみ説明します。

サーバーコード

サーバーの記述には Node.js を使用しています。これが、パブリック URL と内部 Genvid サービスの接続を行います。

routes/streams.t.ts ファイル内の経路再選択を指定します。このファイルは、あらかじめ指定したシークレットを使用して、クライアントから disco サービスへのウェブ要求を転送します。

Consul を使用して、URL、disco のシークレット、コマンドサービスを見つけます。config.ts ファイルは、これらのサービスの検出方法を示しています。

// Copyright 2016-2019 Genvid Technologies LLC All Rights Reserved.
let envs = require("envs");
import * as Consul from "consul";

let consul_addr = envs("CONSUL_HTTP_ADDR", "127.0.0.1:8500").split(":");
let port = consul_addr.pop();
let host = consul_addr.join(":");
export let consul = Consul({host: host, port: port});

export let disco_url = envs("GENVID_DISCO_URL", "http://localhost:8080");
export let disco_secret = envs("GENVID_DISCO_SECRET", "discosecret");

export let command_url = envs("GENVID_COMMAND_URL", "http://localhost:8089");
export let command_secret = envs("GENVID_COMMAND_SECRET", "commandsecret");

export interface ITaggedAddresses {
    lan: string;
    wan: string;
}

export interface IServiceEntry {
    Node: {
        ID: string;
        Node: string;
        Address: string;
        Datacenter: string;
        TaggedAddresses: ITaggedAddresses;
        Meta: {
            [name: string]: string
        };
    };
    Service: {
        ID: string;
        Service: string;
        Tags: string[];
        Address: string;
        Meta: {
            [name: string]: string
        };
        Port: number;
    };
    Checks: {
        Node: string;
        CheckID: string;
        Name: string;
        Status: string;
        Notes: string;
        Output: string;
        ServiceID: string;
        ServiceName: string;
        ServiceTags: string[];
    }[];
}

function wrapIPv6Address(address: string): string {
	if (address.includes(":")) {
		return `[${address}]`
	}
	return address
}

function watchService(serviceName: string, setUrl: (url: string) => void) {

    const watchOptions: Consul.Health.ServiceOptions = {
        service: serviceName,
        passing: true,
    };
    
    const serviceWatch = consul.watch({
        method: consul.health.service,
        options: watchOptions,
    });

    serviceWatch.on("change", (services: IServiceEntry[], _res) => {
        console.log(services);
        if (services.length === 0) {
            console.error(`${serviceName} service is not available from consul`);
        } else {
            let service = services[Math.floor(Math.random() * services.length)];
            let serviceUrl = `http://${wrapIPv6Address(service.Service.Address)}:${service.Service.Port}`;
            setUrl(serviceUrl);
            console.info(`Watch ${serviceName} url: ${serviceUrl}`);
        }
    });

    serviceWatch.on("error", (err) => {
        console.error(`${serviceName} watch error:`, err);
    });
}

watchService("disco", (url) => { disco_url = url; });
watchService("command", (url) => { command_url = url; });

サーバーが 3 つのメソッドを転送します。

このメソッドは、現在のストリームに関する情報を取得します。ブラウザは、このメソッドでストリーム名、説明、チャンネルなどを取得します。

この Web メソッドは、Genvid システムへの接続に使用する URI とトークンを返します。

この Web メソッドはゲームにコマンドを送信します。このチュートリアルでは、パスワードで保護された管理ページからメソッドにアクセスできます。

utils/auth.ts ファイルには、ユーザー名とパスワード設定が含まれています。

ブラウザコード

ブラウザのコードは AngularJS 1.5 に準拠しています。結果は次のようになります。

../../_images/tutorial.png

これは、カラーキューブのライブビデオを表示するシンプルな Web アプリケーションです。このアプリケーションはマルチストリームとも対応しており、ピクチャ・イン・ピクチャ合成を試すこともできます。(詳細は Genvid Studio sample を参照)。キューブの周りには輪を表示するビデオオーバーレイが存在します。輪の位置は、ビデオ内のキューブの位置と一致します。以下のコントロールを使用して、Web ページを直接操作することができます。

グローバル:

  • m = ストリームのミュート、またはミュート解除。
  • z = ストリームのボリュームを下げる。
  • x = ストリームのボリュームを上げる。
  • Space = ストリームの一時停止、または一時停止の解除。
  • + = DelayOffset を増やす。
  • - = DelayOffset を減らす。
  • * = DelayOffset をリセット。
  • g = Genvid オーバーレイの表示、または非表示。
  • h = ヘルプメニューの表示、または非表示。

ビデオ:

  • キューブをクリック = パネルのハイライト、輪を明るくする

ヘッダーボタン:

  • Play = インタラクティブビデオプレイヤーに戻る。
  • Admin = 管理ページにアクセスする (ユーザー名: admin パス: admin)。
  • Genvid アイコンボタン = Genvid オーバーレイの表示、または非表示。
  • ? = ヘルプメニューの表示、または非表示。

ボトムパネル:

  • 👍 = プレイヤーの popularity を変更 (ハートアイコン)。
  • Reset = キューブの位置をリセット。
  • Color = キューブの色を変更。

Genvid オーバーレイ

Genvid オーバーレイは、YouTube プレイヤーとの干渉を防ぎつつも、各キューブの横に、色付きの輪と名前、デバッグ情報を表示します。

  • Local = Web ページを表示するマシンのローカル時間。
  • Raw Video = ビデオプレイヤーの現在の内部時間。
  • Est. Video = ビデオプレイヤーの予測時間。
  • Last Compose = コンポジションサービスからレポートされた直前のデータの時間。
  • Est. Compose = コンポジションサービスの予測時間。
  • Stream = 一般化されたストリームの現在の時間。
  • Latency = ゲームと視聴者間の予測遅延時間。
  • DelayOffset = 同期を行うための遅延オフセット調整値
    (オプション)。

コマンドチャネルには管理ページからアクセスします。管理ページへ移動してログインするには Admin ボタンを選択します (デフォルトのログイン ID は admin、パスワードは admin です)。ページには各キューブのセクションが表示され、以下の機能が存在します。

  • Direction = 矢印をクリックしてキューブの向きを変更する。
  • Reset = キューブの位置をリセットする。
  • Slower = キューブのスピードを落とす。
  • Faster = キューブのスピードを上げる。

channelComponent.ts

フロントエンドコードでは、 IGenvidClient() インスタンスをインスタンス化する必要があります。/api/public/channels/join に POST リクエストを送信後 public/lib/channelComponent.ts ファイルから実行します。このリクエストは、createGenvidClient() を使用して、Genvid クライアント作成に必要なデータを返します。

IGenvidClient() が Genvid サービスと通信を行い、コールバックを提供します。

  • ビデオの準備ができた時に onVideoPlayerReady()
    呼び出します。
  • onAuthenticated() が、認証の成功を
    通知します。
  • ストリームデータを受信すると、 onStreamsReceived()
    実行されます。
  • onDraw() を定期的に呼び出し、
    GUI を更新するために定期的に呼び出されます。
        onDraw(frame: genvid.IDataFrame) {
            // update the overlays to adapt to the composition of the video stream:
            this.updateOverlays(frame.compositionData);

            // Parse the JSON from each elements
            let gameData: IGameData = null;
            const gameDataFrame = frame.streams["GameData"];
            if (gameDataFrame && gameDataFrame.user) {
                gameData = gameDataFrame.user;
                gameData.timeCode = frame.timeCode;
                // Modify the popularity with the latest value
                for (let cube of gameData.cubes) {
                    if (this.latestPopularity) {
                        let pCube = this.latestPopularity.find(c => c.name === cube.name);
                        if (pCube) {
                            cube.popularity = pCube.popularity;
                        }
                    }
                }
            }

            // Send the data to the overlay and controls
            this.overlay.drawFrame(gameData);
            if (this.onChannelDraw) {
                this.onChannelDraw(gameData);
            }

            // Log the annotations
            const colorChangedAnnotation = frame.annotations["ColorChanged"];
            if (colorChangedAnnotation) {
                for (let annotation of colorChangedAnnotation) {
                    let colorChanges = <IGameChangeColorAnnotation[]>annotation.user;
                    for (let colorChange of colorChanges) {
                        console.info(`The cube ${colorChange.name} changed color.`);
                    }
                }
            }

            let isFullScreen = this.checkFullScreen();
            if (isFullScreen !== this.isFullScreen) {
                this.isFullScreen = isFullScreen;
                this.$scope.$digest();
            }
        }

このサンプルでは、onDisconnect() コールバック (IGenvidClient() インターフェイスにより公開) を使って、クライアントのソケットが閉じるタイミングを通知します。指定のコールバックをバインドすることで、実行しています。

this.client.onDisconnect(() => {this.onDisconnectDetected();});

クライアントソケットの閉鎖通知が届いたら、スクリプトに再接続試行の指示を行います。

        onDisconnectDetected() {
            let promise = this.$http.post<genvid.IChannelJoinResponse>("/api/public/channels/join", { channel: this.channelId }).then((res) => {
                this.client.reconnect(res.data.info, res.data.uri, res.data.token);
                this.resetFibNums();
            });
            promise.catch( async () => {
                await this.sleep(this.getNewSleepDuration());
                this.onDisconnectDetected();
            });
        }

ここで、新しい leaf アドレス、新しいトークン、新しい streamInfo の情報リクエストを行います。

リクエストが成功した場合、reconnect() を呼び出して、この情報を使用して新しい websocket 接続を確立します。

リクエストが失敗した場合 (利用可能な leaf がないなど)、しばらく待って再試行します。インクリメンタルなフィボナッチ数の計算結果を使用して、待機時間を決定します。

        private sleep(ms) {
            return new Promise(resolve => setTimeout(resolve, ms));
        }

        private getNewSleepDuration() {
            this.fibNumResult = this.fibNumOne + this.fibNumTwo;
            this.fibNumOne = this.fibNumTwo;
            this.fibNumTwo = this.fibNumResult;
            this.delayBetweenReconnectionAttempts = this.fibNumResult + (this.fibNumResult * Math.floor(Math.random() * 10) * 0.01); // + 0 to 10% 
            return this.delayBetweenReconnectionAttempts * 1000; // milliseconds
        }

        private resetFibNums() {
            this.fibNumOne = 0;
            this.fibNumTwo = 1;
        }

フィボナッチ数列の結果に 10% のランダム要素を追加します。これにより、2 つのサービスが同時に接続を開始する状況を回避します。接続に成功したときにフィボナッチ数をリセットします。

tutorialOverlay.ts

ファイル public/lib/tutorialOverlay.ts では、ゲームデータの読み込み方法と、WebGL キャンバスで 3D の輪をレンダリングするための、この情報の使用方法についての例を紹介しています。

tutorialControlsComponent.ts

このファイルは、キューブの色の変更、キューブの応援、位置のリセットなどのイベントを生成する方法を紹介しています。

例として次のコードは、色を変更するイベントを示します。

        onColorChange(event: UIEvent, name: string, color: string) {
            event.stopPropagation();
            let evt = {
                "key": ["changeColor", name],
                "value": color
            };
            this.parent.client.sendEvent([evt]);
        }

HTML は以下のファイルに含まれています。

  • public/lib/components/tutorialControls.html
  • public/lib/components/tutorialCubeControl.html

adminComponent.ts

このファイルは、コマンドチャネルを使ってゲームにコマンドを送信する方法を示しています。管理ユーザーは、キューブの速度と方向を変更できます。

コマンドがゲームの制御を行うため、管理ページには認証が必要です。デフォルトのユーザーは次のとおりです。

admin admin
user1 user1
user2 user2