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 47class DeviceConnection { 48 constructor(pc, control) { 49 this._pc = pc; 50 this._control = control; 51 this._inputChannel = createDataChannel(pc, 'input-channel'); 52 this._adbChannel = createDataChannel(pc, 'adb-channel', (msg) => { 53 if (this._onAdbMessage) { 54 this._onAdbMessage(msg.data); 55 } else { 56 console.error('Received unexpected ADB message'); 57 } 58 }); 59 this._streams = {}; 60 this._streamPromiseResolvers = {}; 61 62 pc.addEventListener('track', e => { 63 console.log('Got remote stream: ', e); 64 for (const stream of e.streams) { 65 this._streams[stream.id] = stream; 66 if (this._streamPromiseResolvers[stream.id]) { 67 for (let resolver of this._streamPromiseResolvers[stream.id]) { 68 resolver(); 69 } 70 delete this._streamPromiseResolvers[stream.id]; 71 } 72 } 73 }); 74 } 75 76 set description(desc) { 77 this._description = desc; 78 } 79 80 get description() { 81 return this._description; 82 } 83 84 getStream(stream_id) { 85 return new Promise((resolve, reject) => { 86 if (this._streams[stream_id]) { 87 resolve(this._streams[stream_id]); 88 } else { 89 if (!this._streamPromiseResolvers[stream_id]) { 90 this._streamPromiseResolvers[stream_id] = []; 91 } 92 this._streamPromiseResolvers[stream_id].push(resolve); 93 } 94 }); 95 } 96 97 _sendJsonInput(evt) { 98 this._inputChannel.send(JSON.stringify(evt)); 99 } 100 101 sendMousePosition({x, y, down, display_label}) { 102 this._sendJsonInput({ 103 type: 'mouse', 104 down: down ? 1 : 0, 105 x, 106 y, 107 display_label, 108 }); 109 } 110 111 // TODO (b/124121375): This should probably be an array of pointer events and 112 // have different properties. 113 sendMultiTouch({id, x, y, initialDown, slot, display_label}) { 114 this._sendJsonInput({ 115 type: 'multi-touch', 116 id, 117 x, 118 y, 119 initialDown: initialDown ? 1 : 0, 120 slot, 121 display_label, 122 }); 123 } 124 125 sendKeyEvent(code, type) { 126 this._sendJsonInput({type: 'keyboard', keycode: code, event_type: type}); 127 } 128 129 disconnect() { 130 this._pc.close(); 131 } 132 133 // Sends binary data directly to the in-device adb daemon (skipping the host) 134 sendAdbMessage(msg) { 135 this._adbChannel.send(msg); 136 } 137 138 // Provide a callback to receive data from the in-device adb daemon 139 onAdbMessage(cb) { 140 this._onAdbMessage = cb; 141 } 142} 143 144 145class WebRTCControl { 146 constructor({ 147 wsUrl = '', 148 }) { 149 /* 150 * Private attributes: 151 * 152 * _wsPromise: promises the underlying websocket, should resolve when the 153 * socket passes to OPEN state, will be rejecte/replaced by a 154 * rejected promise if an error is detected on the socket. 155 * 156 * _onOffer 157 * _onIceCandidate 158 */ 159 160 this._promiseResolvers = {}; 161 162 this._wsPromise = new Promise((resolve, reject) => { 163 let ws = new WebSocket(wsUrl); 164 ws.onopen = () => { 165 console.info(`Connected to ${wsUrl}`); 166 resolve(ws); 167 }; 168 ws.onerror = evt => { 169 console.error('WebSocket error:', evt); 170 reject(evt); 171 // If the promise was already resolved the previous line has no effect 172 this._wsPromise = Promise.reject(new Error(evt)); 173 }; 174 ws.onmessage = e => { 175 let data = JSON.parse(e.data); 176 this._onWebsocketMessage(data); 177 }; 178 }); 179 } 180 181 _onWebsocketMessage(message) { 182 const type = message.message_type; 183 if (message.error) { 184 console.error(message.error); 185 return; 186 } 187 switch (type) { 188 case 'config': 189 this._infra_config = message; 190 break; 191 case 'device_info': 192 if (this._on_device_available) { 193 this._on_device_available(message.device_info); 194 delete this._on_device_available; 195 } else { 196 console.error('Received unsolicited device info'); 197 } 198 break; 199 case 'device_msg': 200 this._onDeviceMessage(message.payload); 201 break; 202 default: 203 console.error('Unrecognized message type from server: ', type); 204 console.error(message); 205 } 206 } 207 208 _onDeviceMessage(message) { 209 let type = message.type; 210 switch (type) { 211 case 'offer': 212 if (this._onOffer) { 213 this._onOffer({type: 'offer', sdp: message.sdp}); 214 } else { 215 console.error('Receive offer, but nothing is wating for it'); 216 } 217 break; 218 case 'ice-candidate': 219 if (this._onIceCandidate) { 220 this._onIceCandidate(new RTCIceCandidate({ 221 sdpMid: message.mid, 222 sdpMLineIndex: message.mLineIndex, 223 candidate: message.candidate 224 })); 225 } else { 226 console.error('Received ice candidate but nothing is waiting for it'); 227 } 228 break; 229 default: 230 console.error('Unrecognized message type from device: ', type); 231 } 232 } 233 234 async _wsSendJson(obj) { 235 let ws = await this._wsPromise; 236 return ws.send(JSON.stringify(obj)); 237 } 238 async _sendToDevice(payload) { 239 this._wsSendJson({message_type: 'forward', payload}); 240 } 241 242 onOffer(cb) { 243 this._onOffer = cb; 244 } 245 246 onIceCandidate(cb) { 247 this._onIceCandidate = cb; 248 } 249 250 async requestDevice(device_id) { 251 return new Promise((resolve, reject) => { 252 this._on_device_available = (deviceInfo) => resolve({ 253 deviceInfo, 254 infraConfig: this._infra_config, 255 }); 256 this._wsSendJson({ 257 message_type: 'connect', 258 device_id, 259 }); 260 }); 261 } 262 263 ConnectDevice() { 264 console.log('ConnectDevice'); 265 this._sendToDevice({type: 'request-offer'}); 266 } 267 268 /** 269 * Sends a remote description to the device. 270 */ 271 async sendClientDescription(desc) { 272 console.log('sendClientDescription'); 273 this._sendToDevice({type: 'answer', sdp: desc.sdp}); 274 } 275 276 /** 277 * Sends an ICE candidate to the device 278 */ 279 async sendIceCandidate(candidate) { 280 this._sendToDevice({type: 'ice-candidate', candidate}); 281 } 282} 283 284function createPeerConnection(infra_config) { 285 let pc_config = {iceServers: []}; 286 for (const stun of infra_config.ice_servers) { 287 pc_config.iceServers.push({urls: 'stun:' + stun}); 288 } 289 let pc = new RTCPeerConnection(pc_config); 290 291 pc.addEventListener('icecandidate', evt => { 292 console.log('Local ICE Candidate: ', evt.candidate); 293 }); 294 pc.addEventListener('iceconnectionstatechange', evt => { 295 console.log(`ICE State Change: ${pc.iceConnectionState}`); 296 }); 297 pc.addEventListener( 298 'connectionstatechange', 299 evt => 300 console.log(`WebRTC Connection State Change: ${pc.connectionState}`)); 301 return pc; 302} 303 304export async function Connect(deviceId, options) { 305 let control = new WebRTCControl(options); 306 let requestRet = await control.requestDevice(deviceId); 307 let deviceInfo = requestRet.deviceInfo; 308 let infraConfig = requestRet.infraConfig; 309 console.log('Device available:'); 310 console.log(deviceInfo); 311 let pc_config = {iceServers: []}; 312 if (infraConfig.ice_servers && infraConfig.ice_servers.length > 0) { 313 for (const server of infraConfig.ice_servers) { 314 pc_config.iceServers.push(server); 315 } 316 } 317 let pc = createPeerConnection(infraConfig, control); 318 let deviceConnection = new DeviceConnection(pc, control); 319 deviceConnection.description = deviceInfo; 320 async function acceptOfferAndReplyAnswer(offer) { 321 try { 322 await pc.setRemoteDescription(offer); 323 let answer = await pc.createAnswer(); 324 await pc.setLocalDescription(answer); 325 await control.sendClientDescription(answer); 326 } catch (e) { 327 console.error('Error establishing WebRTC connection: ', e) 328 throw e; 329 } 330 } 331 control.onOffer(desc => { 332 console.log('Offer: ', desc); 333 acceptOfferAndReplyAnswer(desc); 334 }); 335 control.onIceCandidate(iceCandidate => { 336 console.log(`Remote ICE Candidate: `, iceCandidate); 337 pc.addIceCandidate(iceCandidate); 338 }); 339 340 pc.addEventListener('icecandidate', evt => { 341 if (evt.candidate) control.sendIceCandidate(evt.candidate); 342 }); 343 let connected_promise = new Promise((resolve, reject) => { 344 pc.addEventListener('connectionstatechange', evt => { 345 let state = pc.connectionState; 346 if (state == 'connected') { 347 resolve(deviceConnection); 348 } else if (state == 'failed') { 349 reject(evt); 350 } 351 }); 352 }); 353 control.ConnectDevice(); 354 355 return connected_promise; 356} 357