-
Notifications
You must be signed in to change notification settings - Fork 47
Expand file tree
/
Copy pathDeviceRendererFactory.js
More file actions
339 lines (310 loc) · 15 KB
/
DeviceRendererFactory.js
File metadata and controls
339 lines (310 loc) · 15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
/* eslint-disable indent */
import DeviceRenderer from './DeviceRenderer';
import {defaultsDeep} from 'lodash';
import store from './store';
import APIManager from './APIManager';
import ToolbarManager from './plugins/util/ToolBarManager';
import TooltipManager from './plugins/util/TooltipManager';
import log from 'loglevel';
log.setDefaultLevel('debug');
// Default options
const defaultOptions = {
toolbarOrder: [
'ButtonsEvents_VOLUME_UP',
'ButtonsEvents_VOLUME_DOWN',
'ButtonsEvents_ROTATE',
'separator',
'unordered',
'separator',
'ButtonsEvents_RECENT_APP',
'ButtonsEvents_HOMEPAGE',
'ButtonsEvents_BACK',
'ButtonsEvents_POWER',
],
toolbarPosition: 'right',
floatingToolbar: true,
touch: true,
mouse: true,
volume: true,
rotation: true,
navbar: true,
power: true,
keyboard: true,
keyboardMapping: true,
fullscreen: true,
camera: true,
microphone: false,
fileUpload: true,
gappsInstall: true,
streamBitrate: false,
clipboard: true,
battery: true,
gps: true,
capture: true,
identifiers: true,
network: true,
mobilethrottling: false,
baseband: false,
phone: true,
streamResolution: true,
diskIO: true,
gamepad: true,
biometrics: true,
token: '',
i18n: {},
stun: {
urls: [
// TODO: remove what we don't have
'stun:stun-eu.genymotion.com:80',
'stun:stun-eu.genymotion.com:443',
'stun:stun-eu.genymotion.com:3478',
'stun:stun-eu.genymotion.com:5349',
'stun:stun-na.genymotion.com:80',
'stun:stun-na.genymotion.com:443',
'stun:stun-na.genymotion.com:3478',
'stun:stun-na.genymotion.com:5349',
],
},
connectionFailedURL: '',
turn: {},
giveFeedbackLink: 'https://github.com/orgs/Genymobile/discussions',
};
/**
* Setup & create instances of the device renderer
*/
export default class DeviceRendererFactory {
constructor() {}
/**
* Setup a device renderer instance in the given dom element, for the device instance identified by its instanceWebRTCUrl.
*
* @param {HTMLElement|string} dom The DOM element (or its ID) to setup the device renderer into.
* @param {string} webRTCUrl WebRTC URL of the instance.
* @param {Object} options Various configuration options.
* @param {boolean} options.showPhoneBorder Show phone border. Default: false.
* @param {string} options.toolbarPosition Toolbar position. Default: 'right'. Available values: 'left', 'right'.
* @param {boolean} options.toolbarOrder Toolbar buttons order. Default: see defaultOptions.
* @param {boolean} options.touch Touch support activated. Default: true.
* @param {boolean} options.mouse Mouse support activated. Default: true.
* @param {boolean} options.volume Audio volume control support activated. Default: true.
* @param {boolean} options.rotation Screen rotation support activated. Default: true.
* @param {boolean} options.navbar Android navbar support activated. Default: true.
* @param {boolean} options.power Power control support activated. Default: true.
* @param {boolean} options.keyboard Keyboad support activated. Default: true.
* @param {boolean} options.keyboardMapping Keyboad mapping support activated. Default: true.
* @param {boolean} options.fullscreen Fullscreen support activated. Default: true.
* @param {boolean} options.camera Camera support activated. Default: true.
* @param {boolean} options.microphone Microphone support activated. Default: false.
* @param {boolean} options.fileUpload File upload support activated. Default: true.
* @param {boolean} options.gappsInstall gapps and APK install support activated. Default: true.
* @param {string} options.fileUploadUrl File upload URL. Required if fileUpload===true.
* @param {boolean} options.streamBitrate Stream bitrate control support activated. Default: false.
* @param {boolean} options.clipboard Clipboard forwarding support activated. Default: true.
* @param {boolean} options.battery Battery support activated. Default: true.
* @param {boolean} options.gps GPS support activated. Default: true.
* @param {boolean} options.capture Screen capture support activated. Default: true.
* @param {boolean} options.identifiers Identifiers (IMEI, etc...) support activated. Default: true.
* @param {boolean} options.network Network throttling support activated. Default: true.
* @param {boolean} options.mobilethrottling Mobile throttling support activated. Default: false.
* @param {boolean} options.baseband Baseband controll support activated. Default: false.
* @param {boolean} options.phone Baseband support activated. Default: true.
* @param {boolean} options.streamResolution Stream resolution control support activated. Default: true.
* @param {boolean} options.diskIO Disk I/O throttling support activated. Default: true.
* @param {boolean} options.gamepad Experimental gamepad support activated. Default: false.
* @param {string} options.token Instance access token (JWT). Default: ''.
* @param {Object} options.i18n Translations keys for the UI. Default: {}.
* @param {Object} options.stun WebRTC STUN servers configuration.
* @param {Array<string>} options.stun.urls WebRTC STUN servers URLs.
* @param {string} options.connectionFailedURL Redirection page in case of connection establishment error.
* @param {Object} options.turn WebRTC TURN servers configuration.
* @param {Array<string>} options.turn.urls WebRTC TURN servers URLs.
* @param {string} options.turn.username WebRTC TURN servers username.
* @param {string} options.turn.credential WebRTC TURN servers password.
* @param {boolean} options.turn.default Whether or not we should use the TURN servers by default. Default: false.
* @param {string} options.giveFeedbackLink URL to the feedback form. Default: 'https://github.com/orgs/Genymobile/discussions'
* @param {Object} RendererClass Class to be instanciated. Defaults to DeviceRenderer.
* @return {Array} An array of API for device renderer instance, see return of APIManager.getExposedApiFunctions.
*/
setupRenderer(dom, webRTCUrl, options, RendererClass = DeviceRenderer) {
if (typeof dom === 'string') {
dom = document.getElementById(dom);
}
if (typeof options === 'undefined') {
options = {};
}
options = defaultsDeep(options, defaultOptions);
options.webRTCUrl = webRTCUrl;
// if we have at least one button to setup, we will instantiate the "buttons" plugin
options.buttons = options.volume || options.rotation || options.navbar || options.power;
log.debug('Creating genymotion display on ' + webRTCUrl);
dom.classList.add('device-renderer-instance');
this.loadTemplate(dom, options);
const instance = new RendererClass(dom, options);
store(instance);
// Add a class to the wrapper when we are waiting for the stream to be ready in order to display a loader
instance.store.subscribe(
({isWebRTCConnectionReady}) => {
if (instance?.wrapper) {
if (isWebRTCConnectionReady) {
instance.wrapper.classList.remove('waitingForStream');
} else {
instance.wrapper.classList.add('waitingForStream');
}
}
},
['isWebRTCConnectionReady'],
);
instance.apiManager = new APIManager(instance);
instance.toolbarManager = new ToolbarManager(instance);
instance.tooltipManager = new TooltipManager(instance);
this.loadPlugins(instance);
this.loadToolbar(instance);
instance.onWebRTCReady();
return instance.apiManager.getExposedApiFunctions();
}
/**
* Loads HTML template.
*
* @param {HTMLElement} dom The DOM element to setup the device renderer into.
* @param {Object} options Various configuration options.
*/
loadTemplate(dom, options) {
dom.innerHTML = `
<div class="gm-wrapper waitingForStream ${options.showPhoneBorder ? 'phoneBorder' : ''}
${options.floatingToolbar ? 'floatingBarDisplayed' : ''}
toolbarPosition-${options.toolbarPosition}">
<div class="player-screen-wrapper">
<div class="gm-video-wrapper">
<video class="gm-video" autoplay preload="none">Your browser does not support the VIDEO tag</video>
${
options.showPhoneBorder
? '<div class="gm-phone-button"></div><div class="gm-phone-border"></div>'
: ''
}
</div>
${
options.floatingToolbar
? // eslint-disable-next-line max-len
'<div class="gm-floating-toolbar-wrapper"><div class="gm-floating-toolbar"><ul></ul></div></div>'
: ''
}
</div>
<div class="gm-toolbar-wrapper">
<div class="gm-toolbar">
<ul></ul>
</div>
</div>
</div>
`;
}
/**
* Initialize all the needed plugins.
*
* @param {DeviceRenderer} instance The DeviceRenderer instance reference to link into each plugin.
* @param {Object} options Various configuration options.
*/
loadPlugins(instance) {
/*
* Load instance dedicated plugins
*/
const pluginInitMap = [];
if (typeof instance.getPlugins === 'function') {
const plugins = instance.getPlugins();
if (Array.isArray(plugins)) {
pluginInitMap.push(...plugins);
}
}
const dependenciesLoaded = new Map();
pluginInitMap.forEach((plugin) => {
const args = plugin.params || [];
if (plugin.enabled) {
// load dependencies
if (plugin.dependencies) {
plugin.dependencies.forEach((Dep) => {
if (dependenciesLoaded.has(Dep)) {
return;
}
new Dep(instance);
dependenciesLoaded.set(Dep, true);
});
}
// eslint-disable-next-line no-unused-expressions
if (plugin.class) {
const widget = new plugin.class(instance, ...args);
instance.widgets.push(widget);
}
}
});
}
/**
* Load toolbar buttons in the specified order:
* Buttons listed in options.toolbarOrder will be rendered in the specified sequence.
* Buttons not included in options.toolbarOrder will be added at the end of the toolbar.
* If the keyword 'unordered' is specified in options.toolbarOrder, unlisted buttons will be inserted at the position of 'unordered'.
* @param {DeviceRenderer} instance The DeviceRenderer instance.
*/
loadToolbar(instance) {
const {toolbarOrder, floatingToolbar} = instance.options;
const orderMap = new Map(toolbarOrder.map((name, index) => [name, index]));
const orderedButtons = [];
const unorderedButtons = [];
instance.toolbarManager.buttonRegistry.forEach((value, key) => {
const order = orderMap.get(key);
if (typeof order !== 'undefined') {
orderedButtons.push({key, value, order});
} else {
unorderedButtons.push({key, value});
}
});
toolbarOrder.forEach((name, index) => {
if (name === 'separator') {
orderedButtons.push({
key: `separator-${index}`,
value: 'separator',
order: index,
});
}
});
orderedButtons.sort((a, b) => a.order - b.order);
const sortedToolbarItems = [];
if (toolbarOrder.includes('unordered')) {
const unOrderedIndex = toolbarOrder.findIndex((name) => name === 'unordered');
sortedToolbarItems.push(
...orderedButtons.slice(0, unOrderedIndex),
...unorderedButtons,
...orderedButtons.slice(unOrderedIndex),
);
} else {
sortedToolbarItems.push(...orderedButtons, ...unorderedButtons);
}
// If floatingToolbar is true, we need to extract the floatingTool element from the toolbarOrder
if (floatingToolbar) {
const floatingToolbarOrder = [
'ButtonsEvents_ROTATE',
'separator',
'Screenshot',
'separator',
'Screenrecord',
'separator',
'Fullscreen',
];
// Extract the floatingToolbarOrder from the toolbarOrder
const filteredToolbarItems = sortedToolbarItems.filter((item) => !floatingToolbarOrder.includes(item.key));
sortedToolbarItems.length = 0;
sortedToolbarItems.push(...filteredToolbarItems);
floatingToolbarOrder.forEach((name) => {
if (name === 'separator') {
instance.toolbarManager.renderSeparator(true);
} else {
instance.toolbarManager.renderButton(name, true);
}
});
}
sortedToolbarItems.forEach(({key, value}) => {
if (value === 'separator') {
instance.toolbarManager.renderSeparator();
} else {
instance.toolbarManager.renderButton(key);
}
});
}
}