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 directly. 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 // Provides a hint to this controller that it should expect messages from the 96 // signaling server soon. This is useful for a connector which polls for 97 // example which might want to poll more quickly for a period of time. 98 expectMessagesSoon(durationMilliseconds) { 99 throw 'Not implemented!'; 100 } 101} 102 103// Returns real implementation for ParentController. 104export function createParentController() { 105 return null; 106} 107 108// ParentController object provides methods for sending information from device 109// UI to operator UI. This class is just an interface and real implementation is 110// at the operator side. This class shouldn't be instantiated directly. 111class ParentController { 112 constructor() { 113 if (this.constructor === ParentController) { 114 throw new Error('ParentController is an abstract class'); 115 } 116 } 117 118 // Create and return a message object that contains display information of 119 // device. Created object can be sent to operator UI using send() method. 120 // rotation argument is device's physycan rotation so it will be commonly 121 // applied to all displays. 122 createDeviceDisplaysMessage(rotation) { 123 throw 'Not implemented'; 124 } 125} 126 127// This class represents displays information for a device. This message is 128// intended to be sent to operator UI to determine panel size of device UI. 129// This is an abstract class and should not be instantiated directly. This 130// message is created using createDeviceDisplaysMessage method of 131// ParentController. Real implementation of this class is at operator side. 132export class DeviceDisplaysMessage { 133 constructor(parentController, rotation) { 134 if (this.constructor === DeviceDisplaysMessage) { 135 throw new Error('DeviceDisplaysMessage is an abstract class'); 136 } 137 } 138 139 // Add a display information to deviceDisplays message. 140 addDisplay(display_id, width, height) { 141 throw 'Not implemented' 142 } 143 144 // Send DeviceDisplaysMessage created using createDeviceDisplaysMessage to 145 // operator UI. If operator UI does not exist (in the case device web page 146 // is opened directly), the message will just be ignored. 147 send() { 148 throw 'Not implemented' 149 } 150} 151 152// End of Server Connector Interface. 153 154// The following code is internal and shouldn't be accessed outside this file. 155 156function httpUrl(path) { 157 return location.protocol + '//' + location.host + '/' + path; 158} 159 160function websocketUrl(path) { 161 return ((location.protocol == 'http:') ? 'ws://' : 'wss://') + location.host + 162 '/' + path; 163} 164 165const kPollConfigUrl = httpUrl('infra_config'); 166const kPollConnectUrl = httpUrl('connect'); 167const kPollForwardUrl = httpUrl('forward'); 168const kPollMessagesUrl = httpUrl('poll_messages'); 169 170async function connectWs() { 171 return new Promise((resolve, reject) => { 172 let url = websocketUrl('connect_client'); 173 let ws = new WebSocket(url); 174 ws.onopen = () => { 175 resolve(ws); 176 }; 177 ws.onerror = evt => { 178 reject(evt); 179 }; 180 }); 181} 182 183async function ajaxPostJson(url, data) { 184 const response = await fetch(url, { 185 method: 'POST', 186 cache: 'no-cache', 187 headers: {'Content-Type': 'application/json'}, 188 redirect: 'follow', 189 body: JSON.stringify(data), 190 }); 191 return response.json(); 192} 193 194// Implementation of the connector interface using websockets 195class WebsocketConnector extends Connector { 196 #websocket; 197 #futures = {}; 198 #onDeviceMsgCb = msg => 199 console.error('Received device message without registered listener'); 200 201 onDeviceMsg(cb) { 202 this.#onDeviceMsgCb = cb; 203 } 204 205 constructor(ws) { 206 super(); 207 ws.onmessage = e => { 208 let data = JSON.parse(e.data); 209 this.#onWebsocketMessage(data); 210 }; 211 this.#websocket = ws; 212 } 213 214 async requestDevice(deviceId) { 215 return new Promise((resolve, reject) => { 216 this.#futures.onDeviceAvailable = (device) => resolve(device); 217 this.#futures.onConnectionFailed = (error) => reject(error); 218 this.#wsSendJson({ 219 message_type: 'connect', 220 device_id: deviceId, 221 }); 222 }); 223 } 224 225 async sendToDevice(msg) { 226 return this.#wsSendJson({message_type: 'forward', payload: msg}); 227 } 228 229 #onWebsocketMessage(message) { 230 const type = message.message_type; 231 if (message.error) { 232 console.error(message.error); 233 this.#futures.onConnectionFailed(message.error); 234 return; 235 } 236 switch (type) { 237 case 'config': 238 this.#futures.infraConfig = message; 239 break; 240 case 'device_info': 241 if (this.#futures.onDeviceAvailable) { 242 this.#futures.onDeviceAvailable({ 243 deviceInfo: message.device_info, 244 infraConfig: this.#futures.infraConfig, 245 }); 246 delete this.#futures.onDeviceAvailable; 247 } else { 248 console.error('Received unsolicited device info'); 249 } 250 break; 251 case 'device_msg': 252 this.#onDeviceMsgCb(message.payload); 253 break; 254 default: 255 console.error('Unrecognized message type from server: ', type); 256 this.#futures.onConnectionFailed( 257 'Unrecognized message type from server: ' + type); 258 console.error(message); 259 } 260 } 261 262 async #wsSendJson(obj) { 263 return this.#websocket.send(JSON.stringify(obj)); 264 } 265 266 expectMessagesSoon(durationMilliseconds) { 267 // No-op 268 } 269} 270 271const SHORT_POLL_DELAY = 1000; 272 273// Implementation of the Connector interface using HTTP long polling 274class PollingConnector extends Connector { 275 #connId = undefined; 276 #config = undefined; 277 #pollerSchedule; 278 #pollQuicklyUntil = Date.now(); 279 #onDeviceMsgCb = msg => 280 console.error('Received device message without registered listener'); 281 282 onDeviceMsg(cb) { 283 this.#onDeviceMsgCb = cb; 284 } 285 286 constructor() { 287 super(); 288 } 289 290 async requestDevice(deviceId) { 291 let config = await this.#getConfig(); 292 let response = await ajaxPostJson(kPollConnectUrl, {device_id: deviceId}); 293 this.#connId = response.connection_id; 294 295 this.#startPolling(); 296 297 return { 298 deviceInfo: response.device_info, 299 infraConfig: config, 300 }; 301 } 302 303 async sendToDevice(msg) { 304 // Forward messages act like polling messages as well 305 let device_messages = await this.#forward(msg); 306 for (const message of device_messages) { 307 this.#onDeviceMsgCb(message); 308 } 309 } 310 311 async #getConfig() { 312 if (this.#config === undefined) { 313 this.#config = await (await fetch(kPollConfigUrl, { 314 method: 'GET', 315 redirect: 'follow', 316 })).json(); 317 } 318 return this.#config; 319 } 320 321 async #forward(msg) { 322 return await ajaxPostJson(kPollForwardUrl, { 323 connection_id: this.#connId, 324 payload: msg, 325 }); 326 } 327 328 async #pollMessages() { 329 return await ajaxPostJson(kPollMessagesUrl, { 330 connection_id: this.#connId, 331 }); 332 } 333 334 #calcNextPollDelay(previousPollDelay) { 335 if (Date.now() < this.#pollQuicklyUntil) { 336 return SHORT_POLL_DELAY; 337 } else { 338 // Do exponential backoff on the polling up to 60 seconds 339 return Math.min(60000, 2 * previousPollDelay); 340 } 341 } 342 343 #startPolling() { 344 if (this.#pollerSchedule !== undefined) { 345 return; 346 } 347 348 let currentPollDelay = SHORT_POLL_DELAY; 349 let pollerRoutine = async () => { 350 let messages = await this.#pollMessages(); 351 352 currentPollDelay = this.#calcNextPollDelay(currentPollDelay); 353 354 for (const message of messages) { 355 this.#onDeviceMsgCb(message); 356 // There is at least one message, poll sooner 357 currentPollDelay = SHORT_POLL_DELAY; 358 } 359 this.#pollerSchedule = setTimeout(pollerRoutine, currentPollDelay); 360 }; 361 362 this.#pollerSchedule = setTimeout(pollerRoutine, currentPollDelay); 363 } 364 365 expectMessagesSoon(durationMilliseconds) { 366 console.debug("Polling frequently for ", durationMilliseconds, " ms."); 367 368 clearTimeout(this.#pollerSchedule); 369 this.#pollerSchedule = undefined; 370 371 this.#pollQuicklyUntil = Date.now() + durationMilliseconds; 372 this.#startPolling(); 373 } 374} 375