Files
UnrealEngineUWP/Engine/Source/Programs/PixelStreaming/WebServers/SignallingWebServer/cirrus.js
JeanMichel Dignard e6d45383d5 Copying //UE4/Dev-Enterprise to //UE4/Dev-Main (Source: //UE4/Dev-Enterprise @ 4341740)
#lockdown Nick.Penwarden
#rb none

============================
  MAJOR FEATURES & CHANGES
============================

Change 4280523 by Patrick.Boutot

	Add option in AjaCustomTimeStep to wait until the frame to be ready. Previously, the frame was there but not yet processed so it was possible that it was not ready by the time we wanted to read it. It won't work with interlaced because the 2 fields are processed at the same time. In interlaced, will get a 30fps behaviour when we actually want a 60fps.
	Fix bug that didn't set and reset bIsOwned properly when it was first initialized as not owned.

Change 4280526 by Patrick.Boutot

	Add accessor to get the leaf media source or output.

Change 4280624 by Patrick.Boutot

	Add timecode acessor to media samples

Change 4280626 by Patrick.Boutot

	Rework the timing for AJA Media Player. Previously, we took the timing of the frame. That was a bad idea because if 2 incomings video frames were coming a the same time, you would only show one. Making the buffering system useless.
	That affects the Custom Time Step since it was waiting for the interrupt signal and in some behavior we would like the frame to be ready to be used by UE. Same the timecode in the MediaSample because we may not used it to stamps the frame.

Change 4283022 by Patrick.Boutot

	[EditorScriptingUtilitites] Check folder names invalid characters separatly from the object's name.
	#jira UE-59886,  UE-62333

Change 4283112 by Patrick.Boutot

	Remove MediaFrameworkUtilititesModule dependency to the Settings module at runtime.
	Rename TimemanagemenetEditor module names.

Change 4283426 by JeanLuc.Corenthin

	Fix crash with FBX file

	#jira UE-62501

Change 4284940 by Patrick.Boutot

	A widget that let you select a single permutation from a list. It groups the values into categories and removes duplicates inside that category.

Change 4285471 by Patrick.Boutot

	Remove MediaFrameworkUtilititesModule dependency to the Settings module at runtime.

Change 4286925 by Patrick.Boutot

	[AJA] Add support to read LTC from the reference In.
	Add more detail on video format and the device.
	MediaSource use the Permutations Selection widget to select his mode and device.
	Remove debugging option to trigger an AJA custom time step and timecode provider.
	Remove the UYVY pixel option from AJA. It's better do to the conversion on the AJA card that on the GPU.
	Change the tooltip and category for some AjaMediaSource properties.

Change 4287026 by Julien.StJean

	Modifed the file STimeCodeProviderTab.cpp to fix the position of a SComboButton that wasn't properly place.

Change 4287663 by Jon.Nabozny

	Add timecode messages into nDisplay, and sync those between Master and Slave

Change 4287884 by Jon.Nabozny

	Create a TimecodeProvider for SystemTime and introduce a notion for DefaultTimecodeProvider in Engine.

Change 4288050 by Jon.Nabozny

	Rework the TimeSynchronization implementation for usability and functionality.

Change 4288283 by Jon.Nabozny

	Fixed swapped MetaClass and DisplayName options on UEngine::DefaultTimecodeProviderClassName;

Change 4288352 by Jon.Nabozny

	Set TimecodeProviderClassName and DefaultTimecodeProviderClassName in BaseEngine.ini

Change 4288378 by Jon.Nabozny

	Fixup some issues in TimecodeSynchronizer where code was reset improperly due to multiple unshelves / resolves.

Change 4288394 by Jon.Nabozny

	Add TimeSync functionality into LiveLink. Also add test cases for this. This should allow us to easily synchronize multiple LiveLink sources together, as well as synchronize those to anything else using the sync system (Relies on CL-4235417)

Change 4288899 by Patrick.Boutot

	Fix initialization order of FMediaIOCorePlayerBase variables

Change 4289157 by Patrick.Boutot

	Allow the user to change the source of a capture without stopping the current capture.
	[AJA] AjaMediaCapture, add support for UpdateSceneViewport & UpdateRenderTarget
	@made by julien.stjean

