webRtcVideo.js 54 KB

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