• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2021 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
17'use strict';
18
19// The public elements in this file implement the Server Connector Interface,
20// part of the contract between the signaling server and the webrtc client.
21// No changes that break backward compatibility are allowed here. Any new
22// features must be added as a new function/class in the interface. Any
23// additions to the interface must be checked for existence by the client before
24// using it.
25
26// The id of the device the client is supposed to connect to.
27// The List Devices page in the signaling server may choose any way to pass the
28// device id to the client page, this function retrieves that information once
29// the client loaded.
30// In this case the device id is passed as a parameter in the url.
31export function deviceId() {
32  const urlParams = new URLSearchParams(window.location.search);
33  return urlParams.get('deviceId');
34}
35
36// Creates a connector capable of communicating with the signaling server.
37export async function createConnector() {
38  try {
39    let ws = await connectWs();
40    console.debug(`Connected to ${ws.url}`);
41    return new WebsocketConnector(ws);
42  } catch (e) {
43    console.error('WebSocket error:', e);
44  }
45  console.warn('Failed to connect websocket, trying polling instead');
46
47  return new PollingConnector();
48}
49
50// A connector object provides high level functions for communicating with the
51// signaling server, while hiding away implementation details.
52// This class is an interface and shouldn't be instantiated direclty.
53// Only the public methods present in this class form part of the Server
54// Connector Interface, any implementations of the interface are considered
55// internal and not accessible to client code.
56class Connector {
57  constructor() {
58    if (this.constructor == Connector) {
59      throw new Error('Connector is an abstract class');
60    }
61  }
62
63  // Registers a callback to receive messages from the device. A race may occur
64  // if this is called after requestDevice() is called in which some device
65  // messages are lost.
66  onDeviceMsg(cb) {
67    throw 'Not implemented!';
68  }
69
70  // Selects a particular device in the signaling server and opens the signaling
71  // channel with it (but doesn't send any message to the device). Returns a
72  // promise to an object with the following properties:
73  // - deviceInfo: The info object provided by the device when it registered
74  // with the server.
75  // - infraConfig: The server's infrastructure configuration (mainly STUN and
76  // TURN servers)
77  // The promise may take a long time to resolve if, for example, the server
78  // decides to wait for a device with the provided id to register with it. The
79  // promise may be rejected if there are connectivity issues, a device with
80  // that id doesn't exist or this client doesn't have rights to access that
81  // device.
82  async requestDevice(deviceId) {
83    throw 'Not implemented!';
84  }
85
86  // Sends a message to the device selected with requestDevice. It's an error to
87  // call this function before the promise from requestDevice() has resolved.
88  // Returns an empty promise that is rejected when the message can not be
89  // delivered, either because the device has not been requested yet or because
90  // of connectivity issues.
91  async sendToDevice(msg) {
92    throw 'Not implemented!';
93  }
94}
95
96// Returns real implementation for ParentController.
97export function createParentController() {
98  return null;
99}
100
101// ParentController object provides methods for sending information from device
102// UI to operator UI. This class is just an interface and real implementation is
103// at the operator side. This class shouldn't be instantiated directly.
104class ParentController {
105  constructor() {
106    if (this.constructor === ParentController) {
107      throw new Error('ParentController is an abstract class');
108    }
109  }
110
111  // Create and return a message object that contains display information of
112  // device. Created object can be sent to operator UI using send() method.
113  // rotation argument is device's physycan rotation so it will be commonly
114  // applied to all displays.
115  createDeviceDisplaysMessage(rotation) {
116    throw 'Not implemented';
117  }
118}
119
120// This class represents displays information for a device. This message is
121// intended to be sent to operator UI to determine panel size of device UI.
122// This is an abstract class and should not be instantiated directly. This
123// message is created using createDeviceDisplaysMessage method of
124// ParentController. Real implementation of this class is at operator side.
125export class DeviceDisplaysMessage {
126  constructor(parentController, rotation) {
127    if (this.constructor === DeviceDisplaysMessage) {
128      throw new Error('DeviceDisplaysMessage is an abstract class');
129    }
130  }
131
132  // Add a display information to deviceDisplays message.
133  addDisplay(display_id, width, height) {
134    throw 'Not implemented'
135  }
136
137  // Send DeviceDisplaysMessage created using createDeviceDisplaysMessage to
138  // operator UI. If operator UI does not exist (in the case device web page
139  // is opened directly), the message will just be ignored.
140  send() {
141    throw 'Not implemented'
142  }
143}
144
145// End of Server Connector Interface.
146
147// The following code is internal and shouldn't be accessed outside this file.
148
149function httpUrl(path) {
150  return location.protocol + '//' + location.host + '/' + path;
151}
152
153function websocketUrl(path) {
154  return ((location.protocol == 'http:') ? 'ws://' : 'wss://') + location.host +
155      '/' + path;
156}
157
158const kPollConfigUrl = httpUrl('infra_config');
159const kPollConnectUrl = httpUrl('connect');
160const kPollForwardUrl = httpUrl('forward');
161const kPollMessagesUrl = httpUrl('poll_messages');
162
163async function connectWs() {
164  return new Promise((resolve, reject) => {
165    let url = websocketUrl('connect_client');
166    let ws = new WebSocket(url);
167    ws.onopen = () => {
168      resolve(ws);
169    };
170    ws.onerror = evt => {
171      reject(evt);
172    };
173  });
174}
175
176async function ajaxPostJson(url, data) {
177  const response = await fetch(url, {
178    method: 'POST',
179    cache: 'no-cache',
180    headers: {'Content-Type': 'application/json'},
181    redirect: 'follow',
182    body: JSON.stringify(data),
183  });
184  return response.json();
185}
186
187// Implementation of the connector interface using websockets
188class WebsocketConnector extends Connector {
189  #websocket;
190  #futures = {};
191  #onDeviceMsgCb = msg =>
192      console.error('Received device message without registered listener');
193
194  onDeviceMsg(cb) {
195    this.#onDeviceMsgCb = cb;
196  }
197
198  constructor(ws) {
199    super();
200    ws.onmessage = e => {
201      let data = JSON.parse(e.data);
202      this.#onWebsocketMessage(data);
203    };
204    this.#websocket = ws;
205  }
206
207  async requestDevice(deviceId) {
208    return new Promise((resolve, reject) => {
209      this.#futures.onDeviceAvailable = (device) => resolve(device);
210      this.#futures.onConnectionFailed = (error) => reject(error);
211      this.#wsSendJson({
212        message_type: 'connect',
213        device_id: deviceId,
214      });
215    });
216  }
217
218  async sendToDevice(msg) {
219    return this.#wsSendJson({message_type: 'forward', payload: msg});
220  }
221
222  #onWebsocketMessage(message) {
223    const type = message.message_type;
224    if (message.error) {
225      console.error(message.error);
226      this.#futures.onConnectionFailed(message.error);
227      return;
228    }
229    switch (type) {
230      case 'config':
231        this.#futures.infraConfig = message;
232        break;
233      case 'device_info':
234        if (this.#futures.onDeviceAvailable) {
235          this.#futures.onDeviceAvailable({
236            deviceInfo: message.device_info,
237            infraConfig: this.#futures.infraConfig,
238          });
239          delete this.#futures.onDeviceAvailable;
240        } else {
241          console.error('Received unsolicited device info');
242        }
243        break;
244      case 'device_msg':
245        this.#onDeviceMsgCb(message.payload);
246        break;
247      default:
248        console.error('Unrecognized message type from server: ', type);
249        this.#futures.onConnectionFailed(
250            'Unrecognized message type from server: ' + type);
251        console.error(message);
252    }
253  }
254
255  async #wsSendJson(obj) {
256    return this.#websocket.send(JSON.stringify(obj));
257  }
258}
259
260// Implementation of the Connector interface using HTTP long polling
261class PollingConnector extends Connector {
262  #connId = undefined;
263  #config = undefined;
264  #pollerSchedule;
265  #onDeviceMsgCb = msg =>
266      console.error('Received device message without registered listener');
267
268  onDeviceMsg(cb) {
269    this.#onDeviceMsgCb = cb;
270  }
271
272  constructor() {
273    super();
274  }
275
276  async requestDevice(deviceId) {
277    let config = await this.#getConfig();
278    let response = await ajaxPostJson(kPollConnectUrl, {device_id: deviceId});
279    this.#connId = response.connection_id;
280
281    this.#startPolling();
282
283    return {
284      deviceInfo: response.device_info,
285      infraConfig: config,
286    };
287  }
288
289  async sendToDevice(msg) {
290    // Forward messages act like polling messages as well
291    let device_messages = await this.#forward(msg);
292    for (const message of device_messages) {
293      this.#onDeviceMsgCb(message);
294    }
295  }
296
297  async #getConfig() {
298    if (this.#config === undefined) {
299      this.#config = await (await fetch(kPollConfigUrl, {
300                       method: 'GET',
301                       redirect: 'follow',
302                     })).json();
303    }
304    return this.#config;
305  }
306
307  async #forward(msg) {
308    return await ajaxPostJson(kPollForwardUrl, {
309      connection_id: this.#connId,
310      payload: msg,
311    });
312  }
313
314  async #pollMessages() {
315    return await ajaxPostJson(kPollMessagesUrl, {
316      connection_id: this.#connId,
317    });
318  }
319
320  #startPolling() {
321    if (this.#pollerSchedule !== undefined) {
322      return;
323    }
324
325    let currentPollDelay = 1000;
326    let pollerRoutine = async () => {
327      let messages = await this.#pollMessages();
328
329      // Do exponential backoff on the polling up to 60 seconds
330      currentPollDelay = Math.min(60000, 2 * currentPollDelay);
331      for (const message of messages) {
332        this.#onDeviceMsgCb(message);
333        // There is at least one message, poll sooner
334        currentPollDelay = 1000;
335      }
336      this.#pollerSchedule = setTimeout(pollerRoutine, currentPollDelay);
337    };
338
339    this.#pollerSchedule = setTimeout(pollerRoutine, currentPollDelay);
340  }
341}
342