Change 4291328 by Jon.Nabozny

	Report the Skeleton Guid with TimeSyncData and track sync state in LiveLinkTimeSynchronizationSource.
	This prevents a crash that can happen if a source is quickly cleared and reset before the next tick of Time Synchronization.

Change 4296294 by Jon.Nabozny

	Fixup errors when TimecodeProviderClassName is empty. It's valid to leave this empty.

Change 4297122 by Patrick.Boutot

	Media Profile with timecode provider & custom time step

Change 4301855 by Austin.Crismore

	Fix for movment scaling and virtual joystick controls. Movement scaling in for truck and dolly is locked to the world xy plane, and virtual joysticks use their own method for movement scaling now.

	#jira UE-61762, UE-62187

Change 4301856 by Austin.Crismore

	Virtual sequence level controller now listens to on object spawned, so that it can intercept the camera actor and disable attatching to HMD to prevent camera movement that isn't from the level sequence

	#jira UE-61766

Change 4301860 by Austin.Crismore

	Fix for touch scrubbing. Added default values back in. Added logic to only allow scrubbing when touch focus was off.

	#jira UE-61865

Change 4302294 by Jamie.Dale

	Added functions to get your the localized spoken and subtitle text from a dialogue wave

Change 4304393 by Jamie.Dale

	Added support for BlueprintAssignable properties in Python

Change 4305852 by Jamie.Dale

	Removed hard-dependency between EditorScriptingUtilities and PythonScriptPlugin

	Backed-out changelist 4259264 and query Python availability based on whether anything is available to handle the command

	#jira UE-62318

Change 4308550 by Jamie.Dale

	Fixed crash when passing a null world to Python actor iterators

Change 4311867 by Homam.Bahnassi

	Revit master material with exposed parameters matching the API when possible.

Change 4314428 by Francis.Hurteau

	Made the usage of the bBuildDeveloperTools switch independent of the bCompileAgainstEngine switch.
	Changed bBuildDeveloperTools TargetRule in UnrealBuildTool to a nullable to keep the old behavior in case where bBuildDeveloperTools wasn't explicitly set in TargetRules

Change 4315134 by Jamie.Dale

	Defer editable text focus selection until mouse-up to allow the user to make an initial selection

	#jira UE-58086

Change 4318615 by Johan.Duparc

	EditorFactories: consistent return values after asset import.

