• 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  #locationChannel;
96  #kmlLocationsChannel;
97  #gpxLocationsChannel;
98
99  #streams;
100  #streamPromiseResolvers;
101  #streamChangeCallback;
102  #micSenders = [];
103  #cameraSenders = [];
104  #camera_res_x;
105  #camera_res_y;
106
107  #onAdbMessage;
108  #onControlMessage;
109  #onBluetoothMessage;
110  #onLocationMessage;
111  #onKmlLocationsMessage;
112  #onGpxLocationsMessage;
113
114  #micRequested = false;
115  #cameraRequested = false;
116
117  constructor(pc, control) {
118    this.#pc = pc;
119    this.#control = control;
120    this.#cameraDataChannel = pc.createDataChannel('camera-data-channel');
121    this.#cameraDataChannel.binaryType = 'arraybuffer';
122    this.#cameraInputQueue = new Array();
123    var self = this;
124    this.#cameraDataChannel.onbufferedamountlow = () => {
125      if (self.#cameraInputQueue.length > 0) {
126        self.sendCameraData(self.#cameraInputQueue.shift());
127      }
128    };
129    this.#inputChannel = createDataChannel(pc, 'input-channel');
130    this.#adbChannel = createDataChannel(pc, 'adb-channel', (msg) => {
131      if (!this.#onAdbMessage) {
132        console.error('Received unexpected ADB message');
133        return;
134      }
135      this.#onAdbMessage(msg.data);
136    });
137    this.#controlChannel = awaitDataChannel(pc, 'device-control', (msg) => {
138      if (!this.#onControlMessage) {
139        console.error('Received unexpected Control message');
140        return;
141      }
142      this.#onControlMessage(msg);
143    });
144    this.#bluetoothChannel =
145        createDataChannel(pc, 'bluetooth-channel', (msg) => {
146          if (!this.#onBluetoothMessage) {
147            console.error('Received unexpected Bluetooth message');
148            return;
149          }
150          this.#onBluetoothMessage(msg.data);
151        });
152    this.#locationChannel =
153        createDataChannel(pc, 'location-channel', (msg) => {
154          if (!this.#onLocationMessage) {
155            console.error('Received unexpected Location message');
156            return;
157          }
158          this.#onLocationMessage(msg.data);
159        });
160
161    this.#kmlLocationsChannel =
162        createDataChannel(pc, 'kml-locations-channel', (msg) => {
163          if (!this.#onKmlLocationsMessage) {
164            console.error('Received unexpected KML Locations message');
165            return;
166          }
167          this.#onKmlLocationsMessage(msg.data);
168        });
169
170    this.#gpxLocationsChannel =
171        createDataChannel(pc, 'gpx-locations-channel', (msg) => {
172          if (!this.#onGpxLocationsMessage) {
173            console.error('Received unexpected KML Locations message');
174            return;
175          }
176          this.#onGpxLocationsMessage(msg.data);
177        });
178    this.#streams = {};
179    this.#streamPromiseResolvers = {};
180
181    pc.addEventListener('track', e => {
182      console.debug('Got remote stream: ', e);
183      for (const stream of e.streams) {
184        this.#streams[stream.id] = stream;
185        if (this.#streamPromiseResolvers[stream.id]) {
186          for (let resolver of this.#streamPromiseResolvers[stream.id]) {
187            resolver();
188          }
189          delete this.#streamPromiseResolvers[stream.id];
190        }
191
192        if (this.#streamChangeCallback) {
193          this.#streamChangeCallback(stream);
194        }
195      }
196    });
197  }
198
199  set description(desc) {
200    this.#description = desc;
201  }
202
203  get description() {
204    return this.#description;
205  }
206
207  get imageCapture() {
208    if (this.#cameraSenders && this.#cameraSenders.length > 0) {
209      let track = this.#cameraSenders[0].track;
210      return new ImageCapture(track);
211    }
212    return undefined;
213  }
214
215  get cameraWidth() {
216    return this.#camera_res_x;
217  }
218
219  get cameraHeight() {
220    return this.#camera_res_y;
221  }
222
223  get cameraEnabled() {
224    return this.#cameraSenders && this.#cameraSenders.length > 0;
225  }
226
227  getStream(stream_id) {
228    if (stream_id in this.#streams) {
229      return this.#streams[stream_id];
230    }
231    return null;
232  }
233
234  onStream(stream_id) {
235    return new Promise((resolve, reject) => {
236      if (this.#streams[stream_id]) {
237        resolve(this.#streams[stream_id]);
238      } else {
239        if (!this.#streamPromiseResolvers[stream_id]) {
240          this.#streamPromiseResolvers[stream_id] = [];
241        }
242        this.#streamPromiseResolvers[stream_id].push(resolve);
243      }
244    });
245  }
246
247  onStreamChange(cb) {
248    this.#streamChangeCallback = cb;
249  }
250
251  #sendJsonInput(evt) {
252    this.#inputChannel.send(JSON.stringify(evt));
253  }
254
255  sendMousePosition({x, y, down, display_label}) {
256    this.#sendJsonInput({
257      type: 'mouse',
258      down: down ? 1 : 0,
259      x,
260      y,
261      display_label,
262    });
263  }
264
265  // TODO (b/124121375): This should probably be an array of pointer events and
266  // have different properties.
267  sendMultiTouch({idArr, xArr, yArr, down, slotArr, display_label}) {
268    this.#sendJsonInput({
269      type: 'multi-touch',
270      id: idArr,
271      x: xArr,
272      y: yArr,
273      down: down ? 1 : 0,
274      slot: slotArr,
275      display_label: display_label,
276    });
277  }
278
279  sendKeyEvent(code, type) {
280    this.#sendJsonInput({type: 'keyboard', keycode: code, event_type: type});
281  }
282
283  disconnect() {
284    this.#pc.close();
285  }
286
287  // Sends binary data directly to the in-device adb daemon (skipping the host)
288  sendAdbMessage(msg) {
289    this.#adbChannel.send(msg);
290  }
291
292  // Provide a callback to receive data from the in-device adb daemon
293  onAdbMessage(cb) {
294    this.#onAdbMessage = cb;
295  }
296
297  // Send control commands to the device
298  sendControlMessage(msg) {
299    this.#controlChannel.send(msg);
300  }
301
302  async #useDevice(
303      in_use, senders_arr, device_opt, requestedFn = () => {in_use}, enabledFn = (stream) => {}) {
304    // An empty array means no tracks are currently in use
305    if (senders_arr.length > 0 === !!in_use) {
306      return in_use;
307    }
308    let renegotiation_needed = false;
309    if (in_use) {
310      try {
311        let stream = await navigator.mediaDevices.getUserMedia(device_opt);
312        // The user may have changed their mind by the time we obtain the
313        // stream, check again
314        if (!!in_use != requestedFn()) {
315          return requestedFn();
316        }
317        enabledFn(stream);
318        stream.getTracks().forEach(track => {
319          console.info(`Using ${track.kind} device: ${track.label}`);
320          senders_arr.push(this.#pc.addTrack(track));
321          renegotiation_needed = true;
322        });
323      } catch (e) {
324        console.error('Failed to add stream to peer connection: ', e);
325        // Don't return yet, if there were errors some tracks may have been
326        // added so the connection should be renegotiated again.
327      }
328    } else {
329      for (const sender of senders_arr) {
330        console.info(
331            `Removing ${sender.track.kind} device: ${sender.track.label}`);
332        let track = sender.track;
333        track.stop();
334        this.#pc.removeTrack(sender);
335        renegotiation_needed = true;
336      }
337      // Empty the array passed by reference, just assigning [] won't do that.
338      senders_arr.length = 0;
339    }
340    if (renegotiation_needed) {
341      await this.#control.renegotiateConnection();
342    }
343    // Return the new state
344    return senders_arr.length > 0;
345  }
346
347  async useMic(in_use) {
348    if (this.#micRequested == !!in_use) {
349      return in_use;
350    }
351    this.#micRequested = !!in_use;
352    return this.#useDevice(
353        in_use, this.#micSenders, {audio: true, video: false},
354        () => this.#micRequested);
355  }
356
357  async useCamera(in_use) {
358    if (this.#cameraRequested == !!in_use) {
359      return in_use;
360    }
361    this.#cameraRequested = !!in_use;
362    return this.#useDevice(
363        in_use, this.#micSenders, {audio: false, video: true},
364        () => this.#cameraRequested,
365        (stream) => this.sendCameraResolution(stream));
366  }
367
368  sendCameraResolution(stream) {
369    const cameraTracks = stream.getVideoTracks();
370    if (cameraTracks.length > 0) {
371      const settings = cameraTracks[0].getSettings();
372      this.#camera_res_x = settings.width;
373      this.#camera_res_y = settings.height;
374      this.sendControlMessage(JSON.stringify({
375        command: 'camera_settings',
376        width: settings.width,
377        height: settings.height,
378        frame_rate: settings.frameRate,
379        facing: settings.facingMode
380      }));
381    }
382  }
383
384  sendOrQueueCameraData(data) {
385    if (this.#cameraDataChannel.bufferedAmount > 0 ||
386        this.#cameraInputQueue.length > 0) {
387      this.#cameraInputQueue.push(data);
388    } else {
389      this.sendCameraData(data);
390    }
391  }
392
393  sendCameraData(data) {
394    const MAX_SIZE = 65535;
395    const END_MARKER = 'EOF';
396    for (let i = 0; i < data.byteLength; i += MAX_SIZE) {
397      // range is clamped to the valid index range
398      this.#cameraDataChannel.send(data.slice(i, i + MAX_SIZE));
399    }
400    this.#cameraDataChannel.send(END_MARKER);
401  }
402
403  // Provide a callback to receive control-related comms from the device
404  onControlMessage(cb) {
405    this.#onControlMessage = cb;
406  }
407
408  sendBluetoothMessage(msg) {
409    this.#bluetoothChannel.send(msg);
410  }
411
412  onBluetoothMessage(cb) {
413    this.#onBluetoothMessage = cb;
414  }
415
416  sendLocationMessage(msg) {
417    this.#locationChannel.send(msg);
418  }
419
420  onLocationMessage(cb) {
421    this.#onLocationMessage = cb;
422  }
423
424  sendKmlLocationsMessage(msg) {
425    this.#kmlLocationsChannel.send(msg);
426  }
427
428  onKmlLocationsMessage(cb) {
429    this.#kmlLocationsChannel = cb;
430  }
431
432  sendGpxLocationsMessage(msg) {
433    this.#gpxLocationsChannel.send(msg);
434  }
435
436  onGpxLocationsMessage(cb) {
437    this.#gpxLocationsChannel = cb;
438  }
439
440  // Provide a callback to receive connectionstatechange states.
441  onConnectionStateChange(cb) {
442    this.#pc.addEventListener(
443        'connectionstatechange', evt => cb(this.#pc.connectionState));
444  }
445}
446
447class Controller {
448  #pc;
449  #serverConnector;
450  #connectedPr = Promise.resolve({});
451  // A list of callbacks that need to be called when the remote description is
452  // successfully added to the peer connection.
453  #onRemoteDescriptionSetCbs = [];
454
455  constructor(serverConnector) {
456    this.#serverConnector = serverConnector;
457    serverConnector.onDeviceMsg(msg => this.#onDeviceMessage(msg));
458  }
459
460  #onDeviceMessage(message) {
461    let type = message.type;
462    switch (type) {
463      case 'offer':
464        this.#onOffer({type: 'offer', sdp: message.sdp});
465        break;
466      case 'answer':
467        this.#onRemoteDescription({type: 'answer', sdp: message.sdp});
468        break;
469      case 'ice-candidate':
470          this.#onIceCandidate(new RTCIceCandidate({
471            sdpMid: message.mid,
472            sdpMLineIndex: message.mLineIndex,
473            candidate: message.candidate
474          }));
475        break;
476      case 'error':
477        console.error('Device responded with error message: ', message.error);
478        break;
479      default:
480        console.error('Unrecognized message type from device: ', type);
481    }
482  }
483
484  async #sendClientDescription(desc) {
485    console.debug('sendClientDescription');
486    return this.#serverConnector.sendToDevice({type: 'answer', sdp: desc.sdp});
487  }
488
489  async #sendIceCandidate(candidate) {
490    console.debug('sendIceCandidate');
491    return this.#serverConnector.sendToDevice({type: 'ice-candidate', candidate});
492  }
493
494  async #onOffer(desc) {
495    try {
496      await this.#onRemoteDescription(desc);
497      let answer = await this.#pc.createAnswer();
498      console.debug('Answer: ', answer);
499      await this.#pc.setLocalDescription(answer);
500      await this.#sendClientDescription(answer);
501    } catch (e) {
502      console.error('Error processing remote description (offer)', e)
503      throw e;
504    }
505  }
506
507  async #onRemoteDescription(desc) {
508    console.debug(`Remote description (${desc.type}): `, desc);
509    try {
510      await this.#pc.setRemoteDescription(desc);
511      for (const cb of this.#onRemoteDescriptionSetCbs) {
512        cb();
513      }
514      this.#onRemoteDescriptionSetCbs = [];
515    } catch (e) {
516      console.error(`Error processing remote description (${desc.type})`, e)
517      throw e;
518    }
519  }
520
521  #onIceCandidate(iceCandidate) {
522    console.debug(`Remote ICE Candidate: `, iceCandidate);
523    this.#pc.addIceCandidate(iceCandidate);
524  }
525
526  // This effectively ensures work that changes connection state doesn't run
527  // concurrently.
528  // Returns a promise that resolves if the connection is successfully
529  // established after the provided work is done.
530  #onReadyToNegotiate(work_cb) {
531    const connectedPr = this.#connectedPr.then(() => {
532      const controller = new AbortController();
533      const pr = new Promise((resolve, reject) => {
534        // The promise resolves when the connection changes state to 'connected'
535        // or when a remote description is set and the connection was already in
536        // 'connected' state.
537        this.#onRemoteDescriptionSetCbs.push(() => {
538          if (this.#pc.connectionState == 'connected') {
539            resolve({});
540          }
541        });
542        this.#pc.addEventListener('connectionstatechange', evt => {
543          let state = this.#pc.connectionState;
544          if (state == 'connected') {
545            resolve(evt);
546          } else if (state == 'failed') {
547            reject(evt);
548          }
549        }, {signal: controller.signal});
550      });
551      // Remove the listener once the promise fulfills.
552      pr.finally(() => controller.abort());
553      work_cb();
554      // Don't return pr.finally() since that is never rejected.
555      return pr;
556    });
557    // A failure is also a sign that renegotiation is possible again
558    this.#connectedPr = connectedPr.catch(_ => {});
559    return connectedPr;
560  }
561
562  async ConnectDevice(pc, infraConfig) {
563    this.#pc = pc;
564    console.debug('ConnectDevice');
565    // ICE candidates will be generated when we add the offer. Adding it here
566    // instead of in #onOffer because this function is called once per peer
567    // connection, while #onOffer may be called more than once due to
568    // renegotiations.
569    this.#pc.addEventListener('icecandidate', evt => {
570      if (evt.candidate) this.#sendIceCandidate(evt.candidate);
571    });
572    return this.#onReadyToNegotiate(_ => {
573      this.#serverConnector.sendToDevice(
574        {type: 'request-offer', ice_servers: infraConfig.ice_servers});
575    });
576  }
577
578  async renegotiateConnection() {
579    return this.#onReadyToNegotiate(async () => {
580      console.debug('Re-negotiating connection');
581      let offer = await this.#pc.createOffer();
582      console.debug('Local description (offer): ', offer);
583      await this.#pc.setLocalDescription(offer);
584      await this.#serverConnector.sendToDevice({type: 'offer', sdp: offer.sdp});
585    });
586  }
587}
588
589function createPeerConnection(infra_config) {
590  let pc_config = {iceServers: infra_config.ice_servers};
591  let pc = new RTCPeerConnection(pc_config);
592
593  pc.addEventListener('icecandidate', evt => {
594    console.debug('Local ICE Candidate: ', evt.candidate);
595  });
596  pc.addEventListener('iceconnectionstatechange', evt => {
597    console.debug(`ICE State Change: ${pc.iceConnectionState}`);
598  });
599  pc.addEventListener(
600      'connectionstatechange',
601      evt => console.debug(
602          `WebRTC Connection State Change: ${pc.connectionState}`));
603  return pc;
604}
605
606export async function Connect(deviceId, serverConnector) {
607  let requestRet = await serverConnector.requestDevice(deviceId);
608  let deviceInfo = requestRet.deviceInfo;
609  let infraConfig = requestRet.infraConfig;
610  console.debug('Device available:');
611  console.debug(deviceInfo);
612  let pc = createPeerConnection(infraConfig);
613
614  let control = new Controller(serverConnector);
615  let deviceConnection = new DeviceConnection(pc, control);
616  deviceConnection.description = deviceInfo;
617
618  return control.ConnectDevice(pc, infraConfig).then(_ => deviceConnection);
619}
620