Unreal Tournament Sample Website

For the Unreal Tournament website, we 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 to benefit from type checking.

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

Server Code

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

We specify the rerouting 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 webgateway services using Consul. The config.ts file shows how to discover those services.

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

let consul_addr = envs("CONSUL_HTTP_ADDR", "").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 webgateway_url = envs(
export let webgateway_secret = envs(

export let webEndpointConfig = {
  endpoint: envs("ENDPOINT", undefined),
  secret: envs("CLIENT_SECRET", undefined),
  ssl: envs("SSL", "false")

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) => {
    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)}:${
      console.info(`Watch ${serviceName} url: ${serviceUrl}`);

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

watchService("disco", url => {
  disco_url = url;
watchService("webgateway", url => {
  webgateway_url = url;

The server forwards traffic for 2 key URL paths.

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

This web 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:


It is a simple web application showing the live video of Unreal Tournament streaming from YouTube. There is a video overlay which allows direct selection of players and highlights them with a circle around their waist. Under the video there is a control for each player letting the viewer select, kill, or cheer any player. he user can interact with the webpage directly with these controls:

Global Shortcuts

A Select or 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


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

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

Genvid overlay

The Genvid overlay prevents interaction with the YouTube player, but also displays the colored circles and names beside each cube along with useful debug information:

Local Local time of the machine displaying the webpage.
Raw Video Current internal time of the video player.
Est. Video Estimated 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


The file ut4_app.ts is responsible for the player keyboard input, video player controls, and help.


The ut4_engage.ts initializes the Genvid system.

The following function shows 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.onDraw((frame) => { this.on_new_frame(frame); });

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

This file also sends 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.");


This file contains the data structures used in the code.


The file ut4_model.ts is responsible for analyzing the received data and transforming it into usable data.

The method onStreamReceived parses 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 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.


The file ut4_gui_playerList.ts 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 that handles click events.

The class PlayerIndicatorControl controls the player death overlay.


The file ut4_gui_infoOverlay.ts contains the debug information overlay.


The file ut4_webgl.ts 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 a player on the overlay, it calls a picking routine to figure out wether a player was selected.