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
47class DeviceConnection {
48  constructor(pc, control) {
49    this._pc = pc;
50    this._control = control;
51    this._inputChannel = createDataChannel(pc, 'input-channel');
52    this._adbChannel = createDataChannel(pc, 'adb-channel', (msg) => {
53      if (this._onAdbMessage) {
54        this._onAdbMessage(msg.data);
55      } else {
56        console.error('Received unexpected ADB message');
57      }
58    });
59    this._streams = {};
60    this._streamPromiseResolvers = {};
61
62    pc.addEventListener('track', e => {
63      console.log('Got remote stream: ', e);
64      for (const stream of e.streams) {
65        this._streams[stream.id] = stream;
66        if (this._streamPromiseResolvers[stream.id]) {
67          for (let resolver of this._streamPromiseResolvers[stream.id]) {
68            resolver();
69          }
70          delete this._streamPromiseResolvers[stream.id];
71        }
72      }
73    });
74  }
75
76  set description(desc) {
77    this._description = desc;
78  }
79
80  get description() {
81    return this._description;
82  }
83
84  getStream(stream_id) {
85    return new Promise((resolve, reject) => {
86      if (this._streams[stream_id]) {
87        resolve(this._streams[stream_id]);
88      } else {
89        if (!this._streamPromiseResolvers[stream_id]) {
90          this._streamPromiseResolvers[stream_id] = [];
91        }
92        this._streamPromiseResolvers[stream_id].push(resolve);
93      }
94    });
95  }
96
97  _sendJsonInput(evt) {
98    this._inputChannel.send(JSON.stringify(evt));
99  }
100
101  sendMousePosition({x, y, down, display_label}) {
102    this._sendJsonInput({
103      type: 'mouse',
104      down: down ? 1 : 0,
105      x,
106      y,
107      display_label,
108    });
109  }
110
111  // TODO (b/124121375): This should probably be an array of pointer events and
112  // have different properties.
113  sendMultiTouch({id, x, y, initialDown, slot, display_label}) {
114    this._sendJsonInput({
115      type: 'multi-touch',
116      id,
117      x,
118      y,
119      initialDown: initialDown ? 1 : 0,
120      slot,
121      display_label,
122    });
123  }
124
125  sendKeyEvent(code, type) {
126    this._sendJsonInput({type: 'keyboard', keycode: code, event_type: type});
127  }
128
129  disconnect() {
130    this._pc.close();
131  }
132
133  // Sends binary data directly to the in-device adb daemon (skipping the host)
134  sendAdbMessage(msg) {
135    this._adbChannel.send(msg);
136  }
137
138  // Provide a callback to receive data from the in-device adb daemon
139  onAdbMessage(cb) {
140    this._onAdbMessage = cb;
141  }
142}
143
144
145class WebRTCControl {
146  constructor({
147    wsUrl = '',
148  }) {
149    /*
150     * Private attributes:
151     *
152     * _wsPromise: promises the underlying websocket, should resolve when the
153     *             socket passes to OPEN state, will be rejecte/replaced by a
154     *             rejected promise if an error is detected on the socket.
155     *
156     * _onOffer
157     * _onIceCandidate
158     */
159
160    this._promiseResolvers = {};
161
162    this._wsPromise = new Promise((resolve, reject) => {
163      let ws = new WebSocket(wsUrl);
164      ws.onopen = () => {
165        console.info(`Connected to ${wsUrl}`);
166        resolve(ws);
167      };
168      ws.onerror = evt => {
169        console.error('WebSocket error:', evt);
170        reject(evt);
171        // If the promise was already resolved the previous line has no effect
172        this._wsPromise = Promise.reject(new Error(evt));
173      };
174      ws.onmessage = e => {
175        let data = JSON.parse(e.data);
176        this._onWebsocketMessage(data);
177      };
178    });
179  }
180
181  _onWebsocketMessage(message) {
182    const type = message.message_type;
183    if (message.error) {
184      console.error(message.error);
185      return;
186    }
187    switch (type) {
188      case 'config':
189        this._infra_config = message;
190        break;
191      case 'device_info':
192        if (this._on_device_available) {
193          this._on_device_available(message.device_info);
194          delete this._on_device_available;
195        } else {
196          console.error('Received unsolicited device info');
197        }
198        break;
199      case 'device_msg':
200        this._onDeviceMessage(message.payload);
201        break;
202      default:
203        console.error('Unrecognized message type from server: ', type);
204        console.error(message);
205    }
206  }
207
208  _onDeviceMessage(message) {
209    let type = message.type;
210    switch (type) {
211      case 'offer':
212        if (this._onOffer) {
213          this._onOffer({type: 'offer', sdp: message.sdp});
214        } else {
215          console.error('Receive offer, but nothing is wating for it');
216        }
217        break;
218      case 'ice-candidate':
219        if (this._onIceCandidate) {
220          this._onIceCandidate(new RTCIceCandidate({
221            sdpMid: message.mid,
222            sdpMLineIndex: message.mLineIndex,
223            candidate: message.candidate
224          }));
225        } else {
226          console.error('Received ice candidate but nothing is waiting for it');
227        }
228        break;
229      default:
230        console.error('Unrecognized message type from device: ', type);
231    }
232  }
233
234  async _wsSendJson(obj) {
235    let ws = await this._wsPromise;
236    return ws.send(JSON.stringify(obj));
237  }
238  async _sendToDevice(payload) {
239    this._wsSendJson({message_type: 'forward', payload});
240  }
241
242  onOffer(cb) {
243    this._onOffer = cb;
244  }
245
246  onIceCandidate(cb) {
247    this._onIceCandidate = cb;
248  }
249
250  async requestDevice(device_id) {
251    return new Promise((resolve, reject) => {
252      this._on_device_available = (deviceInfo) => resolve({
253        deviceInfo,
254        infraConfig: this._infra_config,
255      });
256      this._wsSendJson({
257        message_type: 'connect',
258        device_id,
259      });
260    });
261  }
262
263  ConnectDevice() {
264    console.log('ConnectDevice');
265    this._sendToDevice({type: 'request-offer'});
266  }
267
268  /**
269   * Sends a remote description to the device.
270   */
271  async sendClientDescription(desc) {
272    console.log('sendClientDescription');
273    this._sendToDevice({type: 'answer', sdp: desc.sdp});
274  }
275
276  /**
277   * Sends an ICE candidate to the device
278   */
279  async sendIceCandidate(candidate) {
280    this._sendToDevice({type: 'ice-candidate', candidate});
281  }
282}
283
284function createPeerConnection(infra_config) {
285  let pc_config = {iceServers: []};
286  for (const stun of infra_config.ice_servers) {
287    pc_config.iceServers.push({urls: 'stun:' + stun});
288  }
289  let pc = new RTCPeerConnection(pc_config);
290
291  pc.addEventListener('icecandidate', evt => {
292    console.log('Local ICE Candidate: ', evt.candidate);
293  });
294  pc.addEventListener('iceconnectionstatechange', evt => {
295    console.log(`ICE State Change: ${pc.iceConnectionState}`);
296  });
297  pc.addEventListener(
298      'connectionstatechange',
299      evt =>
300          console.log(`WebRTC Connection State Change: ${pc.connectionState}`));
301  return pc;
302}
303
304export async function Connect(deviceId, options) {
305  let control = new WebRTCControl(options);
306  let requestRet = await control.requestDevice(deviceId);
307  let deviceInfo = requestRet.deviceInfo;
308  let infraConfig = requestRet.infraConfig;
309  console.log('Device available:');
310  console.log(deviceInfo);
311  let pc_config = {iceServers: []};
312  if (infraConfig.ice_servers && infraConfig.ice_servers.length > 0) {
313    for (const server of infraConfig.ice_servers) {
314      pc_config.iceServers.push(server);
315    }
316  }
317  let pc = createPeerConnection(infraConfig, control);
318  let deviceConnection = new DeviceConnection(pc, control);
319  deviceConnection.description = deviceInfo;
320  async function acceptOfferAndReplyAnswer(offer) {
321    try {
322      await pc.setRemoteDescription(offer);
323      let answer = await pc.createAnswer();
324      await pc.setLocalDescription(answer);
325      await control.sendClientDescription(answer);
326    } catch (e) {
327      console.error('Error establishing WebRTC connection: ', e)
328      throw e;
329    }
330  }
331  control.onOffer(desc => {
332    console.log('Offer: ', desc);
333    acceptOfferAndReplyAnswer(desc);
334  });
335  control.onIceCandidate(iceCandidate => {
336    console.log(`Remote ICE Candidate: `, iceCandidate);
337    pc.addIceCandidate(iceCandidate);
338  });
339
340  pc.addEventListener('icecandidate', evt => {
341    if (evt.candidate) control.sendIceCandidate(evt.candidate);
342  });
343  let connected_promise = new Promise((resolve, reject) => {
344    pc.addEventListener('connectionstatechange', evt => {
345      let state = pc.connectionState;
346      if (state == 'connected') {
347        resolve(deviceConnection);
348      } else if (state == 'failed') {
349        reject(evt);
350      }
351    });
352  });
353  control.ConnectDevice();
354
355  return connected_promise;
356}
357