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