ueapp.js 74 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943
  1. // Copyright Epic Games, Inc. All Rights Reserved.
  2. // Window events for a gamepad connecting
  3. let haveEvents = 'GamepadEvent' in window;
  4. let haveWebkitEvents = 'WebKitGamepadEvent' in window;
  5. let controllers = {};
  6. let rAF = window.mozRequestAnimationFrame ||
  7. window.webkitRequestAnimationFrame ||
  8. window.requestAnimationFrame;
  9. let kbEvent = document.createEvent("KeyboardEvent");
  10. let initMethod = typeof kbEvent.initKeyboardEvent !== 'undefined' ? "initKeyboardEvent" : "initKeyEvent";
  11. let webRtcPlayerObj = null;
  12. let print_stats = false;
  13. let print_inputs = false;
  14. let connect_on_load = true;
  15. let is_reconnection = false;
  16. let ws;
  17. const WS_OPEN_STATE = 1;
  18. let qualityControlOwnershipCheckBox;
  19. let matchViewportResolution;
  20. // TODO: Remove this - workaround because of bug causing UE to crash when switching resolutions too quickly
  21. let lastTimeResized = new Date().getTime();
  22. let resizeTimeout;
  23. let onDataChannelConnected;
  24. let responseEventListeners = new Map();
  25. let freezeFrameOverlay = null;
  26. let shouldShowPlayOverlay = true;
  27. // A freeze frame is a still JPEG image shown instead of the video.
  28. let freezeFrame = {
  29. receiving: false,
  30. size: 0,
  31. jpeg: undefined,
  32. height: 0,
  33. width: 0,
  34. valid: false
  35. };
  36. // Optionally detect if the user is not interacting (AFK) and disconnect them.
  37. let afk = {
  38. enabled: false, // Set to true to enable the AFK system.
  39. warnTimeout: 120, // The time to elapse before warning the user they are inactive.
  40. closeTimeout: 10, // The time after the warning when we disconnect the user.
  41. active: false, // Whether the AFK system is currently looking for inactivity.
  42. overlay: undefined, // The UI overlay warning the user that they are inactive.
  43. warnTimer: undefined, // The timer which waits to show the inactivity warning overlay.
  44. countdown: 0, // The inactivity warning overlay has a countdown to show time until disconnect.
  45. countdownTimer: undefined, // The timer used to tick the seconds shown on the inactivity warning overlay.
  46. }
  47. // If the user focuses on a UE4 input widget then we show them a button to open
  48. // the on-screen keyboard. JavaScript security means we can only show the
  49. // on-screen keyboard in response to a user interaction.
  50. let editTextButton = undefined;
  51. // A hidden input text box which is used only for focusing and opening the
  52. // on-screen keyboard.
  53. let hiddenInput = undefined;
  54. let t0 = Date.now();
  55. function log(str) {
  56. console.log(`${Math.floor(Date.now() - t0)}: ` + str);
  57. }
  58. function scanGamepads() {
  59. let gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);
  60. for (let i = 0; i < gamepads.length; i++) {
  61. if (gamepads[i] && (gamepads[i].index in controllers)) {
  62. controllers[gamepads[i].index].currentState = gamepads[i];
  63. }
  64. }
  65. }
  66. function updateStatus() {
  67. scanGamepads();
  68. // Iterate over multiple controllers in the case the mutiple gamepads are connected
  69. for (j in controllers) {
  70. let controller = controllers[j];
  71. let currentState = controller.currentState;
  72. let prevState = controller.prevState;
  73. // Iterate over buttons
  74. for (let i = 0; i < currentState.buttons.length; i++) {
  75. let currButton = currentState.buttons[i];
  76. let prevButton = prevState.buttons[i];
  77. // Button 6 is actually the left trigger, send it to UE as an analog axis
  78. // Button 7 is actually the right trigger, send it to UE as an analog axis
  79. // The rest are normal buttons. Treat as such
  80. if (currButton.pressed && !prevButton.pressed) {
  81. // New press
  82. if (i == 6) {
  83. emitControllerAxisMove(j, 5, currButton.value);
  84. } else if (i == 7) {
  85. emitControllerAxisMove(j, 6, currButton.value);
  86. } else {
  87. emitControllerButtonPressed(j, i, 0);
  88. }
  89. } else if (!currButton.pressed && prevButton.pressed) {
  90. // release
  91. if (i == 6) {
  92. emitControllerAxisMove(j, 5, 0);
  93. } else if (i == 7) {
  94. emitControllerAxisMove(j, 6, 0);
  95. } else {
  96. emitControllerButtonReleased(j, i);
  97. }
  98. } else if (currButton.pressed && prevButton.pressed) {
  99. // repeat press / hold
  100. if (i == 6) {
  101. emitControllerAxisMove(j, 5, currButton.value);
  102. } else if (i == 7) {
  103. emitControllerAxisMove(j, 6, currButton.value);
  104. } else {
  105. emitControllerButtonPressed(j, i, 1);
  106. }
  107. }
  108. // Last case is button isn't currently pressed and wasn't pressed before. This doesn't need an else block
  109. }
  110. // Iterate over gamepad axes
  111. for (let i = 0; i < currentState.axes.length; i += 2) {
  112. let x = parseFloat(currentState.axes[i].toFixed(4));
  113. // https://w3c.github.io/gamepad/#remapping Gamepad broweser side standard mapping has positive down, negative up. This is downright disgusting. So we fix it.
  114. let y = -parseFloat(currentState.axes[i + 1].toFixed(4));
  115. if (i === 0) {
  116. // left stick
  117. // axis 1 = left horizontal
  118. emitControllerAxisMove(j, 1, x);
  119. // axis 2 = left vertical
  120. emitControllerAxisMove(j, 2, y);
  121. } else if (i === 2) {
  122. // right stick
  123. // axis 3 = right horizontal
  124. emitControllerAxisMove(j, 3, x);
  125. // axis 4 = right vertical
  126. emitControllerAxisMove(j, 4, y);
  127. }
  128. }
  129. controllers[j].prevState = currentState;
  130. }
  131. rAF(updateStatus);
  132. }
  133. function emitControllerButtonPressed(controllerIndex, buttonIndex, isRepeat) {
  134. Data = new DataView(new ArrayBuffer(4));
  135. Data.setUint8(0, MessageType.GamepadButtonPressed);
  136. Data.setUint8(1, controllerIndex);
  137. Data.setUint8(2, buttonIndex);
  138. Data.setUint8(3, isRepeat);
  139. }
  140. function emitControllerButtonReleased(controllerIndex, buttonIndex) {
  141. Data = new DataView(new ArrayBuffer(3));
  142. Data.setUint8(0, MessageType.GamepadButtonReleased);
  143. Data.setUint8(1, controllerIndex);
  144. Data.setUint8(2, buttonIndex);
  145. }
  146. function emitControllerAxisMove(controllerIndex, axisIndex, analogValue) {
  147. Data = new DataView(new ArrayBuffer(11));
  148. Data.setUint8(0, MessageType.GamepadAnalog);
  149. Data.setUint8(1, controllerIndex);
  150. Data.setUint8(2, axisIndex);
  151. Data.setFloat64(3, analogValue, true);
  152. sendInputData(Data.buffer);
  153. }
  154. function gamepadConnectHandler(e) {
  155. console.log("Gamepad connect handler");
  156. gamepad = e.gamepad;
  157. controllers[gamepad.index] = {};
  158. controllers[gamepad.index].currentState = gamepad;
  159. controllers[gamepad.index].prevState = gamepad;
  160. console.log("gamepad: " + gamepad.id + " connected");
  161. rAF(updateStatus);
  162. }
  163. function gamepadDisconnectHandler(e) {
  164. console.log("Gamepad disconnect handler");
  165. console.log("gamepad: " + e.gamepad.id + " disconnected");
  166. delete controllers[e.gamepad.index];
  167. }
  168. function setupHtmlEvents() {
  169. //Window events
  170. window.addEventListener('resize', resizePlayerStyle, true);
  171. window.addEventListener('orientationchange', onOrientationChange);
  172. //Gamepad events
  173. if (haveEvents) {
  174. window.addEventListener("gamepadconnected", gamepadConnectHandler);
  175. window.addEventListener("gamepaddisconnected", gamepadDisconnectHandler);
  176. } else if (haveWebkitEvents) {
  177. window.addEventListener("webkitgamepadconnected", gamepadConnectHandler);
  178. window.addEventListener("webkitgamepaddisconnected", gamepadDisconnectHandler);
  179. }
  180. //HTML elements controls
  181. let overlayButton = document.getElementById('overlayButton');
  182. overlayButton.addEventListener('click', onExpandOverlay_Click);
  183. let resizeCheckBox = document.getElementById('enlarge-display-to-fill-window-tgl');
  184. if (resizeCheckBox !== null) {
  185. resizeCheckBox.onchange = function (event) {
  186. resizePlayerStyle();
  187. };
  188. }
  189. qualityControlOwnershipCheckBox = document.getElementById('quality-control-ownership-tgl');
  190. if (qualityControlOwnershipCheckBox !== null) {
  191. qualityControlOwnershipCheckBox.onchange = function (event) {
  192. requestQualityControl();
  193. };
  194. }
  195. let encoderParamsSubmit = document.getElementById('encoder-params-submit');
  196. if (encoderParamsSubmit !== null) {
  197. encoderParamsSubmit.onclick = function (event) {
  198. let rateControl = document.getElementById('encoder-rate-control').value;
  199. let targetBitrate = document.getElementById('encoder-target-bitrate-text').value * 1000;
  200. let maxBitrate = document.getElementById('encoder-max-bitrate-text').value * 1000;
  201. let minQP = document.getElementById('encoder-min-qp-text').value;
  202. let maxQP = document.getElementById('encoder-max-qp-text').value;
  203. let fillerData = document.getElementById('encoder-filler-data-tgl').checked ? 1 : 0;
  204. let multipass = document.getElementById('encoder-multipass').value;
  205. emitUIInteraction({ Console: 'PixelStreaming.Encoder.RateControl ' + rateControl });
  206. emitUIInteraction({ Console: 'PixelStreaming.Encoder.TargetBitrate ' + targetBitrate > 0 ? targetBitrate : -1 });
  207. emitUIInteraction({ Console: 'PixelStreaming.Encoder.MaxBitrateVBR ' + maxBitrate > 0 ? maxBitrate : -1 });
  208. emitUIInteraction({ Console: 'PixelStreaming.Encoder.MinQP ' + minQP });
  209. emitUIInteraction({ Console: 'PixelStreaming.Encoder.MaxQP ' + maxQP });
  210. emitUIInteraction({ Console: 'PixelStreaming.Encoder.EnableFillerData ' + fillerData });
  211. emitUIInteraction({ Console: 'PixelStreaming.Encoder.Multipass ' + multipass });
  212. };
  213. }
  214. let webrtcParamsSubmit = document.getElementById('webrtc-params-submit');
  215. if (webrtcParamsSubmit !== null) {
  216. webrtcParamsSubmit.onclick = function (event) {
  217. let degradationPref = document.getElementById('webrtc-degradation-pref').value;
  218. let maxFPS = document.getElementById('webrtc-max-fps-text').value;
  219. let minBitrate = document.getElementById('webrtc-min-bitrate-text').value * 1000;
  220. let maxBitrate = document.getElementById('webrtc-max-bitrate-text').value * 1000;
  221. let lowQP = document.getElementById('webrtc-low-qp-text').value;
  222. let highQP = document.getElementById('webrtc-high-qp-text').value;
  223. emitUIInteraction({ Console: 'PixelStreaming.WebRTC.DegradationPreference ' + degradationPref });
  224. emitUIInteraction({ Console: 'PixelStreaming.WebRTC.MaxFps ' + maxFPS });
  225. emitUIInteraction({ Console: 'PixelStreaming.WebRTC.MinBitrate ' + minBitrate });
  226. emitUIInteraction({ Console: 'PixelStreaming.WebRTC.MaxBitrate ' + maxBitrate });
  227. emitUIInteraction({ Console: 'PixelStreaming.WebRTC.LowQpThreshold ' + lowQP });
  228. emitUIInteraction({ Console: 'PixelStreaming.WebRTC.HighQpThreshold ' + highQP });
  229. };
  230. }
  231. let showFPSButton = document.getElementById('show-fps-button');
  232. if (showFPSButton !== null) {
  233. showFPSButton.onclick = function (event) {
  234. let consoleDescriptor = {
  235. Console: 'stat fps'
  236. };
  237. emitUIInteraction(consoleDescriptor);
  238. };
  239. }
  240. let matchViewportResolutionCheckBox = document.getElementById('match-viewport-res-tgl');
  241. if (matchViewportResolutionCheckBox !== null) {
  242. matchViewportResolutionCheckBox.onchange = function (event) {
  243. matchViewportResolution = matchViewportResolutionCheckBox.checked;
  244. };
  245. }
  246. let statsCheckBox = document.getElementById('show-stats-tgl');
  247. if (statsCheckBox !== null) {
  248. statsCheckBox.onchange = function (event) {
  249. let stats = document.getElementById('statsContainer');
  250. stats.style.display = event.target.checked ? "block" : "none";
  251. };
  252. }
  253. let kickButton = document.getElementById('kick-other-players-button');
  254. if (kickButton) {
  255. kickButton.onclick = function (event) {
  256. console.log(`-> SS: kick`);
  257. ws.send(JSON.stringify({
  258. type: 'kick'
  259. }));
  260. };
  261. }
  262. let latencyButton = document.getElementById('test-latency-button');
  263. if (latencyButton) {
  264. latencyButton.onclick = () => {
  265. sendStartLatencyTest();
  266. };
  267. }
  268. }
  269. function sendStartLatencyTest() {
  270. // We need WebRTC to be active to do a latency test.
  271. if (!webRtcPlayerObj) {
  272. return;
  273. }
  274. let onTestStarted = function (StartTimeMs) {
  275. let descriptor = {
  276. StartTime: StartTimeMs
  277. };
  278. emitDescriptor(MessageType.LatencyTest, descriptor);
  279. };
  280. webRtcPlayerObj.startLatencyTest(onTestStarted);
  281. }
  282. function setOverlay(htmlClass, htmlElement, onClickFunction) {
  283. let videoPlayOverlay = document.getElementById('videoPlayOverlay');
  284. if (!videoPlayOverlay) {
  285. let playerDiv = document.getElementById('player');
  286. videoPlayOverlay = document.createElement('div');
  287. videoPlayOverlay.id = 'videoPlayOverlay';
  288. playerDiv.appendChild(videoPlayOverlay);
  289. }
  290. // Remove existing html child elements so we can add the new one
  291. while (videoPlayOverlay.lastChild) {
  292. videoPlayOverlay.removeChild(videoPlayOverlay.lastChild);
  293. }
  294. if (htmlElement)
  295. videoPlayOverlay.appendChild(htmlElement);
  296. if (onClickFunction) {
  297. videoPlayOverlay.addEventListener('click', function onOverlayClick(event) {
  298. onClickFunction(event);
  299. videoPlayOverlay.removeEventListener('click', onOverlayClick);
  300. });
  301. }
  302. // Remove existing html classes so we can set the new one
  303. let cl = videoPlayOverlay.classList;
  304. for (let i = cl.length - 1; i >= 0; i--) {
  305. cl.remove(cl[i]);
  306. }
  307. videoPlayOverlay.classList.add(htmlClass);
  308. }
  309. function showConnectOverlay() {
  310. let startText = document.createElement('div');
  311. startText.id = 'playButton';
  312. startText.innerHTML = 'Click to start';
  313. setOverlay('clickableState', startText, event => {
  314. connect();
  315. startAfkWarningTimer();
  316. });
  317. }
  318. function showTextOverlay(text) {
  319. let textOverlay = document.createElement('div');
  320. textOverlay.id = 'messageOverlay';
  321. textOverlay.innerHTML = text ? text : '';
  322. setOverlay('textDisplayState', textOverlay);
  323. }
  324. function playVideoStream() {
  325. if (webRtcPlayerObj && webRtcPlayerObj.video) {
  326. webRtcPlayerObj.video.play().catch(function (onRejectedReason) {
  327. console.error(onRejectedReason);
  328. console.log("Browser does not support autoplaying video without interaction - to resolve this we are going to show the play button overlay.")
  329. showPlayOverlay();
  330. });
  331. requestInitialSettings();
  332. requestQualityControl();
  333. showFreezeFrameOverlay();
  334. hideOverlay();
  335. } else {
  336. console.error("Could not player video stream because webRtcPlayerObj.video was not valid.")
  337. }
  338. }
  339. function showPlayOverlay() {
  340. let img = document.createElement('img');
  341. img.id = 'playButton';
  342. img.src = '/images/Play.png';
  343. img.alt = 'Start Streaming';
  344. setOverlay('clickableState', img, event => {
  345. playVideoStream();
  346. });
  347. shouldShowPlayOverlay = false;
  348. }
  349. function updateAfkOverlayText() {
  350. afk.overlay.innerHTML = '<center>No activity detected<br>Disconnecting in ' + afk.countdown + ' seconds<br>Click to continue<br></center>';
  351. }
  352. function showAfkOverlay() {
  353. // Pause the timer while the user is looking at the inactivity warning overlay.
  354. stopAfkWarningTimer();
  355. // Show the inactivity warning overlay.
  356. afk.overlay = document.createElement('div');
  357. afk.overlay.id = 'afkOverlay';
  358. setOverlay('clickableState', afk.overlay, event => {
  359. // The user clicked so start the timer again and carry on.
  360. hideOverlay();
  361. clearInterval(afk.countdownTimer);
  362. startAfkWarningTimer();
  363. });
  364. afk.countdown = afk.closeTimeout;
  365. updateAfkOverlayText();
  366. if (inputOptions.controlScheme == ControlSchemeType.HoveringMouse) {
  367. document.exitPointerLock();
  368. }
  369. inputOptions.controlScheme = ControlSchemeType.HoveringMouse;
  370. afk.countdownTimer = setInterval(function () {
  371. afk.countdown--;
  372. if (afk.countdown == 0) {
  373. // The user failed to click so disconnect them.
  374. hideOverlay();
  375. ws.close();
  376. } else {
  377. // Update the countdown message.
  378. updateAfkOverlayText();
  379. }
  380. }, 1000);
  381. }
  382. function hideOverlay() {
  383. setOverlay('hiddenState');
  384. }
  385. // Start a timer which when elapsed will warn the user they are inactive.
  386. function startAfkWarningTimer() {
  387. afk.active = afk.enabled;
  388. resetAfkWarningTimer();
  389. }
  390. // Stop the timer which when elapsed will warn the user they are inactive.
  391. function stopAfkWarningTimer() {
  392. afk.active = false;
  393. }
  394. // If the user interacts then reset the warning timer.
  395. function resetAfkWarningTimer() {
  396. if (afk.active) {
  397. clearTimeout(afk.warnTimer);
  398. afk.warnTimer = setTimeout(function () {
  399. showAfkOverlay();
  400. }, afk.warnTimeout * 1000);
  401. }
  402. }
  403. function createWebRtcOffer() {
  404. if (webRtcPlayerObj) {
  405. console.log('Creating offer');
  406. showTextOverlay('Starting connection to server, please wait');
  407. webRtcPlayerObj.createOffer();
  408. } else {
  409. console.log('WebRTC player not setup, cannot create offer');
  410. showTextOverlay('Unable to setup video');
  411. }
  412. }
  413. function sendInputData(data) {
  414. if (webRtcPlayerObj) {
  415. resetAfkWarningTimer();
  416. webRtcPlayerObj.send(data);
  417. }
  418. }
  419. function addResponseEventListener(name, listener) {
  420. responseEventListeners.set(name, listener);
  421. }
  422. function removeResponseEventListener(name) {
  423. // console.log('ssss',responseEventListeners);
  424. // responseEventListeners.set(name, function () { });
  425. responseEventListeners.remove(name);
  426. }
  427. // Must be kept in sync with PixelStreamingProtocol::EToPlayerMsg C++ enum.
  428. const ToClientMessageType = {
  429. QualityControlOwnership: 0,
  430. Response: 1,
  431. Command: 2,
  432. FreezeFrame: 3,
  433. UnfreezeFrame: 4,
  434. VideoEncoderAvgQP: 5,
  435. LatencyTest: 6,
  436. InitialSettings: 7
  437. };
  438. let VideoEncoderQP = "N/A";
  439. function setupWebRtcPlayer(htmlElement, config) {
  440. webRtcPlayerObj = new webRtcPlayer(config);
  441. htmlElement.appendChild(webRtcPlayerObj.video);
  442. htmlElement.appendChild(freezeFrameOverlay);
  443. webRtcPlayerObj.onWebRtcOffer = function (offer) {
  444. if (ws && ws.readyState === WS_OPEN_STATE) {
  445. let offerStr = JSON.stringify(offer);
  446. console.log(`-> SS: offer:\n${offerStr}`);
  447. ws.send(offerStr);
  448. }
  449. };
  450. webRtcPlayerObj.onWebRtcCandidate = function (candidate) {
  451. if (ws && ws.readyState === WS_OPEN_STATE) {
  452. console.log(`-> SS: iceCandidate\n${JSON.stringify(candidate, undefined, 4)}`);
  453. ws.send(JSON.stringify({
  454. type: 'iceCandidate',
  455. candidate: candidate
  456. }));
  457. }
  458. };
  459. webRtcPlayerObj.onVideoInitialised = function () {
  460. if (ws && ws.readyState === WS_OPEN_STATE) {
  461. if (shouldShowPlayOverlay) {
  462. showPlayOverlay();
  463. resizePlayerStyle();
  464. }
  465. else {
  466. resizePlayerStyle();
  467. playVideoStream();
  468. }
  469. }
  470. };
  471. webRtcPlayerObj.onDataChannelConnected = function () {
  472. if (ws && ws.readyState === WS_OPEN_STATE) {
  473. showTextOverlay('WebRTC connected, waiting for video');
  474. if (webRtcPlayerObj.video && webRtcPlayerObj.video.srcObject && webRtcPlayerObj.onVideoInitialised) {
  475. webRtcPlayerObj.onVideoInitialised();
  476. }
  477. }
  478. };
  479. function showFreezeFrame() {
  480. let base64 = btoa(freezeFrame.jpeg.reduce((data, byte) => data + String.fromCharCode(byte), ''));
  481. let freezeFrameImage = document.getElementById("freezeFrameOverlay").childNodes[0];
  482. freezeFrameImage.src = 'data:image/jpeg;base64,' + base64;
  483. freezeFrameImage.onload = function () {
  484. freezeFrame.height = freezeFrameImage.naturalHeight;
  485. freezeFrame.width = freezeFrameImage.naturalWidth;
  486. resizeFreezeFrameOverlay();
  487. if (shouldShowPlayOverlay) {
  488. showPlayOverlay();
  489. resizePlayerStyle();
  490. } else {
  491. showFreezeFrameOverlay();
  492. }
  493. webRtcPlayerObj.setVideoEnabled(false);
  494. };
  495. }
  496. function processFreezeFrameMessage(view) {
  497. // Reset freeze frame if we got a freeze frame message and we are not "receiving" yet.
  498. if (!freezeFrame.receiving) {
  499. freezeFrame.receiving = true;
  500. freezeFrame.valid = false;
  501. freezeFrame.size = 0;
  502. freezeFrame.jpeg = undefined;
  503. }
  504. // Extract total size of freeze frame (across all chunks)
  505. freezeFrame.size = (new DataView(view.slice(1, 5).buffer)).getInt32(0, true);
  506. // Get the jpeg part of the payload
  507. let jpegBytes = view.slice(1 + 4);
  508. // Append to existing jpeg that holds the freeze frame
  509. if (freezeFrame.jpeg) {
  510. let jpeg = new Uint8Array(freezeFrame.jpeg.length + jpegBytes.length);
  511. jpeg.set(freezeFrame.jpeg, 0);
  512. jpeg.set(jpegBytes, freezeFrame.jpeg.length);
  513. freezeFrame.jpeg = jpeg;
  514. }
  515. // No existing freeze frame jpeg, make one
  516. else {
  517. freezeFrame.jpeg = jpegBytes;
  518. freezeFrame.receiving = true;
  519. console.log(`received first chunk of freeze frame: ${freezeFrame.jpeg.length}/${freezeFrame.size}`);
  520. }
  521. // Uncomment for debug
  522. //console.log(`Received freeze frame chunk: ${freezeFrame.jpeg.length}/${freezeFrame.size}`);
  523. // Finished receiving freeze frame, we can show it now
  524. if (freezeFrame.jpeg.length === freezeFrame.size) {
  525. freezeFrame.receiving = false;
  526. freezeFrame.valid = true;
  527. console.log(`received complete freeze frame ${freezeFrame.size}`);
  528. showFreezeFrame();
  529. }
  530. // We received more data than the freeze frame payload message indicate (this is an error)
  531. else if (freezeFrame.jpeg.length > freezeFrame.size) {
  532. console.error(`received bigger freeze frame than advertised: ${freezeFrame.jpeg.length}/${freezeFrame.size}`);
  533. freezeFrame.jpeg = undefined;
  534. freezeFrame.receiving = false;
  535. }
  536. }
  537. webRtcPlayerObj.onDataChannelMessage = function (data) {
  538. let view = new Uint8Array(data);
  539. if (view[0] === ToClientMessageType.QualityControlOwnership) {
  540. let ownership = view[1] === 0 ? false : true;
  541. console.log("Received quality controller message, will control quality: " + ownership);
  542. // If we own the quality control, we can't relenquish it. We only loose
  543. // quality control when another peer asks for it
  544. if (qualityControlOwnershipCheckBox !== null) {
  545. qualityControlOwnershipCheckBox.disabled = ownership;
  546. qualityControlOwnershipCheckBox.checked = ownership;
  547. }
  548. } else if (view[0] === ToClientMessageType.Response) {
  549. let response = new TextDecoder("utf-16").decode(data.slice(1));
  550. for (let listener of responseEventListeners.values()) {
  551. listener(response);
  552. }
  553. } else if (view[0] === ToClientMessageType.Command) {
  554. let commandAsString = new TextDecoder("utf-16").decode(data.slice(1));
  555. console.log(commandAsString);
  556. let command = JSON.parse(commandAsString);
  557. if (command.command === 'onScreenKeyboard') {
  558. showOnScreenKeyboard(command);
  559. }
  560. } else if (view[0] === ToClientMessageType.FreezeFrame) {
  561. processFreezeFrameMessage(view);
  562. } else if (view[0] === ToClientMessageType.UnfreezeFrame) {
  563. invalidateFreezeFrameOverlay();
  564. } else if (view[0] === ToClientMessageType.VideoEncoderAvgQP) {
  565. VideoEncoderQP = new TextDecoder("utf-16").decode(data.slice(1));
  566. //console.log(`received VideoEncoderAvgQP ${VideoEncoderQP}`);
  567. } else if (view[0] == ToClientMessageType.LatencyTest) {
  568. let latencyTimingsAsString = new TextDecoder("utf-16").decode(data.slice(1));
  569. console.log("Got latency timings from UE.")
  570. console.log(latencyTimingsAsString);
  571. let latencyTimingsFromUE = JSON.parse(latencyTimingsAsString);
  572. if (webRtcPlayerObj) {
  573. webRtcPlayerObj.latencyTestTimings.SetUETimings(latencyTimingsFromUE);
  574. }
  575. } else if (view[0] == ToClientMessageType.InitialSettings) {
  576. let settingsString = new TextDecoder("utf-16").decode(data.slice(1));
  577. let settingsJSON = JSON.parse(settingsString);
  578. // reminder bitrates are sent in bps but displayed in kbps
  579. if (settingsJSON.Encoder) {
  580. document.getElementById('encoder-rate-control').value = settingsJSON.Encoder.RateControl;
  581. document.getElementById('encoder-target-bitrate-text').value = settingsJSON.Encoder.TargetBitrate > 0 ? settingsJSON.Encoder.TargetBitrate / 1000 : settingsJSON.Encoder.TargetBitrate;
  582. document.getElementById('encoder-max-bitrate-text').value = settingsJSON.Encoder.MaxBitrate > 0 ? settingsJSON.Encoder.MaxBitrate / 1000 : settingsJSON.Encoder.MaxBitrate;
  583. document.getElementById('encoder-min-qp-text').value = settingsJSON.Encoder.MinQP;
  584. document.getElementById('encoder-max-qp-text').value = settingsJSON.Encoder.MaxQP;
  585. document.getElementById('encoder-filler-data-tgl').checked = settingsJSON.Encoder.FillerData == 1;
  586. document.getElementById('encoder-multipass').value = settingsJSON.Encoder.MultiPass;
  587. }
  588. if (settingsJSON.WebRTC) {
  589. document.getElementById('webrtc-degradation-pref').value = settingsJSON.WebRTC.DegradationPref;
  590. document.getElementById("webrtc-max-fps-text").value = settingsJSON.WebRTC.MaxFPS;
  591. document.getElementById("webrtc-min-bitrate-text").value = settingsJSON.WebRTC.MinBitrate / 1000;
  592. document.getElementById("webrtc-max-bitrate-text").value = settingsJSON.WebRTC.MaxBitrate / 1000;
  593. document.getElementById("webrtc-low-qp-text").value = settingsJSON.WebRTC.LowQP;
  594. document.getElementById("webrtc-high-qp-text").value = settingsJSON.WebRTC.HighQP;
  595. }
  596. } else {
  597. console.error(`unrecognized data received, packet ID ${view[0]}`);
  598. }
  599. };
  600. registerInputs(webRtcPlayerObj.video);
  601. // On a touch device we will need special ways to show the on-screen keyboard.
  602. if ('ontouchstart' in document.documentElement) {
  603. createOnScreenKeyboardHelpers(htmlElement);
  604. }
  605. createWebRtcOffer();
  606. return webRtcPlayerObj.video;
  607. }
  608. function onWebRtcAnswer(webRTCData) {
  609. webRtcPlayerObj.receiveAnswer(webRTCData);
  610. let printInterval = 5 * 60 * 1000; /*Print every 5 minutes*/
  611. let nextPrintDuration = printInterval;
  612. webRtcPlayerObj.onAggregatedStats = (aggregatedStats) => {
  613. let numberFormat = new Intl.NumberFormat(window.navigator.language, {
  614. maximumFractionDigits: 0
  615. });
  616. let timeFormat = new Intl.NumberFormat(window.navigator.language, {
  617. maximumFractionDigits: 0,
  618. minimumIntegerDigits: 2
  619. });
  620. // Calculate duration of run
  621. let runTime = (aggregatedStats.timestamp - aggregatedStats.timestampStart) / 1000;
  622. let timeValues = [];
  623. let timeDurations = [60, 60];
  624. for (let timeIndex = 0; timeIndex < timeDurations.length; timeIndex++) {
  625. timeValues.push(runTime % timeDurations[timeIndex]);
  626. runTime = runTime / timeDurations[timeIndex];
  627. }
  628. timeValues.push(runTime);
  629. let runTimeSeconds = timeValues[0];
  630. let runTimeMinutes = Math.floor(timeValues[1]);
  631. let runTimeHours = Math.floor([timeValues[2]]);
  632. receivedBytesMeasurement = 'B';
  633. receivedBytes = aggregatedStats.hasOwnProperty('bytesReceived') ? aggregatedStats.bytesReceived : 0;
  634. let dataMeasurements = ['kB', 'MB', 'GB'];
  635. for (let index = 0; index < dataMeasurements.length; index++) {
  636. if (receivedBytes < 100 * 1000)
  637. break;
  638. receivedBytes = receivedBytes / 1000;
  639. receivedBytesMeasurement = dataMeasurements[index];
  640. }
  641. let qualityStatus = document.getElementById("qualityStatus");
  642. // "blinks" quality status element for 1 sec by making it transparent, speed = number of blinks
  643. let blinkQualityStatus = function (speed) {
  644. let iter = speed;
  645. let opacity = 1; // [0..1]
  646. let tickId = setInterval(
  647. function () {
  648. opacity -= 0.1;
  649. // map `opacity` to [-0.5..0.5] range, decrement by 0.2 per step and take `abs` to make it blink: 1 -> 0 -> 1
  650. qualityStatus.style = `opacity: ${Math.abs((opacity - 0.5) * 2)}`;
  651. if (opacity <= 0.1) {
  652. if (--iter == 0) {
  653. clearInterval(tickId);
  654. } else { // next blink
  655. opacity = 1;
  656. }
  657. }
  658. },
  659. 100 / speed // msecs
  660. );
  661. };
  662. const orangeQP = 26;
  663. const redQP = 35;
  664. let statsText = '';
  665. let color = "lime";
  666. if (VideoEncoderQP > redQP) {
  667. color = "red";
  668. blinkQualityStatus(2);
  669. statsText += `<div style="color: ${color}">Bad network connection</div>`;
  670. } else if (VideoEncoderQP > orangeQP) {
  671. color = "orange";
  672. blinkQualityStatus(1);
  673. statsText += `<div style="color: ${color}">Spotty network connection</div>`;
  674. }
  675. qualityStatus.className = `${color}Status`;
  676. statsText += `<div>Duration: ${timeFormat.format(runTimeHours)}:${timeFormat.format(runTimeMinutes)}:${timeFormat.format(runTimeSeconds)}</div>`;
  677. statsText += `<div>Video Resolution: ${aggregatedStats.hasOwnProperty('frameWidth') && aggregatedStats.frameWidth && aggregatedStats.hasOwnProperty('frameHeight') && aggregatedStats.frameHeight ?
  678. aggregatedStats.frameWidth + 'x' + aggregatedStats.frameHeight : 'Chrome only'
  679. }</div>`;
  680. statsText += `<div>Received (${receivedBytesMeasurement}): ${numberFormat.format(receivedBytes)}</div>`;
  681. statsText += `<div>Frames Decoded: ${aggregatedStats.hasOwnProperty('framesDecoded') ? numberFormat.format(aggregatedStats.framesDecoded) : 'Chrome only'}</div>`;
  682. statsText += `<div>Packets Lost: ${aggregatedStats.hasOwnProperty('packetsLost') ? numberFormat.format(aggregatedStats.packetsLost) : 'Chrome only'}</div>`;
  683. statsText += `<div style="color: ${color}">Bitrate (kbps): ${aggregatedStats.hasOwnProperty('bitrate') ? numberFormat.format(aggregatedStats.bitrate) : 'Chrome only'}</div>`;
  684. statsText += `<div>Framerate: ${aggregatedStats.hasOwnProperty('framerate') ? numberFormat.format(aggregatedStats.framerate) : 'Chrome only'}</div>`;
  685. statsText += `<div>Frames dropped: ${aggregatedStats.hasOwnProperty('framesDropped') ? numberFormat.format(aggregatedStats.framesDropped) : 'Chrome only'}</div>`;
  686. statsText += `<div>Net RTT (ms): ${aggregatedStats.hasOwnProperty('currentRoundTripTime') ? numberFormat.format(aggregatedStats.currentRoundTripTime * 1000) : 'Can\'t calculate'}</div>`;
  687. statsText += `<div>Browser receive to composite (ms): ${aggregatedStats.hasOwnProperty('receiveToCompositeMs') ? numberFormat.format(aggregatedStats.receiveToCompositeMs) : 'Chrome only'}</div>`;
  688. statsText += `<div style="color: ${color}">Video Quantization Parameter: ${VideoEncoderQP}</div>`;
  689. let statsDiv = document.getElementById("stats");
  690. statsDiv.innerHTML = statsText;
  691. if (print_stats) {
  692. if (aggregatedStats.timestampStart) {
  693. if ((aggregatedStats.timestamp - aggregatedStats.timestampStart) > nextPrintDuration) {
  694. if (ws && ws.readyState === WS_OPEN_STATE) {
  695. console.log(`-> SS: stats\n${JSON.stringify(aggregatedStats)}`);
  696. ws.send(JSON.stringify({
  697. type: 'stats',
  698. data: aggregatedStats
  699. }));
  700. }
  701. nextPrintDuration += printInterval;
  702. }
  703. }
  704. }
  705. };
  706. webRtcPlayerObj.aggregateStats(1 * 1000 /*Check every 1 second*/);
  707. webRtcPlayerObj.latencyTestTimings.OnAllLatencyTimingsReady = function (timings) {
  708. if (!timings.BrowserReceiptTimeMs) {
  709. return;
  710. }
  711. let latencyExcludingDecode = timings.BrowserReceiptTimeMs - timings.TestStartTimeMs;
  712. let uePixelStreamLatency = timings.UEPreEncodeTimeMs == 0 || timings.UEPreCaptureTimeMs == 0 ? "???" : timings.UEPostEncodeTimeMs - timings.UEPreCaptureTimeMs;
  713. let captureLatency = timings.UEPostCaptureTimeMs - timings.UEPreCaptureTimeMs;
  714. let encodeLatency = timings.UEPostEncodeTimeMs - timings.UEPreEncodeTimeMs;
  715. let ueLatency = timings.UETransmissionTimeMs - timings.UEReceiptTimeMs;
  716. let networkLatency = latencyExcludingDecode - ueLatency;
  717. let browserSendLatency = latencyExcludingDecode - networkLatency - ueLatency;
  718. //these ones depend on FrameDisplayDeltaTimeMs
  719. let endToEndLatency = null;
  720. let browserSideLatency = null;
  721. if (timings.FrameDisplayDeltaTimeMs && timings.BrowserReceiptTimeMs) {
  722. endToEndLatency = timings.FrameDisplayDeltaTimeMs + latencyExcludingDecode;
  723. browserSideLatency = endToEndLatency - networkLatency - ueLatency;
  724. }
  725. let latencyStatsInnerHTML = '';
  726. latencyStatsInnerHTML += `<div>Net latency RTT (ms): ${networkLatency}</div>`;
  727. latencyStatsInnerHTML += `<div>UE Capture+Encode (ms): ${uePixelStreamLatency}</div>`;
  728. latencyStatsInnerHTML += `<div>UE Capture (ms): ${captureLatency}</div>`;
  729. latencyStatsInnerHTML += `<div>UE Encode (ms): ${encodeLatency}</div>`;
  730. latencyStatsInnerHTML += `<div>Total UE latency (ms): ${ueLatency}</div>`;
  731. latencyStatsInnerHTML += `<div>Browser send latency (ms): ${browserSendLatency}</div>`
  732. latencyStatsInnerHTML += timings.FrameDisplayDeltaTimeMs && timings.BrowserReceiptTimeMs ? `<div>Browser receive latency (ms): ${timings.FrameDisplayDeltaTimeMs}</div>` : "";
  733. latencyStatsInnerHTML += browserSideLatency ? `<div>Total browser latency (ms): ${browserSideLatency}</div>` : "";
  734. latencyStatsInnerHTML += `<div>Total latency (excluding browser) (ms): ${latencyExcludingDecode}</div>`;
  735. latencyStatsInnerHTML += endToEndLatency ? `<div>Total latency (ms): ${endToEndLatency}</div>` : "";
  736. document.getElementById("LatencyStats").innerHTML = latencyStatsInnerHTML;
  737. }
  738. }
  739. function onWebRtcIce(iceCandidate) {
  740. if (webRtcPlayerObj)
  741. webRtcPlayerObj.handleCandidateFromServer(iceCandidate);
  742. }
  743. let styleWidth;
  744. let styleHeight;
  745. let styleTop;
  746. let styleLeft;
  747. let styleCursor = 'default';
  748. let styleAdditional;
  749. const ControlSchemeType = {
  750. // A mouse can lock inside the WebRTC player so the user can simply move the
  751. // mouse to control the orientation of the camera. The user presses the
  752. // Escape key to unlock the mouse.
  753. LockedMouse: 0,
  754. // A mouse can hover over the WebRTC player so the user needs to click and
  755. // drag to control the orientation of the camera.
  756. HoveringMouse: 1
  757. };
  758. let inputOptions = {
  759. // The control scheme controls the behaviour of the mouse when it interacts
  760. // with the WebRTC player.
  761. controlScheme: ControlSchemeType.HoveringMouse,
  762. // Browser keys are those which are typically used by the browser UI. We
  763. // usually want to suppress these to allow, for example, UE4 to show shader
  764. // complexity with the F5 key without the web page refreshing.
  765. suppressBrowserKeys: false,
  766. // UE4 has a faketouches option which fakes a single finger touch when the
  767. // user drags with their mouse. We may perform the reverse; a single finger
  768. // touch may be converted into a mouse drag UE4 side. This allows a
  769. // non-touch application to be controlled partially via a touch device.
  770. fakeMouseWithTouches: false
  771. };
  772. function resizePlayerStyleToFillWindow(playerElement) {
  773. let videoElement = playerElement.getElementsByTagName("VIDEO");
  774. // Fill the player display in window, keeping picture's aspect ratio.
  775. let windowAspectRatio = window.innerHeight / window.innerWidth;
  776. let playerAspectRatio = playerElement.clientHeight / playerElement.clientWidth;
  777. // We want to keep the video ratio correct for the video stream
  778. let videoAspectRatio = videoElement.videoHeight / videoElement.videoWidth;
  779. if (isNaN(videoAspectRatio)) {
  780. //Video is not initialised yet so set playerElement to size of window
  781. styleWidth = window.innerWidth;
  782. styleHeight = window.innerHeight;
  783. styleTop = 0;
  784. styleLeft = 0;
  785. playerElement.style = "top: " + 0 + "px; left: " + 0 + "px; width: " + styleWidth + "px; height: " + styleHeight + "px; cursor: " + styleCursor + "; " + styleAdditional;
  786. } else if (windowAspectRatio < playerAspectRatio) {
  787. // Window height is the constraining factor so to keep aspect ratio change width appropriately
  788. styleWidth = Math.floor(window.innerHeight / videoAspectRatio);
  789. styleHeight = window.innerHeight;
  790. styleTop = 0;
  791. styleLeft = Math.floor((window.innerWidth - styleWidth) * 0.5);
  792. //Video is now 100% of the playerElement, so set the playerElement style
  793. playerElement.style = "top: " + 0 + "px; left: " + 0 + "px; width: " + styleWidth + "px; height: " + styleHeight + "px; cursor: " + styleCursor + "; " + styleAdditional;
  794. } else {
  795. // Window width is the constraining factor so to keep aspect ratio change height appropriately
  796. styleWidth = window.innerWidth;
  797. styleHeight = Math.floor(window.innerWidth * videoAspectRatio);
  798. styleTop = Math.floor((window.innerHeight - styleHeight) * 0.5);
  799. styleLeft = 0;
  800. //Video is now 100% of the playerElement, so set the playerElement style
  801. playerElement.style = "top: " + 0 + "px; left: " + 0 + "px; width: " + styleWidth + "px; height: " + styleHeight + "px; cursor: " + styleCursor + "; " + styleAdditional;
  802. }
  803. }
  804. function resizePlayerStyleToActualSize(playerElement) {
  805. let videoElement = playerElement.getElementsByTagName("VIDEO");
  806. if (videoElement.length > 0) {
  807. // Display image in its actual size
  808. styleWidth = videoElement[0].videoWidth;
  809. styleHeight = videoElement[0].videoHeight;
  810. let Top = Math.floor((window.innerHeight - styleHeight) * 0.5);
  811. let Left = Math.floor((window.innerWidth - styleWidth) * 0.5);
  812. styleTop = (Top > 0) ? Top : 0;
  813. styleLeft = (Left > 0) ? Left : 0;
  814. //Video is now 100% of the playerElement, so set the playerElement style
  815. playerElement.style = "top: " + 0 + "px; left: " + 0 + "px; width: 100% ; height: 100%; cursor: " + styleCursor + "; " + styleAdditional;
  816. }
  817. }
  818. function resizePlayerStyleToArbitrarySize(playerElement) {
  819. let videoElement = playerElement.getElementsByTagName("VIDEO");
  820. //Video is now 100% of the playerElement, so set the playerElement style
  821. playerElement.style = "top: 0px; left: 0px; width: " + styleWidth + "px; height: " + styleHeight + "px; cursor: " + styleCursor + "; " + styleAdditional;
  822. }
  823. function setupFreezeFrameOverlay() {
  824. freezeFrameOverlay = document.createElement('div');
  825. freezeFrameOverlay.id = 'freezeFrameOverlay';
  826. freezeFrameOverlay.style.display = 'none';
  827. freezeFrameOverlay.style.pointerEvents = 'none';
  828. freezeFrameOverlay.style.position = 'absolute';
  829. freezeFrameOverlay.style.zIndex = '20';
  830. let freezeFrameImage = document.createElement('img');
  831. freezeFrameImage.style.position = 'absolute';
  832. freezeFrameOverlay.appendChild(freezeFrameImage);
  833. }
  834. function showFreezeFrameOverlay() {
  835. if (freezeFrame.valid) {
  836. freezeFrameOverlay.classList.add("freezeframeBackground");
  837. freezeFrameOverlay.style.display = 'block';
  838. }
  839. }
  840. function invalidateFreezeFrameOverlay() {
  841. freezeFrameOverlay.style.display = 'none';
  842. freezeFrame.valid = false;
  843. freezeFrameOverlay.classList.remove("freezeframeBackground");
  844. if (webRtcPlayerObj) {
  845. webRtcPlayerObj.setVideoEnabled(true);
  846. }
  847. }
  848. function resizeFreezeFrameOverlay() {
  849. if (freezeFrame.width !== 0 && freezeFrame.height !== 0) {
  850. let displayWidth = 0;
  851. let displayHeight = 0;
  852. let displayTop = 0;
  853. let displayLeft = 0;
  854. let checkBox = document.getElementById('enlarge-display-to-fill-window-tgl');
  855. let playerElement = document.getElementById('player');
  856. if (checkBox !== null && checkBox.checked) {
  857. // We are fitting video to screen, we care about the screen (window) size
  858. let windowAspectRatio = window.innerWidth / window.innerHeight;
  859. let videoAspectRatio = freezeFrame.width / freezeFrame.height;
  860. if (windowAspectRatio < videoAspectRatio) {
  861. displayWidth = window.innerWidth;
  862. displayHeight = Math.floor(window.innerWidth / videoAspectRatio);
  863. displayTop = Math.floor((window.innerHeight - displayHeight) * 0.5);
  864. displayLeft = 0;
  865. } else {
  866. displayWidth = Math.floor(window.innerHeight * videoAspectRatio);
  867. displayHeight = window.innerHeight;
  868. displayTop = 0;
  869. displayLeft = Math.floor((window.innerWidth - displayWidth) * 0.5);
  870. }
  871. } else {
  872. // Video is coming in at native resolution, we care more about the player size
  873. let playerAspectRatio = playerElement.offsetWidth / playerElement.offsetHeight;
  874. let videoAspectRatio = freezeFrame.width / freezeFrame.height;
  875. if (playerAspectRatio < videoAspectRatio) {
  876. displayWidth = playerElement.offsetWidth;
  877. displayHeight = Math.floor(playerElement.offsetWidth / videoAspectRatio);
  878. displayTop = Math.floor((playerElement.offsetHeight - displayHeight) * 0.5);
  879. displayLeft = 0;
  880. } else {
  881. displayWidth = Math.floor(playerElement.offsetHeight * videoAspectRatio);
  882. displayHeight = playerElement.offsetHeight;
  883. displayTop = 0;
  884. displayLeft = Math.floor((playerElement.offsetWidth - displayWidth) * 0.5);
  885. }
  886. }
  887. let freezeFrameImage = document.getElementById("freezeFrameOverlay").childNodes[0];
  888. freezeFrameOverlay.style.width = playerElement.offsetWidth + 'px';
  889. freezeFrameOverlay.style.height = playerElement.offsetHeight + 'px';
  890. freezeFrameOverlay.style.left = 0 + 'px';
  891. freezeFrameOverlay.style.top = 0 + 'px';
  892. freezeFrameImage.style.width = displayWidth + 'px';
  893. freezeFrameImage.style.height = displayHeight + 'px';
  894. freezeFrameImage.style.left = displayLeft + 'px';
  895. freezeFrameImage.style.top = displayTop + 'px';
  896. }
  897. }
  898. function resizePlayerStyle(event) {
  899. let playerElement = document.getElementById('player');
  900. if (!playerElement)
  901. return;
  902. updateVideoStreamSize();
  903. if (playerElement.classList.contains('fixed-size')) {
  904. setupMouseAndFreezeFrame(playerElement)
  905. return;
  906. }
  907. let checkBox = document.getElementById('enlarge-display-to-fill-window-tgl');
  908. let windowSmallerThanPlayer = window.innerWidth < playerElement.videoWidth || window.innerHeight < playerElement.videoHeight;
  909. if (checkBox !== null) {
  910. if (checkBox.checked || windowSmallerThanPlayer) {
  911. resizePlayerStyleToFillWindow(playerElement);
  912. } else {
  913. resizePlayerStyleToActualSize(playerElement);
  914. }
  915. } else {
  916. resizePlayerStyleToArbitrarySize(playerElement);
  917. }
  918. setupMouseAndFreezeFrame(playerElement)
  919. }
  920. function setupMouseAndFreezeFrame(playerElement) {
  921. // Calculating and normalizing positions depends on the width and height of
  922. // the player.
  923. playerElementClientRect = playerElement.getBoundingClientRect();
  924. setupNormalizeAndQuantize();
  925. resizeFreezeFrameOverlay();
  926. }
  927. function updateVideoStreamSize() {
  928. if (!matchViewportResolution) {
  929. return;
  930. }
  931. let now = new Date().getTime();
  932. if (now - lastTimeResized > 1000) {
  933. let playerElement = document.getElementById('player');
  934. if (!playerElement)
  935. return;
  936. let descriptor = {
  937. Console: 'setres ' + playerElement.clientWidth + 'x' + playerElement.clientHeight
  938. };
  939. emitUIInteraction(descriptor);
  940. /* let descriptor = {
  941. "PageId":"0",
  942. "ExcuteName":"SetResolutionSize",
  943. "x":window.innerWidth,
  944. "y":window.innerHeight,
  945. };
  946. emitUIInteraction(descriptor); */
  947. console.log(descriptor);
  948. lastTimeResized = new Date().getTime();
  949. } else {
  950. console.log('Resizing too often - skipping');
  951. clearTimeout(resizeTimeout);
  952. resizeTimeout = setTimeout(updateVideoStreamSize, 1000);
  953. }
  954. }
  955. // Fix for bug in iOS where windowsize is not correct at instance or orientation change
  956. // https://github.com/dimsemenov/PhotoSwipe/issues/1315
  957. let _orientationChangeTimeout;
  958. function onOrientationChange(event) {
  959. clearTimeout(_orientationChangeTimeout);
  960. _orientationChangeTimeout = setTimeout(function () {
  961. resizePlayerStyle();
  962. }, 500);
  963. }
  964. // Must be kept in sync with PixelStreamingProtocol::EToUE4Msg C++ enum.
  965. const MessageType = {
  966. /**********************************************************************/
  967. /*
  968. * Control Messages. Range = 0..49.
  969. */
  970. IFrameRequest: 0,
  971. RequestQualityControl: 1,
  972. MaxFpsRequest: 2,
  973. AverageBitrateRequest: 3,
  974. StartStreaming: 4,
  975. StopStreaming: 5,
  976. LatencyTest: 6,
  977. RequestInitialSettings: 7,
  978. /**********************************************************************/
  979. /*
  980. * Input Messages. Range = 50..89.
  981. */
  982. // Generic Input Messages. Range = 50..59.
  983. UIInteraction: 50,
  984. Command: 51,
  985. // Keyboard Input Message. Range = 60..69.
  986. KeyDown: 60,
  987. KeyUp: 61,
  988. KeyPress: 62,
  989. // Mouse Input Messages. Range = 70..79.
  990. MouseEnter: 70,
  991. MouseLeave: 71,
  992. MouseDown: 72,
  993. MouseUp: 73,
  994. MouseMove: 74,
  995. MouseWheel: 75,
  996. // Touch Input Messages. Range = 80..89.
  997. TouchStart: 80,
  998. TouchEnd: 81,
  999. TouchMove: 82,
  1000. // Gamepad Input Messages. Range = 90..99
  1001. GamepadButtonPressed: 90,
  1002. GamepadButtonReleased: 91,
  1003. GamepadAnalog: 92
  1004. /**************************************************************************/
  1005. };
  1006. // A generic message has a type and a descriptor.
  1007. function emitDescriptor(messageType, descriptor) {
  1008. // Convert the dscriptor object into a JSON string.
  1009. let descriptorAsString = JSON.stringify(descriptor);
  1010. // Add the UTF-16 JSON string to the array byte buffer, going two bytes at
  1011. // a time.
  1012. let data = new DataView(new ArrayBuffer(1 + 2 + 2 * descriptorAsString.length));
  1013. let byteIdx = 0;
  1014. data.setUint8(byteIdx, messageType);
  1015. byteIdx++;
  1016. data.setUint16(byteIdx, descriptorAsString.length, true);
  1017. byteIdx += 2;
  1018. for (i = 0; i < descriptorAsString.length; i++) {
  1019. data.setUint16(byteIdx, descriptorAsString.charCodeAt(i), true);
  1020. byteIdx += 2;
  1021. }
  1022. sendInputData(data.buffer);
  1023. }
  1024. // A UI interation will occur when the user presses a button powered by
  1025. // JavaScript as opposed to pressing a button which is part of the pixel
  1026. // streamed UI from the UE4 client.
  1027. function emitUIInteraction(descriptor) {
  1028. emitDescriptor(MessageType.UIInteraction, descriptor);
  1029. }
  1030. // A build-in command can be sent to UE4 client. The commands are defined by a
  1031. // JSON descriptor and will be executed automatically.
  1032. // The currently supported commands are:
  1033. //
  1034. // 1. A command to run any console command:
  1035. // "{ ConsoleCommand: <string> }"
  1036. //
  1037. // 2. A command to change the resolution to the given width and height.
  1038. // "{ Resolution.Width: <value>, Resolution.Height: <value> } }"
  1039. //
  1040. function emitCommand(descriptor) {
  1041. emitDescriptor(MessageType.Command, descriptor);
  1042. }
  1043. function requestInitialSettings() {
  1044. sendInputData(new Uint8Array([MessageType.RequestInitialSettings]).buffer);
  1045. }
  1046. function requestQualityControl() {
  1047. sendInputData(new Uint8Array([MessageType.RequestQualityControl]).buffer);
  1048. }
  1049. let playerElementClientRect = undefined;
  1050. let normalizeAndQuantizeUnsigned = undefined;
  1051. let normalizeAndQuantizeSigned = undefined;
  1052. function setupNormalizeAndQuantize() {
  1053. let playerElement = document.getElementById('player');
  1054. let videoElement = playerElement.getElementsByTagName("video");
  1055. if (playerElement && videoElement.length > 0) {
  1056. let playerAspectRatio = playerElement.clientHeight / playerElement.clientWidth;
  1057. let videoAspectRatio = videoElement[0].videoHeight / videoElement[0].videoWidth;
  1058. // Unsigned XY positions are the ratio (0.0..1.0) along a viewport axis,
  1059. // quantized into an uint16 (0..65536).
  1060. // Signed XY deltas are the ratio (-1.0..1.0) along a viewport axis,
  1061. // quantized into an int16 (-32767..32767).
  1062. // This allows the browser viewport and client viewport to have a different
  1063. // size.
  1064. // Hack: Currently we set an out-of-range position to an extreme (65535)
  1065. // as we can't yet accurately detect mouse enter and leave events
  1066. // precisely inside a video with an aspect ratio which causes mattes.
  1067. if (playerAspectRatio > videoAspectRatio) {
  1068. if (print_inputs) {
  1069. console.log('Setup Normalize and Quantize for playerAspectRatio > videoAspectRatio');
  1070. }
  1071. let ratio = playerAspectRatio / videoAspectRatio;
  1072. // Unsigned.
  1073. normalizeAndQuantizeUnsigned = (x, y) => {
  1074. let normalizedX = x / playerElement.clientWidth;
  1075. let normalizedY = ratio * (y / playerElement.clientHeight - 0.5) + 0.5;
  1076. if (normalizedX < 0.0 || normalizedX > 1.0 || normalizedY < 0.0 || normalizedY > 1.0) {
  1077. return {
  1078. inRange: false,
  1079. x: 65535,
  1080. y: 65535
  1081. };
  1082. } else {
  1083. return {
  1084. inRange: true,
  1085. x: normalizedX * 65536,
  1086. y: normalizedY * 65536
  1087. };
  1088. }
  1089. };
  1090. unquantizeAndDenormalizeUnsigned = (x, y) => {
  1091. let normalizedX = x / 65536;
  1092. let normalizedY = (y / 65536 - 0.5) / ratio + 0.5;
  1093. return {
  1094. x: normalizedX * playerElement.clientWidth,
  1095. y: normalizedY * playerElement.clientHeight
  1096. };
  1097. };
  1098. // Signed.
  1099. normalizeAndQuantizeSigned = (x, y) => {
  1100. let normalizedX = x / (0.5 * playerElement.clientWidth);
  1101. let normalizedY = (ratio * y) / (0.5 * playerElement.clientHeight);
  1102. return {
  1103. x: normalizedX * 32767,
  1104. y: normalizedY * 32767
  1105. };
  1106. };
  1107. } else {
  1108. if (print_inputs) {
  1109. console.log('Setup Normalize and Quantize for playerAspectRatio <= videoAspectRatio');
  1110. }
  1111. let ratio = videoAspectRatio / playerAspectRatio;
  1112. // Unsigned.
  1113. normalizeAndQuantizeUnsigned = (x, y) => {
  1114. let normalizedX = ratio * (x / playerElement.clientWidth - 0.5) + 0.5;
  1115. let normalizedY = y / playerElement.clientHeight;
  1116. if (normalizedX < 0.0 || normalizedX > 1.0 || normalizedY < 0.0 || normalizedY > 1.0) {
  1117. return {
  1118. inRange: false,
  1119. x: 65535,
  1120. y: 65535
  1121. };
  1122. } else {
  1123. return {
  1124. inRange: true,
  1125. x: normalizedX * 65536,
  1126. y: normalizedY * 65536
  1127. };
  1128. }
  1129. };
  1130. unquantizeAndDenormalizeUnsigned = (x, y) => {
  1131. let normalizedX = (x / 65536 - 0.5) / ratio + 0.5;
  1132. let normalizedY = y / 65536;
  1133. return {
  1134. x: normalizedX * playerElement.clientWidth,
  1135. y: normalizedY * playerElement.clientHeight
  1136. };
  1137. };
  1138. // Signed.
  1139. normalizeAndQuantizeSigned = (x, y) => {
  1140. let normalizedX = (ratio * x) / (0.5 * playerElement.clientWidth);
  1141. let normalizedY = y / (0.5 * playerElement.clientHeight);
  1142. return {
  1143. x: normalizedX * 32767,
  1144. y: normalizedY * 32767
  1145. };
  1146. };
  1147. }
  1148. }
  1149. }
  1150. function emitMouseMove(x, y, deltaX, deltaY) {
  1151. if (print_inputs) {
  1152. console.log(`x: ${x}, y:${y}, dX: ${deltaX}, dY: ${deltaY}`);
  1153. }
  1154. let coord = normalizeAndQuantizeUnsigned(x, y);
  1155. let delta = normalizeAndQuantizeSigned(deltaX, deltaY);
  1156. let Data = new DataView(new ArrayBuffer(9));
  1157. Data.setUint8(0, MessageType.MouseMove);
  1158. Data.setUint16(1, coord.x, true);
  1159. Data.setUint16(3, coord.y, true);
  1160. Data.setInt16(5, delta.x, true);
  1161. Data.setInt16(7, delta.y, true);
  1162. sendInputData(Data.buffer);
  1163. }
  1164. function emitMouseDown(button, x, y) {
  1165. if (print_inputs) {
  1166. console.log(`mouse button ${button} down at (${x}, ${y})`);
  1167. }
  1168. let coord = normalizeAndQuantizeUnsigned(x, y);
  1169. let Data = new DataView(new ArrayBuffer(6));
  1170. Data.setUint8(0, MessageType.MouseDown);
  1171. Data.setUint8(1, button);
  1172. Data.setUint16(2, coord.x, true);
  1173. Data.setUint16(4, coord.y, true);
  1174. sendInputData(Data.buffer);
  1175. }
  1176. function emitMouseUp(button, x, y) {
  1177. if (print_inputs) {
  1178. console.log(`mouse button ${button} up at (${x}, ${y})`);
  1179. }
  1180. let coord = normalizeAndQuantizeUnsigned(x, y);
  1181. let Data = new DataView(new ArrayBuffer(6));
  1182. Data.setUint8(0, MessageType.MouseUp);
  1183. Data.setUint8(1, button);
  1184. Data.setUint16(2, coord.x, true);
  1185. Data.setUint16(4, coord.y, true);
  1186. sendInputData(Data.buffer);
  1187. }
  1188. function emitMouseWheel(delta, x, y) {
  1189. if (print_inputs) {
  1190. console.log(`mouse wheel with delta ${delta} at (${x}, ${y})`);
  1191. }
  1192. let coord = normalizeAndQuantizeUnsigned(x, y);
  1193. let Data = new DataView(new ArrayBuffer(7));
  1194. Data.setUint8(0, MessageType.MouseWheel);
  1195. Data.setInt16(1, delta, true);
  1196. Data.setUint16(3, coord.x, true);
  1197. Data.setUint16(5, coord.y, true);
  1198. sendInputData(Data.buffer);
  1199. }
  1200. // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
  1201. const MouseButton = {
  1202. MainButton: 0, // Left button.
  1203. AuxiliaryButton: 1, // Wheel button.
  1204. SecondaryButton: 2, // Right button.
  1205. FourthButton: 3, // Browser Back button.
  1206. FifthButton: 4 // Browser Forward button.
  1207. };
  1208. // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
  1209. const MouseButtonsMask = {
  1210. PrimaryButton: 1, // Left button.
  1211. SecondaryButton: 2, // Right button.
  1212. AuxiliaryButton: 4, // Wheel button.
  1213. FourthButton: 8, // Browser Back button.
  1214. FifthButton: 16 // Browser Forward button.
  1215. };
  1216. // If the user has any mouse buttons pressed then release them.
  1217. function releaseMouseButtons(buttons, x, y) {
  1218. if (buttons & MouseButtonsMask.PrimaryButton) {
  1219. emitMouseUp(MouseButton.MainButton, x, y);
  1220. }
  1221. if (buttons & MouseButtonsMask.SecondaryButton) {
  1222. emitMouseUp(MouseButton.SecondaryButton, x, y);
  1223. }
  1224. if (buttons & MouseButtonsMask.AuxiliaryButton) {
  1225. emitMouseUp(MouseButton.AuxiliaryButton, x, y);
  1226. }
  1227. if (buttons & MouseButtonsMask.FourthButton) {
  1228. emitMouseUp(MouseButton.FourthButton, x, y);
  1229. }
  1230. if (buttons & MouseButtonsMask.FifthButton) {
  1231. emitMouseUp(MouseButton.FifthButton, x, y);
  1232. }
  1233. }
  1234. // If the user has any mouse buttons pressed then press them again.
  1235. function pressMouseButtons(buttons, x, y) {
  1236. if (buttons & MouseButtonsMask.PrimaryButton) {
  1237. emitMouseDown(MouseButton.MainButton, x, y);
  1238. }
  1239. if (buttons & MouseButtonsMask.SecondaryButton) {
  1240. emitMouseDown(MouseButton.SecondaryButton, x, y);
  1241. }
  1242. if (buttons & MouseButtonsMask.AuxiliaryButton) {
  1243. emitMouseDown(MouseButton.AuxiliaryButton, x, y);
  1244. }
  1245. if (buttons & MouseButtonsMask.FourthButton) {
  1246. emitMouseDown(MouseButton.FourthButton, x, y);
  1247. }
  1248. if (buttons & MouseButtonsMask.FifthButton) {
  1249. emitMouseDown(MouseButton.FifthButton, x, y);
  1250. }
  1251. }
  1252. function registerInputs(playerElement) {
  1253. if (!playerElement)
  1254. return;
  1255. registerMouseEnterAndLeaveEvents(playerElement);
  1256. registerTouchEvents(playerElement);
  1257. }
  1258. function createOnScreenKeyboardHelpers(htmlElement) {
  1259. if (document.getElementById('hiddenInput') === null) {
  1260. hiddenInput = document.createElement('input');
  1261. hiddenInput.id = 'hiddenInput';
  1262. hiddenInput.maxLength = 0;
  1263. htmlElement.appendChild(hiddenInput);
  1264. }
  1265. if (document.getElementById('editTextButton') === null) {
  1266. editTextButton = document.createElement('button');
  1267. editTextButton.id = 'editTextButton';
  1268. editTextButton.innerHTML = 'edit text';
  1269. htmlElement.appendChild(editTextButton);
  1270. // Hide the 'edit text' button.
  1271. editTextButton.classList.add('hiddenState');
  1272. editTextButton.addEventListener('click', function () {
  1273. // Show the on-screen keyboard.
  1274. hiddenInput.focus();
  1275. });
  1276. }
  1277. }
  1278. function showOnScreenKeyboard(command) {
  1279. if (command.showOnScreenKeyboard) {
  1280. // Show the 'edit text' button.
  1281. editTextButton.classList.remove('hiddenState');
  1282. // Place the 'edit text' button near the UE4 input widget.
  1283. let pos = unquantizeAndDenormalizeUnsigned(command.x, command.y);
  1284. editTextButton.style.top = pos.y.toString() + 'px';
  1285. editTextButton.style.left = (pos.x - 40).toString() + 'px';
  1286. } else {
  1287. // Hide the 'edit text' button.
  1288. editTextButton.classList.add('hiddenState');
  1289. // Hide the on-screen keyboard.
  1290. hiddenInput.blur();
  1291. }
  1292. }
  1293. function registerMouseEnterAndLeaveEvents(playerElement) {
  1294. playerElement.onmouseenter = function (e) {
  1295. if (print_inputs) {
  1296. console.log('mouse enter');
  1297. }
  1298. let Data = new DataView(new ArrayBuffer(1));
  1299. Data.setUint8(0, MessageType.MouseEnter);
  1300. sendInputData(Data.buffer);
  1301. playerElement.pressMouseButtons(e);
  1302. };
  1303. playerElement.onmouseleave = function (e) {
  1304. if (print_inputs) {
  1305. console.log('mouse leave');
  1306. }
  1307. let Data = new DataView(new ArrayBuffer(1));
  1308. Data.setUint8(0, MessageType.MouseLeave);
  1309. sendInputData(Data.buffer);
  1310. playerElement.releaseMouseButtons(e);
  1311. };
  1312. }
  1313. // A locked mouse works by the user clicking in the browser player and the
  1314. // cursor disappears and is locked. The user moves the cursor and the camera
  1315. // moves, for example. The user presses escape to free the mouse.
  1316. function registerLockedMouseEvents(playerElement) {
  1317. let x = playerElement.width / 2;
  1318. let y = playerElement.height / 2;
  1319. playerElement.requestPointerLock = playerElement.requestPointerLock || playerElement.mozRequestPointerLock;
  1320. document.exitPointerLock = document.exitPointerLock || document.mozExitPointerLock;
  1321. playerElement.onclick = function () {
  1322. playerElement.requestPointerLock();
  1323. };
  1324. // Respond to lock state change events
  1325. document.addEventListener('pointerlockchange', lockStateChange, false);
  1326. document.addEventListener('mozpointerlockchange', lockStateChange, false);
  1327. function lockStateChange() {
  1328. if (document.pointerLockElement === playerElement ||
  1329. document.mozPointerLockElement === playerElement) {
  1330. console.log('Pointer locked');
  1331. document.addEventListener("mousemove", updatePosition, false);
  1332. } else {
  1333. console.log('The pointer lock status is now unlocked');
  1334. document.removeEventListener("mousemove", updatePosition, false);
  1335. }
  1336. }
  1337. function updatePosition(e) {
  1338. x += e.movementX;
  1339. y += e.movementY;
  1340. if (x > styleWidth) {
  1341. x -= styleWidth;
  1342. }
  1343. if (y > styleHeight) {
  1344. y -= styleHeight;
  1345. }
  1346. if (x < 0) {
  1347. x = styleWidth + x;
  1348. }
  1349. if (y < 0) {
  1350. y = styleHeight - y;
  1351. }
  1352. emitMouseMove(x, y, e.movementX, e.movementY);
  1353. }
  1354. playerElement.onmousedown = function (e) {
  1355. emitMouseDown(e.button, x, y);
  1356. };
  1357. playerElement.onmouseup = function (e) {
  1358. emitMouseUp(e.button, x, y);
  1359. };
  1360. playerElement.onmousewheel = function (e) {
  1361. emitMouseWheel(e.wheelDelta, x, y);
  1362. };
  1363. playerElement.pressMouseButtons = function (e) {
  1364. pressMouseButtons(e.buttons, x, y);
  1365. };
  1366. playerElement.releaseMouseButtons = function (e) {
  1367. releaseMouseButtons(e.buttons, x, y);
  1368. };
  1369. }
  1370. // A hovering mouse works by the user clicking the mouse button when they want
  1371. // the cursor to have an effect over the video. Otherwise the cursor just
  1372. // passes over the browser.
  1373. function registerHoveringMouseEvents(playerElement) {
  1374. styleCursor = 'false'; // We will rely on UE4 client's software cursor.
  1375. //styleCursor = 'default'; // Showing cursor
  1376. playerElement.onmousemove = function (e) {
  1377. emitMouseMove(e.offsetX, e.offsetY, e.movementX, e.movementY);
  1378. e.preventDefault();
  1379. };
  1380. playerElement.onmousedown = function (e) {
  1381. emitMouseDown(e.button, e.offsetX, e.offsetY);
  1382. e.preventDefault();
  1383. };
  1384. playerElement.onmouseup = function (e) {
  1385. emitMouseUp(e.button, e.offsetX, e.offsetY);
  1386. e.preventDefault();
  1387. };
  1388. // When the context menu is shown then it is safest to release the button
  1389. // which was pressed when the event happened. This will guarantee we will
  1390. // get at least one mouse up corresponding to a mouse down event. Otherwise
  1391. // the mouse can get stuck.
  1392. // https://github.com/facebook/react/issues/5531
  1393. playerElement.oncontextmenu = function (e) {
  1394. emitMouseUp(e.button, e.offsetX, e.offsetY);
  1395. e.preventDefault();
  1396. };
  1397. if ('onmousewheel' in playerElement) {
  1398. playerElement.onmousewheel = function (e) {
  1399. emitMouseWheel(e.wheelDelta, e.offsetX, e.offsetY);
  1400. e.preventDefault();
  1401. };
  1402. } else {
  1403. playerElement.addEventListener('DOMMouseScroll', function (e) {
  1404. emitMouseWheel(e.detail * -120, e.offsetX, e.offsetY);
  1405. e.preventDefault();
  1406. }, false);
  1407. }
  1408. playerElement.pressMouseButtons = function (e) {
  1409. pressMouseButtons(e.buttons, e.offsetX, e.offsetY);
  1410. };
  1411. playerElement.releaseMouseButtons = function (e) {
  1412. releaseMouseButtons(e.buttons, e.offsetX, e.offsetY);
  1413. };
  1414. }
  1415. function registerTouchEvents(playerElement) {
  1416. // We need to assign a unique identifier to each finger.
  1417. // We do this by mapping each Touch object to the identifier.
  1418. let fingers = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0];
  1419. let fingerIds = {};
  1420. function rememberTouch(touch) {
  1421. let finger = fingers.pop();
  1422. if (finger === undefined) {
  1423. console.log('exhausted touch indentifiers');
  1424. }
  1425. fingerIds[touch.identifier] = finger;
  1426. }
  1427. function forgetTouch(touch) {
  1428. fingers.push(fingerIds[touch.identifier]);
  1429. delete fingerIds[touch.identifier];
  1430. }
  1431. function emitTouchData(type, touches) {
  1432. let data = new DataView(new ArrayBuffer(2 + 7 * touches.length));
  1433. data.setUint8(0, type);
  1434. data.setUint8(1, touches.length);
  1435. let byte = 2;
  1436. for (let t = 0; t < touches.length; t++) {
  1437. let touch = touches[t];
  1438. let x = touch.clientX - playerElement.offsetLeft;
  1439. let y = touch.clientY - playerElement.offsetTop;
  1440. if (print_inputs) {
  1441. console.log(`F${fingerIds[touch.identifier]}=(${x}, ${y})`);
  1442. }
  1443. let coord = normalizeAndQuantizeUnsigned(x, y);
  1444. data.setUint16(byte, coord.x, true);
  1445. byte += 2;
  1446. data.setUint16(byte, coord.y, true);
  1447. byte += 2;
  1448. data.setUint8(byte, fingerIds[touch.identifier], true);
  1449. byte += 1;
  1450. data.setUint8(byte, 255 * touch.force, true); // force is between 0.0 and 1.0 so quantize into byte.
  1451. byte += 1;
  1452. data.setUint8(byte, coord.inRange ? 1 : 0, true); // mark the touch as in the player or not
  1453. byte += 1;
  1454. }
  1455. sendInputData(data.buffer);
  1456. }
  1457. if (inputOptions.fakeMouseWithTouches) {
  1458. let finger = undefined;
  1459. playerElement.ontouchstart = function (e) {
  1460. if (finger === undefined) {
  1461. let firstTouch = e.changedTouches[0];
  1462. finger = {
  1463. id: firstTouch.identifier,
  1464. x: firstTouch.clientX - playerElementClientRect.left,
  1465. y: firstTouch.clientY - playerElementClientRect.top
  1466. };
  1467. // Hack: Mouse events require an enter and leave so we just
  1468. // enter and leave manually with each touch as this event
  1469. // is not fired with a touch device.
  1470. playerElement.onmouseenter(e);
  1471. emitMouseDown(MouseButton.MainButton, finger.x, finger.y);
  1472. }
  1473. e.preventDefault();
  1474. };
  1475. playerElement.ontouchend = function (e) {
  1476. for (let t = 0; t < e.changedTouches.length; t++) {
  1477. let touch = e.changedTouches[t];
  1478. if (touch.identifier === finger.id) {
  1479. let x = touch.clientX - playerElementClientRect.left;
  1480. let y = touch.clientY - playerElementClientRect.top;
  1481. emitMouseUp(MouseButton.MainButton, x, y);
  1482. // Hack: Manual mouse leave event.
  1483. playerElement.onmouseleave(e);
  1484. finger = undefined;
  1485. break;
  1486. }
  1487. }
  1488. e.preventDefault();
  1489. };
  1490. playerElement.ontouchmove = function (e) {
  1491. for (let t = 0; t < e.touches.length; t++) {
  1492. let touch = e.touches[t];
  1493. if (touch.identifier === finger.id) {
  1494. let x = touch.clientX - playerElementClientRect.left;
  1495. let y = touch.clientY - playerElementClientRect.top;
  1496. emitMouseMove(x, y, x - finger.x, y - finger.y);
  1497. finger.x = x;
  1498. finger.y = y;
  1499. break;
  1500. }
  1501. }
  1502. e.preventDefault();
  1503. };
  1504. } else {
  1505. playerElement.ontouchstart = function (e) {
  1506. // Assign a unique identifier to each touch.
  1507. for (let t = 0; t < e.changedTouches.length; t++) {
  1508. rememberTouch(e.changedTouches[t]);
  1509. }
  1510. if (print_inputs) {
  1511. console.log('touch start');
  1512. }
  1513. emitTouchData(MessageType.TouchStart, e.changedTouches);
  1514. e.preventDefault();
  1515. };
  1516. playerElement.ontouchend = function (e) {
  1517. if (print_inputs) {
  1518. console.log('touch end');
  1519. }
  1520. emitTouchData(MessageType.TouchEnd, e.changedTouches);
  1521. // Re-cycle unique identifiers previously assigned to each touch.
  1522. for (let t = 0; t < e.changedTouches.length; t++) {
  1523. forgetTouch(e.changedTouches[t]);
  1524. }
  1525. e.preventDefault();
  1526. };
  1527. playerElement.ontouchmove = function (e) {
  1528. if (print_inputs) {
  1529. console.log('touch move');
  1530. }
  1531. emitTouchData(MessageType.TouchMove, e.touches);
  1532. e.preventDefault();
  1533. };
  1534. }
  1535. }
  1536. // Browser keys do not have a charCode so we only need to test keyCode.
  1537. function isKeyCodeBrowserKey(keyCode) {
  1538. // Function keys or tab key.
  1539. return keyCode >= 112 && keyCode <= 123 || keyCode === 9;
  1540. }
  1541. // Must be kept in sync with JavaScriptKeyCodeToFKey C++ array. The index of the
  1542. // entry in the array is the special key code given below.
  1543. const SpecialKeyCodes = {
  1544. BackSpace: 8,
  1545. Shift: 16,
  1546. Control: 17,
  1547. Alt: 18,
  1548. RightShift: 253,
  1549. RightControl: 254,
  1550. RightAlt: 255
  1551. };
  1552. // We want to be able to differentiate between left and right versions of some
  1553. // keys.
  1554. function getKeyCode(e) {
  1555. if (e.keyCode === SpecialKeyCodes.Shift && e.code === 'ShiftRight') return SpecialKeyCodes.RightShift;
  1556. else if (e.keyCode === SpecialKeyCodes.Control && e.code === 'ControlRight') return SpecialKeyCodes.RightControl;
  1557. else if (e.keyCode === SpecialKeyCodes.Alt && e.code === 'AltRight') return SpecialKeyCodes.RightAlt;
  1558. else return e.keyCode;
  1559. }
  1560. function registerKeyboardEvents() {
  1561. document.onkeydown = function (e) {
  1562. if (print_inputs) {
  1563. console.log(`key down ${e.keyCode}, repeat = ${e.repeat}`);
  1564. }
  1565. sendInputData(new Uint8Array([MessageType.KeyDown, getKeyCode(e), e.repeat]).buffer);
  1566. // Backspace is not considered a keypress in JavaScript but we need it
  1567. // to be so characters may be deleted in a UE4 text entry field.
  1568. if (e.keyCode === SpecialKeyCodes.BackSpace) {
  1569. document.onkeypress({
  1570. charCode: SpecialKeyCodes.BackSpace
  1571. });
  1572. }
  1573. if (inputOptions.suppressBrowserKeys && isKeyCodeBrowserKey(e.keyCode)) {
  1574. e.preventDefault();
  1575. }
  1576. };
  1577. document.onkeyup = function (e) {
  1578. if (print_inputs) {
  1579. console.log(`key up ${e.keyCode}`);
  1580. }
  1581. sendInputData(new Uint8Array([MessageType.KeyUp, getKeyCode(e)]).buffer);
  1582. if (inputOptions.suppressBrowserKeys && isKeyCodeBrowserKey(e.keyCode)) {
  1583. e.preventDefault();
  1584. }
  1585. };
  1586. document.onkeypress = function (e) {
  1587. if (print_inputs) {
  1588. console.log(`key press ${e.charCode}`);
  1589. }
  1590. let data = new DataView(new ArrayBuffer(3));
  1591. data.setUint8(0, MessageType.KeyPress);
  1592. data.setUint16(1, e.charCode, true);
  1593. sendInputData(data.buffer);
  1594. };
  1595. }
  1596. function onExpandOverlay_Click( /* e */) {
  1597. let overlay = document.getElementById('overlay');
  1598. overlay.classList.toggle("overlay-shown");
  1599. }
  1600. function start() {
  1601. // update "quality status" to "disconnected" state
  1602. let qualityStatus = document.getElementById("qualityStatus");
  1603. if (qualityStatus) {
  1604. qualityStatus.className = "grey-status";
  1605. }
  1606. let statsDiv = document.getElementById("stats");
  1607. if (statsDiv) {
  1608. statsDiv.innerHTML = 'Not connected';
  1609. }
  1610. if (!connect_on_load || is_reconnection) {
  1611. showConnectOverlay();
  1612. invalidateFreezeFrameOverlay();
  1613. shouldShowPlayOverlay = true;
  1614. resizePlayerStyle();
  1615. } else {
  1616. connect();
  1617. }
  1618. updateKickButton(0);
  1619. }
  1620. function updateKickButton(playersCount) {
  1621. let kickButton = document.getElementById('kick-other-players-button');
  1622. if (kickButton)
  1623. kickButton.value = `Kick (${playersCount})`;
  1624. }
  1625. function connect() {
  1626. "use strict";
  1627. window.WebSocket = window.WebSocket || window.MozWebSocket;
  1628. if (!window.WebSocket) {
  1629. alert('Your browser doesn\'t support WebSocket');
  1630. return;
  1631. };
  1632. ws = new WebSocket(window.g.UE_IP);
  1633. // ws = new WebSocket('ws://192.168.4.137:82/');
  1634. ws.onmessage = function (event) {
  1635. console.log(`<- SS: ${event.data}`);
  1636. let msg = JSON.parse(event.data);
  1637. if (msg.type === 'config') {
  1638. onConfig(msg);
  1639. } else if (msg.type === 'playerCount') {
  1640. updateKickButton(msg.count - 1);
  1641. } else if (msg.type === 'answer') {
  1642. onWebRtcAnswer(msg);
  1643. } else if (msg.type === 'iceCandidate') {
  1644. onWebRtcIce(msg.candidate);
  1645. } else {
  1646. console.log(`invalid SS message type: ${msg.type}`);
  1647. }
  1648. };
  1649. ws.onerror = function (event) {
  1650. console.log(`WS error: ${JSON.stringify(event)}`);
  1651. };
  1652. ws.onclose = function (event) {
  1653. console.log(`WS closed: ${JSON.stringify(event.code)} - ${event.reason}`);
  1654. ws = undefined;
  1655. is_reconnection = true;
  1656. // destroy `webRtcPlayerObj` if any
  1657. let playerDiv = document.getElementById('player');
  1658. if (webRtcPlayerObj) {
  1659. playerDiv.removeChild(webRtcPlayerObj.video);
  1660. webRtcPlayerObj.close();
  1661. webRtcPlayerObj = undefined;
  1662. }
  1663. showTextOverlay(`Disconnected: ${event.reason}`);
  1664. let reclickToStart = setTimeout(start, 4000);
  1665. };
  1666. }
  1667. // Config data received from WebRTC sender via the Cirrus web server
  1668. function onConfig(config) {
  1669. let playerDiv = document.getElementById('player');
  1670. console.log('asjdkasd', playerDiv);
  1671. let playerElement = setupWebRtcPlayer(playerDiv, config);
  1672. resizePlayerStyle();
  1673. switch (inputOptions.controlScheme) {
  1674. case ControlSchemeType.HoveringMouse:
  1675. registerHoveringMouseEvents(playerElement);
  1676. break;
  1677. case ControlSchemeType.LockedMouse:
  1678. registerLockedMouseEvents(playerElement);
  1679. break;
  1680. default:
  1681. console.log(`ERROR: Unknown control scheme ${inputOptions.controlScheme}`);
  1682. registerLockedMouseEvents(playerElement);
  1683. break;
  1684. }
  1685. }
  1686. function load(type) {
  1687. console.log("type", type);
  1688. if (type == 0) {
  1689. registerHoveringMouseEvents = function () { }
  1690. registerLockedMouseEvents = function () { }
  1691. registerTouchEvents = function () { }
  1692. registerKeyboardEvents = function () { }
  1693. }
  1694. setupHtmlEvents();
  1695. setupFreezeFrameOverlay();
  1696. registerKeyboardEvents();
  1697. start();
  1698. onExpandOverlay_Click()
  1699. }