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.
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.