• 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.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