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 17function createDataChannel(pc, label, onMessage) { 18 console.debug('creating data channel: ' + label); 19 let dataChannel = pc.createDataChannel(label); 20 dataChannel.binaryType = "arraybuffer"; 21 // Return an object with a send function like that of the dataChannel, but 22 // that only actually sends over the data channel once it has connected. 23 return { 24 channelPromise: new Promise((resolve, reject) => { 25 dataChannel.onopen = (event) => { 26 resolve(dataChannel); 27 }; 28 dataChannel.onclose = () => { 29 console.debug( 30 'Data channel=' + label + ' state=' + dataChannel.readyState); 31 }; 32 dataChannel.onmessage = onMessage ? onMessage : (msg) => { 33 console.debug('Data channel=' + label + ' data="' + msg.data + '"'); 34 }; 35 dataChannel.onerror = err => { 36 reject(err); 37 }; 38 }), 39 send: function(msg) { 40 this.channelPromise = this.channelPromise.then(channel => { 41 channel.send(msg); 42 return channel; 43 }) 44 }, 45 }; 46} 47 48function awaitDataChannel(pc, label, onMessage) { 49 console.debug('expecting data channel: ' + label); 50 // Return an object with a send function like that of the dataChannel, but 51 // that only actually sends over the data channel once it has connected. 52 return { 53 channelPromise: new Promise((resolve, reject) => { 54 let prev_ondatachannel = pc.ondatachannel; 55 pc.ondatachannel = ev => { 56 let dataChannel = ev.channel; 57 if (dataChannel.label == label) { 58 dataChannel.onopen = (event) => { 59 resolve(dataChannel); 60 }; 61 dataChannel.onclose = () => { 62 console.debug( 63 'Data channel=' + label + ' state=' + dataChannel.readyState); 64 }; 65 dataChannel.onmessage = onMessage ? onMessage : (msg) => { 66 console.debug('Data channel=' + label + ' data="' + msg.data + '"'); 67 }; 68 dataChannel.onerror = err => { 69 reject(err); 70 }; 71 } else if (prev_ondatachannel) { 72 prev_ondatachannel(ev); 73 } 74 }; 75 }), 76 send: function(msg) { 77 this.channelPromise = this.channelPromise.then(channel => { 78 channel.send(msg); 79 return channel; 80 }) 81 }, 82 }; 83} 84 85class DeviceConnection { 86 #pc; 87 #control; 88 #description; 89 90 #cameraDataChannel; 91 #cameraInputQueue; 92 #controlChannel; 93 #inputChannel; 94 #adbChannel; 95 #bluetoothChannel; 96 #lightsChannel; 97 #locationChannel; 98 #sensorsChannel; 99 #kmlLocationsChannel; 100 #gpxLocationsChannel; 101 102 #streams; 103 #streamPromiseResolvers; 104 #streamChangeCallback; 105 #micSenders = []; 106 #cameraSenders = []; 107 #camera_res_x; 108 #camera_res_y; 109 110 #onAdbMessage; 111 #onControlMessage; 112 #onBluetoothMessage; 113 #onSensorsMessage 114 #onLocationMessage; 115 #onKmlLocationsMessage; 116 #onGpxLocationsMessage; 117 #onLightsMessage; 118 119 #micRequested = false; 120 #cameraRequested = false; 121 122 constructor(pc, control) { 123 this.#pc = pc; 124 this.#control = control; 125 this.#cameraDataChannel = pc.createDataChannel('camera-data-channel'); 126 this.#cameraDataChannel.binaryType = 'arraybuffer'; 127 this.#cameraInputQueue = new Array(); 128 var self = this; 129 this.#cameraDataChannel.onbufferedamountlow = () => { 130 if (self.#cameraInputQueue.length > 0) { 131 self.sendCameraData(self.#cameraInputQueue.shift()); 132 } 133 }; 134 this.#inputChannel = createDataChannel(pc, 'input-channel'); 135 this.#sensorsChannel = createDataChannel(pc, 'sensors-channel', (msg) => { 136 if (!this.#onSensorsMessage) { 137 console.error('Received unexpected Sensors message'); 138 return; 139 } 140 this.#onSensorsMessage(msg); 141 }); 142 this.#adbChannel = createDataChannel(pc, 'adb-channel', (msg) => { 143 if (!this.#onAdbMessage) { 144 console.error('Received unexpected ADB message'); 145 return; 146 } 147 this.#onAdbMessage(msg.data); 148 }); 149 this.#controlChannel = awaitDataChannel(pc, 'device-control', (msg) => { 150 if (!this.#onControlMessage) { 151 console.error('Received unexpected Control message'); 152 return; 153 } 154 this.#onControlMessage(msg); 155 }); 156 this.#bluetoothChannel = 157 createDataChannel(pc, 'bluetooth-channel', (msg) => { 158 if (!this.#onBluetoothMessage) { 159 console.error('Received unexpected Bluetooth message'); 160 return; 161 } 162 this.#onBluetoothMessage(msg.data); 163 }); 164 this.#locationChannel = 165 createDataChannel(pc, 'location-channel', (msg) => { 166 if (!this.#onLocationMessage) { 167 console.error('Received unexpected Location message'); 168 return; 169 } 170 this.#onLocationMessage(msg.data); 171 }); 172 173 this.#kmlLocationsChannel = 174 createDataChannel(pc, 'kml-locations-channel', (msg) => { 175 if (!this.#onKmlLocationsMessage) { 176 console.error('Received unexpected KML Locations message'); 177 return; 178 } 179 this.#onKmlLocationsMessage(msg.data); 180 }); 181 182 this.#gpxLocationsChannel = 183 createDataChannel(pc, 'gpx-locations-channel', (msg) => { 184 if (!this.#onGpxLocationsMessage) { 185 console.error('Received unexpected KML Locations message'); 186 return; 187 } 188 this.#onGpxLocationsMessage(msg.data); 189 }); 190 this.#lightsChannel = createDataChannel(pc, 'lights-channel', (msg) => { 191 if (!this.#onLightsMessage) { 192 console.error('Received unexpected Lights message'); 193 return; 194 } 195 this.#onLightsMessage(msg); 196 }); 197 198 this.#streams = {}; 199 this.#streamPromiseResolvers = {}; 200 201 pc.addEventListener('track', e => { 202 console.debug('Got remote stream: ', e); 203 for (const stream of e.streams) { 204 this.#streams[stream.id] = stream; 205 if (this.#streamPromiseResolvers[stream.id]) { 206 for (let resolver of this.#streamPromiseResolvers[stream.id]) { 207 resolver(); 208 } 209 delete this.#streamPromiseResolvers[stream.id]; 210 } 211 212 if (this.#streamChangeCallback) { 213 this.#streamChangeCallback(stream); 214 } 215 } 216 }); 217 } 218 219 set description(desc) { 220 this.#description = desc; 221 } 222 223 get description() { 224 return this.#description; 225 } 226 227 get imageCapture() { 228 if (this.#cameraSenders && this.#cameraSenders.length > 0) { 229 let track = this.#cameraSenders[0].track; 230 return new ImageCapture(track); 231 } 232 return undefined; 233 } 234 235 get cameraWidth() { 236 return this.#camera_res_x; 237 } 238 239 get cameraHeight() { 240 return this.#camera_res_y; 241 } 242 243 get cameraEnabled() { 244 return this.#cameraSenders && this.#cameraSenders.length > 0; 245 } 246 247 getStream(stream_id) { 248 if (stream_id in this.#streams) { 249 return this.#streams[stream_id]; 250 } 251 return null; 252 } 253 254 onStream(stream_id) { 255 return new Promise((resolve, reject) => { 256 if (this.#streams[stream_id]) { 257 resolve(this.#streams[stream_id]); 258 } else { 259 if (!this.#streamPromiseResolvers[stream_id]) { 260 this.#streamPromiseResolvers[stream_id] = []; 261 } 262 this.#streamPromiseResolvers[stream_id].push(resolve); 263 } 264 }); 265 } 266 267 onStreamChange(cb) { 268 this.#streamChangeCallback = cb; 269 } 270 271 expectStreamChange() { 272 this.#control.expectMessagesSoon(5000); 273 } 274 275 #sendJsonInput(evt) { 276 this.#inputChannel.send(JSON.stringify(evt)); 277 } 278 279 sendMouseMove({x, y}) { 280 this.#sendJsonInput({ 281 type: 'mouseMove', 282 x, 283 y, 284 }); 285 } 286 287 sendMouseButton({button, down}) { 288 this.#sendJsonInput({ 289 type: 'mouseButton', 290 button: button, 291 down: down ? 1 : 0, 292 }); 293 } 294 295 // TODO (b/124121375): This should probably be an array of pointer events and 296 // have different properties. 297 sendMultiTouch({idArr, xArr, yArr, down, device_label}) { 298 let events = { 299 type: 'multi-touch', 300 id: idArr, 301 x: xArr, 302 y: yArr, 303 down: down ? 1 : 0, 304 device_label: device_label, 305 }; 306 this.#sendJsonInput(events); 307 } 308 309 sendKeyEvent(code, type) { 310 this.#sendJsonInput({type: 'keyboard', keycode: code, event_type: type}); 311 } 312 313 sendWheelEvent(pixels) { 314 this.#sendJsonInput({ 315 type: 'wheel', 316 // convert double to int, forcing a base 10 conversion. pixels can be fractional. 317 pixels: parseInt(pixels, 10), 318 }); 319 } 320 321 sendMouseWheelEvent(pixels) { 322 this.#sendJsonInput({ 323 type: 'mouseWheel', 324 // convert double to int, forcing a base 10 conversion. pixels can be fractional. 325 pixels: parseInt(pixels, 10), 326 }); 327 } 328 329 disconnect() { 330 this.#pc.close(); 331 } 332 333 // Sends binary data directly to the in-device adb daemon (skipping the host) 334 sendAdbMessage(msg) { 335 this.#adbChannel.send(msg); 336 } 337 338 // Provide a callback to receive data from the in-device adb daemon 339 onAdbMessage(cb) { 340 this.#onAdbMessage = cb; 341 } 342 343 // Send control commands to the device 344 sendControlMessage(msg) { 345 this.#controlChannel.send(msg); 346 } 347 348 async #useDevice(in_use, senders_arr, device_opt, requestedFn = () => {in_use}, 349 enabledFn = (stream) => {}, disabledFn = () => {}) { 350 // An empty array means no tracks are currently in use 351 if (senders_arr.length > 0 === !!in_use) { 352 return in_use; 353 } 354 let renegotiation_needed = false; 355 if (in_use) { 356 try { 357 let stream = await navigator.mediaDevices.getUserMedia(device_opt); 358 // The user may have changed their mind by the time we obtain the 359 // stream, check again 360 if (!!in_use != requestedFn()) { 361 return requestedFn(); 362 } 363 enabledFn(stream); 364 stream.getTracks().forEach(track => { 365 console.info(`Using ${track.kind} device: ${track.label}`); 366 senders_arr.push(this.#pc.addTrack(track)); 367 renegotiation_needed = true; 368 }); 369 } catch (e) { 370 console.error('Failed to add stream to peer connection: ', e); 371 // Don't return yet, if there were errors some tracks may have been 372 // added so the connection should be renegotiated again. 373 } 374 } else { 375 for (const sender of senders_arr) { 376 console.info( 377 `Removing ${sender.track.kind} device: ${sender.track.label}`); 378 let track = sender.track; 379 track.stop(); 380 this.#pc.removeTrack(sender); 381 renegotiation_needed = true; 382 } 383 // Empty the array passed by reference, just assigning [] won't do that. 384 senders_arr.length = 0; 385 disabledFn(); 386 } 387 if (renegotiation_needed) { 388 await this.#control.renegotiateConnection(); 389 } 390 // Return the new state 391 return senders_arr.length > 0; 392 } 393 394 // enabledFn: a callback function that will be called if the mic is successfully enabled. 395 // disabledFn: a callback function that will be called if the mic is successfully disabled. 396 async useMic(in_use, enabledFn = () => {}, disabledFn = () => {}) { 397 if (this.#micRequested == !!in_use) { 398 return in_use; 399 } 400 this.#micRequested = !!in_use; 401 return this.#useDevice( 402 in_use, this.#micSenders, {audio: true, video: false}, 403 () => this.#micRequested, 404 enabledFn, 405 disabledFn); 406 } 407 408 async useCamera(in_use) { 409 if (this.#cameraRequested == !!in_use) { 410 return in_use; 411 } 412 this.#cameraRequested = !!in_use; 413 return this.#useDevice( 414 in_use, this.#micSenders, {audio: false, video: true}, 415 () => this.#cameraRequested, 416 (stream) => this.sendCameraResolution(stream)); 417 } 418 419 sendCameraResolution(stream) { 420 const cameraTracks = stream.getVideoTracks(); 421 if (cameraTracks.length > 0) { 422 const settings = cameraTracks[0].getSettings(); 423 this.#camera_res_x = settings.width; 424 this.#camera_res_y = settings.height; 425 this.sendControlMessage(JSON.stringify({ 426 command: 'camera_settings', 427 width: settings.width, 428 height: settings.height, 429 frame_rate: settings.frameRate, 430 facing: settings.facingMode 431 })); 432 } 433 } 434 435 sendOrQueueCameraData(data) { 436 if (this.#cameraDataChannel.bufferedAmount > 0 || 437 this.#cameraInputQueue.length > 0) { 438 this.#cameraInputQueue.push(data); 439 } else { 440 this.sendCameraData(data); 441 } 442 } 443 444 sendCameraData(data) { 445 const MAX_SIZE = 65535; 446 const END_MARKER = 'EOF'; 447 for (let i = 0; i < data.byteLength; i += MAX_SIZE) { 448 // range is clamped to the valid index range 449 this.#cameraDataChannel.send(data.slice(i, i + MAX_SIZE)); 450 } 451 this.#cameraDataChannel.send(END_MARKER); 452 } 453 454 // Provide a callback to receive control-related comms from the device 455 onControlMessage(cb) { 456 this.#onControlMessage = cb; 457 } 458 459 sendBluetoothMessage(msg) { 460 this.#bluetoothChannel.send(msg); 461 } 462 463 onBluetoothMessage(cb) { 464 this.#onBluetoothMessage = cb; 465 } 466 467 sendLocationMessage(msg) { 468 this.#locationChannel.send(msg); 469 } 470 471 sendSensorsMessage(msg) { 472 this.#sensorsChannel.send(msg); 473 } 474 475 onSensorsMessage(cb) { 476 this.#onSensorsMessage = cb; 477 } 478 479 onLocationMessage(cb) { 480 this.#onLocationMessage = cb; 481 } 482 483 sendKmlLocationsMessage(msg) { 484 this.#kmlLocationsChannel.send(msg); 485 } 486 487 onKmlLocationsMessage(cb) { 488 this.#kmlLocationsChannel = cb; 489 } 490 491 sendGpxLocationsMessage(msg) { 492 this.#gpxLocationsChannel.send(msg); 493 } 494 495 onGpxLocationsMessage(cb) { 496 this.#gpxLocationsChannel = cb; 497 } 498 499 // Provide a callback to receive connectionstatechange states. 500 onConnectionStateChange(cb) { 501 this.#pc.addEventListener( 502 'connectionstatechange', evt => cb(this.#pc.connectionState)); 503 } 504 505 onLightsMessage(cb) { 506 this.#onLightsMessage = cb; 507 } 508} 509 510class Controller { 511 #pc; 512 #serverConnector; 513 #connectedPr = Promise.resolve({}); 514 // A list of callbacks that need to be called when the remote description is 515 // successfully added to the peer connection. 516 #onRemoteDescriptionSetCbs = []; 517 518 constructor(serverConnector) { 519 this.#serverConnector = serverConnector; 520 serverConnector.onDeviceMsg(msg => this.#onDeviceMessage(msg)); 521 } 522 523 #onDeviceMessage(message) { 524 let type = message.type; 525 switch (type) { 526 case 'offer': 527 this.#onOffer({type: 'offer', sdp: message.sdp}); 528 break; 529 case 'answer': 530 this.#onRemoteDescription({type: 'answer', sdp: message.sdp}); 531 break; 532 case 'ice-candidate': 533 this.#onIceCandidate(new RTCIceCandidate({ 534 sdpMid: message.mid, 535 sdpMLineIndex: message.mLineIndex, 536 candidate: message.candidate 537 })); 538 break; 539 case 'error': 540 console.error('Device responded with error message: ', message.error); 541 break; 542 default: 543 console.error('Unrecognized message type from device: ', type); 544 } 545 } 546 547 async #sendClientDescription(desc) { 548 console.debug('sendClientDescription'); 549 return this.#serverConnector.sendToDevice({type: 'answer', sdp: desc.sdp}); 550 } 551 552 async #sendIceCandidate(candidate) { 553 console.debug('sendIceCandidate'); 554 return this.#serverConnector.sendToDevice({type: 'ice-candidate', candidate}); 555 } 556 557 async #onOffer(desc) { 558 try { 559 await this.#onRemoteDescription(desc); 560 let answer = await this.#pc.createAnswer(); 561 console.debug('Answer: ', answer); 562 await this.#pc.setLocalDescription(answer); 563 await this.#sendClientDescription(answer); 564 } catch (e) { 565 console.error('Error processing remote description (offer)', e) 566 throw e; 567 } 568 } 569 570 async #onRemoteDescription(desc) { 571 console.debug(`Remote description (${desc.type}): `, desc); 572 try { 573 await this.#pc.setRemoteDescription(desc); 574 for (const cb of this.#onRemoteDescriptionSetCbs) { 575 cb(); 576 } 577 this.#onRemoteDescriptionSetCbs = []; 578 } catch (e) { 579 console.error(`Error processing remote description (${desc.type})`, e) 580 throw e; 581 } 582 } 583 584 #onIceCandidate(iceCandidate) { 585 console.debug(`Remote ICE Candidate: `, iceCandidate); 586 this.#pc.addIceCandidate(iceCandidate); 587 } 588 589 expectMessagesSoon(durationMilliseconds) { 590 if (this.#serverConnector.expectMessagesSoon) { 591 this.#serverConnector.expectMessagesSoon(durationMilliseconds); 592 } else { 593 console.warn(`Unavailable expectMessagesSoon(). Messages may be slow.`); 594 } 595 } 596 597 // This effectively ensures work that changes connection state doesn't run 598 // concurrently. 599 // Returns a promise that resolves if the connection is successfully 600 // established after the provided work is done. 601 #onReadyToNegotiate(work_cb) { 602 const connectedPr = this.#connectedPr.then(() => { 603 const controller = new AbortController(); 604 const pr = new Promise((resolve, reject) => { 605 // The promise resolves when the connection changes state to 'connected' 606 // or when a remote description is set and the connection was already in 607 // 'connected' state. 608 this.#onRemoteDescriptionSetCbs.push(() => { 609 if (this.#pc.connectionState == 'connected') { 610 resolve({}); 611 } 612 }); 613 this.#pc.addEventListener('connectionstatechange', evt => { 614 let state = this.#pc.connectionState; 615 if (state == 'connected') { 616 resolve(evt); 617 } else if (state == 'failed') { 618 reject(evt); 619 } 620 }, {signal: controller.signal}); 621 }); 622 // Remove the listener once the promise fulfills. 623 pr.finally(() => controller.abort()); 624 work_cb(); 625 // Don't return pr.finally() since that is never rejected. 626 return pr; 627 }); 628 // A failure is also a sign that renegotiation is possible again 629 this.#connectedPr = connectedPr.catch(_ => {}); 630 return connectedPr; 631 } 632 633 async ConnectDevice(pc, infraConfig) { 634 this.#pc = pc; 635 console.debug('ConnectDevice'); 636 // ICE candidates will be generated when we add the offer. Adding it here 637 // instead of in #onOffer because this function is called once per peer 638 // connection, while #onOffer may be called more than once due to 639 // renegotiations. 640 this.#pc.addEventListener('icecandidate', evt => { 641 // The last candidate is null, which indicates the end of ICE gathering. 642 // Firefox's second to last candidate has the candidate property set to 643 // empty, skip that one. 644 if (evt.candidate && evt.candidate.candidate) { 645 this.#sendIceCandidate(evt.candidate); 646 } 647 }); 648 return this.#onReadyToNegotiate(_ => { 649 this.#serverConnector.sendToDevice( 650 {type: 'request-offer', ice_servers: infraConfig.ice_servers}); 651 }); 652 } 653 654 async renegotiateConnection() { 655 return this.#onReadyToNegotiate(async () => { 656 console.debug('Re-negotiating connection'); 657 let offer = await this.#pc.createOffer(); 658 console.debug('Local description (offer): ', offer); 659 await this.#pc.setLocalDescription(offer); 660 await this.#serverConnector.sendToDevice({type: 'offer', sdp: offer.sdp}); 661 }); 662 } 663} 664 665function createPeerConnection(infra_config) { 666 let pc_config = {iceServers: infra_config.ice_servers}; 667 let pc = new RTCPeerConnection(pc_config); 668 669 pc.addEventListener('icecandidate', evt => { 670 console.debug('Local ICE Candidate: ', evt.candidate); 671 }); 672 pc.addEventListener('iceconnectionstatechange', evt => { 673 console.debug(`ICE State Change: ${pc.iceConnectionState}`); 674 }); 675 pc.addEventListener( 676 'connectionstatechange', 677 evt => console.debug( 678 `WebRTC Connection State Change: ${pc.connectionState}`)); 679 return pc; 680} 681 682export async function Connect(deviceId, serverConnector) { 683 let requestRet = await serverConnector.requestDevice(deviceId); 684 let deviceInfo = requestRet.deviceInfo; 685 let infraConfig = requestRet.infraConfig; 686 console.debug('Device available:'); 687 console.debug(deviceInfo); 688 let pc = createPeerConnection(infraConfig); 689 690 let control = new Controller(serverConnector); 691 let deviceConnection = new DeviceConnection(pc, control); 692 deviceConnection.description = deviceInfo; 693 694 return control.ConnectDevice(pc, infraConfig).then(_ => deviceConnection); 695} 696