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  sendMousePosition({x, y, down, display_label}) {
280    this.#sendJsonInput({
281      type: 'mouse',
282      down: down ? 1 : 0,
283      x,
284      y,
285      display_label,
286    });
287  }
288
289  // TODO (b/124121375): This should probably be an array of pointer events and
290  // have different properties.
291  sendMultiTouch({idArr, xArr, yArr, down, device_label}) {
292    let events = {
293            type: 'multi-touch',
294            id: idArr,
295            x: xArr,
296            y: yArr,
297            down: down ? 1 : 0,
298            device_label: device_label,
299          };
300    this.#sendJsonInput(events);
301  }
302
303  sendKeyEvent(code, type) {
304    this.#sendJsonInput({type: 'keyboard', keycode: code, event_type: type});
305  }
306
307  sendWheelEvent(pixels) {
308    this.#sendJsonInput({
309      type: 'wheel',
310      // convert double to int, forcing a base 10 conversion. pixels can be fractional.
311      pixels: parseInt(pixels, 10),
312    });
313  }
314
315  disconnect() {
316    this.#pc.close();
317  }
318
319  // Sends binary data directly to the in-device adb daemon (skipping the host)
320  sendAdbMessage(msg) {
321    this.#adbChannel.send(msg);
322  }
323
324  // Provide a callback to receive data from the in-device adb daemon
325  onAdbMessage(cb) {
326    this.#onAdbMessage = cb;
327  }
328
329  // Send control commands to the device
330  sendControlMessage(msg) {
331    this.#controlChannel.send(msg);
332  }
333
334  async #useDevice(
335      in_use, senders_arr, device_opt, requestedFn = () => {in_use}, enabledFn = (stream) => {}) {
336    // An empty array means no tracks are currently in use
337    if (senders_arr.length > 0 === !!in_use) {
338      return in_use;
339    }
340    let renegotiation_needed = false;
341    if (in_use) {
342      try {
343        let stream = await navigator.mediaDevices.getUserMedia(device_opt);
344        // The user may have changed their mind by the time we obtain the
345        // stream, check again
346        if (!!in_use != requestedFn()) {
347          return requestedFn();
348        }
349        enabledFn(stream);
350        stream.getTracks().forEach(track => {
351          console.info(`Using ${track.kind} device: ${track.label}`);
352          senders_arr.push(this.#pc.addTrack(track));
353          renegotiation_needed = true;
354        });
355      } catch (e) {
356        console.error('Failed to add stream to peer connection: ', e);
357        // Don't return yet, if there were errors some tracks may have been
358        // added so the connection should be renegotiated again.
359      }
360    } else {
361      for (const sender of senders_arr) {
362        console.info(
363            `Removing ${sender.track.kind} device: ${sender.track.label}`);
364        let track = sender.track;
365        track.stop();
366        this.#pc.removeTrack(sender);
367        renegotiation_needed = true;
368      }
369      // Empty the array passed by reference, just assigning [] won't do that.
370      senders_arr.length = 0;
371    }
372    if (renegotiation_needed) {
373      await this.#control.renegotiateConnection();
374    }
375    // Return the new state
376    return senders_arr.length > 0;
377  }
378
379  async useMic(in_use) {
380    if (this.#micRequested == !!in_use) {
381      return in_use;
382    }
383    this.#micRequested = !!in_use;
384    return this.#useDevice(
385        in_use, this.#micSenders, {audio: true, video: false},
386        () => this.#micRequested);
387  }
388
389  async useCamera(in_use) {
390    if (this.#cameraRequested == !!in_use) {
391      return in_use;
392    }
393    this.#cameraRequested = !!in_use;
394    return this.#useDevice(
395        in_use, this.#micSenders, {audio: false, video: true},
396        () => this.#cameraRequested,
397        (stream) => this.sendCameraResolution(stream));
398  }
399
400  sendCameraResolution(stream) {
401    const cameraTracks = stream.getVideoTracks();
402    if (cameraTracks.length > 0) {
403      const settings = cameraTracks[0].getSettings();
404      this.#camera_res_x = settings.width;
405      this.#camera_res_y = settings.height;
406      this.sendControlMessage(JSON.stringify({
407        command: 'camera_settings',
408        width: settings.width,
409        height: settings.height,
410        frame_rate: settings.frameRate,
411        facing: settings.facingMode
412      }));
413    }
414  }
415
416  sendOrQueueCameraData(data) {
417    if (this.#cameraDataChannel.bufferedAmount > 0 ||
418        this.#cameraInputQueue.length > 0) {
419      this.#cameraInputQueue.push(data);
420    } else {
421      this.sendCameraData(data);
422    }
423  }
424
425  sendCameraData(data) {
426    const MAX_SIZE = 65535;
427    const END_MARKER = 'EOF';
428    for (let i = 0; i < data.byteLength; i += MAX_SIZE) {
429      // range is clamped to the valid index range
430      this.#cameraDataChannel.send(data.slice(i, i + MAX_SIZE));
431    }
432    this.#cameraDataChannel.send(END_MARKER);
433  }
434
435  // Provide a callback to receive control-related comms from the device
436  onControlMessage(cb) {
437    this.#onControlMessage = cb;
438  }
439
440  sendBluetoothMessage(msg) {
441    this.#bluetoothChannel.send(msg);
442  }
443
444  onBluetoothMessage(cb) {
445    this.#onBluetoothMessage = cb;
446  }
447
448  sendLocationMessage(msg) {
449    this.#locationChannel.send(msg);
450  }
451
452  sendSensorsMessage(msg) {
453    this.#sensorsChannel.send(msg);
454  }
455
456  onSensorsMessage(cb) {
457    this.#onSensorsMessage = cb;
458  }
459
460  onLocationMessage(cb) {
461    this.#onLocationMessage = cb;
462  }
463
464  sendKmlLocationsMessage(msg) {
465    this.#kmlLocationsChannel.send(msg);
466  }
467
468  onKmlLocationsMessage(cb) {
469    this.#kmlLocationsChannel = cb;
470  }
471
472  sendGpxLocationsMessage(msg) {
473    this.#gpxLocationsChannel.send(msg);
474  }
475
476  onGpxLocationsMessage(cb) {
477    this.#gpxLocationsChannel = cb;
478  }
479
480  // Provide a callback to receive connectionstatechange states.
481  onConnectionStateChange(cb) {
482    this.#pc.addEventListener(
483        'connectionstatechange', evt => cb(this.#pc.connectionState));
484  }
485
486  onLightsMessage(cb) {
487    this.#onLightsMessage = cb;
488  }
489}
490
491class Controller {
492  #pc;
493  #serverConnector;
494  #connectedPr = Promise.resolve({});
495  // A list of callbacks that need to be called when the remote description is
496  // successfully added to the peer connection.
497  #onRemoteDescriptionSetCbs = [];
498
499  constructor(serverConnector) {
500    this.#serverConnector = serverConnector;
501    serverConnector.onDeviceMsg(msg => this.#onDeviceMessage(msg));
502  }
503
504  #onDeviceMessage(message) {
505    let type = message.type;
506    switch (type) {
507      case 'offer':
508        this.#onOffer({type: 'offer', sdp: message.sdp});
509        break;
510      case 'answer':
511        this.#onRemoteDescription({type: 'answer', sdp: message.sdp});
512        break;
513      case 'ice-candidate':
514          this.#onIceCandidate(new RTCIceCandidate({
515            sdpMid: message.mid,
516            sdpMLineIndex: message.mLineIndex,
517            candidate: message.candidate
518          }));
519        break;
520      case 'error':
521        console.error('Device responded with error message: ', message.error);
522        break;
523      default:
524        console.error('Unrecognized message type from device: ', type);
525    }
526  }
527
528  async #sendClientDescription(desc) {
529    console.debug('sendClientDescription');
530    return this.#serverConnector.sendToDevice({type: 'answer', sdp: desc.sdp});
531  }
532
533  async #sendIceCandidate(candidate) {
534    console.debug('sendIceCandidate');
535    return this.#serverConnector.sendToDevice({type: 'ice-candidate', candidate});
536  }
537
538  async #onOffer(desc) {
539    try {
540      await this.#onRemoteDescription(desc);
541      let answer = await this.#pc.createAnswer();
542      console.debug('Answer: ', answer);
543      await this.#pc.setLocalDescription(answer);
544      await this.#sendClientDescription(answer);
545    } catch (e) {
546      console.error('Error processing remote description (offer)', e)
547      throw e;
548    }
549  }
550
551  async #onRemoteDescription(desc) {
552    console.debug(`Remote description (${desc.type}): `, desc);
553    try {
554      await this.#pc.setRemoteDescription(desc);
555      for (const cb of this.#onRemoteDescriptionSetCbs) {
556        cb();
557      }
558      this.#onRemoteDescriptionSetCbs = [];
559    } catch (e) {
560      console.error(`Error processing remote description (${desc.type})`, e)
561      throw e;
562    }
563  }
564
565  #onIceCandidate(iceCandidate) {
566    console.debug(`Remote ICE Candidate: `, iceCandidate);
567    this.#pc.addIceCandidate(iceCandidate);
568  }
569
570  expectMessagesSoon(durationMilliseconds) {
571    if (this.#serverConnector.expectMessagesSoon) {
572      this.#serverConnector.expectMessagesSoon(durationMilliseconds);
573    } else {
574      console.warn(`Unavailable expectMessagesSoon(). Messages may be slow.`);
575    }
576  }
577
578  // This effectively ensures work that changes connection state doesn't run
579  // concurrently.
580  // Returns a promise that resolves if the connection is successfully
581  // established after the provided work is done.
582  #onReadyToNegotiate(work_cb) {
583    const connectedPr = this.#connectedPr.then(() => {
584      const controller = new AbortController();
585      const pr = new Promise((resolve, reject) => {
586        // The promise resolves when the connection changes state to 'connected'
587        // or when a remote description is set and the connection was already in
588        // 'connected' state.
589        this.#onRemoteDescriptionSetCbs.push(() => {
590          if (this.#pc.connectionState == 'connected') {
591            resolve({});
592          }
593        });
594        this.#pc.addEventListener('connectionstatechange', evt => {
595          let state = this.#pc.connectionState;
596          if (state == 'connected') {
597            resolve(evt);
598          } else if (state == 'failed') {
599            reject(evt);
600          }
601        }, {signal: controller.signal});
602      });
603      // Remove the listener once the promise fulfills.
604      pr.finally(() => controller.abort());
605      work_cb();
606      // Don't return pr.finally() since that is never rejected.
607      return pr;
608    });
609    // A failure is also a sign that renegotiation is possible again
610    this.#connectedPr = connectedPr.catch(_ => {});
611    return connectedPr;
612  }
613
614  async ConnectDevice(pc, infraConfig) {
615    this.#pc = pc;
616    console.debug('ConnectDevice');
617    // ICE candidates will be generated when we add the offer. Adding it here
618    // instead of in #onOffer because this function is called once per peer
619    // connection, while #onOffer may be called more than once due to
620    // renegotiations.
621    this.#pc.addEventListener('icecandidate', evt => {
622      // The last candidate is null, which indicates the end of ICE gathering.
623      // Firefox's second to last candidate has the candidate property set to
624      // empty, skip that one.
625      if (evt.candidate && evt.candidate.candidate) {
626        this.#sendIceCandidate(evt.candidate);
627      }
628    });
629    return this.#onReadyToNegotiate(_ => {
630      this.#serverConnector.sendToDevice(
631        {type: 'request-offer', ice_servers: infraConfig.ice_servers});
632    });
633  }
634
635  async renegotiateConnection() {
636    return this.#onReadyToNegotiate(async () => {
637      console.debug('Re-negotiating connection');
638      let offer = await this.#pc.createOffer();
639      console.debug('Local description (offer): ', offer);
640      await this.#pc.setLocalDescription(offer);
641      await this.#serverConnector.sendToDevice({type: 'offer', sdp: offer.sdp});
642    });
643  }
644}
645
646function createPeerConnection(infra_config) {
647  let pc_config = {iceServers: infra_config.ice_servers};
648  let pc = new RTCPeerConnection(pc_config);
649
650  pc.addEventListener('icecandidate', evt => {
651    console.debug('Local ICE Candidate: ', evt.candidate);
652  });
653  pc.addEventListener('iceconnectionstatechange', evt => {
654    console.debug(`ICE State Change: ${pc.iceConnectionState}`);
655  });
656  pc.addEventListener(
657      'connectionstatechange',
658      evt => console.debug(
659          `WebRTC Connection State Change: ${pc.connectionState}`));
660  return pc;
661}
662
663export async function Connect(deviceId, serverConnector) {
664  let requestRet = await serverConnector.requestDevice(deviceId);
665  let deviceInfo = requestRet.deviceInfo;
666  let infraConfig = requestRet.infraConfig;
667  console.debug('Device available:');
668  console.debug(deviceInfo);
669  let pc = createPeerConnection(infraConfig);
670
671  let control = new Controller(serverConnector);
672  let deviceConnection = new DeviceConnection(pc, control);
673  deviceConnection.description = deviceInfo;
674
675  return control.ConnectDevice(pc, infraConfig).then(_ => deviceConnection);
676}
677