Files
UnrealEngineUWP/Engine/Source/Programs/PixelStreaming/WebServers/SignallingWebServer/scripts/app.js
marc audy 01986ae949 Merge in PixelStreaming, AVEncoder, and SignallingServer changes from TensorWorks local repository
Brings in multiple improvements such as parameterisation of many encoder settings, AMF encoder, better pixel streaming stats and some bugfixes

#JIRA
#rb self
[FYI] marco.anastasi, luke.bermingham, matthew.cotton, aidan.possemiers

#ROBOMERGE-OWNER: marc.audy
#ROBOMERGE-AUTHOR: nick.pace
#ROBOMERGE-SOURCE: CL 16433855 in //UE4/Release-4.27/... via CL 16433858
#ROBOMERGE-BOT: STARSHIP (Main -> Release-Engine-Staging) (v814-16434418)
#ROBOMERGE-CONFLICT from-shelf

[CL 16435808 by marc audy in ue5-release-engine-staging branch]
2021-05-24 11:13:14 -04:00

1723 lines
55 KiB
JavaScript

// Copyright Epic Games, Inc. All Rights Reserved.
var webRtcPlayerObj = null;
var print_stats = false;
var print_inputs = false;
var connect_on_load = false;
var is_reconnection = false;
var ws;
const WS_OPEN_STATE = 1;
var qualityControlOwnershipCheckBox;
var matchViewportResolution;
// TODO: Remove this - workaround because of bug causing UE to crash when switching resolutions too quickly
var lastTimeResized = new Date().getTime();
var resizeTimeout;
var onDataChannelConnected;
var responseEventListeners = new Map();
var freezeFrameOverlay = null;
var shouldShowPlayOverlay = true;
// A freeze frame is a still JPEG image shown instead of the video.
var freezeFrame = {
receiving: false,
size: 0,
jpeg: undefined,
height: 0,
width: 0,
valid: false
};
// Optionally detect if the user is not interacting (AFK) and disconnect them.
var afk = {
enabled: false, // Set to true to enable the AFK system.
warnTimeout: 120, // The time to elapse before warning the user they are inactive.
closeTimeout: 10, // The time after the warning when we disconnect the user.
active: false, // Whether the AFK system is currently looking for inactivity.
overlay: undefined, // The UI overlay warning the user that they are inactive.
warnTimer: undefined, // The timer which waits to show the inactivity warning overlay.
countdown: 0, // The inactivity warning overlay has a countdown to show time until disconnect.
countdownTimer: undefined, // The timer used to tick the seconds shown on the inactivity warning overlay.
}
// If the user focuses on a UE4 input widget then we show them a button to open
// the on-screen keyboard. JavaScript security means we can only show the
// on-screen keyboard in response to a user interaction.
var editTextButton = undefined;
// A hidden input text box which is used only for focusing and opening the
// on-screen keyboard.
var hiddenInput = undefined;
var t0 = Date.now();
function log(str) {
console.log(`${Math.floor(Date.now() - t0)}: ` + str);
}
function setupHtmlEvents() {
//Window events
window.addEventListener('resize', resizePlayerStyle, true);
window.addEventListener('orientationchange', onOrientationChange);
//HTML elements controls
let overlayButton = document.getElementById('overlayButton');
overlayButton.addEventListener('click', onExpandOverlay_Click);
let resizeCheckBox = document.getElementById('enlarge-display-to-fill-window-tgl');
if (resizeCheckBox !== null) {
resizeCheckBox.onchange = function (event) {
resizePlayerStyle();
};
}
qualityControlOwnershipCheckBox = document.getElementById('quality-control-ownership-tgl');
if (qualityControlOwnershipCheckBox !== null) {
qualityControlOwnershipCheckBox.onchange = function (event) {
requestQualityControl();
};
}
let prioritiseQualityCheckbox = document.getElementById('prioritise-quality-tgl');
let qualityParamsSubmit = document.getElementById('quality-params-submit');
if (prioritiseQualityCheckbox !== null) {
prioritiseQualityCheckbox.onchange = function (event) {
if (prioritiseQualityCheckbox.checked) {
// TODO: This state should be read from the UE Application rather than from the initial values in the HTML
let lowBitrate = document.getElementById('low-bitrate-text').value;
let highBitrate = document.getElementById('high-bitrate-text').value;
let minFPS = document.getElementById('min-fps-text').value;
let initialDescriptor = {
PrioritiseQuality: 1,
LowBitrate: lowBitrate,
HighBitrate: highBitrate,
MinFPS: minFPS
};
// TODO: The descriptor should be sent as is to a generic handler on the UE side
// but for now we're just sending it as separate console commands
//emitUIInteraction(initialDescriptor);
sendQualityConsoleCommands(initialDescriptor);
console.log(initialDescriptor);
qualityParamsSubmit.onclick = function (event) {
let lowBitrate = document.getElementById('low-bitrate-text').value;
let highBitrate = document.getElementById('high-bitrate-text').value;
let minFPS = document.getElementById('min-fps-text').value;
let descriptor = {
PrioritiseQuality: 1,
LowBitrate: lowBitrate,
HighBitrate: highBitrate,
MinFPS: minFPS
};
//emitUIInteraction(descriptor);
sendQualityConsoleCommands(descriptor);
console.log(descriptor);
};
} else { // Prioritise Quality unchecked
let initialDescriptor = {
PrioritiseQuality: 0
};
//emitUIInteraction(initialDescriptor);
sendQualityConsoleCommands(initialDescriptor);
console.log(initialDescriptor);
qualityParamsSubmit.onclick = null;
}
};
}
// Rate control drop down selection
let rateControlSelect = document.getElementById('rate-control-dropdown')
if (rateControlSelect !== null) {
rateControlSelect.onchange = function (event) {
var selectedValue = event.target.value;
sendRateControlConsoleCommand(selectedValue);
}
}
// Minimum quality prioritisation spinner
let minqpSpinner = document.getElementById('minqp-number')
if (minqpSpinner !== null) {
minqpSpinner.onchange = function (event) {
var selectedValue = event.target.value;
sendMinQPConsoleCommand(selectedValue);
}
}
let showFPSButton = document.getElementById('show-fps-button');
if (showFPSButton !== null) {
showFPSButton.onclick = function (event) {
let consoleDescriptor = {
Console: 'stat fps'
};
emitUIInteraction(consoleDescriptor);
};
}
let matchViewportResolutionCheckBox = document.getElementById('match-viewport-res-tgl');
if (matchViewportResolutionCheckBox !== null) {
matchViewportResolutionCheckBox.onchange = function (event) {
matchViewportResolution = matchViewportResolutionCheckBox.checked;
};
}
let statsCheckBox = document.getElementById('show-stats-tgl');
if (statsCheckBox !== null) {
statsCheckBox.onchange = function (event) {
let stats = document.getElementById('statsContainer');
stats.style.display = event.target.checked ? "block" : "none";
};
}
var kickButton = document.getElementById('kick-other-players-button');
if (kickButton) {
kickButton.onclick = function (event) {
console.log(`-> SS: kick`);
ws.send(JSON.stringify({ type: 'kick' }));
};
}
}
function sendStartLatencyTest() {
// We need WebRTC to be active to do a latency test.
if(!webRtcPlayerObj)
{
return;
}
let onTestStarted = function(StartTimeMs)
{
let descriptor = {
StartTime: StartTimeMs
};
emitDescriptor(MessageType.LatencyTest, descriptor);
};
webRtcPlayerObj.startLatencyTest(onTestStarted);
}
function sendMinQPConsoleCommand(minQP) {
if(minQP) {
let command = 'PixelStreaming.Encoder.MinQP ' + minQP;
let consoleDescriptor = {
Console: command
};
emitUIInteraction(consoleDescriptor);
}
}
function sendRateControlConsoleCommand(rateControlMode) {
if(rateControlMode) {
let command = 'PixelStreaming.Encoder.RateControl ' + rateControlMode;
let consoleDescriptor = {
Console: command
};
emitUIInteraction(consoleDescriptor);
}
}
function sendQualityConsoleCommands(descriptor) {
if (descriptor.PrioritiseQuality !== null) {
let command = 'PixelStreaming.Encoder.PrioritizeQuality ' + descriptor.PrioritiseQuality;
let consoleDescriptor = {
Console: command
};
emitUIInteraction(consoleDescriptor);
}
if (descriptor.LowBitrate !== null) {
let command = 'PixelStreaming.Encoder.LowBitrate ' + descriptor.LowBitrate;
let consoleDescriptor = {
Console: command
};
emitUIInteraction(consoleDescriptor);
}
if (descriptor.HighBitrate !== null) {
let command = 'PixelStreaming.Encoder.HighBitrate ' + descriptor.HighBitrate;
let consoleDescriptor = {
Console: command
};
emitUIInteraction(consoleDescriptor);
}
if (descriptor.MinFPS !== null) {
var command = 'PixelStreaming.Encoder.MinFPS ' + descriptor.MinFPS;
let consoleDescriptor = {
Console: command
};
emitUIInteraction(consoleDescriptor);
}
}
function setOverlay(htmlClass, htmlElement, onClickFunction) {
var videoPlayOverlay = document.getElementById('videoPlayOverlay');
if (!videoPlayOverlay) {
var playerDiv = document.getElementById('player');
videoPlayOverlay = document.createElement('div');
videoPlayOverlay.id = 'videoPlayOverlay';
playerDiv.appendChild(videoPlayOverlay);
}
// Remove existing html child elements so we can add the new one
while (videoPlayOverlay.lastChild) {
videoPlayOverlay.removeChild(videoPlayOverlay.lastChild);
}
if (htmlElement)
videoPlayOverlay.appendChild(htmlElement);
if (onClickFunction) {
videoPlayOverlay.addEventListener('click', function onOverlayClick(event) {
onClickFunction(event);
videoPlayOverlay.removeEventListener('click', onOverlayClick);
});
}
// Remove existing html classes so we can set the new one
var cl = videoPlayOverlay.classList;
for (var i = cl.length - 1; i >= 0; i--) {
cl.remove(cl[i]);
}
videoPlayOverlay.classList.add(htmlClass);
}
function showConnectOverlay() {
var startText = document.createElement('div');
startText.id = 'playButton';
startText.innerHTML = 'Click to start';
setOverlay('clickableState', startText, event => {
connect();
startAfkWarningTimer();
});
}
function showTextOverlay(text) {
var textOverlay = document.createElement('div');
textOverlay.id = 'messageOverlay';
textOverlay.innerHTML = text ? text : '';
setOverlay('textDisplayState', textOverlay);
}
function showPlayOverlay() {
var img = document.createElement('img');
img.id = 'playButton';
img.src = '/images/Play.png';
img.alt = 'Start Streaming';
setOverlay('clickableState', img, event => {
if (webRtcPlayerObj)
webRtcPlayerObj.video.play();
requestQualityControl();
showFreezeFrameOverlay();
hideOverlay();
});
shouldShowPlayOverlay = false;
}
function updateAfkOverlayText() {
afk.overlay.innerHTML = '<center>No activity detected<br>Disconnecting in ' + afk.countdown + ' seconds<br>Click to continue<br></center>';
}
function showAfkOverlay() {
// Pause the timer while the user is looking at the inactivity warning overlay.
stopAfkWarningTimer();
// Show the inactivity warning overlay.
afk.overlay = document.createElement('div');
afk.overlay.id = 'afkOverlay';
setOverlay('clickableState', afk.overlay, event => {
// The user clicked so start the timer again and carry on.
hideOverlay();
clearInterval(afk.countdownTimer);
startAfkWarningTimer();
});
afk.countdown = afk.closeTimeout;
updateAfkOverlayText();
if (inputOptions.controlScheme == ControlSchemeType.LockedMouse) {
document.exitPointerLock();
}
afk.countdownTimer = setInterval(function () {
afk.countdown--;
if (afk.countdown == 0) {
// The user failed to click so disconnect them.
hideOverlay();
ws.close();
} else {
// Update the countdown message.
updateAfkOverlayText();
}
}, 1000);
}
function hideOverlay() {
setOverlay('hiddenState');
}
// Start a timer which when elapsed will warn the user they are inactive.
function startAfkWarningTimer() {
afk.active = afk.enabled;
resetAfkWarningTimer();
}
// Stop the timer which when elapsed will warn the user they are inactive.
function stopAfkWarningTimer() {
afk.active = false;
}
// If the user interacts then reset the warning timer.
function resetAfkWarningTimer() {
if (afk.active) {
clearTimeout(afk.warnTimer);
afk.warnTimer = setTimeout(function () {
showAfkOverlay();
}, afk.warnTimeout * 1000);
}
}
function createWebRtcOffer() {
if (webRtcPlayerObj) {
console.log('Creating offer');
showTextOverlay('Starting connection to server, please wait');
webRtcPlayerObj.createOffer();
} else {
console.log('WebRTC player not setup, cannot create offer');
showTextOverlay('Unable to setup video');
}
}
function sendInputData(data) {
if (webRtcPlayerObj) {
resetAfkWarningTimer();
webRtcPlayerObj.send(data);
}
}
function addResponseEventListener(name, listener) {
responseEventListeners.set(name, listener);
}
function removeResponseEventListener(name) {
responseEventListeners.remove(name);
}
// Must be kept in sync with PixelStreamingProtocol::EToPlayerMsg C++ enum.
const ToClientMessageType = {
QualityControlOwnership: 0,
Response: 1,
Command: 2,
FreezeFrame: 3,
UnfreezeFrame: 4,
VideoEncoderAvgQP: 5,
LatencyTest: 6
};
var VideoEncoderQP = "N/A";
function setupWebRtcPlayer(htmlElement, config) {
webRtcPlayerObj = new webRtcPlayer({ peerConnectionOptions: config.peerConnectionOptions });
htmlElement.appendChild(webRtcPlayerObj.video);
htmlElement.appendChild(freezeFrameOverlay);
webRtcPlayerObj.onWebRtcOffer = function (offer) {
if (ws && ws.readyState === WS_OPEN_STATE) {
let offerStr = JSON.stringify(offer);
console.log(`-> SS: offer:\n${offerStr}`);
ws.send(offerStr);
}
};
webRtcPlayerObj.onWebRtcCandidate = function (candidate) {
if (ws && ws.readyState === WS_OPEN_STATE) {
console.log(`-> SS: iceCandidate\n${JSON.stringify(candidate, undefined, 4)}`);
ws.send(JSON.stringify({ type: 'iceCandidate', candidate: candidate }));
}
};
webRtcPlayerObj.onVideoInitialised = function () {
if (ws && ws.readyState === WS_OPEN_STATE) {
if (shouldShowPlayOverlay) {
showPlayOverlay();
resizePlayerStyle();
}
}
};
webRtcPlayerObj.onDataChannelConnected = function () {
if (ws && ws.readyState === WS_OPEN_STATE) {
showTextOverlay('WebRTC connected, waiting for video');
if(webRtcPlayerObj.video && webRtcPlayerObj.video.srcObject && webRtcPlayerObj.onVideoInitialised){
webRtcPlayerObj.onVideoInitialised();
}
}
};
function showFreezeFrame() {
let base64 = btoa(freezeFrame.jpeg.reduce((data, byte) => data + String.fromCharCode(byte), ''));
freezeFrameOverlay.src = 'data:image/jpeg;base64,' + base64;
freezeFrameOverlay.onload = function () {
freezeFrame.height = freezeFrameOverlay.naturalHeight;
freezeFrame.width = freezeFrameOverlay.naturalWidth;
resizeFreezeFrameOverlay();
if (shouldShowPlayOverlay) {
showPlayOverlay();
resizePlayerStyle();
} else {
showFreezeFrameOverlay();
}
};
}
webRtcPlayerObj.onDataChannelMessage = function (data) {
var view = new Uint8Array(data);
if (freezeFrame.receiving) {
let jpeg = new Uint8Array(freezeFrame.jpeg.length + view.length);
jpeg.set(freezeFrame.jpeg, 0);
jpeg.set(view, freezeFrame.jpeg.length);
freezeFrame.jpeg = jpeg;
if (freezeFrame.jpeg.length === freezeFrame.size) {
freezeFrame.receiving = false;
freezeFrame.valid = true;
console.log(`received complete freeze frame ${freezeFrame.size}`);
showFreezeFrame();
} else if (freezeFrame.jpeg.length > freezeFrame.size) {
console.error(`received bigger freeze frame than advertised: ${freezeFrame.jpeg.length}/${freezeFrame.size}`);
freezeFrame.jpeg = undefined;
freezeFrame.receiving = false;
} else {
console.log(`received next chunk (${view.length} bytes) of freeze frame: ${freezeFrame.jpeg.length}/${freezeFrame.size}`);
}
} else if (view[0] === ToClientMessageType.QualityControlOwnership) {
let ownership = view[1] === 0 ? false : true;
console.log("Received quality controller message, will control quality: " + ownership);
// If we own the quality control, we can't relenquish it. We only loose
// quality control when another peer asks for it
if (qualityControlOwnershipCheckBox !== null) {
qualityControlOwnershipCheckBox.disabled = ownership;
qualityControlOwnershipCheckBox.checked = ownership;
}
} else if (view[0] === ToClientMessageType.Response) {
let response = new TextDecoder("utf-16").decode(data.slice(1));
for (let listener of responseEventListeners.values()) {
listener(response);
}
} else if (view[0] === ToClientMessageType.Command) {
let commandAsString = new TextDecoder("utf-16").decode(data.slice(1));
console.log(commandAsString);
let command = JSON.parse(commandAsString);
if (command.command === 'onScreenKeyboard') {
showOnScreenKeyboard(command);
}
} else if (view[0] === ToClientMessageType.FreezeFrame) {
freezeFrame.size = (new DataView(view.slice(1, 5).buffer)).getInt32(0, true);
freezeFrame.jpeg = view.slice(1 + 4);
if (freezeFrame.jpeg.length < freezeFrame.size) {
console.log(`received first chunk of freeze frame: ${freezeFrame.jpeg.length}/${freezeFrame.size}`);
freezeFrame.receiving = true;
} else {
console.log(`received complete freeze frame: ${freezeFrame.jpeg.length}/${freezeFrame.size}`);
showFreezeFrame();
}
} else if (view[0] === ToClientMessageType.UnfreezeFrame) {
invalidateFreezeFrameOverlay();
} else if (view[0] === ToClientMessageType.VideoEncoderAvgQP) {
VideoEncoderQP = new TextDecoder("utf-16").decode(data.slice(1));
console.log(`received VideoEncoderAvgQP ${VideoEncoderQP}`);
} else if(view[0] == ToClientMessageType.LatencyTest){
let latencyTimingsAsString = new TextDecoder("utf-16").decode(data.slice(1));
console.log("Got latency timings from UE.")
console.log(latencyTimingsAsString);
let latencyTimingsFromUE = JSON.parse(latencyTimingsAsString);
if(webRtcPlayerObj)
{
webRtcPlayerObj.latencyTestTimings.SetUETimings(latencyTimingsFromUE);
}
} else {
console.error(`unrecognized data received, packet ID ${view[0]}`);
}
};
registerInputs(webRtcPlayerObj.video);
// On a touch device we will need special ways to show the on-screen keyboard.
if ('ontouchstart' in document.documentElement) {
createOnScreenKeyboardHelpers(htmlElement);
}
createWebRtcOffer();
return webRtcPlayerObj.video;
}
function onWebRtcAnswer(webRTCData) {
webRtcPlayerObj.receiveAnswer(webRTCData);
let printInterval = 5 * 60 * 1000; /*Print every 5 minutes*/
let nextPrintDuration = printInterval;
webRtcPlayerObj.onAggregatedStats = (aggregatedStats) => {
let numberFormat = new Intl.NumberFormat(window.navigator.language, { maximumFractionDigits: 0 });
let timeFormat = new Intl.NumberFormat(window.navigator.language, { maximumFractionDigits: 0, minimumIntegerDigits: 2 });
// Calculate duration of run
let runTime = (aggregatedStats.timestamp - aggregatedStats.timestampStart) / 1000;
let timeValues = [];
let timeDurations = [60, 60];
for (let timeIndex = 0; timeIndex < timeDurations.length; timeIndex++) {
timeValues.push(runTime % timeDurations[timeIndex]);
runTime = runTime / timeDurations[timeIndex];
}
timeValues.push(runTime);
let runTimeSeconds = timeValues[0];
let runTimeMinutes = Math.floor(timeValues[1]);
let runTimeHours = Math.floor([timeValues[2]]);
receivedBytesMeasurement = 'B';
receivedBytes = aggregatedStats.hasOwnProperty('bytesReceived') ? aggregatedStats.bytesReceived : 0;
let dataMeasurements = ['kB', 'MB', 'GB'];
for (let index = 0; index < dataMeasurements.length; index++) {
if (receivedBytes < 100 * 1000)
break;
receivedBytes = receivedBytes / 1000;
receivedBytesMeasurement = dataMeasurements[index];
}
let qualityStatus = document.getElementById("qualityStatus");
// "blinks" quality status element for 1 sec by making it transparent, speed = number of blinks
let blinkQualityStatus = function(speed) {
let iter = speed;
let opacity = 1; // [0..1]
let tickId = setInterval(
function() {
opacity -= 0.1;
// map `opacity` to [-0.5..0.5] range, decrement by 0.2 per step and take `abs` to make it blink: 1 -> 0 -> 1
qualityStatus.style = `opacity: ${Math.abs((opacity - 0.5) * 2)}`;
if (opacity <= 0.1) {
if (--iter == 0) {
clearInterval(tickId);
} else { // next blink
opacity = 1;
}
}
},
100 / speed // msecs
);
};
const orangeQP = 26;
const redQP = 35;
let statsText = '';
let color = "lime";
if (VideoEncoderQP > redQP) {
color = "red";
blinkQualityStatus(2);
statsText += `<div style="color: ${color}">Bad network connection</div>`;
} else if (VideoEncoderQP > orangeQP) {
color = "orange";
blinkQualityStatus(1);
statsText += `<div style="color: ${color}">Spotty network connection</div>`;
}
qualityStatus.className = `${color}Status`;
statsText += `<div>Duration: ${timeFormat.format(runTimeHours)}:${timeFormat.format(runTimeMinutes)}:${timeFormat.format(runTimeSeconds)}</div>`;
statsText += `<div>Video Resolution: ${
aggregatedStats.hasOwnProperty('frameWidth') && aggregatedStats.frameWidth && aggregatedStats.hasOwnProperty('frameHeight') && aggregatedStats.frameHeight ?
aggregatedStats.frameWidth + 'x' + aggregatedStats.frameHeight : 'N/A'
}</div>`;
statsText += `<div>Received (${receivedBytesMeasurement}): ${numberFormat.format(receivedBytes)}</div>`;
statsText += `<div>Frames Decoded: ${aggregatedStats.hasOwnProperty('framesDecoded') ? numberFormat.format(aggregatedStats.framesDecoded) : 'N/A'}</div>`;
statsText += `<div>Packets Lost: ${aggregatedStats.hasOwnProperty('packetsLost') ? numberFormat.format(aggregatedStats.packetsLost) : 'N/A'}</div>`;
statsText += `<div style="color: ${color}">Bitrate (kbps): ${aggregatedStats.hasOwnProperty('bitrate') ? numberFormat.format(aggregatedStats.bitrate) : 'N/A'}</div>`;
statsText += `<div>Framerate: ${aggregatedStats.hasOwnProperty('framerate') ? numberFormat.format(aggregatedStats.framerate) : 'N/A'}</div>`;
statsText += `<div>Frames dropped: ${aggregatedStats.hasOwnProperty('framesDropped') ? numberFormat.format(aggregatedStats.framesDropped) : 'N/A'}</div>`;
statsText += `<div>Net RTT (ms): ${aggregatedStats.hasOwnProperty('currentRoundTripTime') ? numberFormat.format(aggregatedStats.currentRoundTripTime * 1000) : 'N/A'}</div>`;
statsText += `<div>Browser receive to composite (ms): ${aggregatedStats.hasOwnProperty('receiveToCompositeMs') ? numberFormat.format(aggregatedStats.receiveToCompositeMs) : 'N/A'}</div>`;
statsText += `<div style="color: ${color}">Video Quantization Parameter: ${VideoEncoderQP}</div>`;
let statsDiv = document.getElementById("stats");
statsDiv.innerHTML = statsText;
if (print_stats) {
if (aggregatedStats.timestampStart) {
if ((aggregatedStats.timestamp - aggregatedStats.timestampStart) > nextPrintDuration) {
if (ws && ws.readyState === WS_OPEN_STATE) {
console.log(`-> SS: stats\n${JSON.stringify(aggregatedStats)}`);
ws.send(JSON.stringify({ type: 'stats', data: aggregatedStats }));
}
nextPrintDuration += printInterval;
}
}
}
};
webRtcPlayerObj.aggregateStats(1 * 1000 /*Check every 1 second*/);
webRtcPlayerObj.latencyTestTimings.OnAllLatencyTimingsReady = function(timings)
{
let endToEndLatency = timings.FrameDisplayTimeMs - timings.TestStartTimeMs;
let browserToUELatency = timings.ReceiptTimeMs - timings.TestStartTimeMs;
let ueToDisplayLatency = timings.FrameDisplayTimeMs - timings.PreCaptureTimeMs;
let uePreCapture = timings.PreCaptureTimeMs - timings.ReceiptTimeMs;
let uePixelStreamLatency = timings.PostEncodeTimeMs - timings.PreCaptureTimeMs;
let captureLatency = timings.PostCaptureTimeMs - timings.PreCaptureTimeMs;
let encodeLatency = timings.PostEncodeTimeMs - timings.PreEncodeTimeMs;
let browserSideLatency = endToEndLatency - uePreCapture - uePixelStreamLatency - (browserToUELatency * 2.0);
let latencyStatsInnerHTML = '';
latencyStatsInnerHTML += `<div>Total stream latency (ms): ${endToEndLatency}</div>`;
latencyStatsInnerHTML += `<div>Browser to UE network (ms): ${browserToUELatency}</div>`;
latencyStatsInnerHTML += `<div>UE Capture to Browser Display (ms): ${ueToDisplayLatency}</div>`;
latencyStatsInnerHTML += `<div>UE Pre-capture (ms): ${uePreCapture}</div>`;
latencyStatsInnerHTML += `<div>UE Capture+Encode (ms): ${uePixelStreamLatency}</div>`;
latencyStatsInnerHTML += `<div>UE Capture (ms): ${captureLatency}</div>`;
latencyStatsInnerHTML += `<div>UE Encode (ms): ${encodeLatency}</div>`;
latencyStatsInnerHTML += `<div>Browser receive to display (ms): ${browserSideLatency}</div>`;
document.getElementById("LatencyStats").innerHTML = latencyStatsInnerHTML;
}
}
function onWebRtcIce(iceCandidate) {
if (webRtcPlayerObj)
webRtcPlayerObj.handleCandidateFromServer(iceCandidate);
}
var styleWidth;
var styleHeight;
var styleTop;
var styleLeft;
var styleCursor = 'default';
var styleAdditional;
const ControlSchemeType = {
// A mouse can lock inside the WebRTC player so the user can simply move the
// mouse to control the orientation of the camera. The user presses the
// Escape key to unlock the mouse.
LockedMouse: 0,
// A mouse can hover over the WebRTC player so the user needs to click and
// drag to control the orientation of the camera.
HoveringMouse: 1
};
var inputOptions = {
// The control scheme controls the behaviour of the mouse when it interacts
// with the WebRTC player.
controlScheme: ControlSchemeType.LockedMouse,
// Browser keys are those which are typically used by the browser UI. We
// usually want to suppress these to allow, for example, UE4 to show shader
// complexity with the F5 key without the web page refreshing.
suppressBrowserKeys: true,
// UE4 has a faketouches option which fakes a single finger touch when the
// user drags with their mouse. We may perform the reverse; a single finger
// touch may be converted into a mouse drag UE4 side. This allows a
// non-touch application to be controlled partially via a touch device.
fakeMouseWithTouches: false
};
function resizePlayerStyleToFillWindow(playerElement) {
let videoElement = playerElement.getElementsByTagName("VIDEO");
// Fill the player display in window, keeping picture's aspect ratio.
let windowAspectRatio = window.innerHeight / window.innerWidth;
let playerAspectRatio = playerElement.clientHeight / playerElement.clientWidth;
// We want to keep the video ratio correct for the video stream
let videoAspectRatio = videoElement.videoHeight / videoElement.videoWidth;
if (isNaN(videoAspectRatio)) {
//Video is not initialised yet so set playerElement to size of window
styleWidth = window.innerWidth;
styleHeight = window.innerHeight;
styleTop = 0;
styleLeft = 0;
playerElement.style = "top: " + styleTop + "px; left: " + styleLeft + "px; width: " + styleWidth + "px; height: " + styleHeight + "px; cursor: " + styleCursor + "; " + styleAdditional;
} else if (windowAspectRatio < playerAspectRatio) {
// Window height is the constraining factor so to keep aspect ratio change width appropriately
styleWidth = Math.floor(window.innerHeight / videoAspectRatio);
styleHeight = window.innerHeight;
styleTop = 0;
styleLeft = Math.floor((window.innerWidth - styleWidth) * 0.5);
//Video is now 100% of the playerElement, so set the playerElement style
playerElement.style = "top: " + styleTop + "px; left: " + styleLeft + "px; width: " + styleWidth + "px; height: " + styleHeight + "px; cursor: " + styleCursor + "; " + styleAdditional;
} else {
// Window width is the constraining factor so to keep aspect ratio change height appropriately
styleWidth = window.innerWidth;
styleHeight = Math.floor(window.innerWidth * videoAspectRatio);
styleTop = Math.floor((window.innerHeight - styleHeight) * 0.5);
styleLeft = 0;
//Video is now 100% of the playerElement, so set the playerElement style
playerElement.style = "top: " + styleTop + "px; left: " + styleLeft + "px; width: " + styleWidth + "px; height: " + styleHeight + "px; cursor: " + styleCursor + "; " + styleAdditional;
}
}
function resizePlayerStyleToActualSize(playerElement) {
let videoElement = playerElement.getElementsByTagName("VIDEO");
if (videoElement.length > 0) {
// Display image in its actual size
styleWidth = videoElement[0].videoWidth;
styleHeight = videoElement[0].videoHeight;
styleTop = Math.floor((window.innerHeight - styleHeight) * 0.5);
styleLeft = Math.floor((window.innerWidth - styleWidth) * 0.5);
//Video is now 100% of the playerElement, so set the playerElement style
playerElement.style = "top: " + styleTop + "px; left: " + styleLeft + "px; width: " + styleWidth + "px; height: " + styleHeight + "px; cursor: " + styleCursor + "; " + styleAdditional;
}
}
function resizePlayerStyleToArbitrarySize(playerElement) {
let videoElement = playerElement.getElementsByTagName("VIDEO");
//Video is now 100% of the playerElement, so set the playerElement style
playerElement.style = "top: 0px; left: 0px; width: " + styleWidth + "px; height: " + styleHeight + "px; cursor: " + styleCursor + "; " + styleAdditional;
}
function setupFreezeFrameOverlay() {
freezeFrameOverlay = document.createElement('img');
freezeFrameOverlay.id = 'freezeFrameOverlay';
freezeFrameOverlay.style.display = 'none';
freezeFrameOverlay.style.pointerEvents = 'none';
freezeFrameOverlay.style.position = 'absolute';
freezeFrameOverlay.style.zIndex = '30';
}
function showFreezeFrameOverlay() {
if (freezeFrame.valid) {
freezeFrameOverlay.style.display = 'block';
}
}
function invalidateFreezeFrameOverlay() {
freezeFrameOverlay.style.display = 'none';
freezeFrame.valid = false;
}
function resizeFreezeFrameOverlay() {
if (freezeFrame.width !== 0 && freezeFrame.height !== 0) {
let displayWidth = 0;
let displayHeight = 0;
let displayTop = 0;
let displayLeft = 0;
let checkBox = document.getElementById('enlarge-display-to-fill-window-tgl');
if (checkBox !== null && checkBox.checked) {
let windowAspectRatio = window.innerWidth / window.innerHeight;
let videoAspectRatio = freezeFrame.width / freezeFrame.height;
if (windowAspectRatio < videoAspectRatio) {
displayWidth = window.innerWidth;
displayHeight = Math.floor(window.innerWidth / videoAspectRatio);
displayTop = Math.floor((window.innerHeight - displayHeight) * 0.5);
displayLeft = 0;
} else {
displayWidth = Math.floor(window.innerHeight * videoAspectRatio);
displayHeight = window.innerHeight;
displayTop = 0;
displayLeft = Math.floor((window.innerWidth - displayWidth) * 0.5);
}
} else {
displayWidth = freezeFrame.width;
displayHeight = freezeFrame.height;
displayTop = 0;
displayLeft = 0;
}
freezeFrameOverlay.style.width = displayWidth + 'px';
freezeFrameOverlay.style.height = displayHeight + 'px';
freezeFrameOverlay.style.left = displayLeft + 'px';
freezeFrameOverlay.style.top = displayTop + 'px';
}
}
function resizePlayerStyle(event) {
var playerElement = document.getElementById('player');
if (!playerElement)
return;
updateVideoStreamSize();
if (playerElement.classList.contains('fixed-size')){
setupMouseAndFreezeFrame(playerElement)
return;
}
let checkBox = document.getElementById('enlarge-display-to-fill-window-tgl');
let windowSmallerThanPlayer = window.innerWidth < playerElement.videoWidth || window.innerHeight < playerElement.videoHeight;
if (checkBox !== null) {
if (checkBox.checked || windowSmallerThanPlayer) {
resizePlayerStyleToFillWindow(playerElement);
} else {
resizePlayerStyleToActualSize(playerElement);
}
} else {
resizePlayerStyleToArbitrarySize(playerElement);
}
setupMouseAndFreezeFrame(playerElement)
}
function setupMouseAndFreezeFrame(playerElement) {
// Calculating and normalizing positions depends on the width and height of
// the player.
playerElementClientRect = playerElement.getBoundingClientRect();
setupNormalizeAndQuantize();
resizeFreezeFrameOverlay();
}
function updateVideoStreamSize() {
if (!matchViewportResolution) {
return;
}
var now = new Date().getTime();
if (now - lastTimeResized > 1000) {
var playerElement = document.getElementById('player');
if (!playerElement)
return;
let descriptor = {
Console: 'setres ' + playerElement.clientWidth + 'x' + playerElement.clientHeight
};
emitUIInteraction(descriptor);
console.log(descriptor);
lastTimeResized = new Date().getTime();
}
else {
console.log('Resizing too often - skipping');
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(updateVideoStreamSize, 1000);
}
}
// Fix for bug in iOS where windowsize is not correct at instance or orientation change
// https://github.com/dimsemenov/PhotoSwipe/issues/1315
var _orientationChangeTimeout;
function onOrientationChange(event) {
clearTimeout(_orientationChangeTimeout);
_orientationChangeTimeout = setTimeout(function () {
resizePlayerStyle();
}, 500);
}
// Must be kept in sync with PixelStreamingProtocol::EToUE4Msg C++ enum.
const MessageType = {
/**********************************************************************/
/*
* Control Messages. Range = 0..49.
*/
IFrameRequest: 0,
RequestQualityControl: 1,
MaxFpsRequest: 2,
AverageBitrateRequest: 3,
StartStreaming: 4,
StopStreaming: 5,
LatencyTest: 6,
/**********************************************************************/
/*
* Input Messages. Range = 50..89.
*/
// Generic Input Messages. Range = 50..59.
UIInteraction: 50,
Command: 51,
// Keyboard Input Message. Range = 60..69.
KeyDown: 60,
KeyUp: 61,
KeyPress: 62,
// Mouse Input Messages. Range = 70..79.
MouseEnter: 70,
MouseLeave: 71,
MouseDown: 72,
MouseUp: 73,
MouseMove: 74,
MouseWheel: 75,
// Touch Input Messages. Range = 80..89.
TouchStart: 80,
TouchEnd: 81,
TouchMove: 82
/**************************************************************************/
};
// A generic message has a type and a descriptor.
function emitDescriptor(messageType, descriptor) {
// Convert the dscriptor object into a JSON string.
let descriptorAsString = JSON.stringify(descriptor);
// Add the UTF-16 JSON string to the array byte buffer, going two bytes at
// a time.
let data = new DataView(new ArrayBuffer(1 + 2 + 2 * descriptorAsString.length));
let byteIdx = 0;
data.setUint8(byteIdx, messageType);
byteIdx++;
data.setUint16(byteIdx, descriptorAsString.length, true);
byteIdx += 2;
for (i = 0; i < descriptorAsString.length; i++) {
data.setUint16(byteIdx, descriptorAsString.charCodeAt(i), true);
byteIdx += 2;
}
sendInputData(data.buffer);
}
// A UI interation will occur when the user presses a button powered by
// JavaScript as opposed to pressing a button which is part of the pixel
// streamed UI from the UE4 client.
function emitUIInteraction(descriptor) {
emitDescriptor(MessageType.UIInteraction, descriptor);
}
// A build-in command can be sent to UE4 client. The commands are defined by a
// JSON descriptor and will be executed automatically.
// The currently supported commands are:
//
// 1. A command to run any console command:
// "{ ConsoleCommand: <string> }"
//
// 2. A command to change the resolution to the given width and height.
// "{ Resolution: { Width: <value>, Height: <value> } }"
//
// 3. A command to change the encoder settings by reducing the bitrate by the
// given percentage.
// "{ Encoder: { BitrateReduction: <value> } }"
function emitCommand(descriptor) {
emitDescriptor(MessageType.Command, descriptor);
}
function requestQualityControl() {
sendInputData(new Uint8Array([MessageType.RequestQualityControl]).buffer);
}
var playerElementClientRect = undefined;
var normalizeAndQuantizeUnsigned = undefined;
var normalizeAndQuantizeSigned = undefined;
function setupNormalizeAndQuantize() {
let playerElement = document.getElementById('player');
let videoElement = playerElement.getElementsByTagName("video");
if (playerElement && videoElement.length > 0) {
let playerAspectRatio = playerElement.clientHeight / playerElement.clientWidth;
let videoAspectRatio = videoElement[0].videoHeight / videoElement[0].videoWidth;
// Unsigned XY positions are the ratio (0.0..1.0) along a viewport axis,
// quantized into an uint16 (0..65536).
// Signed XY deltas are the ratio (-1.0..1.0) along a viewport axis,
// quantized into an int16 (-32767..32767).
// This allows the browser viewport and client viewport to have a different
// size.
// Hack: Currently we set an out-of-range position to an extreme (65535)
// as we can't yet accurately detect mouse enter and leave events
// precisely inside a video with an aspect ratio which causes mattes.
if (playerAspectRatio > videoAspectRatio) {
if (print_inputs) {
console.log('Setup Normalize and Quantize for playerAspectRatio > videoAspectRatio');
}
let ratio = playerAspectRatio / videoAspectRatio;
// Unsigned.
normalizeAndQuantizeUnsigned = (x, y) => {
let normalizedX = x / playerElement.clientWidth;
let normalizedY = ratio * (y / playerElement.clientHeight - 0.5) + 0.5;
if (normalizedX < 0.0 || normalizedX > 1.0 || normalizedY < 0.0 || normalizedY > 1.0) {
return {
inRange: false,
x: 65535,
y: 65535
};
} else {
return {
inRange: true,
x: normalizedX * 65536,
y: normalizedY * 65536
};
}
};
unquantizeAndDenormalizeUnsigned = (x, y) => {
let normalizedX = x / 65536;
let normalizedY = (y / 65536 - 0.5) / ratio + 0.5;
return {
x: normalizedX * playerElement.clientWidth,
y: normalizedY * playerElement.clientHeight
};
};
// Signed.
normalizeAndQuantizeSigned = (x, y) => {
let normalizedX = x / (0.5 * playerElement.clientWidth);
let normalizedY = (ratio * y) / (0.5 * playerElement.clientHeight);
return {
x: normalizedX * 32767,
y: normalizedY * 32767
};
};
} else {
if (print_inputs) {
console.log('Setup Normalize and Quantize for playerAspectRatio <= videoAspectRatio');
}
let ratio = videoAspectRatio / playerAspectRatio;
// Unsigned.
normalizeAndQuantizeUnsigned = (x, y) => {
let normalizedX = ratio * (x / playerElement.clientWidth - 0.5) + 0.5;
let normalizedY = y / playerElement.clientHeight;
if (normalizedX < 0.0 || normalizedX > 1.0 || normalizedY < 0.0 || normalizedY > 1.0) {
return {
inRange: false,
x: 65535,
y: 65535
};
} else {
return {
inRange: true,
x: normalizedX * 65536,
y: normalizedY * 65536
};
}
};
unquantizeAndDenormalizeUnsigned = (x, y) => {
let normalizedX = (x / 65536 - 0.5) / ratio + 0.5;
let normalizedY = y / 65536;
return {
x: normalizedX * playerElement.clientWidth,
y: normalizedY * playerElement.clientHeight
};
};
// Signed.
normalizeAndQuantizeSigned = (x, y) => {
let normalizedX = (ratio * x) / (0.5 * playerElement.clientWidth);
let normalizedY = y / (0.5 * playerElement.clientHeight);
return {
x: normalizedX * 32767,
y: normalizedY * 32767
};
};
}
}
}
function emitMouseMove(x, y, deltaX, deltaY) {
if (print_inputs) {
console.log(`x: ${x}, y:${y}, dX: ${deltaX}, dY: ${deltaY}`);
}
let coord = normalizeAndQuantizeUnsigned(x, y);
let delta = normalizeAndQuantizeSigned(deltaX, deltaY);
var Data = new DataView(new ArrayBuffer(9));
Data.setUint8(0, MessageType.MouseMove);
Data.setUint16(1, coord.x, true);
Data.setUint16(3, coord.y, true);
Data.setInt16(5, delta.x, true);
Data.setInt16(7, delta.y, true);
sendInputData(Data.buffer);
}
function emitMouseDown(button, x, y) {
if (print_inputs) {
console.log(`mouse button ${button} down at (${x}, ${y})`);
}
let coord = normalizeAndQuantizeUnsigned(x, y);
var Data = new DataView(new ArrayBuffer(6));
Data.setUint8(0, MessageType.MouseDown);
Data.setUint8(1, button);
Data.setUint16(2, coord.x, true);
Data.setUint16(4, coord.y, true);
sendInputData(Data.buffer);
}
function emitMouseUp(button, x, y) {
if (print_inputs) {
console.log(`mouse button ${button} up at (${x}, ${y})`);
}
let coord = normalizeAndQuantizeUnsigned(x, y);
var Data = new DataView(new ArrayBuffer(6));
Data.setUint8(0, MessageType.MouseUp);
Data.setUint8(1, button);
Data.setUint16(2, coord.x, true);
Data.setUint16(4, coord.y, true);
sendInputData(Data.buffer);
}
function emitMouseWheel(delta, x, y) {
if (print_inputs) {
console.log(`mouse wheel with delta ${delta} at (${x}, ${y})`);
}
let coord = normalizeAndQuantizeUnsigned(x, y);
var Data = new DataView(new ArrayBuffer(7));
Data.setUint8(0, MessageType.MouseWheel);
Data.setInt16(1, delta, true);
Data.setUint16(3, coord.x, true);
Data.setUint16(5, coord.y, true);
sendInputData(Data.buffer);
}
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
const MouseButton = {
MainButton: 0, // Left button.
AuxiliaryButton: 1, // Wheel button.
SecondaryButton: 2, // Right button.
FourthButton: 3, // Browser Back button.
FifthButton: 4 // Browser Forward button.
};
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
const MouseButtonsMask = {
PrimaryButton: 1, // Left button.
SecondaryButton: 2, // Right button.
AuxiliaryButton: 4, // Wheel button.
FourthButton: 8, // Browser Back button.
FifthButton: 16 // Browser Forward button.
};
// If the user has any mouse buttons pressed then release them.
function releaseMouseButtons(buttons, x, y) {
if (buttons & MouseButtonsMask.PrimaryButton) {
emitMouseUp(MouseButton.MainButton, x, y);
}
if (buttons & MouseButtonsMask.SecondaryButton) {
emitMouseUp(MouseButton.SecondaryButton, x, y);
}
if (buttons & MouseButtonsMask.AuxiliaryButton) {
emitMouseUp(MouseButton.AuxiliaryButton, x, y);
}
if (buttons & MouseButtonsMask.FourthButton) {
emitMouseUp(MouseButton.FourthButton, x, y);
}
if (buttons & MouseButtonsMask.FifthButton) {
emitMouseUp(MouseButton.FifthButton, x, y);
}
}
// If the user has any mouse buttons pressed then press them again.
function pressMouseButtons(buttons, x, y) {
if (buttons & MouseButtonsMask.PrimaryButton) {
emitMouseDown(MouseButton.MainButton, x, y);
}
if (buttons & MouseButtonsMask.SecondaryButton) {
emitMouseDown(MouseButton.SecondaryButton, x, y);
}
if (buttons & MouseButtonsMask.AuxiliaryButton) {
emitMouseDown(MouseButton.AuxiliaryButton, x, y);
}
if (buttons & MouseButtonsMask.FourthButton) {
emitMouseDown(MouseButton.FourthButton, x, y);
}
if (buttons & MouseButtonsMask.FifthButton) {
emitMouseDown(MouseButton.FifthButton, x, y);
}
}
function registerInputs(playerElement) {
if (!playerElement)
return;
registerMouseEnterAndLeaveEvents(playerElement);
registerTouchEvents(playerElement);
}
function createOnScreenKeyboardHelpers(htmlElement) {
if (document.getElementById('hiddenInput') === null) {
hiddenInput = document.createElement('input');
hiddenInput.id = 'hiddenInput';
hiddenInput.maxLength = 0;
htmlElement.appendChild(hiddenInput);
}
if (document.getElementById('editTextButton') === null) {
editTextButton = document.createElement('button');
editTextButton.id = 'editTextButton';
editTextButton.innerHTML = 'edit text';
htmlElement.appendChild(editTextButton);
// Hide the 'edit text' button.
editTextButton.classList.add('hiddenState');
editTextButton.addEventListener('click', function () {
// Show the on-screen keyboard.
hiddenInput.focus();
});
}
}
function showOnScreenKeyboard(command) {
if (command.showOnScreenKeyboard) {
// Show the 'edit text' button.
editTextButton.classList.remove('hiddenState');
// Place the 'edit text' button near the UE4 input widget.
let pos = unquantizeAndDenormalizeUnsigned(command.x, command.y);
editTextButton.style.top = pos.y.toString() + 'px';
editTextButton.style.left = (pos.x - 40).toString() + 'px';
} else {
// Hide the 'edit text' button.
editTextButton.classList.add('hiddenState');
// Hide the on-screen keyboard.
hiddenInput.blur();
}
}
function registerMouseEnterAndLeaveEvents(playerElement) {
playerElement.onmouseenter = function (e) {
if (print_inputs) {
console.log('mouse enter');
}
var Data = new DataView(new ArrayBuffer(1));
Data.setUint8(0, MessageType.MouseEnter);
sendInputData(Data.buffer);
playerElement.pressMouseButtons(e);
};
playerElement.onmouseleave = function (e) {
if (print_inputs) {
console.log('mouse leave');
}
var Data = new DataView(new ArrayBuffer(1));
Data.setUint8(0, MessageType.MouseLeave);
sendInputData(Data.buffer);
playerElement.releaseMouseButtons(e);
};
}
// A locked mouse works by the user clicking in the browser player and the
// cursor disappears and is locked. The user moves the cursor and the camera
// moves, for example. The user presses escape to free the mouse.
function registerLockedMouseEvents(playerElement) {
var x = playerElement.width / 2;
var y = playerElement.height / 2;
playerElement.requestPointerLock = playerElement.requestPointerLock || playerElement.mozRequestPointerLock;
document.exitPointerLock = document.exitPointerLock || document.mozExitPointerLock;
playerElement.onclick = function () {
playerElement.requestPointerLock();
};
// Respond to lock state change events
document.addEventListener('pointerlockchange', lockStateChange, false);
document.addEventListener('mozpointerlockchange', lockStateChange, false);
function lockStateChange() {
if (document.pointerLockElement === playerElement ||
document.mozPointerLockElement === playerElement) {
console.log('Pointer locked');
document.addEventListener("mousemove", updatePosition, false);
} else {
console.log('The pointer lock status is now unlocked');
document.removeEventListener("mousemove", updatePosition, false);
}
}
function updatePosition(e) {
x += e.movementX;
y += e.movementY;
if (x > styleWidth) {
x -= styleWidth;
}
if (y > styleHeight) {
y -= styleHeight;
}
if (x < 0) {
x = styleWidth + x;
}
if (y < 0) {
y = styleHeight - y;
}
emitMouseMove(x, y, e.movementX, e.movementY);
}
playerElement.onmousedown = function (e) {
emitMouseDown(e.button, x, y);
};
playerElement.onmouseup = function (e) {
emitMouseUp(e.button, x, y);
};
playerElement.onmousewheel = function (e) {
emitMouseWheel(e.wheelDelta, x, y);
};
playerElement.pressMouseButtons = function (e) {
pressMouseButtons(e.buttons, x, y);
};
playerElement.releaseMouseButtons = function (e) {
releaseMouseButtons(e.buttons, x, y);
};
}
// A hovering mouse works by the user clicking the mouse button when they want
// the cursor to have an effect over the video. Otherwise the cursor just
// passes over the browser.
function registerHoveringMouseEvents(playerElement) {
styleCursor = 'none'; // We will rely on UE4 client's software cursor.
//styleCursor = 'default'; // Showing cursor
playerElement.onmousemove = function (e) {
emitMouseMove(e.offsetX, e.offsetY, e.movementX, e.movementY);
e.preventDefault();
};
playerElement.onmousedown = function (e) {
emitMouseDown(e.button, e.offsetX, e.offsetY);
e.preventDefault();
};
playerElement.onmouseup = function (e) {
emitMouseUp(e.button, e.offsetX, e.offsetY);
e.preventDefault();
};
// When the context menu is shown then it is safest to release the button
// which was pressed when the event happened. This will guarantee we will
// get at least one mouse up corresponding to a mouse down event. Otherwise
// the mouse can get stuck.
// https://github.com/facebook/react/issues/5531
playerElement.oncontextmenu = function (e) {
emitMouseUp(e.button, e.offsetX, e.offsetY);
e.preventDefault();
};
if ('onmousewheel' in playerElement) {
playerElement.onmousewheel = function (e) {
emitMouseWheel(e.wheelDelta, e.offsetX, e.offsetY);
e.preventDefault();
};
} else {
playerElement.addEventListener('DOMMouseScroll', function (e) {
emitMouseWheel(e.detail * -120, e.offsetX, e.offsetY);
e.preventDefault();
}, false);
}
playerElement.pressMouseButtons = function (e) {
pressMouseButtons(e.buttons, e.offsetX, e.offsetY);
};
playerElement.releaseMouseButtons = function (e) {
releaseMouseButtons(e.buttons, e.offsetX, e.offsetY);
};
}
function registerTouchEvents(playerElement) {
// We need to assign a unique identifier to each finger.
// We do this by mapping each Touch object to the identifier.
var fingers = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0];
var fingerIds = {};
function rememberTouch(touch) {
let finger = fingers.pop();
if (finger === undefined) {
console.log('exhausted touch indentifiers');
}
fingerIds[touch.identifier] = finger;
}
function forgetTouch(touch) {
fingers.push(fingerIds[touch.identifier]);
delete fingerIds[touch.identifier];
}
function emitTouchData(type, touches) {
let data = new DataView(new ArrayBuffer(2 + 6 * touches.length));
data.setUint8(0, type);
data.setUint8(1, touches.length);
let byte = 2;
for (let t = 0; t < touches.length; t++) {
let touch = touches[t];
let x = touch.clientX - playerElement.offsetLeft;
let y = touch.clientY - playerElement.offsetTop;
if (print_inputs) {
console.log(`F${fingerIds[touch.identifier]}=(${x}, ${y})`);
}
let coord = normalizeAndQuantizeUnsigned(x, y);
data.setUint16(byte, coord.x, true);
byte += 2;
data.setUint16(byte, coord.y, true);
byte += 2;
data.setUint8(byte, fingerIds[touch.identifier], true);
byte += 1;
data.setUint8(byte, 255 * touch.force, true); // force is between 0.0 and 1.0 so quantize into byte.
byte += 1;
}
sendInputData(data.buffer);
}
if (inputOptions.fakeMouseWithTouches) {
var finger = undefined;
playerElement.ontouchstart = function (e) {
if (finger === undefined) {
let firstTouch = e.changedTouches[0];
finger = {
id: firstTouch.identifier,
x: firstTouch.clientX - playerElementClientRect.left,
y: firstTouch.clientY - playerElementClientRect.top
};
// Hack: Mouse events require an enter and leave so we just
// enter and leave manually with each touch as this event
// is not fired with a touch device.
playerElement.onmouseenter(e);
emitMouseDown(MouseButton.MainButton, finger.x, finger.y);
}
e.preventDefault();
};
playerElement.ontouchend = function (e) {
for (let t = 0; t < e.changedTouches.length; t++) {
let touch = e.changedTouches[t];
if (touch.identifier === finger.id) {
let x = touch.clientX - playerElementClientRect.left;
let y = touch.clientY - playerElementClientRect.top;
emitMouseUp(MouseButton.MainButton, x, y);
// Hack: Manual mouse leave event.
playerElement.onmouseleave(e);
finger = undefined;
break;
}
}
e.preventDefault();
};
playerElement.ontouchmove = function (e) {
for (let t = 0; t < e.touches.length; t++) {
let touch = e.touches[t];
if (touch.identifier === finger.id) {
let x = touch.clientX - playerElementClientRect.left;
let y = touch.clientY - playerElementClientRect.top;
emitMouseMove(x, y, x - finger.x, y - finger.y);
finger.x = x;
finger.y = y;
break;
}
}
e.preventDefault();
};
} else {
playerElement.ontouchstart = function (e) {
// Assign a unique identifier to each touch.
for (let t = 0; t < e.changedTouches.length; t++) {
rememberTouch(e.changedTouches[t]);
}
if (print_inputs) {
console.log('touch start');
}
emitTouchData(MessageType.TouchStart, e.changedTouches);
e.preventDefault();
};
playerElement.ontouchend = function (e) {
if (print_inputs) {
console.log('touch end');
}
emitTouchData(MessageType.TouchEnd, e.changedTouches);
// Re-cycle unique identifiers previously assigned to each touch.
for (let t = 0; t < e.changedTouches.length; t++) {
forgetTouch(e.changedTouches[t]);
}
e.preventDefault();
};
playerElement.ontouchmove = function (e) {
if (print_inputs) {
console.log('touch move');
}
emitTouchData(MessageType.TouchMove, e.touches);
e.preventDefault();
};
}
}
// Browser keys do not have a charCode so we only need to test keyCode.
function isKeyCodeBrowserKey(keyCode) {
// Function keys or tab key.
return keyCode >= 112 && keyCode <= 123 || keyCode === 9;
}
// Must be kept in sync with JavaScriptKeyCodeToFKey C++ array. The index of the
// entry in the array is the special key code given below.
const SpecialKeyCodes = {
BackSpace: 8,
Shift: 16,
Control: 17,
Alt: 18,
RightShift: 253,
RightControl: 254,
RightAlt: 255
};
// We want to be able to differentiate between left and right versions of some
// keys.
function getKeyCode(e) {
if (e.keyCode === SpecialKeyCodes.Shift && e.code === 'ShiftRight') return SpecialKeyCodes.RightShift;
else if (e.keyCode === SpecialKeyCodes.Control && e.code === 'ControlRight') return SpecialKeyCodes.RightControl;
else if (e.keyCode === SpecialKeyCodes.Alt && e.code === 'AltRight') return SpecialKeyCodes.RightAlt;
else return e.keyCode;
}
function registerKeyboardEvents() {
document.onkeydown = function (e) {
if (print_inputs) {
console.log(`key down ${e.keyCode}, repeat = ${e.repeat}`);
}
sendInputData(new Uint8Array([MessageType.KeyDown, getKeyCode(e), e.repeat]).buffer);
// Backspace is not considered a keypress in JavaScript but we need it
// to be so characters may be deleted in a UE4 text entry field.
if (e.keyCode === SpecialKeyCodes.BackSpace) {
document.onkeypress({ charCode: SpecialKeyCodes.BackSpace });
}
if (inputOptions.suppressBrowserKeys && isKeyCodeBrowserKey(e.keyCode)) {
e.preventDefault();
}
};
document.onkeyup = function (e) {
if (print_inputs) {
console.log(`key up ${e.keyCode}`);
}
sendInputData(new Uint8Array([MessageType.KeyUp, getKeyCode(e)]).buffer);
if (inputOptions.suppressBrowserKeys && isKeyCodeBrowserKey(e.keyCode)) {
e.preventDefault();
}
};
document.onkeypress = function (e) {
if (print_inputs) {
console.log(`key press ${e.charCode}`);
}
let data = new DataView(new ArrayBuffer(3));
data.setUint8(0, MessageType.KeyPress);
data.setUint16(1, e.charCode, true);
sendInputData(data.buffer);
};
}
function onExpandOverlay_Click(/* e */) {
let overlay = document.getElementById('overlay');
overlay.classList.toggle("overlay-shown");
}
function start() {
// update "quality status" to "disconnected" state
let qualityStatus = document.getElementById("qualityStatus");
if (qualityStatus) {
qualityStatus.className = "grey-status";
}
let statsDiv = document.getElementById("stats");
if (statsDiv) {
statsDiv.innerHTML = 'Not connected';
}
if (!connect_on_load || is_reconnection) {
showConnectOverlay();
invalidateFreezeFrameOverlay();
shouldShowPlayOverlay = true;
resizePlayerStyle();
} else {
connect();
}
updateKickButton(0);
}
function updateKickButton(playersCount) {
var kickButton = document.getElementById('kick-other-players-button');
if (kickButton)
kickButton.value = `Kick (${playersCount})`;
}
function connect() {
"use strict";
window.WebSocket = window.WebSocket || window.MozWebSocket;
if (!window.WebSocket) {
alert('Your browser doesn\'t support WebSocket');
return;
}
ws = new WebSocket(window.location.href.replace('http://', 'ws://').replace('https://', 'wss://'));
ws.onmessage = function (event) {
console.log(`<- SS: ${event.data}`);
var msg = JSON.parse(event.data);
if (msg.type === 'config') {
onConfig(msg);
} else if (msg.type === 'playerCount') {
updateKickButton(msg.count - 1);
} else if (msg.type === 'answer') {
onWebRtcAnswer(msg);
} else if (msg.type === 'iceCandidate') {
onWebRtcIce(msg.candidate);
} else {
console.log(`invalid SS message type: ${msg.type}`);
}
};
ws.onerror = function (event) {
console.log(`WS error: ${JSON.stringify(event)}`);
};
ws.onclose = function (event) {
console.log(`WS closed: ${JSON.stringify(event.code)} - ${event.reason}`);
ws = undefined;
is_reconnection = true;
// destroy `webRtcPlayerObj` if any
let playerDiv = document.getElementById('player');
if (webRtcPlayerObj) {
playerDiv.removeChild(webRtcPlayerObj.video);
webRtcPlayerObj.close();
webRtcPlayerObj = undefined;
}
showTextOverlay(`Disconnected: ${event.reason}`);
var reclickToStart = setTimeout(start, 4000);
};
}
// Config data received from WebRTC sender via the Cirrus web server
function onConfig(config) {
let playerDiv = document.getElementById('player');
let playerElement = setupWebRtcPlayer(playerDiv, config);
resizePlayerStyle();
switch (inputOptions.controlScheme) {
case ControlSchemeType.HoveringMouse:
registerHoveringMouseEvents(playerElement);
break;
case ControlSchemeType.LockedMouse:
registerLockedMouseEvents(playerElement);
break;
default:
console.log(`ERROR: Unknown control scheme ${inputOptions.controlScheme}`);
registerLockedMouseEvents(playerElement);
break;
}
}
function load() {
setupHtmlEvents();
setupFreezeFrameOverlay();
registerKeyboardEvents();
start();
}