• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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