Admin.ts

The admin page can be accessed by clicking on Admin on the index.html. After entering the username and password (admin for both), the User can interact with the game content by interacting with the player tables. In the document adminUnity.ts, we have all the code used to perform these interactions.

In the file, we have one interface and one class.

interface ICommandRequest

The ICommandRequest interface sends a command to the game.

    interface ICommandRequest {
        id: string;
        value: string;
    }

class AdminController

The AdminController class contains all the methods needed to connect to the stream, display the proper information, and send the commands to the game.

Outside of the class, we create an instance of AdminController and perform an onConnect() on it to start the connection to the stream.

let admin = new unitySample.AdminController("video_player_hidden");
admin.onConnect();

We will cover the content of the AdminController class in the following sections.

onConnect

onConnect()

The onConnect method starts the connection to the services. If the connection executes properly, we proceed to the on_channel_join method with the IChannelJoinResponse() information found. We do this process in exactly the same way in the unity.ts file.

        // Start the connection to the services
        onConnect() {
            let promise = $.post("/api/public/channels/join", {}, (joinRep) => {
                this.on_channel_join(<genvid.IChannelJoinResponse>joinRep);
            });
            promise.fail((err) => {
                alert("Can't get the stream info:" + err);
            });
        }

on_channel_join

on_channel_join(info: genvid.IChannelJoinResponse)

The on_channel_join method creates the Genvid client after connecting to the services and channel. We use the information found during this process and the video_player_id sent during the class creation process to create the client (it is an argument to the AdminController class). We do this process in a similar way in the unity.ts file, although we do not need some methods in this case.

Afterwards, we need to associate specific events to functions in this class:

onStreamsReceived
Triggered when the stream content is received (used to get the timecode and
game data).
onDraw
Triggered when drawing a new frame in the video.

We then proceed to start the client with the start() method.

        // Create the genvid Client and the function listening to it
        private on_channel_join(joinRep: genvid.IChannelJoinResponse) {
            this.streamInfo = joinRep.info;
            this.client = genvid.createGenvidClient(this.streamInfo, joinRep.uri, joinRep.token, this.video_player_id);
            this.client.onStreamsReceived((streams) => { this.on_streams_received(streams); });
            this.client.onDraw((frame) => { this.on_new_frame(frame); });
            this.client.start();
        }

on_streams_received

on_streams_received(dataStreams: genvid.IDataStreams)

The on_streams_received method uses loops to get the latest timecode associated with the game data. We also convert the JSON data received into IGameData. We call it when the website receives the stream. We do this process in exactly the same way in the unity.ts file.

        // Upon receving the stream
        private on_streams_received(dataStreams: genvid.IDataStreams) {
            for (let stream of dataStreams.streams) {
                for (let frame of stream.frames) {
                    if (this.last_game_time_received < frame.timeCode) {
                        this.last_game_time_received = frame.timeCode;
                    }
                }
            }

            // Parse the JSON from each elements
            for (let stream of [...dataStreams.streams, ...dataStreams.annotations]) {
                for (let frame of stream.frames) {
                    frame.user = JSON.parse(frame.data);
                }
            }
        }

on_new_frame

on_new_frame(frameSource: genvid.IDataFrame)

