You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1979 lines
74 KiB
JavaScript
1979 lines
74 KiB
JavaScript
8 months ago
|
// Copyright Epic Games, Inc. All Rights Reserved.
|
||
|
|
||
|
// Window events for a gamepad connecting
|
||
|
let haveEvents = 'GamepadEvent' in window;
|
||
|
let haveWebkitEvents = 'WebKitGamepadEvent' in window;
|
||
|
let controllers = {};
|
||
|
let rAF = window.mozRequestAnimationFrame ||
|
||
|
window.webkitRequestAnimationFrame ||
|
||
|
window.requestAnimationFrame;
|
||
|
let kbEvent = document.createEvent("KeyboardEvent");
|
||
|
let initMethod = typeof kbEvent.initKeyboardEvent !== 'undefined' ? "initKeyboardEvent" : "initKeyEvent";
|
||
|
|
||
|
let webRtcPlayerObj = null;
|
||
|
let print_stats = false;
|
||
|
let print_inputs = false;
|
||
|
let connect_on_load = false;
|
||
|
|
||
|
let is_reconnection = false;
|
||
|
let ws;
|
||
|
const WS_OPEN_STATE = 1;
|
||
|
|
||
|
let qualityControlOwnershipCheckBox;
|
||
|
let matchViewportResolution;
|
||
|
// TODO: Remove this - workaround because of bug causing UE to crash when switching resolutions too quickly
|
||
|
let lastTimeResized = new Date().getTime();
|
||
|
let resizeTimeout;
|
||
|
|
||
|
let onDataChannelConnected;
|
||
|
let responseEventListeners = new Map();
|
||
|
|
||
|
let freezeFrameOverlay = null;
|
||
|
let shouldShowPlayOverlay = true;
|
||
|
// A freeze frame is a still JPEG image shown instead of the video.
|
||
|
let 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.
|
||
|
let 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.
|
||
|
let editTextButton = undefined;
|
||
|
|
||
|
// A hidden input text box which is used only for focusing and opening the
|
||
|
// on-screen keyboard.
|
||
|
let hiddenInput = undefined;
|
||
|
|
||
|
let t0 = Date.now();
|
||
|
|
||
|
function log(str) {
|
||
|
console.log(`${Math.floor(Date.now() - t0)}: ` + str);
|
||
|
}
|
||
|
|
||
|
function scanGamepads() {
|
||
|
let gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);
|
||
|
for (let i = 0; i < gamepads.length; i++) {
|
||
|
if (gamepads[i] && (gamepads[i].index in controllers)) {
|
||
|
controllers[gamepads[i].index].currentState = gamepads[i];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
function updateStatus() {
|
||
|
scanGamepads();
|
||
|
// Iterate over multiple controllers in the case the mutiple gamepads are connected
|
||
|
for (j in controllers) {
|
||
|
let controller = controllers[j];
|
||
|
let currentState = controller.currentState;
|
||
|
let prevState = controller.prevState;
|
||
|
// Iterate over buttons
|
||
|
for (let i = 0; i < currentState.buttons.length; i++) {
|
||
|
let currButton = currentState.buttons[i];
|
||
|
let prevButton = prevState.buttons[i];
|
||
|
// Button 6 is actually the left trigger, send it to UE as an analog axis
|
||
|
// Button 7 is actually the right trigger, send it to UE as an analog axis
|
||
|
// The rest are normal buttons. Treat as such
|
||
|
if (currButton.pressed && !prevButton.pressed) {
|
||
|
// New press
|
||
|
if (i == 6) {
|
||
|
emitControllerAxisMove(j, 5, currButton.value);
|
||
|
} else if (i == 7) {
|
||
|
emitControllerAxisMove(j, 6, currButton.value);
|
||
|
} else {
|
||
|
emitControllerButtonPressed(j, i, 0);
|
||
|
}
|
||
|
} else if (!currButton.pressed && prevButton.pressed) {
|
||
|
// release
|
||
|
if (i == 6) {
|
||
|
emitControllerAxisMove(j, 5, 0);
|
||
|
} else if (i == 7) {
|
||
|
emitControllerAxisMove(j, 6, 0);
|
||
|
} else {
|
||
|
emitControllerButtonReleased(j, i);
|
||
|
}
|
||
|
} else if (currButton.pressed && prevButton.pressed) {
|
||
|
// repeat press / hold
|
||
|
if (i == 6) {
|
||
|
emitControllerAxisMove(j, 5, currButton.value);
|
||
|
} else if (i == 7) {
|
||
|
emitControllerAxisMove(j, 6, currButton.value);
|
||
|
} else {
|
||
|
emitControllerButtonPressed(j, i, 1);
|
||
|
}
|
||
|
}
|
||
|
// Last case is button isn't currently pressed and wasn't pressed before. This doesn't need an else block
|
||
|
}
|
||
|
// Iterate over gamepad axes
|
||
|
for (let i = 0; i < currentState.axes.length; i += 2) {
|
||
|
let x = parseFloat(currentState.axes[i].toFixed(4));
|
||
|
// https://w3c.github.io/gamepad/#remapping Gamepad broweser side standard mapping has positive down, negative up. This is downright disgusting. So we fix it.
|
||
|
let y = -parseFloat(currentState.axes[i + 1].toFixed(4));
|
||
|
if (i === 0) {
|
||
|
// left stick
|
||
|
// axis 1 = left horizontal
|
||
|
emitControllerAxisMove(j, 1, x);
|
||
|
// axis 2 = left vertical
|
||
|
emitControllerAxisMove(j, 2, y);
|
||
|
} else if (i === 2) {
|
||
|
// right stick
|
||
|
// axis 3 = right horizontal
|
||
|
emitControllerAxisMove(j, 3, x);
|
||
|
// axis 4 = right vertical
|
||
|
emitControllerAxisMove(j, 4, y);
|
||
|
}
|
||
|
}
|
||
|
controllers[j].prevState = currentState;
|
||
|
}
|
||
|
rAF(updateStatus);
|
||
|
}
|
||
|
|
||
|
function emitControllerButtonPressed(controllerIndex, buttonIndex, isRepeat) {
|
||
|
Data = new DataView(new ArrayBuffer(4));
|
||
|
Data.setUint8(0, MessageType.GamepadButtonPressed);
|
||
|
Data.setUint8(1, controllerIndex);
|
||
|
Data.setUint8(2, buttonIndex);
|
||
|
Data.setUint8(3, isRepeat);
|
||
|
}
|
||
|
|
||
|
function emitControllerButtonReleased(controllerIndex, buttonIndex) {
|
||
|
Data = new DataView(new ArrayBuffer(3));
|
||
|
Data.setUint8(0, MessageType.GamepadButtonReleased);
|
||
|
Data.setUint8(1, controllerIndex);
|
||
|
Data.setUint8(2, buttonIndex);
|
||
|
}
|
||
|
|
||
|
function emitControllerAxisMove(controllerIndex, axisIndex, analogValue) {
|
||
|
Data = new DataView(new ArrayBuffer(11));
|
||
|
Data.setUint8(0, MessageType.GamepadAnalog);
|
||
|
Data.setUint8(1, controllerIndex);
|
||
|
Data.setUint8(2, axisIndex);
|
||
|
Data.setFloat64(3, analogValue, true);
|
||
|
sendInputData(Data.buffer);
|
||
|
}
|
||
|
|
||
|
function gamepadConnectHandler(e) {
|
||
|
console.log("Gamepad connect handler");
|
||
|
gamepad = e.gamepad;
|
||
|
controllers[gamepad.index] = {};
|
||
|
controllers[gamepad.index].currentState = gamepad;
|
||
|
controllers[gamepad.index].prevState = gamepad;
|
||
|
console.log("gamepad: " + gamepad.id + " connected");
|
||
|
rAF(updateStatus);
|
||
|
}
|
||
|
|
||
|
function gamepadDisconnectHandler(e) {
|
||
|
console.log("Gamepad disconnect handler");
|
||
|
console.log("gamepad: " + e.gamepad.id + " disconnected");
|
||
|
delete controllers[e.gamepad.index];
|
||
|
}
|
||
|
|
||
|
function setupHtmlEvents() {
|
||
|
//Window events
|
||
|
window.addEventListener('resize', resizePlayerStyle, true);
|
||
|
window.addEventListener('orientationchange', onOrientationChange);
|
||
|
|
||
|
//Gamepad events
|
||
|
if (haveEvents) {
|
||
|
window.addEventListener("gamepadconnected", gamepadConnectHandler);
|
||
|
window.addEventListener("gamepaddisconnected", gamepadDisconnectHandler);
|
||
|
} else if (haveWebkitEvents) {
|
||
|
window.addEventListener("webkitgamepadconnected", gamepadConnectHandler);
|
||
|
window.addEventListener("webkitgamepaddisconnected", gamepadDisconnectHandler);
|
||
|
}
|
||
|
|
||
|
//HTML elements controls
|
||
|
// let overlayButton = document.getElementById('overlayButton');
|
||
|
// overlayButton.addEventListener('click', onExpandOverlay_Click);
|
||
|
|
||
|
|
||
|
// let tabScreen1 = document.getElementById('tabScreen1');
|
||
|
// tabScreen1.addEventListener('click', screen1Clicked);
|
||
|
|
||
|
// let tabScreen2 = document.getElementById('tabScreen2');
|
||
|
// tabScreen2.addEventListener('click', screen2Clicked);
|
||
|
|
||
|
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 encoderParamsSubmit = document.getElementById('encoder-params-submit');
|
||
|
if (encoderParamsSubmit !== null) {
|
||
|
encoderParamsSubmit.onclick = function(event) {
|
||
|
let rateControl = document.getElementById('encoder-rate-control').value;
|
||
|
let targetBitrate = document.getElementById('encoder-target-bitrate-text').value * 1000;
|
||
|
let maxBitrate = document.getElementById('encoder-max-bitrate-text').value * 1000;
|
||
|
let minQP = document.getElementById('encoder-min-qp-text').value;
|
||
|
let maxQP = document.getElementById('encoder-max-qp-text').value;
|
||
|
let fillerData = document.getElementById('encoder-filler-data-tgl').checked ? 1 : 0;
|
||
|
let multipass = document.getElementById('encoder-multipass').value;
|
||
|
|
||
|
emitUIInteraction({ Console: 'PixelStreaming.Encoder.RateControl ' + rateControl });
|
||
|
emitUIInteraction({ Console: 'PixelStreaming.Encoder.TargetBitrate ' + targetBitrate > 0 ? targetBitrate : -1 });
|
||
|
emitUIInteraction({ Console: 'PixelStreaming.Encoder.MaxBitrateVBR ' + maxBitrate > 0 ? maxBitrate : -1 });
|
||
|
emitUIInteraction({ Console: 'PixelStreaming.Encoder.MinQP ' + minQP });
|
||
|
emitUIInteraction({ Console: 'PixelStreaming.Encoder.MaxQP ' + maxQP });
|
||
|
emitUIInteraction({ Console: 'PixelStreaming.Encoder.EnableFillerData ' + fillerData });
|
||
|
emitUIInteraction({ Console: 'PixelStreaming.Encoder.Multipass ' + multipass });
|
||
|
};
|
||
|
}
|
||
|
|
||
|
let webrtcParamsSubmit = document.getElementById('webrtc-params-submit');
|
||
|
if (webrtcParamsSubmit !== null) {
|
||
|
webrtcParamsSubmit.onclick = function(event) {
|
||
|
let degradationPref = document.getElementById('webrtc-degradation-pref').value;
|
||
|
let maxFPS = document.getElementById('webrtc-max-fps-text').value;
|
||
|
let minBitrate = document.getElementById('webrtc-min-bitrate-text').value * 1000;
|
||
|
let maxBitrate = document.getElementById('webrtc-max-bitrate-text').value * 1000;
|
||
|
let lowQP = document.getElementById('webrtc-low-qp-text').value;
|
||
|
let highQP = document.getElementById('webrtc-high-qp-text').value;
|
||
|
|
||
|
emitUIInteraction({ Console: 'PixelStreaming.WebRTC.DegradationPreference ' + degradationPref });
|
||
|
emitUIInteraction({ Console: 'PixelStreaming.WebRTC.MaxFps ' + maxFPS });
|
||
|
emitUIInteraction({ Console: 'PixelStreaming.WebRTC.MinBitrate ' + minBitrate });
|
||
|
emitUIInteraction({ Console: 'PixelStreaming.WebRTC.MaxBitrate ' + maxBitrate });
|
||
|
emitUIInteraction({ Console: 'PixelStreaming.WebRTC.LowQpThreshold ' + lowQP });
|
||
|
emitUIInteraction({ Console: 'PixelStreaming.WebRTC.HighQpThreshold ' + highQP });
|
||
|
};
|
||
|
}
|
||
|
|
||
|
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";
|
||
|
};
|
||
|
}
|
||
|
|
||
|
let kickButton = document.getElementById('kick-other-players-button');
|
||
|
if (kickButton) {
|
||
|
kickButton.onclick = function (event) {
|
||
|
console.log(`-> SS: kick`);
|
||
|
ws.send(JSON.stringify({
|
||
|
type: 'kick'
|
||
|
}));
|
||
|
};
|
||
|
}
|
||
|
|
||
|
let latencyButton = document.getElementById('test-latency-button');
|
||
|
if (latencyButton) {
|
||
|
latencyButton.onclick = () => {
|
||
|
sendStartLatencyTest();
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
var connectSuccess = false
|
||
|
|
||
|
var connectSuccessListeners = []
|
||
|
|
||
|
window.addPsConnectSuccessListener = (listener) => {
|
||
|
if (connectSuccess) {
|
||
|
listener()
|
||
|
} else {
|
||
|
connectSuccessListeners.push(listener)
|
||
|
}
|
||
|
|
||
|
}
|
||
|
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 setOverlay(htmlClass, htmlElement, onClickFunction) {
|
||
|
let videoPlayOverlay = document.getElementById('videoPlayOverlay');
|
||
|
if (!videoPlayOverlay) {
|
||
|
let 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
|
||
|
let cl = videoPlayOverlay.classList;
|
||
|
for (let i = cl.length - 1; i >= 0; i--) {
|
||
|
cl.remove(cl[i]);
|
||
|
}
|
||
|
|
||
|
videoPlayOverlay.classList.add(htmlClass);
|
||
|
}
|
||
|
|
||
|
function showConnectOverlay() {
|
||
|
let startText = document.createElement('div');
|
||
|
startText.id = 'playButton';
|
||
|
startText.innerHTML = 'loading';
|
||
|
|
||
|
setOverlay('clickableState', startText, event => {
|
||
|
connect();
|
||
|
startAfkWarningTimer();
|
||
|
});
|
||
|
_showConnectOverlayTimeout = setTimeout(function () {
|
||
|
clearTimeout(_showConnectOverlayTimeout);
|
||
|
connect();
|
||
|
startAfkWarningTimer();
|
||
|
}, 100);
|
||
|
|
||
|
}
|
||
|
function screen1Clicked(){
|
||
|
// alert(1)
|
||
|
let descriptor = {
|
||
|
SwitchCamera:{
|
||
|
CameraName1:"Camera1"
|
||
|
}
|
||
|
}
|
||
|
// console.log('descriptor1', descriptor)
|
||
|
emitUIInteraction(descriptor);
|
||
|
}
|
||
|
|
||
|
function screen2Clicked(){
|
||
|
let descriptor = {
|
||
|
SwitchCamera:{
|
||
|
CameraName2:"Camera2"
|
||
|
}
|
||
|
}
|
||
|
// console.log('descriptor2', descriptor)
|
||
|
emitUIInteraction(descriptor);
|
||
|
}
|
||
|
function showTextOverlay(text) {
|
||
|
let textOverlay = document.createElement('div');
|
||
|
textOverlay.id = 'messageOverlay';
|
||
|
textOverlay.innerHTML = text ? text : '';
|
||
|
setOverlay('textDisplayState', textOverlay);
|
||
|
}
|
||
|
|
||
|
function playVideoStream() {
|
||
|
if (webRtcPlayerObj && webRtcPlayerObj.video) {
|
||
|
|
||
|
webRtcPlayerObj.video.play().catch(function(onRejectedReason){
|
||
|
console.error(onRejectedReason);
|
||
|
console.log("Browser does not support autoplaying video without interaction - to resolve this we are going to show the play button overlay.")
|
||
|
showPlayOverlay();
|
||
|
});
|
||
|
|
||
|
requestInitialSettings();
|
||
|
requestQualityControl();
|
||
|
showFreezeFrameOverlay();
|
||
|
hideOverlay();
|
||
|
} else {
|
||
|
console.error("Could not player video stream because webRtcPlayerObj.video was not valid.")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function showPlayOverlay() {
|
||
|
let img = document.createElement('img');
|
||
|
img.id = 'playButton';
|
||
|
img.src = '/src/assets/Play.png';
|
||
|
// img.alt = 'Start Streaming';
|
||
|
setOverlay('clickableState', img, event => {
|
||
|
playVideoStream();
|
||
|
});
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
window.addResponseEventListener = addResponseEventListener;
|
||
|
|
||
|
function removeResponseEventListener(name) {
|
||
|
responseEventListeners.remove(name);
|
||
|
}
|
||
|
window.removeResponseEventListener = removeResponseEventListener;
|
||
|
// 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,
|
||
|
InitialSettings: 7
|
||
|
};
|
||
|
|
||
|
let VideoEncoderQP = "N/A";
|
||
|
|
||
|
function setupWebRtcPlayer(htmlElement, config) {
|
||
|
webRtcPlayerObj = new webRtcPlayer(config);
|
||
|
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();
|
||
|
}
|
||
|
else {
|
||
|
resizePlayerStyle();
|
||
|
playVideoStream();
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
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), ''));
|
||
|
let freezeFrameImage = document.getElementById("freezeFrameOverlay").childNodes[0];
|
||
|
freezeFrameImage.src = 'data:image/jpeg;base64,' + base64;
|
||
|
freezeFrameImage.onload = function() {
|
||
|
freezeFrame.height = freezeFrameImage.naturalHeight;
|
||
|
freezeFrame.width = freezeFrameImage.naturalWidth;
|
||
|
resizeFreezeFrameOverlay();
|
||
|
if (shouldShowPlayOverlay) {
|
||
|
showPlayOverlay();
|
||
|
resizePlayerStyle();
|
||
|
} else {
|
||
|
showFreezeFrameOverlay();
|
||
|
}
|
||
|
webRtcPlayerObj.setVideoEnabled(false);
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function processFreezeFrameMessage(view) {
|
||
|
// Reset freeze frame if we got a freeze frame message and we are not "receiving" yet.
|
||
|
if (!freezeFrame.receiving) {
|
||
|
freezeFrame.receiving = true;
|
||
|
freezeFrame.valid = false;
|
||
|
freezeFrame.size = 0;
|
||
|
freezeFrame.jpeg = undefined;
|
||
|
}
|
||
|
|
||
|
// Extract total size of freeze frame (across all chunks)
|
||
|
freezeFrame.size = (new DataView(view.slice(1, 5).buffer)).getInt32(0, true);
|
||
|
|
||
|
// Get the jpeg part of the payload
|
||
|
let jpegBytes = view.slice(1 + 4);
|
||
|
|
||
|
// Append to existing jpeg that holds the freeze frame
|
||
|
if (freezeFrame.jpeg) {
|
||
|
let jpeg = new Uint8Array(freezeFrame.jpeg.length + jpegBytes.length);
|
||
|
jpeg.set(freezeFrame.jpeg, 0);
|
||
|
jpeg.set(jpegBytes, freezeFrame.jpeg.length);
|
||
|
freezeFrame.jpeg = jpeg;
|
||
|
}
|
||
|
// No existing freeze frame jpeg, make one
|
||
|
else {
|
||
|
freezeFrame.jpeg = jpegBytes;
|
||
|
freezeFrame.receiving = true;
|
||
|
console.log(`received first chunk of freeze frame: ${freezeFrame.jpeg.length}/${freezeFrame.size}`);
|
||
|
}
|
||
|
|
||
|
// Uncomment for debug
|
||
|
//console.log(`Received freeze frame chunk: ${freezeFrame.jpeg.length}/${freezeFrame.size}`);
|
||
|
|
||
|
// Finished receiving freeze frame, we can show it now
|
||
|
if (freezeFrame.jpeg.length === freezeFrame.size) {
|
||
|
freezeFrame.receiving = false;
|
||
|
freezeFrame.valid = true;
|
||
|
console.log(`received complete freeze frame ${freezeFrame.size}`);
|
||
|
showFreezeFrame();
|
||
|
}
|
||
|
// We received more data than the freeze frame payload message indicate (this is an error)
|
||
|
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;
|
||
|
}
|
||
|
}
|
||
|
// console.log('webRtcPlayerObj', webRtcPlayerObj)
|
||
|
webRtcPlayerObj.onDataChannelMessage = function(data) {
|
||
|
let view = new Uint8Array(data);
|
||
|
// console.log('webRtcPlayerObj', view[0])
|
||
|
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) {
|
||
|
processFreezeFrameMessage(view);
|
||
|
} 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 if (view[0] == ToClientMessageType.InitialSettings) {
|
||
|
let settingsString = new TextDecoder("utf-16").decode(data.slice(1));
|
||
|
let settingsJSON = JSON.parse(settingsString);
|
||
|
|
||
|
// reminder bitrates are sent in bps but displayed in kbps
|
||
|
|
||
|
if (settingsJSON.Encoder) {
|
||
|
}
|
||
|
if (settingsJSON.WebRTC) {
|
||
|
}
|
||
|
} 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");
|
||
|
if(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 : 'Chrome only'
|
||
|
}</div>`;
|
||
|
statsText += `<div>Received (${receivedBytesMeasurement}): ${numberFormat.format(receivedBytes)}</div>`;
|
||
|
statsText += `<div>Frames Decoded: ${aggregatedStats.hasOwnProperty('framesDecoded') ? numberFormat.format(aggregatedStats.framesDecoded) : 'Chrome only'}</div>`;
|
||
|
statsText += `<div>Packets Lost: ${aggregatedStats.hasOwnProperty('packetsLost') ? numberFormat.format(aggregatedStats.packetsLost) : 'Chrome only'}</div>`;
|
||
|
statsText += `<div style="color: ${color}">Bitrate (kbps): ${aggregatedStats.hasOwnProperty('bitrate') ? numberFormat.format(aggregatedStats.bitrate) : 'Chrome only'}</div>`;
|
||
|
statsText += `<div>Framerate: ${aggregatedStats.hasOwnProperty('framerate') ? numberFormat.format(aggregatedStats.framerate) : 'Chrome only'}</div>`;
|
||
|
statsText += `<div>Frames dropped: ${aggregatedStats.hasOwnProperty('framesDropped') ? numberFormat.format(aggregatedStats.framesDropped) : 'Chrome only'}</div>`;
|
||
|
statsText += `<div>Net RTT (ms): ${aggregatedStats.hasOwnProperty('currentRoundTripTime') ? numberFormat.format(aggregatedStats.currentRoundTripTime * 1000) : 'Can\'t calculate'}</div>`;
|
||
|
statsText += `<div>Browser receive to composite (ms): ${aggregatedStats.hasOwnProperty('receiveToCompositeMs') ? numberFormat.format(aggregatedStats.receiveToCompositeMs) : 'Chrome only'}</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) {
|
||
|
|
||
|
if (!timings.BrowserReceiptTimeMs) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let latencyExcludingDecode = timings.BrowserReceiptTimeMs - timings.TestStartTimeMs;
|
||
|
let uePixelStreamLatency = timings.UEPreEncodeTimeMs == 0 || timings.UEPreCaptureTimeMs == 0 ? "???" : timings.UEPostEncodeTimeMs - timings.UEPreCaptureTimeMs;
|
||
|
let captureLatency = timings.UEPostCaptureTimeMs - timings.UEPreCaptureTimeMs;
|
||
|
let encodeLatency = timings.UEPostEncodeTimeMs - timings.UEPreEncodeTimeMs;
|
||
|
let ueLatency = timings.UETransmissionTimeMs - timings.UEReceiptTimeMs;
|
||
|
let networkLatency = latencyExcludingDecode - ueLatency;
|
||
|
let browserSendLatency = latencyExcludingDecode - networkLatency - ueLatency;
|
||
|
|
||
|
//these ones depend on FrameDisplayDeltaTimeMs
|
||
|
let endToEndLatency = null;
|
||
|
let browserSideLatency = null;
|
||
|
|
||
|
if (timings.FrameDisplayDeltaTimeMs && timings.BrowserReceiptTimeMs) {
|
||
|
endToEndLatency = timings.FrameDisplayDeltaTimeMs + latencyExcludingDecode;
|
||
|
browserSideLatency = endToEndLatency - networkLatency - ueLatency;
|
||
|
}
|
||
|
|
||
|
let latencyStatsInnerHTML = '';
|
||
|
latencyStatsInnerHTML += `<div>Net latency RTT (ms): ${networkLatency}</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>Total UE latency (ms): ${ueLatency}</div>`;
|
||
|
latencyStatsInnerHTML += `<div>Browser send latency (ms): ${browserSendLatency}</div>`
|
||
|
latencyStatsInnerHTML += timings.FrameDisplayDeltaTimeMs && timings.BrowserReceiptTimeMs ? `<div>Browser receive latency (ms): ${timings.FrameDisplayDeltaTimeMs}</div>` : "";
|
||
|
latencyStatsInnerHTML += browserSideLatency ? `<div>Total browser latency (ms): ${browserSideLatency}</div>` : "";
|
||
|
latencyStatsInnerHTML += `<div>Total latency (excluding browser) (ms): ${latencyExcludingDecode}</div>`;
|
||
|
latencyStatsInnerHTML += endToEndLatency ? `<div>Total latency (ms): ${endToEndLatency}</div>` : "";
|
||
|
document.getElementById("LatencyStats").innerHTML = latencyStatsInnerHTML;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
function onWebRtcIce(iceCandidate) {
|
||
|
if (webRtcPlayerObj)
|
||
|
webRtcPlayerObj.handleCandidateFromServer(iceCandidate);
|
||
|
}
|
||
|
|
||
|
let styleWidth;
|
||
|
let styleHeight;
|
||
|
let styleTop;
|
||
|
let styleLeft;
|
||
|
let styleCursor = 'default';
|
||
|
let 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
|
||
|
};
|
||
|
|
||
|
let 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,
|
||
|
// touchInput : 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
|
||
|
};
|
||
|
inputOptions.controlScheme = ControlSchemeType.HoveringMouse;
|
||
|
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;
|
||
|
let Top = Math.floor((window.innerHeight - styleHeight) * 0.5);
|
||
|
let Left = Math.floor((window.innerWidth - styleWidth) * 0.5);
|
||
|
styleTop = (Top > 0) ? Top : 0;
|
||
|
styleLeft = (Left > 0) ? Left : 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 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('div');
|
||
|
freezeFrameOverlay.id = 'freezeFrameOverlay';
|
||
|
freezeFrameOverlay.style.display = 'none';
|
||
|
freezeFrameOverlay.style.pointerEvents = 'none';
|
||
|
freezeFrameOverlay.style.position = 'absolute';
|
||
|
freezeFrameOverlay.style.zIndex = '20';
|
||
|
|
||
|
let freezeFrameImage = document.createElement('img');
|
||
|
freezeFrameImage.style.position = 'absolute';
|
||
|
freezeFrameOverlay.appendChild(freezeFrameImage);
|
||
|
}
|
||
|
|
||
|
function showFreezeFrameOverlay() {
|
||
|
if (freezeFrame.valid) {
|
||
|
freezeFrameOverlay.classList.add("freezeframeBackground");
|
||
|
freezeFrameOverlay.style.display = 'block';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function invalidateFreezeFrameOverlay() {
|
||
|
freezeFrameOverlay.style.display = 'none';
|
||
|
freezeFrame.valid = false;
|
||
|
freezeFrameOverlay.classList.remove("freezeframeBackground");
|
||
|
|
||
|
if (webRtcPlayerObj) {
|
||
|
webRtcPlayerObj.setVideoEnabled(true);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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');
|
||
|
let playerElement = document.getElementById('player');
|
||
|
if (checkBox !== null && checkBox.checked) {
|
||
|
// We are fitting video to screen, we care about the screen (window) size
|
||
|
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 {
|
||
|
// Video is coming in at native resolution, we care more about the player size
|
||
|
let playerAspectRatio = playerElement.offsetWidth / playerElement.offsetHeight;
|
||
|
let videoAspectRatio = freezeFrame.width / freezeFrame.height;
|
||
|
if (playerAspectRatio < videoAspectRatio) {
|
||
|
displayWidth = playerElement.offsetWidth;
|
||
|
displayHeight = Math.floor(playerElement.offsetWidth / videoAspectRatio);
|
||
|
displayTop = Math.floor((playerElement.offsetHeight - displayHeight) * 0.5);
|
||
|
displayLeft = 0;
|
||
|
} else {
|
||
|
displayWidth = Math.floor(playerElement.offsetHeight * videoAspectRatio);
|
||
|
displayHeight = playerElement.offsetHeight;
|
||
|
displayTop = 0;
|
||
|
displayLeft = Math.floor((playerElement.offsetWidth - displayWidth) * 0.5);
|
||
|
}
|
||
|
}
|
||
|
let freezeFrameImage = document.getElementById("freezeFrameOverlay").childNodes[0];
|
||
|
freezeFrameOverlay.style.width = playerElement.offsetWidth + 'px';
|
||
|
freezeFrameOverlay.style.height = playerElement.offsetHeight + 'px';
|
||
|
freezeFrameOverlay.style.left = 0 + 'px';
|
||
|
freezeFrameOverlay.style.top = 0 + 'px';
|
||
|
|
||
|
freezeFrameImage.style.width = displayWidth + 'px';
|
||
|
freezeFrameImage.style.height = displayHeight + 'px';
|
||
|
freezeFrameImage.style.left = displayLeft + 'px';
|
||
|
freezeFrameImage.style.top = displayTop + 'px';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function resizePlayerStyle(event) {
|
||
|
let 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 wsStatus(){
|
||
|
return ws;
|
||
|
}
|
||
|
function closeWS(){
|
||
|
ws.close();
|
||
|
}
|
||
|
function show2d(){
|
||
|
let play2d = document.getElementById('play2d');
|
||
|
play2d.style.display = 'block';
|
||
|
// play2d.style.backgroundImage = "url('/2dBG.png')";
|
||
|
let playerDiv = document.getElementById('player');
|
||
|
// playerDiv.style.display = 'none';
|
||
|
// play2d.style.backgroundColor = '#000';
|
||
|
play2d.style.width = '100%';
|
||
|
play2d.style.height = '100%';
|
||
|
}
|
||
|
function hide2d(){
|
||
|
let play2d = document.getElementById('play2d');
|
||
|
play2d.style.display = 'none';
|
||
|
let playerDiv = document.getElementById('player');
|
||
|
playerDiv.style.display = 'block';
|
||
|
}
|
||
|
|
||
|
function updateVideoStreamSize() {
|
||
|
if (!matchViewportResolution) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let now = new Date().getTime();
|
||
|
if (now - lastTimeResized > 1000) {
|
||
|
let 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
|
||
|
let _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,
|
||
|
RequestInitialSettings: 7,
|
||
|
|
||
|
/**********************************************************************/
|
||
|
|
||
|
/*
|
||
|
* 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,
|
||
|
|
||
|
// Gamepad Input Messages. Range = 90..99
|
||
|
GamepadButtonPressed: 90,
|
||
|
GamepadButtonReleased: 91,
|
||
|
GamepadAnalog: 92
|
||
|
|
||
|
/**************************************************************************/
|
||
|
};
|
||
|
|
||
|
// 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);
|
||
|
}
|
||
|
window.emitUIInteraction = emitUIInteraction;
|
||
|
// 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>, Resolution.Height: <value> } }"
|
||
|
//
|
||
|
function emitCommand(descriptor) {
|
||
|
emitDescriptor(MessageType.Command, descriptor);
|
||
|
}
|
||
|
|
||
|
function requestInitialSettings() {
|
||
|
sendInputData(new Uint8Array([MessageType.RequestInitialSettings]).buffer);
|
||
|
}
|
||
|
|
||
|
function requestQualityControl() {
|
||
|
sendInputData(new Uint8Array([MessageType.RequestQualityControl]).buffer);
|
||
|
}
|
||
|
|
||
|
let playerElementClientRect = undefined;
|
||
|
let normalizeAndQuantizeUnsigned = undefined;
|
||
|
let 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);
|
||
|
let 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);
|
||
|
let 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);
|
||
|
let 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);
|
||
|
let 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');
|
||
|
}
|
||
|
let 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');
|
||
|
}
|
||
|
let 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) {
|
||
|
let x = playerElement.width / 2;
|
||
|
let 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) {
|
||
|
// console.log('e',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.
|
||
|
let fingers = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0];
|
||
|
let 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 + 7 * 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;
|
||
|
data.setUint8(byte, coord.inRange ? 1 : 0, true); // mark the touch as in the player or not
|
||
|
byte += 1;
|
||
|
}
|
||
|
|
||
|
sendInputData(data.buffer);
|
||
|
}
|
||
|
|
||
|
if (inputOptions.fakeMouseWithTouches) {
|
||
|
let finger = undefined;
|
||
|
playerElement.addEventListener('touchstart', 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.addEventListener('touchend', function(e) {
|
||
|
console.log('eend',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.addEventListener('touchmove', function(e) {
|
||
|
console.log('emove',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.addEventListener('touchstart', 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.addEventListener('touchend', 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.addEventListener('touchmove', 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}`);
|
||
|
}
|
||
|
if (e.key === "F11" || e.key === "F12") {
|
||
|
return
|
||
|
}
|
||
|
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) {
|
||
|
let 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://'));
|
||
|
// const url = 'wss://192.168.3.4:4433/';
|
||
|
const url = sessionStorage.getItem('wsurl');
|
||
|
ws = new WebSocket(url);
|
||
|
ws.onmessage = function(event) {
|
||
|
// console.log(`<- SS: ${event.data}`);
|
||
|
let 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');
|
||
|
console.log('webRtcPlayerObj', webRtcPlayerObj)
|
||
|
if (webRtcPlayerObj) {
|
||
|
playerDiv.removeChild(webRtcPlayerObj.video);
|
||
|
webRtcPlayerObj.close();
|
||
|
webRtcPlayerObj = undefined;
|
||
|
}
|
||
|
|
||
|
showTextOverlay(`Disconnected: ${event.reason}`);
|
||
|
let 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();
|
||
|
// addResponseEventListener('ShowDeviceInfo', (response) => {console.log(`Received response message from streamer: "${response}"`)})
|
||
|
start();
|
||
|
}
|