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