The on_new_frame method performs various tasks that need constant updating. We call it every frame for the Genvid client we created.

  1. We need to get the game data and verify that this data is valid before doing any operation with it.

  2. Once we are certain about the data validity and that the player table is not created, we proceed to get the object list from the data (cubes).

  3. We create a loop for each object and we create the player table for each.

  4. We add the event listeners for clicking on the buttons of the player table.

  5. After the loop we create two buttons for the scene and camera change.

  6. We add event listeners for clicking the scene and camera change buttons.

        // During a new frame, if the game data is valid, the player table is created
        private on_new_frame(frameSource: genvid.IDataFrame) {
            // Parse the JSON from each elements
            let gameDataFrame = frameSource.streams["GameData"];
            let gameData = null;

            if (gameDataFrame && gameDataFrame.user) {

                if (this.message === "Unable to retreive data from the stream, is it still active ?") {
                    this.message = "Success: data received from the stream";
                    this.displayMessage();
                }
                if (this.playerTableSetup === false) {
                    this.playerTableSetup = true;

                    gameData = gameDataFrame.user;
                    let cubes = gameData.cubes;

                    for (let cube of cubes) {
                        let cubeAdd = "<table>" +
                                "<tr>" +
                                    "<td id='table_name'>" + cube.name + "</td>" +
                                "</tr>" +
                                "<tr>" +
                                    "<td>" +
                                        "<div id='command_button' class='" + cube.name + "_upDirection'>↑</div>" +
                                        "<div id='command_button' class='" + cube.name + "_downDirection'>↓</div>" +
                                        "<div id='command_button' class='" + cube.name + "_leftDirection'>←</div>" +
                                        "<div id='command_button' class='" + cube.name + "_rightDirection'>→</div>" +
                                    "</td>" +
                                "</tr>" +
                                "<tr>" +
                                    "<td id='command_button'><div class='" + cube.name + "_reset'>Reset</div></td>" +
                                "</tr>" +
                                "<tr>" +
                                    "<td id='command_button'><div class='" + cube.name + "_slower'>Slower</div></td>" +
                                "</tr>" +
                                "<tr>" +
                                    "<td id='command_button'><div class='" + cube.name + "_faster'>Faster</div></td>" +
                                "</tr>" +
                            "</table>";

                        $(".admin_table_section").append(cubeAdd);
                        let upButton = <HTMLButtonElement>document.querySelector("." + cube.name + "_upDirection");
                        upButton.addEventListener("click", (_event) => { this.setDirection(cube.name, 0, 1); }, false);

                        let downButton = <HTMLButtonElement>document.querySelector("." + cube.name + "_downDirection");
                        downButton.addEventListener("click", (_event) => { this.setDirection(cube.name, 0, -1); }, false);

                        let leftButton = <HTMLButtonElement>document.querySelector("." + cube.name + "_leftDirection");
                        leftButton.addEventListener("click", (_event) => { this.setDirection(cube.name, -1, 0); }, false);

                        let rightButton = <HTMLButtonElement>document.querySelector("." + cube.name + "_rightDirection");
                        rightButton.addEventListener("click", (_event) => { this.setDirection(cube.name, 1, 0); }, false);

                        let resetButton = <HTMLButtonElement>document.querySelector("." + cube.name + "_reset");
                        resetButton.addEventListener("click", (_event) => { this.reset(cube.name); }, false);

                        let slowerButton = <HTMLButtonElement>document.querySelector("." + cube.name + "_slower");
                        slowerButton.addEventListener("click", (_event) => { this.changeSpeed(cube.name, 0.8); }, false);

                        let fasterButton = <HTMLButtonElement>document.querySelector("." + cube.name + "_faster");
                        fasterButton.addEventListener("click", (_event) => { this.changeSpeed(cube.name, 1.25); }, false);
                    }
                    let sceneCameraButtons = "<table>" +
                                "<tr>" +
                                    "<td id='command_button' class='cameraChange'>Camera change</td>" +
                                "</tr>" +
                                "<tr>" +
                                    "<td>" +
                                        "<div id='command_button' class='sceneChange'>Scene change</div>" +
                                    "</td>" +
                                "</tr>" +
                            "</table>";
                    $(".admin_table_section").append(sceneCameraButtons);

                    let cameraButton = <HTMLButtonElement>document.querySelector("." + "cameraChange");
                    cameraButton.addEventListener("click", (_event) => { this.changeCamera(); }, false);

                    let sceneButton = <HTMLButtonElement>document.querySelector("." + "sceneChange");
                    sceneButton.addEventListener("click", (_event) => { this.changeScene(); }, false);

                }
            }
            else {
                this.message = "Unable to retreive data from the stream, is it still active ?";
                this.displayErrorMessage();
            }
        }

setDirection

setDirection(cubeName: string, x: number, z: number)

