844 lines
28 KiB
JavaScript
844 lines
28 KiB
JavaScript
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
|
|
let webRtcPlayerObj = null;
|
|
|
|
let is_reconnection = false;
|
|
let connect_on_load = false;
|
|
let ws;
|
|
const WS_OPEN_STATE = 1;
|
|
|
|
// 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 shouldShowPlayOverlay = true;
|
|
|
|
// 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 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 = 'Click to start';
|
|
|
|
setOverlay('clickableState', startText, event => {
|
|
connect();
|
|
});
|
|
}
|
|
|
|
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.")
|
|
shouldShowPlayOverlay = false;
|
|
});
|
|
|
|
requestInitialSettings();
|
|
requestQualityControl();
|
|
hideOverlay();
|
|
} else {
|
|
console.error("Could not player video stream because webRtcPlayerObj.video was not valid.")
|
|
}
|
|
}
|
|
|
|
function updateAfkOverlayText() {
|
|
afk.overlay.innerHTML = '<center>No activity detected<br>Disconnecting in ' + afk.countdown + ' seconds<br>Click to continue<br></center>';
|
|
}
|
|
|
|
function hideOverlay() {
|
|
setOverlay('hiddenState');
|
|
}
|
|
|
|
function createWebRtcOffer() {
|
|
if (webRtcPlayerObj) {
|
|
console.log('Creating offer');
|
|
webRtcPlayerObj.createOffer();
|
|
} else {
|
|
console.log('WebRTC player not setup, cannot create offer');
|
|
}
|
|
}
|
|
|
|
function sendInputData(data) {
|
|
if (webRtcPlayerObj) {
|
|
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,
|
|
InitialSettings: 7
|
|
};
|
|
|
|
let VideoEncoderQP = "N/A";
|
|
|
|
function setupWebRtcPlayer(htmlElement, config) {
|
|
webRtcPlayerObj = new webRtcPlayer(config);
|
|
htmlElement.appendChild(webRtcPlayerObj.video);
|
|
|
|
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) {
|
|
shouldShowPlayOverlay = false;
|
|
resizePlayerStyle();
|
|
}
|
|
else {
|
|
resizePlayerStyle();
|
|
playVideoStream();
|
|
}
|
|
}
|
|
};
|
|
|
|
webRtcPlayerObj.onDataChannelConnected = function() {
|
|
if (ws && ws.readyState === WS_OPEN_STATE) {
|
|
if (webRtcPlayerObj.video && webRtcPlayerObj.video.srcObject && webRtcPlayerObj.onVideoInitialised) {
|
|
webRtcPlayerObj.onVideoInitialised();
|
|
}
|
|
}
|
|
};
|
|
|
|
registerInputs(webRtcPlayerObj.video);
|
|
|
|
createWebRtcOffer();
|
|
|
|
return webRtcPlayerObj.video;
|
|
}
|
|
|
|
function onWebRtcAnswer(webRTCData) {
|
|
webRtcPlayerObj.receiveAnswer(webRTCData);
|
|
|
|
webRtcPlayerObj.aggregateStats(1 * 1000 );
|
|
}
|
|
|
|
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,
|
|
|
|
// 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;
|
|
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 resizePlayerStyle(event) {
|
|
let playerElement = document.getElementById('player');
|
|
|
|
if (!playerElement)
|
|
return;
|
|
|
|
resizePlayerStyleToFillWindow(playerElement);
|
|
//resizePlayerStyleToActualSize(playerElement);
|
|
setupMouseAndFreezeFrame(playerElement)
|
|
}
|
|
|
|
function setupMouseAndFreezeFrame(playerElement) {
|
|
// Calculating and normalizing positions depends on the width and height of
|
|
// the player.
|
|
playerElementClientRect = playerElement.getBoundingClientRect();
|
|
setupNormalizeAndQuantize();
|
|
}
|
|
|
|
// 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
|
|
|
|
/**************************************************************************/
|
|
};
|
|
|
|
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;
|
|
|
|
if (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 {
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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);
|
|
}
|
|
|
|
function registerMouseEnterAndLeaveEvents(playerElement) {
|
|
playerElement.onmouseenter = function(e) {
|
|
let Data = new DataView(new ArrayBuffer(1));
|
|
Data.setUint8(0, MessageType.MouseEnter);
|
|
sendInputData(Data.buffer);
|
|
playerElement.pressMouseButtons(e);
|
|
};
|
|
|
|
playerElement.onmouseleave = function(e) {
|
|
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) {
|
|
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);
|
|
};
|
|
}
|
|
|
|
|
|
// 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) {
|
|
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) {
|
|
sendInputData(new Uint8Array([MessageType.KeyUp, getKeyCode(e)]).buffer);
|
|
if (inputOptions.suppressBrowserKeys && isKeyCodeBrowserKey(e.keyCode)) {
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
|
|
document.onkeypress = function(e) {
|
|
let data = new DataView(new ArrayBuffer(3));
|
|
data.setUint8(0, MessageType.KeyPress);
|
|
data.setUint16(1, e.charCode, true);
|
|
sendInputData(data.buffer);
|
|
};
|
|
}
|
|
|
|
function start() {
|
|
showConnectOverlay();
|
|
if (webRtcPlayerObj) {
|
|
webRtcPlayerObj.setVideoEnabled(true);
|
|
}
|
|
|
|
|
|
if (!connect_on_load || is_reconnection) {
|
|
shouldShowPlayOverlay = true;
|
|
resizePlayerStyle();
|
|
} else {
|
|
connect();
|
|
}
|
|
// connect();
|
|
setTimeout(connect, 2000);
|
|
}
|
|
|
|
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 = new WebSocket(window.location.href.replace('http://', 'ws://').replace('https://', 'wss://'));
|
|
let connectionUrl = window.location.href.replace('http://', 'ws://').replace('https://', 'wss://').replace(/:\d+/, '');
|
|
console.log(`Creating a websocket connection to: ${connectionUrl}`);
|
|
ws = new WebSocket(connectionUrl);
|
|
ws.attemptStreamReconnection = true;
|
|
|
|
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 === '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;
|
|
}
|
|
|
|
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() {
|
|
registerKeyboardEvents();
|
|
start();
|
|
}
|