Change 4322459 by Jamie.Dale

	Made SequencerScripting an Editor plugin as it depends on PythonScriptPlugin which is an Editor plugin

	This was causing issues at runtime when SequencerScripting was enabled, as it failed to load PythonScriptPlugin (which hadn't been built).

Change 4323341 by Francis.Hurteau

	Implement proper message bus protocol version negociation with static nodes

Change 4323733 by Francis.Hurteau

	Fix VR Pausing Sequence Scrubbing just setting playback speed to 0.0

Change 4324319 by Jamie.Dale

	Exposed transactions to Blueprints

Change 4325847 by Alistair.White

	Copying //Tasks/UE4/Private-PixelStreaming@4325566 to Dev-Enterprise-Minimal (//UE4/Dev-Enterprise-Minimal)

	This adds the new experimental PixelStreaming plugin to allow streaming of an Unreal client's audio & video stream to a browser through the WebRTC protocol to support new uses for enterprise customers.

Change 4326282 by Simon.Tourangeau

	nDisplay native present handler

Change 4326581 by Jamie.Dale

	Replacing FDateTime with int64 Ticks value to workaround UE-63485

Change 4326599 by Homam.Bahnassi

	Moving texture coords outside UVEdit function to allow using different UV channels.

Change 4333250 by Francis.Hurteau

	Small TFuture changes:
	* cleans up TFuture::Then with usage of TUniqueFunction
	* added TFuture::Reset to invalidate it and remove continuation from a future shared state

Change 4333359 by Homam.Bahnassi

	Support scaling and rotating UVs around arbitrary pivot

Change 4333566 by Johan.Duparc

	Expose ProxyLOD functionalities to Scripting
	#jira UEENT-1788

Change 4333988 by Jamie.Dale

	Allow UHT to parse FText default parameter values

	INVTEXT, NSLOCTEXT, LOCTABLE, and FText::GetEmpty() are supported. LOCTEXT isn't as it relies on an external macro that is known to C++ but not to UHT (NSLOCTEXT can easily be used instead).

Change 4335020 by Francis.Hurteau

	Uncomment MessageBus::Send deprecation notice for 4.21
	Update MessageBus Send usage to new API

Change 4335195 by JeanMichel.Dignard

	Add a SetLodFromStaticMesh script utility function

	#jira UEENT-1789

Change 4335231 by Anousack.Kitisa

	Added functions to generate planar, cylindrical, box UV mapping.

	#jira UEENT-1598

Change 4335373 by Jamie.Dale

	Cleaned up some places creating empty literal texts

Change 4335458 by Jamie.Dale

	Allow UHT to parse FText() as an alias of FText::GetEmpty() when processing default values

Change 4335875 by Max.Chen

	Sequencer: Clear RF_Transient on pasted tracks/sections

	#jira UE-63537

Change 4336497 by Johan.Duparc

	ProxyLOD: Fix progress bar issue
	- removed duplicated code
	- removed duplicated LongTask object
	#jira UEENT-1788

Change 4336723 by Jamie.Dale

	Ensure that Python generated types create their CDO at the correct point

	#jira UE-62895

Change 4340594 by Ben.Marsh

	Fix manifest being invalidated when building two enterprise targets in a row. Fixes CIS error.

	#jira UE-63644

[CL 4342443 by JeanMichel Dignard in Main branch]
2018-09-04 16:35:02 -04:00

790 lines
23 KiB
JavaScript

// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.
//-- Server side logic. Serves pixel streaming WebRTC-based page, proxies data back to WebRTC proxy --//
var express = require('express');
var app = express();
const fs = require('fs');
const path = require('path');
const querystring = require('querystring');
const bodyParser = require('body-parser');
const logging = require('./modules/logging.js');
logging.RegisterConsoleLogger();
// Command line argument --configFile needs to be checked before loading the config, all other command line arguments are dealt with through the config object
const defaultConfig = {
UseFrontend: false,
UseMatchmaker: false,
UseHTTPS: false,
UseAuthentication: false,
LogToFile: true,
HomepageFile: 'player.htm',
AdditionalRoutes: new Map()
};
const argv = require('yargs').argv;
var configFile = (typeof argv.configFile != 'undefined') ? argv.configFile.toString() : '.\\config.json';
const config = require('./modules/config.js').init(configFile, defaultConfig)
if (config.LogToFile) {
logging.RegisterFileLogger('./logs');
}
console.log("Config: " + JSON.stringify(config, null, '\t'))
var http = require('http').Server(app);
if(config.UseHTTPS){
//HTTPS certificate details
const options = {
key: fs.readFileSync(path.join(__dirname, './certificates/client-key.pem')),
cert: fs.readFileSync(path.join(__dirname, './certificates/client-cert.pem'))
};
var https = require('https').Server(options, app);
var io = require('socket.io')(https);
} else {
var io = require('socket.io')(http);
}
//If not using authetication then just move on to the next function/middleware
var isAuthenticated = redirectUrl => function(req, res, next){ return next(); }
if(config.UseAuthentication && config.UseHTTPS){
var passport = require('passport');
require('./modules/authentication').init(app);
// Replace the isAuthenticated with the one setup on passport module
isAuthenticated = passport.authenticationMiddleware ? passport.authenticationMiddleware : isAuthenticated
} else if(config.UseAuthentication && !config.UseHTTPS) {
console.log('ERROR: Trying to use authentication without using HTTPS, this is not allowed and so authentication will NOT be turned on, please turn on HTTPS to turn on authentication');
}
const helmet = require('helmet');
var hsts = require('hsts');
var net = require('net');
var FRONTEND_WEBSERVER = 'https://localhost';
if(config.UseFrontend){
var httpPort = 3000;
var httpsPort = 8000;
//Required for self signed certs otherwise just get an error back when sending request to frontend see https://stackoverflow.com/a/35633993
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"
const httpsClient = require('./modules/httpsClient.js');
var webRequest = new httpsClient();
} else {
var httpPort = 80;
var httpsPort = 443;
}
var proxyPort = 8888; // port to listen to WebRTC proxy connections
var proxyBuffer = new Buffer(0);
var matchmakerAddress = '127.0.0.1';
var matchmakerPort = 9999;
var gameSessionId;
var userSessionId;
var serverPublicIp;
//Example of STUN server setting
//let clientConfig = {peerConnectionOptions: { 'iceServers': [{'urls': ['stun:34.250.222.95:19302']}] }};
var clientConfig = {peerConnectionOptions: {}};
// Parse public server address from command line
// --publicIp <public address>
try {
if(typeof config.publicIp != 'undefined'){
serverPublicIp = config.publicIp.toString();
}
if(typeof config.httpPort != 'undefined'){
httpPort = config.httpPort;
}
if(typeof config.httpsPort != 'undefined'){
httpsPort = config.httpsPort;
}
if(typeof config.proxyPort != 'undefined'){
proxyPort = config.proxyPort;
}
if(typeof config.frontendUrl != 'undefined'){
FRONTEND_WEBSERVER = config.frontendUrl;
}
if(typeof config.peerConnectionOptions != 'undefined'){
clientConfig.peerConnectionOptions = JSON.parse(config.peerConnectionOptions);
console.log(`peerConnectionOptions = ${JSON.stringify(clientConfig.peerConnectionOptions)}`);
}
if (typeof config.matchmakerAddress != 'undefined') {
matchmakerAddress = config.matchmakerAddress;
}
if (typeof config.matchmakerPort != 'undefined') {
matchmakerPort = config.matchmakerPort;
}
} catch (e) {
console.error(e);
process.exit(2);
}
if(config.UseHTTPS){
app.use(helmet());
app.use(hsts({
maxAge: 15552000 // 180 days in seconds
}));
//Setup http -> https redirect
console.log('Redirecting http->https');
app.use(function (req, res, next) {
if (!req.secure) {
if(req.get('Host')){
var hostAddressParts = req.get('Host').split(':');
var hostAddress = hostAddressParts[0];
if(httpsPort != 443) {
hostAddress = `${hostAddress}:${httpsPort}`;
}
return res.redirect(['https://', hostAddress, req.originalUrl].join(''));
} else {
console.log(`ERROR unable to get host name from header. Requestor ${req.ip}, url path: '${req.originalUrl}', available headers ${JSON.stringify(req.headers)}`);
return res.status(400).send('Bad Request');
}
}
next();
});
}
sendGameSessionData();
//Setup folders
app.use(express.static(path.join(__dirname, '/public')))
app.use('/images', express.static(path.join(__dirname, './images')))
app.use('/scripts', [isAuthenticated('/login'), express.static(path.join(__dirname, '/scripts'))]);
app.use('/', [isAuthenticated('/login'), express.static(path.join(__dirname, '/custom_html'))])
try{
for (var property in config.AdditionalRoutes) {
if (config.AdditionalRoutes.hasOwnProperty(property)) {
console.log(`Adding additional routes "${property}" -> "${config.AdditionalRoutes[property]}"`)
app.use(property, [isAuthenticated('/login'), express.static(path.join(__dirname, config.AdditionalRoutes[property]))]);
}
}
} catch(err) {
console.log(`Error reading config.AdditionalRoutes: ${err}`)
}
app.get('/', isAuthenticated('/login'), function(req, res){
homepageFile = (typeof config.HomepageFile != 'undefined' && config.HomepageFile != '') ? config.HomepageFile.toString() : defaultConfig.HomepageFile;
homepageFilePath = path.join(__dirname, homepageFile)
fs.access(homepageFilePath, (err) => {
if (err) {
console.log('Unable to locate file ' + homepageFilePath)
res.status(404).send('Unable to locate file ' + homepageFile);
}
else {
res.sendFile(homepageFilePath);
}
});
});
//Setup the login page if we are using authentication
if(config.UseAuthentication){
app.get('/login', function(req, res){
res.sendFile(__dirname + '/login.htm');
});
// create application/x-www-form-urlencoded parser
var urlencodedParser = bodyParser.urlencoded({ extended: false })
//login page form data is posted here
app.post('/login',
urlencodedParser,
passport.authenticate('local', { failureRedirect: '/login' }),
function(req, res){
//On success try to redirect to the page that they originally tired to get to, default to '/' if no redirect was found
var redirectTo = req.session.redirectTo ? req.session.redirectTo : '/';
delete req.session.redirectTo;
console.log(`Redirecting to: '${redirectTo}'`);
res.redirect(redirectTo);
}
);
}
/*
app.get('/:sessionId', isAuthenticated('/login'), function(req, res){
let sessionId = req.params.sessionId;
console.log(sessionId);
//For now don't verify session id is valid, just send player.htm if they get the right server
res.sendFile(__dirname + '/player.htm');
});
*/
/*
app.get('/custom_html/:htmlFilename', isAuthenticated('/login'), function(req, res){
let htmlFilename = req.params.htmlFilename;
let htmlPathname = __dirname + '/custom_html/' + htmlFilename;
console.log(htmlPathname);
fs.access(htmlPathname, (err) => {
if (err) {
res.status(404).send('Unable to locate file ' + htmlPathname);
}
else {
res.sendFile(htmlPathname);
}
});
});
*/
let clients = []; // either web-browsers or native webrtc receivers
let nextClientId = 100;
let proxySocket;
function cleanUpProxyConnection() {
if(proxySocket){
proxySocket.end();
proxySocket = undefined;
proxyBuffer = new Buffer(0);
// make a copy of `clients` array as it will be modified in the loop
let clientsCopy = clients.slice();
clientsCopy.forEach(function (c) {
c.ws.disconnect();
});
}
}
let proxyListener = net.createServer(function(socket) {
// 'connection' listener
console.log('proxy connected');
socket.setNoDelay();
socket.on('data', function (data) {
proxyBuffer = Buffer.concat([proxyBuffer, data]);
// WebRTC proxy uses json messages instead of binary blob so need to read messages differently
while (handleProxyMessage(socket)) { }
});
socket.on('end', function () {
console.log('proxy connection end');
cleanUpProxyConnection();
});
socket.on('disconnect', function () {
console.log('proxy disconnected');
cleanUpProxyConnection();
});
socket.on('close', function() {
sendServerDisconnect();
console.log('proxy connection closed');
proxySocket = undefined;
});
socket.on('error', function (error) {
console.log(`proxy connection error ${JSON.stringify(error)}`);
cleanUpProxyConnection();
});
proxySocket = socket;
sendConfigToProxy();
});
proxyListener.maxConnections = 1;
proxyListener.listen(proxyPort, () => {
console.log('Listening to proxy connections on: ' + proxyPort);
});
// Must be kept in sync with PixelStreamingProtocol::EProxyToCirrusMsg C++ enum.
const EProxyToCirrusMsg = {
answer: 0, // [msgId:1][clientId:4][size:4][string:size]
iceCandidate: 1, // [msgId:1][clientId:4][size:4][string:size]
disconnectClient: 2 // [msgId:1][clientId:4]
}
// Must be kept in sync with PixelStreamingProtocol::ECirrusToProxyMsg C++ enum.
const ECirrusToProxyMsg = {
offer: 0, // [msgId: 1][clientId:4][size:4][string:size]
iceCandidate: 1, // [msgId:1][clientId:4][size:4][string:size]
clientDisconnected: 2, // [msgId:1][clientId:4]
config: 3 // [msgId:1][size:4][config:size]
}
function readJsonMsg(consumed) {
// format: [size:4][string:size]
if (proxyBuffer.length < consumed + 4)
return [0, ""];
let msgSize = proxyBuffer.readUInt32LE(consumed);
consumed += 4;
if (proxyBuffer.length < consumed + msgSize)
return [0, ""];
let msg = proxyBuffer.toString('ascii', consumed, consumed + msgSize);
consumed += msgSize;
return [consumed, JSON.parse(msg)];
}
function handleProxyMessage(socket) {
// msgId
if(proxyBuffer.length == 0)
return false;
let msgId = proxyBuffer.readUInt8(0);
let consumed = 1;
// clientId
if (proxyBuffer.length < consumed + 4)
return false;
let clientId = proxyBuffer.readUInt32LE(consumed);
consumed += 4;
let client = clients.find(function(c) { return c.id == clientId; });
if (!client) {
// Client is likely no longer connected, but this can also occur if bad data is recieved, this can not be validated as yet so assume former
console.error(`proxy message ${msgId}: client ${clientId} not found. Check proxy->cirrus protocol consistency`);
}
switch (msgId) {
case EProxyToCirrusMsg.answer: // fall through
case EProxyToCirrusMsg.iceCandidate:
let [localConsumed, msg] = readJsonMsg(consumed);
if (localConsumed == 0)
return false;
consumed = localConsumed;
if(client){
switch (msgId)
{
case EProxyToCirrusMsg.answer:
console.log(`answer -> client ${clientId}`);
client.ws.emit('webrtc-answer', msg);
break;
case EProxyToCirrusMsg.iceCandidate:
console.log(`ICE candidate -> client ${clientId}`);
client.ws.emit('webrtc-ice', msg);
break;
default:
throw "unhandled case, check all \"fall through\" cases from above";
}
}
break;
case EProxyToCirrusMsg.disconnectClient:
console.warn(`Proxy instructed to disconnect client ${clientId}`);
if(client){
client.ws.onclose = function() {};
client.ws.disconnect(true);
let idx = clients.map(function(p) { return p.id; }).indexOf(clientId);
clients.splice(idx, 1); // remove it
sendClientDisconnectedToProxy(clientId);
}
break;
default:
console.error(`Invalid message id ${msgId} from proxy`);
cleanUpProxyConnection();
return false;
}
proxyBuffer = proxyBuffer.slice(consumed);
return true;
}
function sendConfigToProxy() {
// [msgId:1][size:4][string:size]
if (!proxySocket)
return false;
let cfg = {};
cfg.peerConnectionConfig = clientConfig.peerConnectionOptions;
let msg = JSON.stringify(cfg);
console.log(`config to Proxy: ${msg}`);
let data = new DataView(new ArrayBuffer(1 + 4 + msg.length));
data.setUint8(0, ECirrusToProxyMsg.config);
data.setUint32(1, msg.length, true);
for (let i = 0; i != msg.length; ++i)
data.setUint8(1 + 4 + i, msg.charCodeAt(i));
proxySocket.write(Buffer.from(data.buffer));
return true;
}
function sendClientDisconnectedToProxy(clientId) {
// [msgId:1][clientId:4]
if (!proxySocket)
return;
let data = new DataView(new ArrayBuffer(1 + 4));
data.setUint8(0, ECirrusToProxyMsg.clientDisconnected);
data.setUint32(1, clientId, true);
proxySocket.write(Buffer.from(data.buffer));
}
function sendStringMsgToProxy(msgId, clientId, msg) {
// [msgId:1][clientId:4][size:4][string:size]
if (!proxySocket)
return false;
let data = new DataView(new ArrayBuffer(1 + 4 + 4 + msg.length));
data.setUint8(0, msgId);
data.setUint32(1, clientId, true);
data.setUint32(1 + 4, msg.length, true);
for (let i = 0; i != msg.length; ++i)
data.setUint8(1 + 4 + 4 + i, msg.charCodeAt(i));
proxySocket.write(Buffer.from(data.buffer));
return true;
}
function sendOfferToProxy(clientId, offer) {
sendStringMsgToProxy(ECirrusToProxyMsg.offer, clientId, offer);
}
function sendIceCandidateToProxy(clientId, iceCandidate) {
sendStringMsgToProxy(ECirrusToProxyMsg.iceCandidate, clientId, iceCandidate);
}
/**
* Function that handles the connection to the matchmaker.
*/
if (config.UseMatchmaker) {
var matchmaker = net.connect(matchmakerPort, matchmakerAddress, () => {
console.log(`Cirrus connected to Matchmaker ${matchmakerAddress}:${matchmakerPort}`);
message = {
type: 'connect',
address: typeof serverPublicIp === 'undefined' ? '127.0.0.1' : serverPublicIp,
port: httpPort
};
matchmaker.write(JSON.stringify(message));
});
matchmaker.on('error', () => {
console.log('Cirrus disconnected from matchmaker');
});
}
/**
* Function that handles an incoming client connection.
*/
function handleNewClient(ws) {
// NOTE: This needs to be the first thing to be sent
ws.emit('clientConfig', clientConfig);
var clientId = ++nextClientId;
console.log(`client ${clientId} (${ws.request.connection.remoteAddress}) connected`);
clients.push({ws: ws, id: clientId});
// Send client counts to all connected clients
ws.emit('clientCount', {count: clients.length - 1});
clients.forEach(function(c){
if(c.id == clientId)
return;
c.ws.emit('clientCount', {count: clients.length - 1});
});
ws.on('userConfig', function(userConfig) {
receiveUserConfig(clientId, userConfig, ws);
});
/**
* This is where events received from client are translated
* and sent on to the proxy socket
*/
ws.on('message', function (msg) {
console.error(`client #${clientId}: unexpected msg "${msg}"`);
});
ws.on('kick', function(msg){
// make a copy of `clients` cos the array will be modified in the loop
let clientsCopy = clients.slice();
clientsCopy.forEach(function(c){
if(c.id == clientId)
return;
console.log('Kicking client ' + c.id);
c.ws.disconnect();
})
ws.emit('clientCount', {count: 0});
})
var removeClient = function() {
let idx = clients.map(function(c) { return c.ws; }).indexOf(ws);
let clientId = clients[idx].id;
clients.splice(idx, 1); // remove it
sendClientDisconnectedToProxy(clientId);
sendClientDisconnectedToFrontend();
sendClientDisconnectedToMatchmaker();
}
ws.on('disconnect', function () {
console.log(`client ${clientId} disconnected`);
removeClient();
});
ws.on('close', function (code, reason) {
console.log(`client ${clientId} connection closed: ${code} - ${reason}`);
removeClient();
});
ws.on('error', function (err) {
console.log(`client ${clientId} connection error: ${err}`);
removeClient();
});
};
/**
* Config data received from the web browser or device native client.
*/
function receiveUserConfig(clientId, userConfigString, ws) {
console.log(`client ${clientId}: userConfig = ${userConfigString}`);
userConfig = JSON.parse(userConfigString)
// Check the sort of data the web browser or device native client will send.
switch (userConfig.emitData)
{
case "ArrayBuffer":
{
ws.on('webrtc-offer', function(offer) {
console.log(`offer <- client ${clientId}`);
sendOfferToProxy(clientId, offer);
});
ws.on('webrtc-ice', function(candidate) {
console.log(`ICE candidate <- client ${clientId}`);
sendIceCandidateToProxy(clientId, candidate);
});
ws.on('webrtc-stats', function(stats){
console.log(`Received webRTC stats from player ID: ${clientId} \r\n${JSON.stringify(stats)}`);
});
break;
}
case "Array":
{
//TODO: this is untested as requires iOS WebRTC integration
ws.on('webrtc-offer', function(offer) {
console.log(`offer <- client ${clientId}`);
sendOfferToProxy(clientId, offer);
});
ws.on('webrtc-ice', function(candidate) {
console.log(`ICE candidate <- client ${clientId}`);
sendIceCandidateToProxy(clientId, candidate);
});
ws.on('webrtc-stats', function(stats){
console.log(`Received webRTC stats from player ID: ${clientId} \r\n${JSON.stringify(stats)}`);
});
break;
}
default:
{
console.log(`Unknown user config emit data type ${userConfig.emitData}`);
break;
}
}
}
//IO events
io.on('connection', function (ws) {
// Reject connection if proxy is not connected
if (!proxySocket) {
ws.disconnect();
return;
}
handleNewClient(ws);
sendClientConnectedToFrontend();
sendClientConnectedToMatchmaker();
});
//Setup http and https servers
http.listen(httpPort, function () {
console.logColor(logging.Green, 'Http listening on *: ' + httpPort);
});
if(config.UseHTTPS){
https.listen(httpsPort, function () {
console.logColor(logging.Green, 'Https listening on *: ' + httpsPort);
});
}
//Keep trying to send gameSessionId in case the server isn't ready yet
function sendGameSessionData(){
//If we are not using the frontend web server don't try and make requests to it
if(!config.UseFrontend)
return;
webRequest.get(`${FRONTEND_WEBSERVER}/server/requestSessionId`,
function(response, body) {
if(response.statusCode === 200){
gameSessionId = body;
console.log('SessionId: ' + gameSessionId);
}
else{
console.log('Status code: ' + response.statusCode);
console.log(body);
}
},
function(err){
//Repeatedly try in cases where the connection timed out or never connected
if (err.code === "ECONNRESET") {
//timeout
sendGameSessionData();
} else if(err.code === 'ECONNREFUSED') {
console.log('Frontend server not running, unable to setup game session');
} else {
console.log(err);
}
});
}
function sendUserSessionData(serverPort){
//If we are not using the frontend web server don't try and make requests to it
if(!config.UseFrontend)
return;
webRequest.get(`${FRONTEND_WEBSERVER}/server/requestUserSessionId?gameSessionId=${gameSessionId}&serverPort=${serverPort}&appName=${querystring.escape(clientConfig.AppName)}&appDescription=${querystring.escape(clientConfig.AppDescription)}${(typeof serverPublicIp === 'undefined' ? '' : '&serverHost=' + serverPublicIp)}`,
function(response, body) {
if(response.statusCode === 410){
sendUserSessionData(serverPort);
}else if(response.statusCode === 200){
userSessionId = body;
console.log('UserSessionId: ' + userSessionId);
} else {
console.log('Status code: ' + response.statusCode);
console.log(body);
}
},
function(err){
//Repeatedly try in cases where the connection timed out or never connected
if (err.code === "ECONNRESET") {
//timeout
sendUserSessionData(serverPort);
} else if(err.code === 'ECONNREFUSED') {
console.log('Frontend server not running, unable to setup user session');
} else {
console.log(err);
}
});
}
function sendServerDisconnect(){
//If we are not using the frontend web server don't try and make requests to it
if(!config.UseFrontend)
return;
webRequest.get(`${FRONTEND_WEBSERVER}/server/serverDisconnected?gameSessionId=${gameSessionId}&appName=${querystring.escape(clientConfig.AppName)}`,
function(response, body) {
if(response.statusCode === 200){
console.log('serverDisconnected acknowledged by Frontend');
} else {
console.log('Status code: ' + response.statusCode);
console.log(body);
}
},
function(err){
//Repeatedly try in cases where the connection timed out or never connected
if (err.code === "ECONNRESET") {
//timeout
sendServerDisconnect();
} else if(err.code === 'ECONNREFUSED') {
console.log('Frontend server not running, unable to setup user session');
} else {
console.log(err);
}
});
}
function sendClientConnectedToFrontend(){
//If we are not using the frontend web server don't try and make requests to it
if(!config.UseFrontend)
return;
webRequest.get(`${FRONTEND_WEBSERVER}/server/clientConnected?gameSessionId=${gameSessionId}&appName=${querystring.escape(clientConfig.AppName)}`,
function(response, body) {
if(response.statusCode === 200){
console.log('clientConnected acknowledged by Frontend');
}
else{
console.log('Status code: ' + response.statusCode);
console.log(body);
}
},
function(err){
//Repeatedly try in cases where the connection timed out or never connected
if (err.code === "ECONNRESET") {
//timeout
sendClientConnectedToFrontend();
} else if(err.code === 'ECONNREFUSED') {
console.log('Frontend server not running, unable to setup game session');
} else {
console.log(err);
}
});
}
function sendClientDisconnectedToFrontend(){
//If we are not using the frontend web server don't try and make requests to it
if(!config.UseFrontend)
return;
webRequest.get(`${FRONTEND_WEBSERVER}/server/clientDisconnected?gameSessionId=${gameSessionId}&appName=${querystring.escape(clientConfig.AppName)}`,
function(response, body) {
if(response.statusCode === 200){
console.log('clientDisconnected acknowledged by Frontend');
}
else{
console.log('Status code: ' + response.statusCode);
console.log(body);
}
},
function(err){
//Repeatedly try in cases where the connection timed out or never connected
if (err.code === "ECONNRESET") {
//timeout
sendClientDisconnectedEvent();
} else if(err.code === 'ECONNREFUSED') {
console.log('Frontend server not running, unable to setup game session');
} else {
console.log(err);
}
});
}
// The Matchmaker will not re-direct clients to this Cirrus server if any client
// is connected.
function sendClientConnectedToMatchmaker() {
if (!config.UseMatchmaker)
return;
message = {
type: 'clientConnected'
};
matchmaker.write(JSON.stringify(message));
}
// The Matchmaker is interested when nobody is connected to a Cirrus server
// because then it can re-direct clients to this re-cycled Cirrus server.
function sendClientDisconnectedToMatchmaker() {
if (!config.UseMatchmaker)
return;
message = {
type: 'clientDisconnected'
};
matchmaker.write(JSON.stringify(message));
}