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 sendMousePosition({x, y, down, display_label}) { 280 this.#sendJsonInput({ 281 type: 'mouse', 282 down: down ? 1 : 0, 283 x, 284 y, 285 display_label, 286 }); 287 } 288 289 // TODO (b/124121375): This should probably be an array of pointer events and 290 // have different properties. 291 sendMultiTouch({idArr, xArr, yArr, down, device_label}) { 292 let events = { 293 type: 'multi-touch', 294 id: idArr, 295 x: xArr, 296 y: yArr, 297 down: down ? 1 : 0, 298 device_label: device_label, 299 }; 300 this.#sendJsonInput(events); 301 } 302 303 sendKeyEvent(code, type) { 304 this.#sendJsonInput({type: 'keyboard', keycode: code, event_type: type}); 305 } 306 307 sendWheelEvent(pixels) { 308 this.#sendJsonInput({ 309 type: 'wheel', 310 // convert double to int, forcing a base 10 conversion. pixels can be fractional. 311 pixels: parseInt(pixels, 10), 312 }); 313 } 314 315 disconnect() { 316 this.#pc.close(); 317 } 318 319 // Sends binary data directly to the in-device adb daemon (skipping the host) 320 sendAdbMessage(msg) { 321 this.#adbChannel.send(msg); 322 } 323 324 // Provide a callback to receive data from the in-device adb daemon 325 onAdbMessage(cb) { 326 this.#onAdbMessage = cb; 327 } 328 329 // Send control commands to the device 330 sendControlMessage(msg) { 331 this.#controlChannel.send(msg); 332 } 333 334 async #useDevice( 335 in_use, senders_arr, device_opt, requestedFn = () => {in_use}, enabledFn = (stream) => {}) { 336 // An empty array means no tracks are currently in use 337 if (senders_arr.length > 0 === !!in_use) { 338 return in_use; 339 } 340 let renegotiation_needed = false; 341 if (in_use) { 342 try { 343 let stream = await navigator.mediaDevices.getUserMedia(device_opt); 344 // The user may have changed their mind by the time we obtain the 345 // stream, check again 346 if (!!in_use != requestedFn()) { 347 return requestedFn(); 348 } 349 enabledFn(stream); 350 stream.getTracks().forEach(track => { 351 console.info(`Using ${track.kind} device: ${track.label}`); 352 senders_arr.push(this.#pc.addTrack(track)); 353 renegotiation_needed = true; 354 }); 355 } catch (e) { 356 console.error('Failed to add stream to peer connection: ', e); 357 // Don't return yet, if there were errors some tracks may have been 358 // added so the connection should be renegotiated again. 359 } 360 } else { 361 for (const sender of senders_arr) { 362 console.info( 363 `Removing ${sender.track.kind} device: ${sender.track.label}`); 364 let track = sender.track; 365 track.stop(); 366 this.#pc.removeTrack(sender); 367 renegotiation_needed = true; 368 } 369 // Empty the array passed by reference, just assigning [] won't do that. 370 senders_arr.length = 0; 371 } 372 if (renegotiation_needed) { 373 await this.#control.renegotiateConnection(); 374 } 375 // Return the new state 376 return senders_arr.length > 0; 377 } 378 379 async useMic(in_use) { 380 if (this.#micRequested == !!in_use) { 381 return in_use; 382 } 383 this.#micRequested = !!in_use; 384 return this.#useDevice( 385 in_use, this.#micSenders, {audio: true, video: false}, 386 () => this.#micRequested); 387 } 388 389 async useCamera(in_use) { 390 if (this.#cameraRequested == !!in_use) { 391 return in_use; 392 } 393 this.#cameraRequested = !!in_use; 394 return this.#useDevice( 395 in_use, this.#micSenders, {audio: false, video: true}, 396 () => this.#cameraRequested, 397 (stream) => this.sendCameraResolution(stream)); 398 } 399 400 sendCameraResolution(stream) { 401 const cameraTracks = stream.getVideoTracks(); 402 if (cameraTracks.length > 0) { 403 const settings = cameraTracks[0].getSettings(); 404 this.#camera_res_x = settings.width; 405 this.#camera_res_y = settings.height; 406 this.sendControlMessage(JSON.stringify({ 407 command: 'camera_settings', 408 width: settings.width, 409 height: settings.height, 410 frame_rate: settings.frameRate, 411 facing: settings.facingMode 412 })); 413 } 414 } 415 416 sendOrQueueCameraData(data) { 417 if (this.#cameraDataChannel.bufferedAmount > 0 || 418 this.#cameraInputQueue.length > 0) { 419 this.#cameraInputQueue.push(data); 420 } else { 421 this.sendCameraData(data); 422 } 423 } 424 425 sendCameraData(data) { 426 const MAX_SIZE = 65535; 427 const END_MARKER = 'EOF'; 428 for (let i = 0; i < data.byteLength; i += MAX_SIZE) { 429 // range is clamped to the valid index range 430 this.#cameraDataChannel.send(data.slice(i, i + MAX_SIZE)); 431 } 432 this.#cameraDataChannel.send(END_MARKER); 433 } 434 435 // Provide a callback to receive control-related comms from the device 436 onControlMessage(cb) { 437 this.#onControlMessage = cb; 438 } 439 440 sendBluetoothMessage(msg) { 441 this.#bluetoothChannel.send(msg); 442 } 443 444 onBluetoothMessage(cb) { 445 this.#onBluetoothMessage = cb; 446 } 447 448 sendLocationMessage(msg) { 449 this.#locationChannel.send(msg); 450 } 451 452 sendSensorsMessage(msg) { 453 this.#sensorsChannel.send(msg); 454 } 455 456 onSensorsMessage(cb) { 457 this.#onSensorsMessage = cb; 458 } 459 460 onLocationMessage(cb) { 461 this.#onLocationMessage = cb; 462 } 463 464 sendKmlLocationsMessage(msg) { 465 this.#kmlLocationsChannel.send(msg); 466 } 467 468 onKmlLocationsMessage(cb) { 469 this.#kmlLocationsChannel = cb; 470 } 471 472 sendGpxLocationsMessage(msg) { 473 this.#gpxLocationsChannel.send(msg); 474 } 475 476 onGpxLocationsMessage(cb) { 477 this.#gpxLocationsChannel = cb; 478 } 479 480 // Provide a callback to receive connectionstatechange states. 481 onConnectionStateChange(cb) { 482 this.#pc.addEventListener( 483 'connectionstatechange', evt => cb(this.#pc.connectionState)); 484 } 485 486 onLightsMessage(cb) { 487 this.#onLightsMessage = cb; 488 } 489} 490 491class Controller { 492 #pc; 493 #serverConnector; 494 #connectedPr = Promise.resolve({}); 495 // A list of callbacks that need to be called when the remote description is 496 // successfully added to the peer connection. 497 #onRemoteDescriptionSetCbs = []; 498 499 constructor(serverConnector) { 500 this.#serverConnector = serverConnector; 501 serverConnector.onDeviceMsg(msg => this.#onDeviceMessage(msg)); 502 } 503 504 #onDeviceMessage(message) { 505 let type = message.type; 506 switch (type) { 507 case 'offer': 508 this.#onOffer({type: 'offer', sdp: message.sdp}); 509 break; 510 case 'answer': 511 this.#onRemoteDescription({type: 'answer', sdp: message.sdp}); 512 break; 513 case 'ice-candidate': 514 this.#onIceCandidate(new RTCIceCandidate({ 515 sdpMid: message.mid, 516 sdpMLineIndex: message.mLineIndex, 517 candidate: message.candidate 518 })); 519 break; 520 case 'error': 521 console.error('Device responded with error message: ', message.error); 522 break; 523 default: 524 console.error('Unrecognized message type from device: ', type); 525 } 526 } 527 528 async #sendClientDescription(desc) { 529 console.debug('sendClientDescription'); 530 return this.#serverConnector.sendToDevice({type: 'answer', sdp: desc.sdp}); 531 } 532 533 async #sendIceCandidate(candidate) { 534 console.debug('sendIceCandidate'); 535 return this.#serverConnector.sendToDevice({type: 'ice-candidate', candidate}); 536 } 537 538 async #onOffer(desc) { 539 try { 540 await this.#onRemoteDescription(desc); 541 let answer = await this.#pc.createAnswer(); 542 console.debug('Answer: ', answer); 543 await this.#pc.setLocalDescription(answer); 544 await this.#sendClientDescription(answer); 545 } catch (e) { 546 console.error('Error processing remote description (offer)', e) 547 throw e; 548 } 549 } 550 551 async #onRemoteDescription(desc) { 552 console.debug(`Remote description (${desc.type}): `, desc); 553 try { 554 await this.#pc.setRemoteDescription(desc); 555 for (const cb of this.#onRemoteDescriptionSetCbs) { 556 cb(); 557 } 558 this.#onRemoteDescriptionSetCbs = []; 559 } catch (e) { 560 console.error(`Error processing remote description (${desc.type})`, e) 561 throw e; 562 } 563 } 564 565 #onIceCandidate(iceCandidate) { 566 console.debug(`Remote ICE Candidate: `, iceCandidate); 567 this.#pc.addIceCandidate(iceCandidate); 568 } 569 570 expectMessagesSoon(durationMilliseconds) { 571 if (this.#serverConnector.expectMessagesSoon) { 572 this.#serverConnector.expectMessagesSoon(durationMilliseconds); 573 } else { 574 console.warn(`Unavailable expectMessagesSoon(). Messages may be slow.`); 575 } 576 } 577 578 // This effectively ensures work that changes connection state doesn't run 579 // concurrently. 580 // Returns a promise that resolves if the connection is successfully 581 // established after the provided work is done. 582 #onReadyToNegotiate(work_cb) { 583 const connectedPr = this.#connectedPr.then(() => { 584 const controller = new AbortController(); 585 const pr = new Promise((resolve, reject) => { 586 // The promise resolves when the connection changes state to 'connected' 587 // or when a remote description is set and the connection was already in 588 // 'connected' state. 589 this.#onRemoteDescriptionSetCbs.push(() => { 590 if (this.#pc.connectionState == 'connected') { 591 resolve({}); 592 } 593 }); 594 this.#pc.addEventListener('connectionstatechange', evt => { 595 let state = this.#pc.connectionState; 596 if (state == 'connected') { 597 resolve(evt); 598 } else if (state == 'failed') { 599 reject(evt); 600 } 601 }, {signal: controller.signal}); 602 }); 603 // Remove the listener once the promise fulfills. 604 pr.finally(() => controller.abort()); 605 work_cb(); 606 // Don't return pr.finally() since that is never rejected. 607 return pr; 608 }); 609 // A failure is also a sign that renegotiation is possible again 610 this.#connectedPr = connectedPr.catch(_ => {}); 611 return connectedPr; 612 } 613 614 async ConnectDevice(pc, infraConfig) { 615 this.#pc = pc; 616 console.debug('ConnectDevice'); 617 // ICE candidates will be generated when we add the offer. Adding it here 618 // instead of in #onOffer because this function is called once per peer 619 // connection, while #onOffer may be called more than once due to 620 // renegotiations. 621 this.#pc.addEventListener('icecandidate', evt => { 622 // The last candidate is null, which indicates the end of ICE gathering. 623 // Firefox's second to last candidate has the candidate property set to 624 // empty, skip that one. 625 if (evt.candidate && evt.candidate.candidate) { 626 this.#sendIceCandidate(evt.candidate); 627 } 628 }); 629 return this.#onReadyToNegotiate(_ => { 630 this.#serverConnector.sendToDevice( 631 {type: 'request-offer', ice_servers: infraConfig.ice_servers}); 632 }); 633 } 634 635 async renegotiateConnection() { 636 return this.#onReadyToNegotiate(async () => { 637 console.debug('Re-negotiating connection'); 638 let offer = await this.#pc.createOffer(); 639 console.debug('Local description (offer): ', offer); 640 await this.#pc.setLocalDescription(offer); 641 await this.#serverConnector.sendToDevice({type: 'offer', sdp: offer.sdp}); 642 }); 643 } 644} 645 646function createPeerConnection(infra_config) { 647 let pc_config = {iceServers: infra_config.ice_servers}; 648 let pc = new RTCPeerConnection(pc_config); 649 650 pc.addEventListener('icecandidate', evt => { 651 console.debug('Local ICE Candidate: ', evt.candidate); 652 }); 653 pc.addEventListener('iceconnectionstatechange', evt => { 654 console.debug(`ICE State Change: ${pc.iceConnectionState}`); 655 }); 656 pc.addEventListener( 657 'connectionstatechange', 658 evt => console.debug( 659 `WebRTC Connection State Change: ${pc.connectionState}`)); 660 return pc; 661} 662 663export async function Connect(deviceId, serverConnector) { 664 let requestRet = await serverConnector.requestDevice(deviceId); 665 let deviceInfo = requestRet.deviceInfo; 666 let infraConfig = requestRet.infraConfig; 667 console.debug('Device available:'); 668 console.debug(deviceInfo); 669 let pc = createPeerConnection(infraConfig); 670 671 let control = new Controller(serverConnector); 672 let deviceConnection = new DeviceConnection(pc, control); 673 deviceConnection.description = deviceInfo; 674 675 return control.ConnectDevice(pc, infraConfig).then(_ => deviceConnection); 676} 677