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