local-kvm.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753
  1. /*
  2. USB Serial
  3. The following section handles USB serial communication using the Web Serial API.
  4. */
  5. let serialPort = null;
  6. let serialReader = null;
  7. let serialWriter = null;
  8. let serialReadBuffer = [];
  9. // Update selected port display
  10. function updateSelectedPortDisplay(port) {
  11. const selectedPortElem = document.getElementById('selectedPort');
  12. if (port && port.getInfo) {
  13. const info = port.getInfo();
  14. selectedPortElem.textContent = `VID: ${info.usbVendorId || '-'}, PID: ${info.usbProductId || '-'}`;
  15. } else if (port) {
  16. selectedPortElem.textContent = 'KVM Connected';
  17. } else {
  18. selectedPortElem.textContent = 'KVM Not Connected';
  19. }
  20. }
  21. // Request a new serial port
  22. async function requestSerialPort() {
  23. try {
  24. // Disconnect previous port if connected
  25. if (serialPort) {
  26. await disconnectSerialPort();
  27. }
  28. serialPort = await navigator.serial.requestPort();
  29. await serialPort.open({ baudRate: 115200 });
  30. serialReader = serialPort.readable.getReader();
  31. serialWriter = serialPort.writable.getWriter();
  32. updateSelectedPortDisplay(serialPort);
  33. // Change button to indicate connected state
  34. document.getElementById('selectSerialPort').classList.add('is-negative');
  35. document.querySelector('#selectSerialPort span').className = 'ts-icon is-link-slash-icon';
  36. // Start reading loop for incoming data
  37. readSerialLoop();
  38. } catch (e) {
  39. updateSelectedPortDisplay(null);
  40. alert('Failed to open serial port');
  41. }
  42. }
  43. // Disconnect serial port
  44. async function disconnectSerialPort() {
  45. try {
  46. if (serialReader) {
  47. await serialReader.cancel();
  48. serialReader.releaseLock();
  49. serialReader = null;
  50. }
  51. if (serialWriter) {
  52. serialWriter.releaseLock();
  53. serialWriter = null;
  54. }
  55. if (serialPort) {
  56. await serialPort.close();
  57. serialPort = null;
  58. }
  59. } catch (e) {}
  60. updateSelectedPortDisplay(null);
  61. }
  62. // Read loop for incoming serial data, dispatches 'data' events on parent
  63. async function readSerialLoop() {
  64. while (serialPort && serialReader) {
  65. try {
  66. const { value, done } = await serialReader.read();
  67. if (done) break;
  68. if (value) {
  69. // Append to buffer
  70. //serialReadBuffer.push(...value);
  71. //console.log('Received data:', Array.from(value).map(b => b.toString(16).padStart(2, '0')).join(' '));
  72. }
  73. } catch (e) {
  74. break;
  75. }
  76. }
  77. }
  78. // Send data over serial
  79. async function sendSerial(data) {
  80. if (!serialWriter) throw new Error('Serial port not open');
  81. await serialWriter.write(data);
  82. }
  83. // Button event to select serial port
  84. document.getElementById('selectSerialPort').addEventListener('click', function(){
  85. if (serialPort) {
  86. disconnectSerialPort();
  87. document.getElementById('selectSerialPort').classList.remove('is-negative');
  88. document.querySelector('#selectSerialPort span').className = 'ts-icon is-keyboard-icon';
  89. } else {
  90. requestSerialPort();
  91. }
  92. });
  93. /*
  94. CH9329 HID bytecode converter
  95. */
  96. function calcChecksum(arr) {
  97. // Simple checksum: sum of bytes, lowest 8 bits
  98. return arr.reduce((sum, b) => (sum + b) & 0xFF, 0);
  99. }
  100. function resizeTouchscreenToVideo() {
  101. const video = document.getElementById('video');
  102. const touchscreen = document.getElementById('touchscreen');
  103. if (video && touchscreen) {
  104. const rect = video.getBoundingClientRect();
  105. // Assume video stream is always 16:9 (1920x1080)
  106. const aspectRatio = 16 / 9;
  107. let displayWidth = rect.width;
  108. let displayHeight = rect.height;
  109. let offsetX = 0;
  110. let offsetY = 0;
  111. // Calculate the actual displayed video area (may be letterboxed/pillarboxed)
  112. if (rect.width / rect.height > aspectRatio) {
  113. // Pillarbox: black bars left/right
  114. displayHeight = rect.height;
  115. displayWidth = rect.height * aspectRatio;
  116. offsetX = rect.left + (rect.width - displayWidth) / 2;
  117. offsetY = rect.top;
  118. } else {
  119. // Letterbox: black bars top/bottom
  120. displayWidth = rect.width;
  121. displayHeight = rect.width / aspectRatio;
  122. offsetX = rect.left;
  123. offsetY = rect.top + (rect.height - displayHeight) / 2;
  124. }
  125. touchscreen.style.position = 'absolute';
  126. touchscreen.style.left = offsetX + 'px';
  127. touchscreen.style.top = offsetY + 'px';
  128. touchscreen.style.width = displayWidth + 'px';
  129. touchscreen.style.height = displayHeight + 'px';
  130. touchscreen.width = displayWidth;
  131. touchscreen.height = displayHeight;
  132. }
  133. }
  134. // Call on load and on resize
  135. window.addEventListener('resize', resizeTouchscreenToVideo);
  136. window.addEventListener('DOMContentLoaded', resizeTouchscreenToVideo);
  137. setTimeout(resizeTouchscreenToVideo, 1000); // Also after 1s to ensure video is loaded
  138. class HIDController {
  139. constructor() {
  140. this.hidState = {
  141. MouseButtons: 0x00,
  142. Modkey: 0x00,
  143. KeyboardButtons: [0, 0, 0, 0, 0, 0]
  144. };
  145. this.Config = {
  146. ScrollSensitivity: 1
  147. };
  148. }
  149. // Calculates checksum for a given array of bytes
  150. calcChecksum(arr) {
  151. return arr.reduce((sum, b) => (sum + b) & 0xFF, 0);
  152. }
  153. // Soft reset the CH9329 chip
  154. async softReset() {
  155. if (!serialPort || !serialPort.readable || !serialPort.writable) {
  156. throw new Error('Serial port not open');
  157. }
  158. const packet = [
  159. 0x57, 0xAB, 0x00, 0x0F, 0x00 // checksum placeholder
  160. ];
  161. packet[4] = this.calcChecksum(packet.slice(0, 4));
  162. await this.sendPacketAndWait(packet, 0x0F);
  163. }
  164. // Sends a packet over serial and waits for a reply with a specific command code
  165. async sendPacketAndWait(packet, replyCmd) {
  166. const timeout = 300; // 300ms timeout
  167. const succReplyByte = replyCmd | 0x80;
  168. const errorReplyByte = replyCmd | 0xC0;
  169. // Succ example for cmd 0x04: 57 AB 00 84 01 00 87
  170. // Header is 57 AB 00, we can skip that
  171. // then the 0x84 is the replyCmd | 0x80 (or if error, 0xC4)
  172. // 0x01 is the data length (1 byte)
  173. // 0x00 is the data (success)
  174. // 0x87 is the checksum
  175. serialReadBuffer = [];
  176. await sendSerial(new Uint8Array(packet));
  177. const startTime = Date.now();
  178. /*
  179. while (true) {
  180. // Check for timeout
  181. if (Date.now() - startTime > timeout) {
  182. //Timeout, ignore this reply
  183. return Promise.reject(new Error('timeout waiting for reply'));
  184. }
  185. // Check if we have enough data for a reply
  186. if (serialReadBuffer.length >= 5) {
  187. // Look for the start of a packet
  188. for (let i = 0; i <= serialReadBuffer.length - 5; i++) {
  189. if (serialReadBuffer[i] === 0x57 && serialReadBuffer[i + 1] === 0xAB) {
  190. //Discard bytes before the packet
  191. if (i > 0) {
  192. serialReadBuffer.splice(0, i);
  193. }
  194. // Now we have 57 AB at the start, check if we have the full packet
  195. const len = serialReadBuffer[3];
  196. const fullPacketLength = 4 + len + 1;
  197. if (serialReadBuffer.length >= fullPacketLength) {
  198. const packet = serialReadBuffer.slice(0, fullPacketLength);
  199. serialReadBuffer = serialReadBuffer.slice(fullPacketLength);
  200. const checksum = this.calcChecksum(packet.slice(0, fullPacketLength - 1));
  201. if (checksum !== packet[fullPacketLength - 1]) {
  202. // Invalid checksum, discard packet
  203. continue;
  204. }
  205. if (packet[4] === replyCmd) {
  206. return Promise.resolve();
  207. }
  208. }
  209. }
  210. }
  211. }
  212. }*/
  213. // Seems the speed required to get a reply is too high for the browser
  214. // so reply check is not implemented for now
  215. await new Promise(resolve => setTimeout(resolve, 30));
  216. return Promise.resolve();
  217. }
  218. // Mouse move absolute
  219. async MouseMoveAbsolute(xLSB, xMSB, yLSB, yMSB) {
  220. if (!serialPort || !serialPort.readable || !serialPort.writable) {
  221. return;
  222. }
  223. const packet = [
  224. 0x57, 0xAB, 0x00, 0x04, 0x07, 0x02,
  225. this.hidState.MouseButtons,
  226. xLSB,
  227. xMSB,
  228. yLSB,
  229. yMSB,
  230. 0x00, // Scroll
  231. 0x00 // Checksum placeholder
  232. ];
  233. packet[12] = this.calcChecksum(packet.slice(0, 12));
  234. await this.sendPacketAndWait(packet, 0x04);
  235. }
  236. // Mouse move relative
  237. async MouseMoveRelative(dx, dy, wheel) {
  238. if (!serialPort || !serialPort.readable || !serialPort.writable) {
  239. return;
  240. }
  241. // Ensure 0x80 is not used
  242. if (dx === 0x80) dx = 0x81;
  243. if (dy === 0x80) dy = 0x81;
  244. const packet = [
  245. 0x57, 0xAB, 0x00, 0x05, 0x05, 0x01,
  246. this.hidState.MouseButtons,
  247. dx,
  248. dy,
  249. wheel,
  250. 0x00 // Checksum placeholder
  251. ];
  252. packet[10] = this.calcChecksum(packet.slice(0, 10));
  253. await this.sendPacketAndWait(packet, 0x05);
  254. }
  255. // Mouse button press
  256. async MouseButtonPress(button) {
  257. switch (button) {
  258. case 0x01: // Left
  259. this.hidState.MouseButtons |= 0x01;
  260. break;
  261. case 0x02: // Right
  262. this.hidState.MouseButtons |= 0x02;
  263. break;
  264. case 0x03: // Middle
  265. this.hidState.MouseButtons |= 0x04;
  266. break;
  267. default:
  268. throw new Error("invalid opcode for mouse button press");
  269. }
  270. await this.MouseMoveRelative(0, 0, 0);
  271. }
  272. // Mouse button release
  273. async MouseButtonRelease(button) {
  274. switch (button) {
  275. case 0x00: // Release all
  276. this.hidState.MouseButtons = 0x00;
  277. break;
  278. case 0x01: // Left
  279. this.hidState.MouseButtons &= ~0x01;
  280. break;
  281. case 0x02: // Right
  282. this.hidState.MouseButtons &= ~0x02;
  283. break;
  284. case 0x03: // Middle
  285. this.hidState.MouseButtons &= ~0x04;
  286. break;
  287. default:
  288. throw new Error("invalid opcode for mouse button release");
  289. }
  290. await this.MouseMoveRelative(0, 0, 0);
  291. }
  292. // Mouse scroll
  293. async MouseScroll(tilt) {
  294. if (tilt === 0) return;
  295. let wheel;
  296. if (tilt < 0) {
  297. wheel = this.Config.ScrollSensitivity;
  298. } else {
  299. wheel = 0xFF - this.Config.ScrollSensitivity;
  300. }
  301. await this.MouseMoveRelative(0, 0, wheel);
  302. }
  303. // --- Keyboard Emulation ---
  304. // Set modifier key (Ctrl, Shift, Alt, GUI)
  305. async SetModifierKey(keycode, isRight) {
  306. const MOD_LCTRL = 0x01, MOD_LSHIFT = 0x02, MOD_LALT = 0x04, MOD_LGUI = 0x08;
  307. const MOD_RCTRL = 0x10, MOD_RSHIFT = 0x20, MOD_RALT = 0x40, MOD_RGUI = 0x80;
  308. let modifierBit = 0;
  309. switch (keycode) {
  310. case 17: modifierBit = isRight ? MOD_RCTRL : MOD_LCTRL; break;
  311. case 16: modifierBit = isRight ? MOD_RSHIFT : MOD_LSHIFT; break;
  312. case 18: modifierBit = isRight ? MOD_RALT : MOD_LALT; break;
  313. case 91: modifierBit = isRight ? MOD_RGUI : MOD_LGUI; break;
  314. default: throw new Error("Not a modifier key");
  315. }
  316. this.hidState.Modkey |= modifierBit;
  317. await this.keyboardSendKeyCombinations();
  318. }
  319. // Unset modifier key (Ctrl, Shift, Alt, GUI)
  320. async UnsetModifierKey(keycode, isRight) {
  321. const MOD_LCTRL = 0x01, MOD_LSHIFT = 0x02, MOD_LALT = 0x04, MOD_LGUI = 0x08;
  322. const MOD_RCTRL = 0x10, MOD_RSHIFT = 0x20, MOD_RALT = 0x40, MOD_RGUI = 0x80;
  323. let modifierBit = 0;
  324. switch (keycode) {
  325. case 17: modifierBit = isRight ? MOD_RCTRL : MOD_LCTRL; break;
  326. case 16: modifierBit = isRight ? MOD_RSHIFT : MOD_LSHIFT; break;
  327. case 18: modifierBit = isRight ? MOD_RALT : MOD_LALT; break;
  328. case 91: modifierBit = isRight ? MOD_RGUI : MOD_LGUI; break;
  329. default: throw new Error("Not a modifier key");
  330. }
  331. this.hidState.Modkey &= ~modifierBit;
  332. await this.keyboardSendKeyCombinations();
  333. }
  334. // Send a keyboard press by JavaScript keycode
  335. async SendKeyboardPress(keycode) {
  336. const hid = this.javaScriptKeycodeToHIDOpcode(keycode);
  337. if (hid === 0x00) throw new Error("Unsupported keycode: " + keycode);
  338. // Already pressed?
  339. for (let i = 0; i < 6; i++) {
  340. if (this.hidState.KeyboardButtons[i] === hid) return;
  341. }
  342. // Find empty slot
  343. for (let i = 0; i < 6; i++) {
  344. if (this.hidState.KeyboardButtons[i] === 0x00) {
  345. this.hidState.KeyboardButtons[i] = hid;
  346. await this.keyboardSendKeyCombinations();
  347. return;
  348. }
  349. }
  350. throw new Error("No space left in keyboard state to press key: " + keycode);
  351. }
  352. // Send a keyboard release by JavaScript keycode
  353. async SendKeyboardRelease(keycode) {
  354. const hid = this.javaScriptKeycodeToHIDOpcode(keycode);
  355. if (hid === 0x00) throw new Error("Unsupported keycode: " + keycode);
  356. for (let i = 0; i < 6; i++) {
  357. if (this.hidState.KeyboardButtons[i] === hid) {
  358. this.hidState.KeyboardButtons[i] = 0x00;
  359. await this.keyboardSendKeyCombinations();
  360. return;
  361. }
  362. }
  363. // Not pressed, do nothing
  364. }
  365. // Send the current key combinations (modifiers + up to 6 keys)
  366. async keyboardSendKeyCombinations() {
  367. const packet = [
  368. 0x57, 0xAB, 0x00, 0x02, 0x08,
  369. this.hidState.Modkey, 0x00,
  370. 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  371. 0x00
  372. ];
  373. for (let i = 0; i < 6; i++) {
  374. packet[7 + i] = this.hidState.KeyboardButtons[i] || 0x00;
  375. }
  376. packet[13] = this.calcChecksum(packet.slice(0, 13));
  377. await this.sendPacketAndWait(packet, 0x02);
  378. }
  379. // Convert JavaScript keycode to HID keycode
  380. javaScriptKeycodeToHIDOpcode(keycode) {
  381. // Letters A-Z
  382. if (keycode >= 65 && keycode <= 90) return (keycode - 65) + 0x04;
  383. // Numbers 1-9 (top row, not numpad)
  384. if (keycode >= 49 && keycode <= 57) return (keycode - 49) + 0x1E;
  385. // F1 to F12
  386. if (keycode >= 112 && keycode <= 123) return (keycode - 112) + 0x3A;
  387. switch (keycode) {
  388. case 8: return 0x2A; // Backspace
  389. case 9: return 0x2B; // Tab
  390. case 13: return 0x28; // Enter
  391. case 16: return 0xE1; // Left shift
  392. case 17: return 0xE0; // Left Ctrl
  393. case 18: return 0xE6; // Left Alt
  394. case 19: return 0x48; // Pause
  395. case 20: return 0x39; // Caps Lock
  396. case 27: return 0x29; // Escape
  397. case 32: return 0x2C; // Spacebar
  398. case 33: return 0x4B; // Page Up
  399. case 34: return 0x4E; // Page Down
  400. case 35: return 0x4D; // End
  401. case 36: return 0x4A; // Home
  402. case 37: return 0x50; // Left Arrow
  403. case 38: return 0x52; // Up Arrow
  404. case 39: return 0x4F; // Right Arrow
  405. case 40: return 0x51; // Down Arrow
  406. case 44: return 0x46; // Print Screen or F13 (Firefox)
  407. case 45: return 0x49; // Insert
  408. case 46: return 0x4C; // Delete
  409. case 48: return 0x27; // 0 (not Numpads)
  410. case 59: return 0x33; // ';'
  411. case 61: return 0x2E; // '='
  412. case 91: return 0xE3; // Left GUI (Windows)
  413. case 92: return 0xE7; // Right GUI
  414. case 93: return 0x65; // Menu key
  415. case 96: return 0x62; // 0 (Numpads)
  416. case 97: return 0x59; // 1 (Numpads)
  417. case 98: return 0x5A; // 2 (Numpads)
  418. case 99: return 0x5B; // 3 (Numpads)
  419. case 100: return 0x5C; // 4 (Numpads)
  420. case 101: return 0x5D; // 5 (Numpads)
  421. case 102: return 0x5E; // 6 (Numpads)
  422. case 103: return 0x5F; // 7 (Numpads)
  423. case 104: return 0x60; // 8 (Numpads)
  424. case 105: return 0x61; // 9 (Numpads)
  425. case 106: return 0x55; // * (Numpads)
  426. case 107: return 0x57; // + (Numpads)
  427. case 109: return 0x56; // - (Numpads)
  428. case 110: return 0x63; // dot (Numpads)
  429. case 111: return 0x54; // divide (Numpads)
  430. case 144: return 0x53; // Num Lock
  431. case 145: return 0x47; // Scroll Lock
  432. case 146: return 0x58; // Numpad enter
  433. case 173: return 0x2D; // -
  434. case 186: return 0x33; // ';'
  435. case 187: return 0x2E; // '='
  436. case 188: return 0x36; // ','
  437. case 189: return 0x2D; // '-'
  438. case 190: return 0x37; // '.'
  439. case 191: return 0x38; // '/'
  440. case 192: return 0x35; // '`'
  441. case 219: return 0x2F; // '['
  442. case 220: return 0x31; // backslash
  443. case 221: return 0x30; // ']'
  444. case 222: return 0x34; // '\''
  445. default: return 0x00;
  446. }
  447. }
  448. }
  449. // Instantiate HID controller
  450. const controller = new HIDController();
  451. const videoOverlayElement = document.getElementById('touchscreen');
  452. let isMouseDown = false;
  453. let lastX = 0;
  454. let lastY = 0;
  455. // Mouse down
  456. videoOverlayElement.addEventListener('mousedown', async (e) => {
  457. isMouseDown = true;
  458. lastX = e.clientX;
  459. lastY = e.clientY;
  460. if (e.button === 0) {
  461. await controller.MouseButtonPress(0x01); // Left
  462. } else if (e.button === 2) {
  463. await controller.MouseButtonPress(0x02); // Right
  464. } else if (e.button === 1) {
  465. await controller.MouseButtonPress(0x03); // Middle
  466. }
  467. });
  468. // Mouse up
  469. videoOverlayElement.addEventListener('mouseup', async (e) => {
  470. isMouseDown = false;
  471. if (e.button === 0) {
  472. await controller.MouseButtonRelease(0x01); // Left
  473. } else if (e.button === 2) {
  474. await controller.MouseButtonRelease(0x02); // Right
  475. } else if (e.button === 1) {
  476. await controller.MouseButtonRelease(0x03); // Middle
  477. }
  478. });
  479. // Mouse move (absolute positioning)
  480. videoOverlayElement.addEventListener('mousemove', async (e) => {
  481. const rect = videoOverlayElement.getBoundingClientRect();
  482. const x = e.clientX - rect.left;
  483. const y = e.clientY - rect.top;
  484. const width = rect.width;
  485. const height = rect.height;
  486. const offsetX = x / width;
  487. const offsetY = y / height;
  488. //console.log('Offset ratio:', { offsetX, offsetY });
  489. const absX = Math.round(offsetX * 4095);
  490. const absY = Math.round(offsetY * 4095);
  491. await controller.MouseMoveAbsolute(absX & 0xFF, (absX >> 8) & 0xFF, absY & 0xFF, (absY >> 8) & 0xFF);
  492. });
  493. // Context menu disable (for right click)
  494. videoOverlayElement.addEventListener('contextmenu', (e) => {
  495. e.preventDefault();
  496. });
  497. // Mouse wheel (scroll)
  498. videoOverlayElement.addEventListener('wheel', async (e) => {
  499. e.preventDefault();
  500. let tilt = e.deltaY > 0 ? 1 : -1;
  501. await controller.MouseScroll(tilt);
  502. });
  503. // Keyboard events for HID emulation
  504. window.addEventListener('keydown', async (e) => {
  505. // Ignore repeated events
  506. //if (e.repeat) return;
  507. try {
  508. // Modifier keys
  509. if (e.key === 'Control' || e.key === 'Shift' || e.key === 'Alt' || e.key === 'Meta') {
  510. await controller.SetModifierKey(e.keyCode, e.location === KeyboardEvent.DOM_KEY_LOCATION_RIGHT);
  511. } else {
  512. await controller.SendKeyboardPress(e.keyCode);
  513. }
  514. e.preventDefault();
  515. } catch (err) {
  516. // Ignore unsupported keys
  517. }
  518. });
  519. window.addEventListener('keyup', async (e) => {
  520. try {
  521. if (e.key === 'Control' || e.key === 'Shift' || e.key === 'Alt' || e.key === 'Meta') {
  522. await controller.UnsetModifierKey(e.keyCode, e.location === KeyboardEvent.DOM_KEY_LOCATION_RIGHT);
  523. } else {
  524. await controller.SendKeyboardRelease(e.keyCode);
  525. }
  526. e.preventDefault();
  527. } catch (err) {
  528. // Ignore unsupported keys
  529. }
  530. });
  531. document.getElementById('resetHIDBtn').addEventListener('click', async () => {
  532. try {
  533. await controller.softReset();
  534. alert('HID soft reset sent.');
  535. } catch (e) {
  536. alert('Failed to reset HID: ' + e.message);
  537. }
  538. });
  539. /*
  540. Audio Capture
  541. */
  542. const audioSelect = document.getElementById('audioSource');
  543. const refreshAudioBtn = document.getElementById('refreshAudioSources');
  544. let currentAudioStream = null;
  545. // List audio input devices
  546. async function listAudioSources() {
  547. const devices = await navigator.mediaDevices.enumerateDevices();
  548. audioSelect.innerHTML = '';
  549. devices
  550. .filter(device => device.kind === 'audioinput')
  551. .forEach(device => {
  552. const option = document.createElement('option');
  553. option.value = device.deviceId;
  554. option.text = device.label || `Microphone ${audioSelect.length + 1}`;
  555. audioSelect.appendChild(option);
  556. });
  557. }
  558. // Start streaming selected audio source
  559. async function startAudioStream() {
  560. if (currentAudioStream) {
  561. currentAudioStream.getTracks().forEach(track => track.stop());
  562. }
  563. const deviceId = audioSelect.value;
  564. try {
  565. const stream = await navigator.mediaDevices.getUserMedia({
  566. audio: {
  567. deviceId: deviceId ? { exact: deviceId } : undefined,
  568. echoCancellation: false,
  569. noiseSuppression: false,
  570. autoGainControl: false,
  571. sampleRate: 48000,
  572. channelCount: 2
  573. }
  574. });
  575. currentAudioStream = stream;
  576. // Create audio element if not exists
  577. let audioElem = document.getElementById('audioStream');
  578. if (!audioElem) {
  579. audioElem = document.createElement('audio');
  580. audioElem.id = 'audioStream';
  581. audioElem.autoplay = true;
  582. audioElem.controls = true;
  583. audioElem.style.position = 'fixed';
  584. audioElem.style.bottom = '10px';
  585. audioElem.style.left = '10px';
  586. audioElem.style.zIndex = 1001;
  587. document.body.appendChild(audioElem);
  588. audioElem.style.display = 'none';
  589. }
  590. audioElem.srcObject = stream;
  591. } catch (err) {
  592. alert('Error accessing audio device: ' + err.message);
  593. }
  594. }
  595. // Event listeners
  596. refreshAudioBtn.addEventListener('click', listAudioSources);
  597. audioSelect.addEventListener('change', startAudioStream);
  598. // Initial population
  599. listAudioSources().then(startAudioStream);
  600. /*
  601. Video Captures
  602. The following section handles HDMI capture via connected webcams.
  603. */
  604. async function ensureCameraPermission() {
  605. try {
  606. // Request permission to access the camera to get device labels
  607. await navigator.mediaDevices.getUserMedia({ video: true });
  608. } catch (e) {
  609. alert('Unable to access camera');
  610. }
  611. }
  612. async function getCameras() {
  613. const devices = await navigator.mediaDevices.enumerateDevices();
  614. const videoSelect = document.getElementById('videoSource');
  615. videoSelect.innerHTML = '';
  616. devices.forEach(device => {
  617. if (device.kind === 'videoinput') {
  618. const option = document.createElement('option');
  619. option.value = device.deviceId;
  620. option.text = device.label || `Camera ${videoSelect.length + 1}`;
  621. videoSelect.appendChild(option);
  622. }
  623. });
  624. }
  625. async function startStream() {
  626. const videoSelect = document.getElementById('videoSource');
  627. const deviceId = videoSelect.value;
  628. if (window.currentStream) {
  629. window.currentStream.getTracks().forEach(track => track.stop());
  630. }
  631. const constraints = {
  632. video: {
  633. deviceId: { exact: deviceId },
  634. width: { ideal: 1920 },
  635. height: { ideal: 1080 }
  636. }
  637. };
  638. try {
  639. const stream = await navigator.mediaDevices.getUserMedia(constraints);
  640. document.getElementById('video').srcObject = stream;
  641. window.currentStream = stream;
  642. // Resize touchscreen overlay after a short delay to ensure video is loaded
  643. setTimeout(resizeTouchscreenToVideo, 500);
  644. } catch (e) {
  645. alert('Unable to access camera');
  646. }
  647. }
  648. document.getElementById('videoSource').addEventListener('change', startStream);
  649. document.getElementById('fullscreenBtn').addEventListener('click', () => {
  650. if (
  651. document.fullscreenElement ||
  652. document.webkitFullscreenElement ||
  653. document.mozFullScreenElement ||
  654. document.msFullscreenElement
  655. ) {
  656. if (document.exitFullscreen) {
  657. document.exitFullscreen();
  658. } else if (document.webkitExitFullscreen) {
  659. document.webkitExitFullscreen();
  660. } else if (document.mozCancelFullScreen) {
  661. document.mozCancelFullScreen();
  662. } else if (document.msExitFullscreen) {
  663. document.msExitFullscreen();
  664. }
  665. document.querySelector('#fullscreenBtn span').className = 'ts-icon is-maximize-icon';
  666. } else {
  667. if (document.body.requestFullscreen) {
  668. document.body.requestFullscreen();
  669. } else if (document.body.webkitRequestFullscreen) {
  670. document.body.webkitRequestFullscreen();
  671. } else if (document.body.mozRequestFullScreen) {
  672. document.body.mozRequestFullScreen();
  673. } else if (document.body.msRequestFullscreen) {
  674. document.body.msRequestFullscreen();
  675. }
  676. document.querySelector('#fullscreenBtn span').className = 'ts-icon is-minimize-icon';
  677. }
  678. });
  679. document.getElementById('refreshCameras').addEventListener('click', async () => {
  680. await getCameras();
  681. await startStream();
  682. });
  683. // Ensure permission, then populate cameras and start stream
  684. ensureCameraPermission().then(() => {
  685. getCameras().then(startStream);
  686. });
  687. navigator.mediaDevices.addEventListener('devicechange', () => {
  688. getCameras().then(startStream);
  689. });