The setDirection sends a command to the game to set the direction for a player. We call it when clicking a direction for a player.

        // Change the direction of the cube
        setDirection(cubeName: string, x: number, z: number) {
            this.message = this.error = "";

            let command: ICommandRequest = {
                id: "direction",
                value: `${cubeName}:${x}:0:${z}`
            };

            let promise = $.post("/api/admin/commands/game", command).then(() => {
                this.message = `Set direction to ${cubeName}:${x}:${z}`;
                this.displayMessage();
            });

            promise.fail((err) => {
                this.message = `Failed with error ${err} to do Set direction to ${cubeName}:${x}:${z}`;
                this.displayErrorMessage();
            });
        }

changeSpeed

changeSpeed(cubeName: string, factor: number)

The changeSpeed method sends a command to the game to indicate that an object needs to go faster or slower. We call it when clicking on Slower or Faster for a player.

        // Change the speed of the cube
        changeSpeed(cubeName: string, factor: number) {
            this.message = this.error = "";

            let command: ICommandRequest = {
                id: "speed",
                value: `${cubeName}:${factor}`
            };

            let promise = $.post("/api/admin/commands/game", command).then(() => {
                this.message = `changeSpeed ${cubeName}:${factor}`;
                this.displayMessage();
            });

            promise.fail((err) => {
                this.message = `Failed with error ${err} to do changeSpeed ${cubeName}:${factor}`;
                this.displayErrorMessage();
            });
        }

reset

reset(cubeName: string)

The reset method sends a command to the game to set an object to its original position. We call it when clicking on Reset for a player.

        // Reset the position of the cube
        reset(cubeName: string) {
            this.message = this.error = "";

            let command: ICommandRequest = {
                id: "reset",
                value: cubeName
            };

            let promise = $.post("/api/admin/commands/game", command).then(() => {
                this.message = `reset ${cubeName}`;
                this.displayMessage();
            });

            promise.fail((err) => {
                this.message = `Failed with error ${err} to do reset ${cubeName}`;
                this.displayErrorMessage();
            });
        }

changeCamera

changeCamera()

The changeCamera method sends a command to the game to change the current camera. We call it when clicking on the Camera change button.

        changeCamera() {
            this.message = this.error = "";

            let command: ICommandRequest = {
                id: "camera",
                value: "change"
            };

            let promise = $.post("/api/admin/commands/game", command).then(() => {
                this.message = `camera change done`;
                this.displayMessage();
            });

            promise.fail((err) => {
                this.message = `Failed with error ${err} to change the camera`;
                this.displayErrorMessage();
            });
        }

changeScene

changeScene()

The changeScene method sends a command to the game to change the current scene. We call it when clicking on the Scene change button.

        changeScene() {
            this.message = this.error = "";

            let command: ICommandRequest = {
                id: "scene",
                value: "change"
            };

            let promise = $.post("/api/admin/commands/game", command).then(() => {
                this.message = `scene change done`;
                this.displayMessage();
            });

            promise.fail((err) => {
                this.message = `Failed with error ${err} to change the scene`;
                this.displayErrorMessage();
            });
        }

displayMessage

displayMessage()

The displayMessage method displays a success message about a completed operation. It’s useful for knowing if we get the data from the stream properly and to indicate if a command operation is successful.

        // Display a message in the page as a sucess
        displayMessage() {
                let messageErrorDiv = <HTMLDivElement>document.querySelector("#alert_error_cube");
                messageErrorDiv.style.visibility = "hidden";

                let messageDiv = <HTMLDivElement>document.querySelector("#alert_sucess_cube");
                messageDiv.style.visibility = "visible";
                let messageSpan = <HTMLSpanElement>document.querySelector("#sucess_message_cube");
                messageSpan.textContent = this.message;
        }

displayErrorMessage

displayErrorMessage()

The displayErrorMessage method displays an error message about a completed operation. It’s useful for knowing if we didn’t get the data from the stream properly and to indicate if a command operation isn’t successful.

        // Display a message in the page as an error
        displayErrorMessage() {
                let messageDiv = <HTMLDivElement>document.querySelector("#alert_sucess_cube");
                messageDiv.style.visibility = "hidden";

                let messageErrorDiv = <HTMLDivElement>document.querySelector("#alert_error_cube");
                messageErrorDiv.style.visibility = "visible";
                let messageSpan = <HTMLSpanElement>document.querySelector("#error_message_cube");
                messageSpan.textContent = this.message;
        }