You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
#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]
790 lines
23 KiB
JavaScript
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));
|
|
} |