Unreal Tournament Sample Website

For the Unreal Tournament website, we have opted to use Node.js for the server, and rely on jQuery for the front-end. We also decided to use TypeScript throughout our code in order to benefit from type checking.

In this section, we will only describe the parts of the code that link each component with the Genvid Web API.

Server Code

The server is written using Node.js, and is responsible for connecting some public URLs to the internal Genvid Services.

All of this re-routing is specified in the routes/streams.t.ts file. This file is responsible for forwarding web requests from the client to the disco service using a pre-determined secret.

We discover the url and secret of the disco and commands services using consul. The config.ts file shows how to discover those services.

// Copyright 2016-2017 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", "secretcode");

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

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

export interface IServiceNode {
    Node: string;
    Address: string;
    TaggedAddresses: ITaggedAddresses;
    ServiceID: string;
    ServiceName: string;
    ServiceTags: any[];
    ServiceAddress: string;
    ServicePort: number;
    ServiceEnableTagOverride: boolean;
    CreateIndex: number;
    ModifyIndex: number;
}

function watchService(serviceName: string, setUrl: (url: string) => void) {
    let nodesOptions: Consul.Catalog.Service.NodesOptions = {
        service: serviceName,        
    };

    let serviceWatch = consul.watch({
        method: consul.catalog.service.nodes,
        options: nodesOptions,
    });

    serviceWatch.on("change", (nodes: IServiceNode[], _res) => {
        if (nodes.length === 0) {
            console.error(`${serviceName} service is not available from consul`);
        } else {
            let node = nodes[0];
            // Use the first disco service
            let serviceUrl = `http://${node.ServiceAddress}:${node.ServicePort}`;
            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; });

The server forwards traffic for 2 key URL paths:

  • /api/streams ➡ /disco/stream/info

This method is used to get information about the current stream. This is how the browser is able to get the stream name, description, channel, etc.

  • /api/channels/join ➡ /disco/stream/join

This HTTP method returns a URI and a token used to connect to the Genvid system.

Browser Code

The sample Unreal Tournament Genvid website looks like this.

../../../_images/ut4.png

It is a simple web application showing the live video of Unreal Tournament streaming from YouTube. There is a video overlay allowing to select players directly, and highlight selected players using a circle around their waist. Under the video there is a control for each player. The viewer can select, kill or cheer any player. The user is able to interact with the webpage directly with these controls:

Global Shortcuts

A Select/Deselect all players
M Mute or unmute the stream
Z Reduce volume of the stream
X Increase volume of the stream
Space Pause or unpause the stream
+ Increase the DelayOffset
- Decrease the DelayOffset
* Reset the DelayOffset
F Display the floor grid
G Show or hide the Genvid overlay
H Open or close the help menu

Video

Click on player Select the player (adds circle highlight over video)

Header Buttons

Genvid icon button Show or hide the Genvid overlay
? Open or close the help menu

Bottom panels

Click 👍 Change the camera at this player
Click ☠ Kill the player
Click player panel Select the player (adds circle highlight over video)

Genvid overlay

The Genvid overlay is used to prevent interaction with the Youtube player, but it is also used to display the colored circles and names beside the players. The overlay is also used to display useful debug information:

Local Local time of the machine displaying the webpage
Raw Video Current internal time of the video player
Est. Video Estimates video player time
Last Compose Time of the last data reported by the Compose service
Est. Compose Estimated time of the Compose service
Stream Current time of the generalized stream
Latency Estimated latency between the game and the spectators
DelayOffset Optional delay offset used for minute synchronization adjustments

ut4_app.ts

This file is responsible of the player keyboard input, the video player controls and the help.

ut4_engage.ts

The initialization of the Genvid system is found in this file.

The next function is showing how to initialize the GenvidClient. It is called when the browser receives the stream information.

private on_channel_join(info: genvid.IChannelJoinResponse) {
    this.client = genvid.createGenvidClient(
        this.streamInfo, info.uri, info.token, this.video_player_id);
    this.client.onVideoPlayerReady((elem) => { this.on_video_player_ready(elem); });
    this.client.onStreamsReceived((streams) => { this.on_streams_received(streams); });
    this.client.onNotificationsReceived(this.on_notifications_received.bind(this));
    this.client.onDraw((frame) => { this.on_new_frame(frame); });
    this.client.start();
}

The important functions are

  • onVideoPlayerReady: called when the video player is ready.
  • onStreamsReceived: called when a stream information was received.
  • onNotificationsReceived: called when a notification was received.
  • onDraw: called 30 times per second to draw the overlay and controls.

This file is also responsible of sending events through the GenvidClient.

public cheer_player(player_name) {
    if (this.client) {
        console.log(`Sending cheer ${player_name}`);
        this.client.sendEventObject({ "cheer": player_name });
    } else {
        console.log("No client connected.");
    }
}

ut4_data.ts

This file contains the data structures used in the code.

ut4_model.ts

This file is responsible for analyzing the received data and transforming it into usable data.

The method onStreamReceived is parsing the JSON information from the streams. It is also where annotations are interpreted.

// Shows how to parse the JSON inside the streams and annotations
for (let stream of [...streams.streams, ...streams.annotations]) {
    for (let frame of stream.frames) {
        try {
            frame.user = JSON.parse(frame.data);
        }
        catch (err) {
            console.error("Not able to parse JSON: ", err);
        }
    }
}

The method onNotificationsReceived interprets the notifications. The cheers are handled using notifications so they can be honored on reception rather than when being synchronized with the video.

The method newGameFrame simply converts the stream information into a format easier to manipulate and use.

The method updateGameFrame is responsible for analyzing and updating the information in the frame. At the end of this method, the data should be ready to display.

ut4_gui_playerList.ts

This file encapsulates the view of the controls below the video. It also controls the player death icon over a player.

The class PlayerControl contains a single control. It is here that click events are handled.

The class PlayerIndicatorControl controls the player death overlay.

ut4_gui_infoOverlay.ts

This file contains the debug information overlay.

ut4_webgl.ts

This file is responsible for showing a WebGL overlay on top of the video. The overlay contains the player selection and a grid. When a viewer clicks over a player on the overlay, a picking routine is called in order to figure out wether or not a player was selected.