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 96 #streams; 97 #streamPromiseResolvers; 98 #micSenders = []; 99 #cameraSenders = []; 100 #camera_res_x; 101 #camera_res_y; 102 103 #onAdbMessage; 104 #onControlMessage; 105 #onBluetoothMessage; 106 107 constructor(pc, control) { 108 this.#pc = pc; 109 this.#control = control; 110 this.#cameraDataChannel = pc.createDataChannel('camera-data-channel'); 111 this.#cameraDataChannel.binaryType = 'arraybuffer'; 112 this.#cameraInputQueue = new Array(); 113 var self = this; 114 this.#cameraDataChannel.onbufferedamountlow = () => { 115 if (self.#cameraInputQueue.length > 0) { 116 self.sendCameraData(self.#cameraInputQueue.shift()); 117 } 118 }; 119 this.#inputChannel = createDataChannel(pc, 'input-channel'); 120 this.#adbChannel = createDataChannel(pc, 'adb-channel', (msg) => { 121 if (this.#onAdbMessage) { 122 this.#onAdbMessage(msg.data); 123 } else { 124 console.error('Received unexpected ADB message'); 125 } 126 }); 127 this.#controlChannel = awaitDataChannel(pc, 'device-control', (msg) => { 128 if (this.#onControlMessage) { 129 this.#onControlMessage(msg); 130 } else { 131 console.error('Received unexpected Control message'); 132 } 133 }); 134 this.#bluetoothChannel = 135 createDataChannel(pc, 'bluetooth-channel', (msg) => { 136 if (this.#onBluetoothMessage) { 137 this.#onBluetoothMessage(msg.data); 138 } else { 139 console.error('Received unexpected Bluetooth message'); 140 } 141 }); 142 this.#streams = {}; 143 this.#streamPromiseResolvers = {}; 144 145 pc.addEventListener('track', e => { 146 console.debug('Got remote stream: ', e); 147 for (const stream of e.streams) { 148 this.#streams[stream.id] = stream; 149 if (this.#streamPromiseResolvers[stream.id]) { 150 for (let resolver of this.#streamPromiseResolvers[stream.id]) { 151 resolver(); 152 } 153 delete this.#streamPromiseResolvers[stream.id]; 154 } 155 } 156 }); 157 } 158 159 set description(desc) { 160 this.#description = desc; 161 } 162 163 get description() { 164 return this.#description; 165 } 166 167 get imageCapture() { 168 if (this.#cameraSenders && this.#cameraSenders.length > 0) { 169 let track = this.#cameraSenders[0].track; 170 return new ImageCapture(track); 171 } 172 return undefined; 173 } 174 175 get cameraWidth() { 176 return this.#camera_res_x; 177 } 178 179 get cameraHeight() { 180 return this.#camera_res_y; 181 } 182 183 get cameraEnabled() { 184 return this.#cameraSenders && this.#cameraSenders.length > 0; 185 } 186 187 getStream(stream_id) { 188 return new Promise((resolve, reject) => { 189 if (this.#streams[stream_id]) { 190 resolve(this.#streams[stream_id]); 191 } else { 192 if (!this.#streamPromiseResolvers[stream_id]) { 193 this.#streamPromiseResolvers[stream_id] = []; 194 } 195 this.#streamPromiseResolvers[stream_id].push(resolve); 196 } 197 }); 198 } 199 200 #sendJsonInput(evt) { 201 this.#inputChannel.send(JSON.stringify(evt)); 202 } 203 204 sendMousePosition({x, y, down, display_label}) { 205 this.#sendJsonInput({ 206 type: 'mouse', 207 down: down ? 1 : 0, 208 x, 209 y, 210 display_label, 211 }); 212 } 213 214 // TODO (b/124121375): This should probably be an array of pointer events and 215 // have different properties. 216 sendMultiTouch({idArr, xArr, yArr, down, slotArr, display_label}) { 217 this.#sendJsonInput({ 218 type: 'multi-touch', 219 id: idArr, 220 x: xArr, 221 y: yArr, 222 down: down ? 1 : 0, 223 slot: slotArr, 224 display_label: display_label, 225 }); 226 } 227 228 sendKeyEvent(code, type) { 229 this.#sendJsonInput({type: 'keyboard', keycode: code, event_type: type}); 230 } 231 232 disconnect() { 233 this.#pc.close(); 234 } 235 236 // Sends binary data directly to the in-device adb daemon (skipping the host) 237 sendAdbMessage(msg) { 238 this.#adbChannel.send(msg); 239 } 240 241 // Provide a callback to receive data from the in-device adb daemon 242 onAdbMessage(cb) { 243 this.#onAdbMessage = cb; 244 } 245 246 // Send control commands to the device 247 sendControlMessage(msg) { 248 this.#controlChannel.send(msg); 249 } 250 251 async #useDevice(in_use, senders_arr, device_opt) { 252 // An empty array means no tracks are currently in use 253 if (senders_arr.length > 0 === !!in_use) { 254 console.warn('Device is already ' + (in_use ? '' : 'not ') + 'in use'); 255 return in_use; 256 } 257 let renegotiation_needed = false; 258 if (in_use) { 259 try { 260 let stream = await navigator.mediaDevices.getUserMedia(device_opt); 261 stream.getTracks().forEach(track => { 262 console.info(`Using ${track.kind} device: ${track.label}`); 263 senders_arr.push(this.#pc.addTrack(track)); 264 renegotiation_needed = true; 265 }); 266 } catch (e) { 267 console.error('Failed to add stream to peer connection: ', e); 268 // Don't return yet, if there were errors some tracks may have been 269 // added so the connection should be renegotiated again. 270 } 271 } else { 272 for (const sender of senders_arr) { 273 console.info( 274 `Removing ${sender.track.kind} device: ${sender.track.label}`); 275 let track = sender.track; 276 track.stop(); 277 this.#pc.removeTrack(sender); 278 renegotiation_needed = true; 279 } 280 // Empty the array passed by reference, just assigning [] won't do that. 281 senders_arr.length = 0; 282 } 283 if (renegotiation_needed) { 284 this.#control.renegotiateConnection(); 285 } 286 // Return the new state 287 return senders_arr.length > 0; 288 } 289 290 async useMic(in_use) { 291 return this.#useDevice(in_use, this.#micSenders, {audio: true, video: false}); 292 } 293 294 async useCamera(in_use) { 295 return this.#useDevice(in_use, this.#micSenders, {audio: false, video: true}); 296 } 297 298 sendCameraResolution(stream) { 299 const cameraTracks = stream.getVideoTracks(); 300 if (cameraTracks.length > 0) { 301 const settings = cameraTracks[0].getSettings(); 302 this.#camera_res_x = settings.width; 303 this.#camera_res_y = settings.height; 304 this.sendControlMessage(JSON.stringify({ 305 command: 'camera_settings', 306 width: settings.width, 307 height: settings.height, 308 frame_rate: settings.frameRate, 309 facing: settings.facingMode 310 })); 311 } 312 } 313 314 sendOrQueueCameraData(data) { 315 if (this.#cameraDataChannel.bufferedAmount > 0 || 316 this.#cameraInputQueue.length > 0) { 317 this.#cameraInputQueue.push(data); 318 } else { 319 this.sendCameraData(data); 320 } 321 } 322 323 sendCameraData(data) { 324 const MAX_SIZE = 65535; 325 const END_MARKER = 'EOF'; 326 for (let i = 0; i < data.byteLength; i += MAX_SIZE) { 327 // range is clamped to the valid index range 328 this.#cameraDataChannel.send(data.slice(i, i + MAX_SIZE)); 329 } 330 this.#cameraDataChannel.send(END_MARKER); 331 } 332 333 // Provide a callback to receive control-related comms from the device 334 onControlMessage(cb) { 335 this.#onControlMessage = cb; 336 } 337 338 sendBluetoothMessage(msg) { 339 this.#bluetoothChannel.send(msg); 340 } 341 342 onBluetoothMessage(cb) { 343 this.#onBluetoothMessage = cb; 344 } 345 346 // Provide a callback to receive connectionstatechange states. 347 onConnectionStateChange(cb) { 348 this.#pc.addEventListener( 349 'connectionstatechange', evt => cb(this.#pc.connectionState)); 350 } 351} 352 353class Controller { 354 #pc; 355 #serverConnector; 356 357 constructor(serverConnector) { 358 this.#serverConnector = serverConnector; 359 serverConnector.onDeviceMsg(msg => this.#onDeviceMessage(msg)); 360 } 361 362 #onDeviceMessage(message) { 363 let type = message.type; 364 switch (type) { 365 case 'offer': 366 this.#onOffer({type: 'offer', sdp: message.sdp}); 367 break; 368 case 'answer': 369 this.#onAnswer({type: 'answer', sdp: message.sdp}); 370 break; 371 case 'ice-candidate': 372 this.#onIceCandidate(new RTCIceCandidate({ 373 sdpMid: message.mid, 374 sdpMLineIndex: message.mLineIndex, 375 candidate: message.candidate 376 })); 377 break; 378 case 'error': 379 console.error('Device responded with error message: ', message.error); 380 break; 381 default: 382 console.error('Unrecognized message type from device: ', type); 383 } 384 } 385 386 async #sendClientDescription(desc) { 387 console.debug('sendClientDescription'); 388 return this.#serverConnector.sendToDevice({type: 'answer', sdp: desc.sdp}); 389 } 390 391 async #sendIceCandidate(candidate) { 392 console.debug('sendIceCandidate'); 393 return this.#serverConnector.sendToDevice({type: 'ice-candidate', candidate}); 394 } 395 396 async #onOffer(desc) { 397 console.debug('Remote description (offer): ', desc); 398 try { 399 await this.#pc.setRemoteDescription(desc); 400 let answer = await this.#pc.createAnswer(); 401 console.debug('Answer: ', answer); 402 await this.#pc.setLocalDescription(answer); 403 await this.#sendClientDescription(answer); 404 } catch (e) { 405 console.error('Error processing remote description (offer)', e) 406 throw e; 407 } 408 } 409 410 async #onAnswer(answer) { 411 console.debug('Remote description (answer): ', answer); 412 try { 413 await this.#pc.setRemoteDescription(answer); 414 } catch (e) { 415 console.error('Error processing remote description (answer)', e) 416 throw e; 417 } 418 } 419 420 #onIceCandidate(iceCandidate) { 421 console.debug(`Remote ICE Candidate: `, iceCandidate); 422 this.#pc.addIceCandidate(iceCandidate); 423 } 424 425 ConnectDevice(pc) { 426 this.#pc = pc; 427 console.debug('ConnectDevice'); 428 // ICE candidates will be generated when we add the offer. Adding it here 429 // instead of in _onOffer because this function is called once per peer 430 // connection, while _onOffer may be called more than once due to 431 // renegotiations. 432 this.#pc.addEventListener('icecandidate', evt => { 433 if (evt.candidate) this.#sendIceCandidate(evt.candidate); 434 }); 435 this.#serverConnector.sendToDevice({type: 'request-offer'}); 436 } 437 438 async renegotiateConnection() { 439 console.debug('Re-negotiating connection'); 440 let offer = await this.#pc.createOffer(); 441 console.debug('Local description (offer): ', offer); 442 await this.#pc.setLocalDescription(offer); 443 this.#serverConnector.sendToDevice({type: 'offer', sdp: offer.sdp}); 444 } 445} 446 447function createPeerConnection(infra_config) { 448 let pc_config = {iceServers: []}; 449 for (const stun of infra_config.ice_servers) { 450 pc_config.iceServers.push({urls: 'stun:' + stun}); 451 } 452 let pc = new RTCPeerConnection(pc_config); 453 454 pc.addEventListener('icecandidate', evt => { 455 console.debug('Local ICE Candidate: ', evt.candidate); 456 }); 457 pc.addEventListener('iceconnectionstatechange', evt => { 458 console.debug(`ICE State Change: ${pc.iceConnectionState}`); 459 }); 460 pc.addEventListener( 461 'connectionstatechange', 462 evt => console.debug( 463 `WebRTC Connection State Change: ${pc.connectionState}`)); 464 return pc; 465} 466 467export async function Connect(deviceId, serverConnector) { 468 let requestRet = await serverConnector.requestDevice(deviceId); 469 let deviceInfo = requestRet.deviceInfo; 470 let infraConfig = requestRet.infraConfig; 471 console.debug('Device available:'); 472 console.debug(deviceInfo); 473 let pc_config = {iceServers: []}; 474 if (infraConfig.ice_servers && infraConfig.ice_servers.length > 0) { 475 for (const server of infraConfig.ice_servers) { 476 pc_config.iceServers.push(server); 477 } 478 } 479 let pc = createPeerConnection(infraConfig); 480 481 let control = new Controller(serverConnector); 482 let deviceConnection = new DeviceConnection(pc, control); 483 deviceConnection.description = deviceInfo; 484 485 return new Promise((resolve, reject) => { 486 pc.addEventListener('connectionstatechange', evt => { 487 let state = pc.connectionState; 488 if (state == 'connected') { 489 resolve(deviceConnection); 490 } else if (state == 'failed') { 491 reject(evt); 492 } 493 }); 494 control.ConnectDevice(pc); 495 }); 496} 497