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.log('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.log( 29 'Data channel=' + label + ' state=' + dataChannel.readyState); 30 }; 31 dataChannel.onmessage = onMessage ? onMessage : (msg) => { 32 console.log('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.log('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.log( 62 'Data channel=' + label + ' state=' + dataChannel.readyState); 63 }; 64 dataChannel.onmessage = onMessage ? onMessage : (msg) => { 65 console.log('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 constructor(pc, control, audio_stream) { 86 this._pc = pc; 87 this._control = control; 88 this._audio_stream = audio_stream; 89 // Disable the microphone by default 90 this.useMic(false); 91 this._inputChannel = createDataChannel(pc, 'input-channel'); 92 this._adbChannel = createDataChannel(pc, 'adb-channel', (msg) => { 93 if (this._onAdbMessage) { 94 this._onAdbMessage(msg.data); 95 } else { 96 console.error('Received unexpected ADB message'); 97 } 98 }); 99 this._controlChannel = awaitDataChannel(pc, 'device-control', (msg) => { 100 if (this._onControlMessage) { 101 this._onControlMessage(msg); 102 } else { 103 console.error('Received unexpected Control message'); 104 } 105 }); 106 this._bluetoothChannel = createDataChannel(pc, 'bluetooth-channel', (msg) => { 107 if (this._onBluetoothMessage) { 108 this._onBluetoothMessage(msg.data); 109 } else { 110 console.error('Received unexpected Bluetooth message'); 111 } 112 }); 113 this._streams = {}; 114 this._streamPromiseResolvers = {}; 115 116 pc.addEventListener('track', e => { 117 console.log('Got remote stream: ', e); 118 for (const stream of e.streams) { 119 this._streams[stream.id] = stream; 120 if (this._streamPromiseResolvers[stream.id]) { 121 for (let resolver of this._streamPromiseResolvers[stream.id]) { 122 resolver(); 123 } 124 delete this._streamPromiseResolvers[stream.id]; 125 } 126 } 127 }); 128 } 129 130 set description(desc) { 131 this._description = desc; 132 } 133 134 get description() { 135 return this._description; 136 } 137 138 getStream(stream_id) { 139 return new Promise((resolve, reject) => { 140 if (this._streams[stream_id]) { 141 resolve(this._streams[stream_id]); 142 } else { 143 if (!this._streamPromiseResolvers[stream_id]) { 144 this._streamPromiseResolvers[stream_id] = []; 145 } 146 this._streamPromiseResolvers[stream_id].push(resolve); 147 } 148 }); 149 } 150 151 _sendJsonInput(evt) { 152 this._inputChannel.send(JSON.stringify(evt)); 153 } 154 155 sendMousePosition({x, y, down, display_label}) { 156 this._sendJsonInput({ 157 type: 'mouse', 158 down: down ? 1 : 0, 159 x, 160 y, 161 display_label, 162 }); 163 } 164 165 // TODO (b/124121375): This should probably be an array of pointer events and 166 // have different properties. 167 sendMultiTouch({idArr, xArr, yArr, down, slotArr, display_label}) { 168 this._sendJsonInput({ 169 type: 'multi-touch', 170 id: idArr, 171 x: xArr, 172 y: yArr, 173 down: down ? 1 : 0, 174 slot: slotArr, 175 display_label: display_label, 176 }); 177 } 178 179 sendKeyEvent(code, type) { 180 this._sendJsonInput({type: 'keyboard', keycode: code, event_type: type}); 181 } 182 183 disconnect() { 184 this._pc.close(); 185 } 186 187 // Sends binary data directly to the in-device adb daemon (skipping the host) 188 sendAdbMessage(msg) { 189 this._adbChannel.send(msg); 190 } 191 192 // Provide a callback to receive data from the in-device adb daemon 193 onAdbMessage(cb) { 194 this._onAdbMessage = cb; 195 } 196 197 // Send control commands to the device 198 sendControlMessage(msg) { 199 this._controlChannel.send(msg); 200 } 201 202 useMic(in_use) { 203 if (this._audio_stream) { 204 this._audio_stream.getTracks().forEach(track => track.enabled = in_use); 205 } 206 } 207 208 // Provide a callback to receive control-related comms from the device 209 onControlMessage(cb) { 210 this._onControlMessage = cb; 211 } 212 213 sendBluetoothMessage(msg) { 214 this._bluetoothChannel.send(msg); 215 } 216 217 onBluetoothMessage(cb) { 218 this._onBluetoothMessage = cb; 219 } 220 221 // Provide a callback to receive connectionstatechange states. 222 onConnectionStateChange(cb) { 223 this._pc.addEventListener( 224 'connectionstatechange', 225 evt => cb(this._pc.connectionState)); 226 } 227} 228 229 230class WebRTCControl { 231 constructor({ 232 wsUrl = '', 233 }) { 234 /* 235 * Private attributes: 236 * 237 * _wsPromise: promises the underlying websocket, should resolve when the 238 * socket passes to OPEN state, will be rejecte/replaced by a 239 * rejected promise if an error is detected on the socket. 240 * 241 * _onOffer 242 * _onIceCandidate 243 */ 244 245 this._promiseResolvers = {}; 246 247 this._wsPromise = new Promise((resolve, reject) => { 248 let ws = new WebSocket(wsUrl); 249 ws.onopen = () => { 250 console.info(`Connected to ${wsUrl}`); 251 resolve(ws); 252 }; 253 ws.onerror = evt => { 254 console.error('WebSocket error:', evt); 255 reject(evt); 256 // If the promise was already resolved the previous line has no effect 257 this._wsPromise = Promise.reject(new Error(evt)); 258 }; 259 ws.onmessage = e => { 260 let data = JSON.parse(e.data); 261 this._onWebsocketMessage(data); 262 }; 263 }); 264 } 265 266 _onWebsocketMessage(message) { 267 const type = message.message_type; 268 if (message.error) { 269 console.error(message.error); 270 this._on_connection_failed(message.error); 271 return; 272 } 273 switch (type) { 274 case 'config': 275 this._infra_config = message; 276 break; 277 case 'device_info': 278 if (this._on_device_available) { 279 this._on_device_available(message.device_info); 280 delete this._on_device_available; 281 } else { 282 console.error('Received unsolicited device info'); 283 } 284 break; 285 case 'device_msg': 286 this._onDeviceMessage(message.payload); 287 break; 288 default: 289 console.error('Unrecognized message type from server: ', type); 290 this._on_connection_failed('Unrecognized message type from server: ' + type); 291 console.error(message); 292 } 293 } 294 295 _onDeviceMessage(message) { 296 let type = message.type; 297 switch (type) { 298 case 'offer': 299 if (this._onOffer) { 300 this._onOffer({type: 'offer', sdp: message.sdp}); 301 } else { 302 console.error('Receive offer, but nothing is wating for it'); 303 } 304 break; 305 case 'ice-candidate': 306 if (this._onIceCandidate) { 307 this._onIceCandidate(new RTCIceCandidate({ 308 sdpMid: message.mid, 309 sdpMLineIndex: message.mLineIndex, 310 candidate: message.candidate 311 })); 312 } else { 313 console.error('Received ice candidate but nothing is waiting for it'); 314 } 315 break; 316 default: 317 console.error('Unrecognized message type from device: ', type); 318 } 319 } 320 321 async _wsSendJson(obj) { 322 let ws = await this._wsPromise; 323 return ws.send(JSON.stringify(obj)); 324 } 325 async _sendToDevice(payload) { 326 this._wsSendJson({message_type: 'forward', payload}); 327 } 328 329 onOffer(cb) { 330 this._onOffer = cb; 331 } 332 333 onIceCandidate(cb) { 334 this._onIceCandidate = cb; 335 } 336 337 async requestDevice(device_id) { 338 return new Promise((resolve, reject) => { 339 this._on_device_available = (deviceInfo) => resolve({ 340 deviceInfo, 341 infraConfig: this._infra_config, 342 }); 343 this._on_connection_failed = (error) => reject(error); 344 this._wsSendJson({ 345 message_type: 'connect', 346 device_id, 347 }); 348 }); 349 } 350 351 ConnectDevice() { 352 console.log('ConnectDevice'); 353 this._sendToDevice({type: 'request-offer'}); 354 } 355 356 /** 357 * Sends a remote description to the device. 358 */ 359 async sendClientDescription(desc) { 360 console.log('sendClientDescription'); 361 this._sendToDevice({type: 'answer', sdp: desc.sdp}); 362 } 363 364 /** 365 * Sends an ICE candidate to the device 366 */ 367 async sendIceCandidate(candidate) { 368 this._sendToDevice({type: 'ice-candidate', candidate}); 369 } 370} 371 372function createPeerConnection(infra_config) { 373 let pc_config = {iceServers: []}; 374 for (const stun of infra_config.ice_servers) { 375 pc_config.iceServers.push({urls: 'stun:' + stun}); 376 } 377 let pc = new RTCPeerConnection(pc_config); 378 379 pc.addEventListener('icecandidate', evt => { 380 console.log('Local ICE Candidate: ', evt.candidate); 381 }); 382 pc.addEventListener('iceconnectionstatechange', evt => { 383 console.log(`ICE State Change: ${pc.iceConnectionState}`); 384 }); 385 pc.addEventListener( 386 'connectionstatechange', 387 evt => 388 console.log(`WebRTC Connection State Change: ${pc.connectionState}`)); 389 return pc; 390} 391 392export async function Connect(deviceId, options) { 393 let control = new WebRTCControl(options); 394 let requestRet = await control.requestDevice(deviceId); 395 let deviceInfo = requestRet.deviceInfo; 396 let infraConfig = requestRet.infraConfig; 397 console.log('Device available:'); 398 console.log(deviceInfo); 399 let pc_config = {iceServers: []}; 400 if (infraConfig.ice_servers && infraConfig.ice_servers.length > 0) { 401 for (const server of infraConfig.ice_servers) { 402 pc_config.iceServers.push(server); 403 } 404 } 405 let pc = createPeerConnection(infraConfig, control); 406 407 let audioStream; 408 try { 409 audioStream = 410 await navigator.mediaDevices.getUserMedia({video: false, audio: true}); 411 const audioTracks = audioStream.getAudioTracks(); 412 if (audioTracks.length > 0) { 413 console.log(`Using Audio device: ${audioTracks[0].label}, with ${ 414 audioTracks.length} tracks`); 415 audioTracks.forEach(track => pc.addTrack(track, audioStream)); 416 } 417 } catch (e) { 418 console.error("Failed to open audio device: ", e); 419 } 420 421 let deviceConnection = new DeviceConnection(pc, control, audioStream); 422 deviceConnection.description = deviceInfo; 423 async function acceptOfferAndReplyAnswer(offer) { 424 try { 425 await pc.setRemoteDescription(offer); 426 let answer = await pc.createAnswer(); 427 console.log('Answer: ', answer); 428 await pc.setLocalDescription(answer); 429 await control.sendClientDescription(answer); 430 } catch (e) { 431 console.error('Error establishing WebRTC connection: ', e) 432 throw e; 433 } 434 } 435 control.onOffer(desc => { 436 console.log('Offer: ', desc); 437 acceptOfferAndReplyAnswer(desc); 438 }); 439 control.onIceCandidate(iceCandidate => { 440 console.log(`Remote ICE Candidate: `, iceCandidate); 441 pc.addIceCandidate(iceCandidate); 442 }); 443 444 pc.addEventListener('icecandidate', evt => { 445 if (evt.candidate) control.sendIceCandidate(evt.candidate); 446 }); 447 let connected_promise = new Promise((resolve, reject) => { 448 pc.addEventListener('connectionstatechange', evt => { 449 let state = pc.connectionState; 450 if (state == 'connected') { 451 resolve(deviceConnection); 452 } else if (state == 'failed') { 453 reject(evt); 454 } 455 }); 456 }); 457 control.ConnectDevice(); 458 459 return connected_promise; 460} 461