1/* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17'use strict'; 18 19function ConnectToDevice(device_id) { 20 console.log('ConnectToDevice ', device_id); 21 const keyboardCaptureCtrl = document.getElementById('keyboard-capture-control'); 22 createToggleControl(keyboardCaptureCtrl, "keyboard", onKeyboardCaptureToggle); 23 const micCaptureCtrl = document.getElementById('mic-capture-control'); 24 createToggleControl(micCaptureCtrl, "mic", onMicCaptureToggle); 25 26 const deviceScreen = document.getElementById('device-screen'); 27 const deviceAudio = document.getElementById('device-audio'); 28 const statusMessage = document.getElementById('status-message'); 29 30 let connectionAttemptDuration = 0; 31 const intervalMs = 500; 32 let deviceStatusEllipsisCount = 0; 33 let animateDeviceStatusMessage = setInterval(function() { 34 connectionAttemptDuration += intervalMs; 35 if (connectionAttemptDuration > 30000) { 36 statusMessage.className = 'error'; 37 statusMessage.textContent = 'Connection should have occurred by now. ' + 38 'Please attempt to restart the guest device.'; 39 } else { 40 if (connectionAttemptDuration > 15000) { 41 statusMessage.textContent = 'Connection is taking longer than expected'; 42 } else { 43 statusMessage.textContent = 'Connecting to device'; 44 } 45 deviceStatusEllipsisCount = (deviceStatusEllipsisCount + 1) % 4; 46 statusMessage.textContent += '.'.repeat(deviceStatusEllipsisCount); 47 } 48 }, intervalMs); 49 50 deviceScreen.addEventListener('loadeddata', (evt) => { 51 clearInterval(animateDeviceStatusMessage); 52 statusMessage.textContent = 'Awaiting adb connection...'; 53 resizeDeviceView(); 54 deviceScreen.style.visibility = 'visible'; 55 // Enable the buttons after the screen is visible. 56 for (const [_, button] of Object.entries(buttons)) { 57 if (!button.adb) { 58 button.button.disabled = false; 59 } 60 } 61 // Start the adb connection if it is not already started. 62 initializeAdb(); 63 }); 64 65 let videoStream; 66 let display_label; 67 let buttons = {}; 68 let mouseIsDown = false; 69 let deviceConnection; 70 let touchIdSlotMap = new Map(); 71 let touchSlots = new Array(); 72 let deviceStateLidSwitchOpen = null; 73 let deviceStateHingeAngleValue = null; 74 75 function showAdbConnected() { 76 // Screen changed messages are not reported until after boot has completed. 77 // Certain default adb buttons change screen state, so wait for boot 78 // completion before enabling these buttons. 79 statusMessage.className = 'connected'; 80 statusMessage.textContent = 81 'adb connection established successfully.'; 82 setTimeout(function() { 83 statusMessage.style.visibility = 'hidden'; 84 }, 5000); 85 for (const [_, button] of Object.entries(buttons)) { 86 if (button.adb) { 87 button.button.disabled = false; 88 } 89 } 90 } 91 92 function initializeAdb() { 93 init_adb( 94 deviceConnection, 95 showAdbConnected, 96 function() { 97 statusMessage.className = 'error'; 98 statusMessage.textContent = 'adb connection failed.'; 99 statusMessage.style.visibility = 'visible'; 100 for (const [_, button] of Object.entries(buttons)) { 101 if (button.adb) { 102 button.button.disabled = true; 103 } 104 } 105 }); 106 } 107 108 let currentRotation = 0; 109 let currentDisplayDetails; 110 function onControlMessage(message) { 111 let message_data = JSON.parse(message.data); 112 console.log(message_data) 113 let metadata = message_data.metadata; 114 if (message_data.event == 'VIRTUAL_DEVICE_BOOT_STARTED') { 115 // Start the adb connection after receiving the BOOT_STARTED message. 116 // (This is after the adbd start message. Attempting to connect 117 // immediately after adbd starts causes issues.) 118 initializeAdb(); 119 } 120 if (message_data.event == 'VIRTUAL_DEVICE_SCREEN_CHANGED') { 121 if (metadata.rotation != currentRotation) { 122 // Animate the screen rotation. 123 deviceScreen.style.transition = 'transform 1s'; 124 } else { 125 // Don't animate screen resizes, since these appear as odd sliding 126 // animations if the screen is rotated due to the translateY. 127 deviceScreen.style.transition = ''; 128 } 129 130 currentRotation = metadata.rotation; 131 updateDeviceDisplayDetails({ 132 dpi: metadata.dpi, 133 x_res: metadata.width, 134 y_res: metadata.height 135 }); 136 137 resizeDeviceView(); 138 } 139 } 140 141 const screensDiv = document.getElementById('screens'); 142 function resizeDeviceView() { 143 // Auto-scale the screen based on window size. 144 // Max window width of 70%, allowing space for the control panel. 145 let ww = screensDiv.offsetWidth * 0.7; 146 let wh = screensDiv.offsetHeight; 147 let vw = currentDisplayDetails.x_res; 148 let vh = currentDisplayDetails.y_res; 149 let scaling = vw * wh > vh * ww ? ww / vw : wh / vh; 150 if (currentRotation == 0) { 151 deviceScreen.style.transform = null; 152 deviceScreen.style.width = vw * scaling; 153 deviceScreen.style.height = vh * scaling; 154 } else if (currentRotation == 1) { 155 deviceScreen.style.transform = 156 `rotateZ(-90deg) translateY(-${vh * scaling}px)`; 157 // When rotated, w and h are swapped. 158 deviceScreen.style.width = vh * scaling; 159 deviceScreen.style.height = vw * scaling; 160 } 161 } 162 window.onresize = resizeDeviceView; 163 164 function createControlPanelButton(command, title, icon_name, 165 listener=onControlPanelButton, 166 parent_id='control-panel-default-buttons') { 167 let button = document.createElement('button'); 168 document.getElementById(parent_id).appendChild(button); 169 button.title = title; 170 button.dataset.command = command; 171 button.disabled = true; 172 // Capture mousedown/up/out commands instead of click to enable 173 // hold detection. mouseout is used to catch if the user moves the 174 // mouse outside the button while holding down. 175 button.addEventListener('mousedown', listener); 176 button.addEventListener('mouseup', listener); 177 button.addEventListener('mouseout', listener); 178 // Set the button image using Material Design icons. 179 // See http://google.github.io/material-design-icons 180 // and https://material.io/resources/icons 181 button.classList.add('material-icons'); 182 button.innerHTML = icon_name; 183 buttons[command] = { 'button': button } 184 return buttons[command]; 185 } 186 createControlPanelButton('power', 'Power', 'power_settings_new'); 187 createControlPanelButton('home', 'Home', 'home'); 188 createControlPanelButton('menu', 'Menu', 'menu'); 189 createControlPanelButton('rotate', 'Rotate', 'screen_rotation', onRotateButton); 190 buttons['rotate'].adb = true; 191 createControlPanelButton('volumemute', 'Volume Mute', 'volume_mute'); 192 createControlPanelButton('volumedown', 'Volume Down', 'volume_down'); 193 createControlPanelButton('volumeup', 'Volume Up', 'volume_up'); 194 195 let modalOffsets = {} 196 function createModalButton(button_id, modal_id, close_id) { 197 const modalButton = document.getElementById(button_id); 198 const modalDiv = document.getElementById(modal_id); 199 const modalHeader = modalDiv.querySelector('.modal-header'); 200 const modalClose = document.getElementById(close_id); 201 202 // Position the modal to the right of the show modal button. 203 modalDiv.style.top = modalButton.offsetTop; 204 modalDiv.style.left = modalButton.offsetWidth + 30; 205 206 function showHideModal(show) { 207 if (show) { 208 modalButton.classList.add('modal-button-opened') 209 modalDiv.style.display = 'block'; 210 } else { 211 modalButton.classList.remove('modal-button-opened') 212 modalDiv.style.display = 'none'; 213 } 214 } 215 // Allow the show modal button to toggle the modal, 216 modalButton.addEventListener('click', 217 evt => showHideModal(modalDiv.style.display != 'block')); 218 // but the close button always closes. 219 modalClose.addEventListener('click', 220 evt => showHideModal(false)); 221 222 // Allow the modal to be dragged by the header. 223 modalOffsets[modal_id] = { 224 midDrag: false, 225 mouseDownOffsetX: null, 226 mouseDownOffsetY: null, 227 } 228 modalHeader.addEventListener('mousedown', 229 evt => { 230 modalOffsets[modal_id].midDrag = true; 231 // Store the offset of the mouse location from the 232 // modal's current location. 233 modalOffsets[modal_id].mouseDownOffsetX = 234 parseInt(modalDiv.style.left) - evt.clientX; 235 modalOffsets[modal_id].mouseDownOffsetY = 236 parseInt(modalDiv.style.top) - evt.clientY; 237 }); 238 modalHeader.addEventListener('mousemove', 239 evt => { 240 let offsets = modalOffsets[modal_id]; 241 if (offsets.midDrag) { 242 // Move the modal to the mouse location plus the 243 // offset calculated on the initial mouse-down. 244 modalDiv.style.left = 245 evt.clientX + offsets.mouseDownOffsetX; 246 modalDiv.style.top = 247 evt.clientY + offsets.mouseDownOffsetY; 248 } 249 }); 250 document.addEventListener('mouseup', 251 evt => { 252 modalOffsets[modal_id].midDrag = false; 253 }); 254 } 255 256 createModalButton( 257 'device-details-button', 'device-details-modal', 'device-details-close'); 258 createModalButton( 259 'bluetooth-console-button', 'bluetooth-console-modal', 'bluetooth-console-close'); 260 261 let options = { 262 wsUrl: ((location.protocol == 'http:') ? 'ws://' : 'wss://') + 263 location.host + '/connect_client', 264 }; 265 266 function showWebrtcError() { 267 statusMessage.className = 'error'; 268 statusMessage.textContent = 'No connection to the guest device. ' + 269 'Please ensure the WebRTC process on the host machine is active.'; 270 statusMessage.style.visibility = 'visible'; 271 deviceScreen.style.display = 'none'; 272 for (const [_, button] of Object.entries(buttons)) { 273 button.button.disabled = true; 274 } 275 } 276 277 import('./cf_webrtc.js') 278 .then(webrtcModule => webrtcModule.Connect(device_id, options)) 279 .then(devConn => { 280 deviceConnection = devConn; 281 // TODO(b/143667633): get multiple display configuration from the 282 // description object 283 console.log(deviceConnection.description); 284 let stream_id = devConn.description.displays[0].stream_id; 285 devConn.getStream(stream_id).then(stream => { 286 videoStream = stream; 287 display_label = stream_id; 288 deviceScreen.srcObject = videoStream; 289 }).catch(e => console.error('Unable to get display stream: ', e)); 290 for (const audio_desc of devConn.description.audio_streams) { 291 let stream_id = audio_desc.stream_id; 292 devConn.getStream(stream_id).then(stream => { 293 deviceAudio.srcObject = stream; 294 }).catch(e => console.error('Unable to get audio stream: ', e)); 295 } 296 startMouseTracking(); // TODO stopMouseTracking() when disconnected 297 updateDeviceHardwareDetails(deviceConnection.description.hardware); 298 updateDeviceDisplayDetails(deviceConnection.description.displays[0]); 299 if (deviceConnection.description.custom_control_panel_buttons.length > 0) { 300 document.getElementById('control-panel-custom-buttons').style.display = 'flex'; 301 for (const button of deviceConnection.description.custom_control_panel_buttons) { 302 if (button.shell_command) { 303 // This button's command is handled by sending an ADB shell command. 304 createControlPanelButton(button.command, button.title, button.icon_name, 305 e => onCustomShellButton(button.shell_command, e), 306 'control-panel-custom-buttons'); 307 buttons[button.command].adb = true; 308 } else if (button.device_states) { 309 // This button corresponds to variable hardware device state(s). 310 createControlPanelButton(button.command, button.title, button.icon_name, 311 getCustomDeviceStateButtonCb(button.device_states), 312 'control-panel-custom-buttons'); 313 for (const device_state of button.device_states) { 314 // hinge_angle is currently injected via an adb shell command that 315 // triggers a guest binary. 316 if ('hinge_angle_value' in device_state) { 317 buttons[button.command].adb = true; 318 } 319 } 320 } else { 321 // This button's command is handled by custom action server. 322 createControlPanelButton(button.command, button.title, button.icon_name, 323 onControlPanelButton, 324 'control-panel-custom-buttons'); 325 } 326 } 327 } 328 deviceConnection.onControlMessage(msg => onControlMessage(msg)); 329 // Start the screen as hidden. Only show when data is ready. 330 deviceScreen.style.visibility = 'hidden'; 331 // Show the error message and disable buttons when the WebRTC connection fails. 332 deviceConnection.onConnectionStateChange(state => { 333 if (state == 'disconnected' || state == 'failed') { 334 showWebrtcError(); 335 } 336 }); 337 deviceConnection.onBluetoothMessage(msg => { 338 bluetoothConsole.addLine(decodeRootcanalMessage(msg)); 339 }); 340 }, rejection => { 341 console.error('Unable to connect: ', rejection); 342 showWebrtcError(); 343 }); 344 345 let hardwareDetailsText = ''; 346 let displayDetailsText = ''; 347 let deviceStateDetailsText = ''; 348 function updateDeviceDetailsText() { 349 document.getElementById('device-details-hardware').textContent = [ 350 hardwareDetailsText, 351 deviceStateDetailsText, 352 displayDetailsText, 353 ].filter(e => e /*remove empty*/).join('\n'); 354 } 355 function updateDeviceHardwareDetails(hardware) { 356 let hardwareDetailsTextLines = []; 357 Object.keys(hardware).forEach(function(key) { 358 let value = hardware[key]; 359 hardwareDetailsTextLines.push(`${key} - ${value}`); 360 }); 361 362 hardwareDetailsText = hardwareDetailsTextLines.join('\n'); 363 updateDeviceDetailsText(); 364 } 365 function updateDeviceDisplayDetails(display) { 366 currentDisplayDetails = display; 367 let dpi = display.dpi; 368 let x_res = display.x_res; 369 let y_res = display.y_res; 370 let rotated = currentRotation == 1 ? ' (Rotated)' : ''; 371 displayDetailsText = `Display - ${x_res}x${y_res} (${dpi}DPI)${rotated}`; 372 updateDeviceDetailsText(); 373 } 374 function updateDeviceStateDetails() { 375 let deviceStateDetailsTextLines = []; 376 if (deviceStateLidSwitchOpen != null) { 377 let state = deviceStateLidSwitchOpen ? 'Opened' : 'Closed'; 378 deviceStateDetailsTextLines.push(`Lid Switch - ${state}`); 379 } 380 if (deviceStateHingeAngleValue != null) { 381 deviceStateDetailsTextLines.push(`Hinge Angle - ${deviceStateHingeAngleValue}`); 382 } 383 deviceStateDetailsText = deviceStateDetailsTextLines.join('\n'); 384 updateDeviceDetailsText(); 385 } 386 387 function onKeyboardCaptureToggle(enabled) { 388 if (enabled) { 389 startKeyboardTracking(); 390 } else { 391 stopKeyboardTracking(); 392 } 393 } 394 395 function onMicCaptureToggle(enabled) { 396 deviceConnection.useMic(enabled); 397 } 398 399 function cmdConsole(consoleViewName, consoleInputName) { 400 let consoleView = document.getElementById(consoleViewName); 401 402 let addString = function(str) { 403 consoleView.value += str; 404 consoleView.scrollTop = consoleView.scrollHeight; 405 } 406 407 let addLine = function(line) { 408 addString(line + "\r\n"); 409 } 410 411 let commandCallbacks = []; 412 413 let addCommandListener = function(f) { 414 commandCallbacks.push(f); 415 } 416 417 let onCommand = function(cmd) { 418 cmd = cmd.trim(); 419 420 if (cmd.length == 0) return; 421 422 commandCallbacks.forEach(f => { 423 f(cmd); 424 }) 425 } 426 427 addCommandListener(cmd => addLine(">> " + cmd)); 428 429 let consoleInput = document.getElementById(consoleInputName); 430 431 consoleInput.addEventListener('keydown', e => { 432 if ((e.key && e.key == 'Enter') || e.keyCode == 13) { 433 let command = e.target.value; 434 435 e.target.value = ''; 436 437 onCommand(command); 438 } 439 }) 440 441 return { 442 consoleView: consoleView, 443 consoleInput: consoleInput, 444 addLine: addLine, 445 addString: addString, 446 addCommandListener: addCommandListener, 447 }; 448 } 449 450 var bluetoothConsole = cmdConsole( 451 'bluetooth-console-view', 'bluetooth-console-input'); 452 453 bluetoothConsole.addCommandListener(cmd => { 454 let inputArr = cmd.split(' '); 455 let command = inputArr[0]; 456 inputArr.shift(); 457 let args = inputArr; 458 deviceConnection.sendBluetoothMessage(createRootcanalMessage(command, args)); 459 }) 460 461 function onControlPanelButton(e) { 462 if (e.type == 'mouseout' && e.which == 0) { 463 // Ignore mouseout events if no mouse button is pressed. 464 return; 465 } 466 deviceConnection.sendControlMessage(JSON.stringify({ 467 command: e.target.dataset.command, 468 button_state: e.type == 'mousedown' ? "down" : "up", 469 })); 470 } 471 472 function onRotateButton(e) { 473 // Attempt to init adb again, in case the initial connection failed. 474 // This succeeds immediately if already connected. 475 initializeAdb(); 476 if (e.type == 'mousedown') { 477 adbShell( 478 '/vendor/bin/cuttlefish_sensor_injection rotate ' + 479 (currentRotation == 0 ? 'landscape' : 'portrait')) 480 } 481 } 482 483 function onCustomShellButton(shell_command, e) { 484 // Attempt to init adb again, in case the initial connection failed. 485 // This succeeds immediately if already connected. 486 initializeAdb(); 487 if (e.type == 'mousedown') { 488 adbShell(shell_command); 489 } 490 } 491 492 function getCustomDeviceStateButtonCb(device_states) { 493 let states = device_states; 494 let index = 0; 495 return e => { 496 if (e.type == 'mousedown') { 497 // Reset any overridden device state. 498 adbShell('cmd device_state state reset'); 499 // Send a device_state message for the current state. 500 let message = { 501 command: 'device_state', 502 ...states[index], 503 }; 504 deviceConnection.sendControlMessage(JSON.stringify(message)); 505 console.log(JSON.stringify(message)); 506 if ('lid_switch_open' in states[index]) { 507 deviceStateLidSwitchOpen = states[index].lid_switch_open; 508 } 509 if ('hinge_angle_value' in states[index]) { 510 deviceStateHingeAngleValue = states[index].hinge_angle_value; 511 // TODO(b/181157794): Use a custom Sensor HAL for hinge_angle injection 512 // instead of this guest binary. 513 adbShell( 514 '/vendor/bin/cuttlefish_sensor_injection hinge_angle ' + 515 states[index].hinge_angle_value); 516 } 517 // Update the Device Details view. 518 updateDeviceStateDetails(); 519 // Cycle to the next state. 520 index = (index + 1) % states.length; 521 } 522 } 523 } 524 525 function startMouseTracking() { 526 if (window.PointerEvent) { 527 deviceScreen.addEventListener('pointerdown', onStartDrag); 528 deviceScreen.addEventListener('pointermove', onContinueDrag); 529 deviceScreen.addEventListener('pointerup', onEndDrag); 530 } else if (window.TouchEvent) { 531 deviceScreen.addEventListener('touchstart', onStartDrag); 532 deviceScreen.addEventListener('touchmove', onContinueDrag); 533 deviceScreen.addEventListener('touchend', onEndDrag); 534 } else if (window.MouseEvent) { 535 deviceScreen.addEventListener('mousedown', onStartDrag); 536 deviceScreen.addEventListener('mousemove', onContinueDrag); 537 deviceScreen.addEventListener('mouseup', onEndDrag); 538 } 539 } 540 541 function stopMouseTracking() { 542 if (window.PointerEvent) { 543 deviceScreen.removeEventListener('pointerdown', onStartDrag); 544 deviceScreen.removeEventListener('pointermove', onContinueDrag); 545 deviceScreen.removeEventListener('pointerup', onEndDrag); 546 } else if (window.TouchEvent) { 547 deviceScreen.removeEventListener('touchstart', onStartDrag); 548 deviceScreen.removeEventListener('touchmove', onContinueDrag); 549 deviceScreen.removeEventListener('touchend', onEndDrag); 550 } else if (window.MouseEvent) { 551 deviceScreen.removeEventListener('mousedown', onStartDrag); 552 deviceScreen.removeEventListener('mousemove', onContinueDrag); 553 deviceScreen.removeEventListener('mouseup', onEndDrag); 554 } 555 } 556 557 function startKeyboardTracking() { 558 document.addEventListener('keydown', onKeyEvent); 559 document.addEventListener('keyup', onKeyEvent); 560 } 561 562 function stopKeyboardTracking() { 563 document.removeEventListener('keydown', onKeyEvent); 564 document.removeEventListener('keyup', onKeyEvent); 565 } 566 567 function onStartDrag(e) { 568 e.preventDefault(); 569 570 // console.log("mousedown at " + e.pageX + " / " + e.pageY); 571 mouseIsDown = true; 572 573 sendEventUpdate(true, e); 574 } 575 576 function onEndDrag(e) { 577 e.preventDefault(); 578 579 // console.log("mouseup at " + e.pageX + " / " + e.pageY); 580 mouseIsDown = false; 581 582 sendEventUpdate(false, e); 583 } 584 585 function onContinueDrag(e) { 586 e.preventDefault(); 587 588 // console.log("mousemove at " + e.pageX + " / " + e.pageY + ", down=" + 589 // mouseIsDown); 590 if (mouseIsDown) { 591 sendEventUpdate(true, e); 592 } 593 } 594 595 function sendEventUpdate(down, e) { 596 console.assert(deviceConnection, 'Can\'t send mouse update without device'); 597 var eventType = e.type.substring(0, 5); 598 599 // Before the first video frame arrives there is no way to know width and 600 // height of the device's screen, so turn every click into a click at 0x0. 601 // A click at that position is not more dangerous than anywhere else since 602 // the user is clicking blind anyways. 603 const videoWidth = deviceScreen.videoWidth? deviceScreen.videoWidth: 1; 604 const videoHeight = deviceScreen.videoHeight? deviceScreen.videoHeight: 1; 605 const elementWidth = deviceScreen.offsetWidth? deviceScreen.offsetWidth: 1; 606 const elementHeight = deviceScreen.offsetHeight? deviceScreen.offsetHeight: 1; 607 608 // vh*ew > eh*vw? then scale h instead of w 609 const scaleHeight = videoHeight * elementWidth > videoWidth * elementHeight; 610 var elementScaling = 0, videoScaling = 0; 611 if (scaleHeight) { 612 elementScaling = elementHeight; 613 videoScaling = videoHeight; 614 } else { 615 elementScaling = elementWidth; 616 videoScaling = videoWidth; 617 } 618 619 // The screen uses the 'object-fit: cover' property in order to completely 620 // fill the element while maintaining the screen content's aspect ratio. 621 // Therefore: 622 // - If vh*ew > eh*vw, w is scaled so that content width == element width 623 // - Otherwise, h is scaled so that content height == element height 624 const scaleWidth = videoHeight * elementWidth > videoWidth * elementHeight; 625 626 // Convert to coordinates relative to the video by scaling. 627 // (This matches the scaling used by 'object-fit: cover'.) 628 // 629 // This scaling is needed to translate from the in-browser x/y to the 630 // on-device x/y. 631 // - When the device screen has not been resized, this is simple: scale 632 // the coordinates based on the ratio between the input video size and 633 // the in-browser size. 634 // - When the device screen has been resized, this scaling is still needed 635 // even though the in-browser size and device size are identical. This 636 // is due to the way WindowManager handles a resized screen, resized via 637 // `adb shell wm size`: 638 // - The ABS_X and ABS_Y max values of the screen retain their 639 // original values equal to the value set when launching the device 640 // (which equals the video size here). 641 // - The sent ABS_X and ABS_Y values need to be scaled based on the 642 // ratio between the max size (video size) and in-browser size. 643 const scaling = scaleWidth ? videoWidth / elementWidth : videoHeight / elementHeight; 644 645 var xArr = []; 646 var yArr = []; 647 var idArr = []; 648 var slotArr = []; 649 650 if (eventType == "mouse" || eventType == "point") { 651 xArr.push(e.offsetX); 652 yArr.push(e.offsetY); 653 654 let thisId = -1; 655 if (eventType == "point") { 656 thisId = e.pointerId; 657 } 658 659 slotArr.push(0); 660 idArr.push(thisId); 661 } else if (eventType == "touch") { 662 // touchstart: list of touch points that became active 663 // touchmove: list of touch points that changed 664 // touchend: list of touch points that were removed 665 let changes = e.changedTouches; 666 let rect = e.target.getBoundingClientRect(); 667 for (var i=0; i < changes.length; i++) { 668 xArr.push(changes[i].pageX - rect.left); 669 yArr.push(changes[i].pageY - rect.top); 670 if (touchIdSlotMap.has(changes[i].identifier)) { 671 let slot = touchIdSlotMap.get(changes[i].identifier); 672 673 slotArr.push(slot); 674 if (e.type == 'touchstart') { 675 // error 676 console.error('touchstart when already have slot'); 677 return; 678 } else if (e.type == 'touchmove') { 679 idArr.push(changes[i].identifier); 680 } else if (e.type == 'touchend') { 681 touchSlots[slot] = false; 682 touchIdSlotMap.delete(changes[i].identifier); 683 idArr.push(-1); 684 } 685 } else { 686 if (e.type == 'touchstart') { 687 let slot = -1; 688 for (var j=0; j < touchSlots.length; j++) { 689 if (!touchSlots[j]) { 690 slot = j; 691 break; 692 } 693 } 694 if (slot == -1) { 695 slot = touchSlots.length; 696 touchSlots.push(true); 697 } 698 slotArr.push(slot); 699 touchSlots[slot] = true; 700 touchIdSlotMap.set(changes[i].identifier, slot); 701 idArr.push(changes[i].identifier); 702 } else if (e.type == 'touchmove') { 703 // error 704 console.error('touchmove when no slot'); 705 return; 706 } else if (e.type == 'touchend') { 707 // error 708 console.error('touchend when no slot'); 709 return; 710 } 711 } 712 } 713 } 714 715 for (var i=0; i < xArr.length; i++) { 716 xArr[i] = xArr[i] * scaling; 717 yArr[i] = yArr[i] * scaling; 718 719 // Substract the offset produced by the difference in aspect ratio, if any. 720 if (scaleWidth) { 721 // Width was scaled, leaving excess content height, so subtract from y. 722 yArr[i] -= (elementHeight * scaling - videoHeight) / 2; 723 } else { 724 // Height was scaled, leaving excess content width, so subtract from x. 725 xArr[i] -= (elementWidth * scaling - videoWidth) / 2; 726 } 727 728 xArr[i] = Math.trunc(xArr[i]); 729 yArr[i] = Math.trunc(yArr[i]); 730 } 731 732 // NOTE: Rotation is handled automatically because the CSS rotation through 733 // transforms also rotates the coordinates of events on the object. 734 735 deviceConnection.sendMultiTouch( 736 {idArr, xArr, yArr, down, slotArr, display_label}); 737 } 738 739 function onKeyEvent(e) { 740 e.preventDefault(); 741 console.assert(deviceConnection, 'Can\'t send key event without device'); 742 deviceConnection.sendKeyEvent(e.code, e.type); 743 } 744} 745 746/******************************************************************************/ 747 748function ConnectDeviceCb(dev_id) { 749 console.log('Connect: ' + dev_id); 750 // Hide the device selection screen 751 document.getElementById('device-selector').style.display = 'none'; 752 // Show the device control screen 753 document.getElementById('device-connection').style.visibility = 'visible'; 754 ConnectToDevice(dev_id); 755} 756 757function ShowNewDeviceList(device_ids) { 758 let ul = document.getElementById('device-list'); 759 ul.innerHTML = ""; 760 let count = 1; 761 let device_to_button_map = {}; 762 for (const dev_id of device_ids) { 763 const button_id = 'connect_' + count++; 764 ul.innerHTML += ('<li class="device_entry" title="Connect to ' + dev_id 765 + '">' + dev_id + '<button id="' + button_id 766 + '" >Connect</button></li>'); 767 device_to_button_map[dev_id] = button_id; 768 } 769 770 for (const [dev_id, button_id] of Object.entries(device_to_button_map)) { 771 document.getElementById(button_id).addEventListener( 772 'click', evt => ConnectDeviceCb(dev_id)); 773 } 774} 775 776function UpdateDeviceList() { 777 let url = ((location.protocol == 'http:') ? 'ws:' : 'wss:') + location.host + 778 '/list_devices'; 779 let ws = new WebSocket(url); 780 ws.onopen = () => { 781 ws.send("give me those device ids"); 782 }; 783 ws.onmessage = msg => { 784 let device_ids = JSON.parse(msg.data); 785 ShowNewDeviceList(device_ids); 786 }; 787} 788 789// Get any devices that are already connected 790UpdateDeviceList(); 791// Update the list at the user's request 792document.getElementById('refresh-list') 793 .addEventListener('click', evt => UpdateDeviceList()); 794