@flashphoner/websdk 2.0.271 → 2.0.274
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docTemplate/README.md +1 -1
- package/examples/demo/streaming/media_devices_manager/manager.js +83 -80
- package/flashphoner-no-flash.js +286 -73
- package/flashphoner-no-flash.min.js +2 -2
- package/flashphoner-no-webrtc.js +85 -33
- package/flashphoner-no-webrtc.min.js +2 -2
- package/flashphoner-no-wsplayer.js +286 -73
- package/flashphoner-no-wsplayer.min.js +2 -2
- package/flashphoner-room-api-webrtc-only.js +286 -73
- package/flashphoner-room-api-webrtc-only.min.js +1 -1
- package/flashphoner-room-api.js +221 -72
- package/flashphoner-room-api.min.js +2 -2
- package/flashphoner-temasys-flash-websocket-without-adapterjs.js +85 -33
- package/flashphoner-temasys-flash-websocket.js +85 -33
- package/flashphoner-temasys-flash-websocket.min.js +1 -1
- package/flashphoner-webrtc-only.js +286 -73
- package/flashphoner-webrtc-only.min.js +1 -1
- package/flashphoner.js +286 -73
- package/flashphoner.min.js +2 -2
- package/package.json +1 -1
- package/src/flashphoner-core.js +67 -27
- package/src/stats-collector.js +12 -1
- package/src/webrtc-media-provider.js +142 -44
package/flashphoner-room-api.js
CHANGED
|
@@ -9679,6 +9679,34 @@ var getMediaDevices = function (mediaProvider, labels, kind, deviceConstraints)
|
|
|
9679
9679
|
return MediaProvider[mediaProvider].listDevices(labels, kind, deviceConstraints);
|
|
9680
9680
|
};
|
|
9681
9681
|
|
|
9682
|
+
/**
|
|
9683
|
+
* Get mobile local media devices
|
|
9684
|
+
*
|
|
9685
|
+
* @param {String=} mediaProvider Media provider that will be asked for device list
|
|
9686
|
+
* @param {Flashphoner.constants.MEDIA_DEVICE_KIND} kind Media devices kind to access:
|
|
9687
|
+
* MEDIA_DEVICE_KIND.INPUT (default) get access to input devices only (camera, mic).
|
|
9688
|
+
* MEDIA_DEVICE_KIND.OUTPUT get access to output devices only (speaker, headphone).
|
|
9689
|
+
* MEDIA_DEVICE_KIND.ALL get access to all devices (cam, mic, speaker, headphone).
|
|
9690
|
+
* @param {Object=} deviceConstraints
|
|
9691
|
+
* If {audio: true, video: false}, then access to the camera will not be requested.
|
|
9692
|
+
* If {audio: false, video: true}, then access to the microphone will not be requested.
|
|
9693
|
+
* @returns {Promise.<Flashphoner.MediaDeviceList>} Promise with media device list on fulfill
|
|
9694
|
+
* @throws {Error} Error if API is not initialized
|
|
9695
|
+
* @memberof Flashphoner
|
|
9696
|
+
*/
|
|
9697
|
+
var getMobileDevices = function (mediaProvider, kind, deviceConstraints) {
|
|
9698
|
+
if (!initialized) {
|
|
9699
|
+
throw new Error("Flashphoner API is not initialized");
|
|
9700
|
+
}
|
|
9701
|
+
if (!mediaProvider) {
|
|
9702
|
+
mediaProvider = getMediaProviders()[0];
|
|
9703
|
+
}
|
|
9704
|
+
if (MediaProvider[mediaProvider].getMobileDevices) {
|
|
9705
|
+
return MediaProvider[mediaProvider].getMobileDevices(kind, deviceConstraints);
|
|
9706
|
+
}
|
|
9707
|
+
return [];
|
|
9708
|
+
};
|
|
9709
|
+
|
|
9682
9710
|
/**
|
|
9683
9711
|
* Get access to local media
|
|
9684
9712
|
*
|
|
@@ -10116,33 +10144,19 @@ var createSession = function (options) {
|
|
|
10116
10144
|
streamRefreshHandlers[obj.mediaSessionId](obj);
|
|
10117
10145
|
}
|
|
10118
10146
|
break;
|
|
10119
|
-
case
|
|
10120
|
-
|
|
10121
|
-
|
|
10122
|
-
|
|
10123
|
-
|
|
10124
|
-
|
|
10125
|
-
|
|
10126
|
-
}
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
}
|
|
10133
|
-
if (obj.sampling) {
|
|
10134
|
-
webRTCMetricsServerDescription.sampling = obj.sampling;
|
|
10135
|
-
}
|
|
10136
|
-
if (obj.types) {
|
|
10137
|
-
webRTCMetricsServerDescription.types = obj.types;
|
|
10138
|
-
}
|
|
10139
|
-
if (obj.collect) {
|
|
10140
|
-
webRTCMetricsServerDescription.collect = obj.collect;
|
|
10141
|
-
}
|
|
10142
|
-
for (const [id, handler] of Object.entries(streamRefreshHandlers)) {
|
|
10143
|
-
handler(obj);
|
|
10144
|
-
}
|
|
10145
|
-
}
|
|
10147
|
+
case 'webRTCMetricsDescriptionUpdate':
|
|
10148
|
+
handleWebRTCMetricsUpdate(obj, {
|
|
10149
|
+
compression: "compression",
|
|
10150
|
+
batchSize: "batchSize",
|
|
10151
|
+
sampling: "sampling",
|
|
10152
|
+
types: "types",
|
|
10153
|
+
collect: "collect"
|
|
10154
|
+
});
|
|
10155
|
+
break;
|
|
10156
|
+
case 'webRTCMetricsTokenRefresh':
|
|
10157
|
+
handleWebRTCMetricsUpdate(obj, {
|
|
10158
|
+
authorization: "authorization"
|
|
10159
|
+
});
|
|
10146
10160
|
break;
|
|
10147
10161
|
default:
|
|
10148
10162
|
logger.info(LOG_PREFIX, "Unknown server message " + data.message);
|
|
@@ -10153,6 +10167,26 @@ var createSession = function (options) {
|
|
|
10153
10167
|
};
|
|
10154
10168
|
}
|
|
10155
10169
|
|
|
10170
|
+
function handleWebRTCMetricsUpdate(obj, updateFields = {}) {
|
|
10171
|
+
if (obj.ids) {
|
|
10172
|
+
obj.ids.forEach((id) => {
|
|
10173
|
+
if (streamRefreshHandlers[id]) {
|
|
10174
|
+
streamRefreshHandlers[id](obj);
|
|
10175
|
+
}
|
|
10176
|
+
});
|
|
10177
|
+
} else {
|
|
10178
|
+
Object.entries(updateFields).forEach(([key, value]) => {
|
|
10179
|
+
if (obj[value] !== undefined) {
|
|
10180
|
+
webRTCMetricsServerDescription[key] = obj[value];
|
|
10181
|
+
}
|
|
10182
|
+
});
|
|
10183
|
+
|
|
10184
|
+
for (const [id, handler] of Object.entries(streamRefreshHandlers)) {
|
|
10185
|
+
handler(obj);
|
|
10186
|
+
}
|
|
10187
|
+
}
|
|
10188
|
+
}
|
|
10189
|
+
|
|
10156
10190
|
//WebSocket send helper
|
|
10157
10191
|
function send(message, data) {
|
|
10158
10192
|
if (wsConnection.readyState === WebSocket.OPEN) {
|
|
@@ -11306,6 +11340,11 @@ var createSession = function (options) {
|
|
|
11306
11340
|
}
|
|
11307
11341
|
}
|
|
11308
11342
|
|
|
11343
|
+
if (streamInfo.authorization && statsCollector && statsCollector.description.ingestPoint) {
|
|
11344
|
+
statsCollector.description.authorization = streamInfo.authorization;
|
|
11345
|
+
statsCollector.updateHttpConnection(statsCollector.description.ingestPoint, statsCollector.description.authorization);
|
|
11346
|
+
}
|
|
11347
|
+
|
|
11309
11348
|
// Pause or resume metrics collection
|
|
11310
11349
|
if (!streamInfo.status && streamInfo.collect !== undefined && statsCollector) {
|
|
11311
11350
|
statsCollector.update(streamInfo);
|
|
@@ -12526,6 +12565,7 @@ module.exports = {
|
|
|
12526
12565
|
isUsingTemasys: isUsingTemasys,
|
|
12527
12566
|
getMediaProviders: getMediaProviders,
|
|
12528
12567
|
getMediaDevices: getMediaDevices,
|
|
12568
|
+
getMobileDevices: getMobileDevices,
|
|
12529
12569
|
getMediaAccess: getMediaAccess,
|
|
12530
12570
|
releaseLocalMedia: releaseLocalMedia,
|
|
12531
12571
|
getSessions: getSessions,
|
|
@@ -13333,6 +13373,11 @@ const StreamStatsCollector = function(description, id, mediaConnection, wsConnec
|
|
|
13333
13373
|
}
|
|
13334
13374
|
}
|
|
13335
13375
|
},
|
|
13376
|
+
updateHttpConnection: function(url, authorization) {
|
|
13377
|
+
if (url.startsWith(CONNECTION_TYPE.HTTP) && authorization) {
|
|
13378
|
+
statCollector.connection.http.setAuthorization(authorization);
|
|
13379
|
+
}
|
|
13380
|
+
},
|
|
13336
13381
|
checkForCompression: async function(compression) {
|
|
13337
13382
|
try {
|
|
13338
13383
|
await util.compress(compression, "test", false);
|
|
@@ -13373,6 +13418,9 @@ const StreamStatsCollector = function(description, id, mediaConnection, wsConnec
|
|
|
13373
13418
|
statCollector.metricsBatch = null;
|
|
13374
13419
|
}
|
|
13375
13420
|
},
|
|
13421
|
+
isMetricValid: function(value) {
|
|
13422
|
+
return value != null && value !== "" && value !== "undefined" && value !== "null";
|
|
13423
|
+
},
|
|
13376
13424
|
collectMetrics: async function() {
|
|
13377
13425
|
if (statCollector.timer && !statCollector.timerBusy) {
|
|
13378
13426
|
// Unfortunately there are no real atomics in JS unless SharedArrayBuffer is used
|
|
@@ -13402,7 +13450,7 @@ const StreamStatsCollector = function(description, id, mediaConnection, wsConnec
|
|
|
13402
13450
|
}
|
|
13403
13451
|
}
|
|
13404
13452
|
}
|
|
13405
|
-
if (value) {
|
|
13453
|
+
if (statCollector.isMetricValid(value)) {
|
|
13406
13454
|
metrics.push(value);
|
|
13407
13455
|
} else {
|
|
13408
13456
|
lostMetrics.push(descriptor);
|
|
@@ -13576,6 +13624,9 @@ const HttpConnection = function(url, headers) {
|
|
|
13576
13624
|
const connection = {
|
|
13577
13625
|
url: addSlash(url),
|
|
13578
13626
|
headers: headers,
|
|
13627
|
+
setAuthorization(token) {
|
|
13628
|
+
this.headers.Authorization = token;
|
|
13629
|
+
},
|
|
13579
13630
|
send: async function(message, data) {
|
|
13580
13631
|
let code = CONNECTION_STATUS.BAD_REQUEST;
|
|
13581
13632
|
if (connection.url) {
|
|
@@ -16344,6 +16395,63 @@ var available = function () {
|
|
|
16344
16395
|
return ('getUserMedia' in navigator && 'RTCPeerConnection' in window);
|
|
16345
16396
|
};
|
|
16346
16397
|
|
|
16398
|
+
/**
|
|
16399
|
+
* Helper function to get media devices list in id, label, type form
|
|
16400
|
+
*
|
|
16401
|
+
* @param devices
|
|
16402
|
+
* @param kind
|
|
16403
|
+
* @param videoFilter
|
|
16404
|
+
* @returns {{audio: *[], video: *[]}}
|
|
16405
|
+
*/
|
|
16406
|
+
const getList = function (devices, kind, videoFilter = null) {
|
|
16407
|
+
var list = {
|
|
16408
|
+
audio: [],
|
|
16409
|
+
video: []
|
|
16410
|
+
};
|
|
16411
|
+
|
|
16412
|
+
var micCount = 0;
|
|
16413
|
+
var outputCount = 0;
|
|
16414
|
+
var camCount = 0;
|
|
16415
|
+
for (var i = 0; i < devices.length; i++) {
|
|
16416
|
+
var device = devices[i];
|
|
16417
|
+
var ret = {
|
|
16418
|
+
id: device.deviceId,
|
|
16419
|
+
label: device.label
|
|
16420
|
+
};
|
|
16421
|
+
if (device.kind.indexOf("audio" + kind) === 0 && device.deviceId !== "communications") {
|
|
16422
|
+
ret.type = (device.kind === "audioinput") ? "mic" : "speaker";
|
|
16423
|
+
if (ret.type === "mic" && ret.label === "") {
|
|
16424
|
+
ret.label = 'microphone' + ++micCount;
|
|
16425
|
+
}
|
|
16426
|
+
if (ret.type === "speaker" && ret.label === "") {
|
|
16427
|
+
ret.label = 'speaker' + ++outputCount;
|
|
16428
|
+
}
|
|
16429
|
+
list.audio.push(ret);
|
|
16430
|
+
} else if (device.kind.indexOf("video" + kind) === 0) {
|
|
16431
|
+
if (!videoFilter || videoFilter.find((id) => id === device.deviceId)) {
|
|
16432
|
+
if (ret.label === "") {
|
|
16433
|
+
ret.label = 'camera' + ++camCount;
|
|
16434
|
+
}
|
|
16435
|
+
ret.type = "camera";
|
|
16436
|
+
list.video.push(ret);
|
|
16437
|
+
} else {
|
|
16438
|
+
logger.debug(LOG_PREFIX, "Video device " + device.deviceId + "does not conform the filter " + JSON.stringify(videoFilter));
|
|
16439
|
+
}
|
|
16440
|
+
} else {
|
|
16441
|
+
logger.debug(LOG_PREFIX, "unknown device " + device.kind + " id " + device.deviceId);
|
|
16442
|
+
}
|
|
16443
|
+
}
|
|
16444
|
+
return list;
|
|
16445
|
+
}
|
|
16446
|
+
|
|
16447
|
+
/**
|
|
16448
|
+
* Get media devices list
|
|
16449
|
+
*
|
|
16450
|
+
* @param labels
|
|
16451
|
+
* @param kind
|
|
16452
|
+
* @param deviceConstraints
|
|
16453
|
+
* @returns {Promise<{audio: [], video: []}>}
|
|
16454
|
+
*/
|
|
16347
16455
|
var listDevices = function (labels, kind, deviceConstraints) {
|
|
16348
16456
|
//WCS-1963. added deviceConstraints.
|
|
16349
16457
|
if (!deviceConstraints) {
|
|
@@ -16354,7 +16462,7 @@ var listDevices = function (labels, kind, deviceConstraints) {
|
|
|
16354
16462
|
}
|
|
16355
16463
|
if (!kind) {
|
|
16356
16464
|
kind = constants.MEDIA_DEVICE_KIND.INPUT;
|
|
16357
|
-
} else if (kind
|
|
16465
|
+
} else if (kind === constants.MEDIA_DEVICE_KIND.ALL) {
|
|
16358
16466
|
kind = "";
|
|
16359
16467
|
}
|
|
16360
16468
|
var getConstraints = function (devices) {
|
|
@@ -16362,9 +16470,9 @@ var listDevices = function (labels, kind, deviceConstraints) {
|
|
|
16362
16470
|
for (var i = 0; i < devices.length; i++) {
|
|
16363
16471
|
var device = devices[i];
|
|
16364
16472
|
if (device.kind.indexOf("audio" + kind) === 0 && deviceConstraints.audio) {
|
|
16365
|
-
constraints.audio =
|
|
16473
|
+
constraints.audio = deviceConstraints.audio;
|
|
16366
16474
|
} else if (device.kind.indexOf("video" + kind) === 0 && deviceConstraints.video) {
|
|
16367
|
-
constraints.video =
|
|
16475
|
+
constraints.video = deviceConstraints.video;
|
|
16368
16476
|
} else {
|
|
16369
16477
|
logger.debug(LOG_PREFIX, "unknown device " + device.kind + " id " + device.deviceId);
|
|
16370
16478
|
}
|
|
@@ -16372,43 +16480,6 @@ var listDevices = function (labels, kind, deviceConstraints) {
|
|
|
16372
16480
|
return constraints;
|
|
16373
16481
|
};
|
|
16374
16482
|
|
|
16375
|
-
var getList = function (devices) {
|
|
16376
|
-
var list = {
|
|
16377
|
-
audio: [],
|
|
16378
|
-
video: []
|
|
16379
|
-
};
|
|
16380
|
-
|
|
16381
|
-
var micCount = 0;
|
|
16382
|
-
var outputCount = 0;
|
|
16383
|
-
var camCount = 0;
|
|
16384
|
-
for (var i = 0; i < devices.length; i++) {
|
|
16385
|
-
var device = devices[i];
|
|
16386
|
-
var ret = {
|
|
16387
|
-
id: device.deviceId,
|
|
16388
|
-
label: device.label
|
|
16389
|
-
};
|
|
16390
|
-
if (device.kind.indexOf("audio" + kind) === 0 && device.deviceId != "communications") {
|
|
16391
|
-
ret.type = (device.kind == "audioinput") ? "mic" : "speaker";
|
|
16392
|
-
if (ret.type == "mic" && ret.label == "") {
|
|
16393
|
-
ret.label = 'microphone' + ++micCount;
|
|
16394
|
-
}
|
|
16395
|
-
if (ret.type == "speaker" && ret.label == "") {
|
|
16396
|
-
ret.label = 'speaker' + ++outputCount;
|
|
16397
|
-
}
|
|
16398
|
-
list.audio.push(ret);
|
|
16399
|
-
} else if (device.kind.indexOf("video" + kind) === 0) {
|
|
16400
|
-
if (ret.label == "") {
|
|
16401
|
-
ret.label = 'camera' + ++camCount;
|
|
16402
|
-
}
|
|
16403
|
-
ret.type = "camera";
|
|
16404
|
-
list.video.push(ret);
|
|
16405
|
-
} else {
|
|
16406
|
-
logger.debug(LOG_PREFIX, "unknown device " + device.kind + " id " + device.deviceId);
|
|
16407
|
-
}
|
|
16408
|
-
}
|
|
16409
|
-
return list;
|
|
16410
|
-
};
|
|
16411
|
-
|
|
16412
16483
|
return new Promise(function (resolve, reject) {
|
|
16413
16484
|
navigator.mediaDevices.enumerateDevices().then(function (devices) {
|
|
16414
16485
|
if (labels) {
|
|
@@ -16420,19 +16491,96 @@ var listDevices = function (labels, kind, deviceConstraints) {
|
|
|
16420
16491
|
}
|
|
16421
16492
|
navigator.getUserMedia(constraints, function (stream) {
|
|
16422
16493
|
navigator.mediaDevices.enumerateDevices().then(function (devicesWithLabels) {
|
|
16423
|
-
resolve(getList(devicesWithLabels));
|
|
16494
|
+
resolve(getList(devicesWithLabels, kind));
|
|
16424
16495
|
stream.getTracks().forEach(function (track) {
|
|
16425
16496
|
track.stop();
|
|
16426
16497
|
});
|
|
16427
16498
|
}, reject);
|
|
16428
16499
|
}, reject);
|
|
16429
16500
|
} else {
|
|
16430
|
-
resolve(getList(devices));
|
|
16501
|
+
resolve(getList(devices, kind));
|
|
16431
16502
|
}
|
|
16432
16503
|
}, reject);
|
|
16433
|
-
|
|
16434
16504
|
});
|
|
16435
|
-
}
|
|
16505
|
+
}
|
|
16506
|
+
|
|
16507
|
+
const getMobileDevices = async function (kind, deviceConstraints = null) {
|
|
16508
|
+
let constraints = {};
|
|
16509
|
+
let videoFilter = [];
|
|
16510
|
+
let list = null;
|
|
16511
|
+
if (!kind) {
|
|
16512
|
+
kind = constants.MEDIA_DEVICE_KIND.INPUT;
|
|
16513
|
+
} else if (kind === constants.MEDIA_DEVICE_KIND.ALL) {
|
|
16514
|
+
kind = "";
|
|
16515
|
+
}
|
|
16516
|
+
if (deviceConstraints && deviceConstraints.audio) {
|
|
16517
|
+
constraints.audio = deviceConstraints.audio;
|
|
16518
|
+
} else {
|
|
16519
|
+
constraints.audio = true;
|
|
16520
|
+
}
|
|
16521
|
+
if (deviceConstraints && deviceConstraints.video) {
|
|
16522
|
+
if (typeof deviceConstraints.video === 'object') {
|
|
16523
|
+
constraints.video = deviceConstraints.video;
|
|
16524
|
+
}
|
|
16525
|
+
else {
|
|
16526
|
+
constraints.video = {};
|
|
16527
|
+
}
|
|
16528
|
+
} else {
|
|
16529
|
+
constraints.video = {};
|
|
16530
|
+
}
|
|
16531
|
+
|
|
16532
|
+
const getCamera = async function (constraints, facingMode) {
|
|
16533
|
+
let deviceId = null;
|
|
16534
|
+
let mediaConstraints = {
|
|
16535
|
+
audio: false,
|
|
16536
|
+
video: constraints.video
|
|
16537
|
+
};
|
|
16538
|
+
mediaConstraints.video.facingMode = facingMode;
|
|
16539
|
+
try {
|
|
16540
|
+
stream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
|
|
16541
|
+
if (stream) {
|
|
16542
|
+
if (stream.getVideoTracks().length > 0) {
|
|
16543
|
+
deviceId = stream.getVideoTracks()[0].getSettings().deviceId;
|
|
16544
|
+
}
|
|
16545
|
+
stream.getTracks().forEach((track) => {
|
|
16546
|
+
track.stop();
|
|
16547
|
+
});
|
|
16548
|
+
}
|
|
16549
|
+
} catch (error) {
|
|
16550
|
+
logger.error(LOG_PREFIX, "Can't get device access with video constraints " + JSON.stringify(constraints.video) + ", error " + error);
|
|
16551
|
+
}
|
|
16552
|
+
return deviceId;
|
|
16553
|
+
}
|
|
16554
|
+
|
|
16555
|
+
let front = await getCamera(constraints, { ideal: 'user' });
|
|
16556
|
+
if (front && front !== "") {
|
|
16557
|
+
logger.debug(LOG_PREFIX, "Front camera id: " + front);
|
|
16558
|
+
videoFilter.push(front);
|
|
16559
|
+
}
|
|
16560
|
+
let back = await getCamera(constraints, { ideal: 'environment' });
|
|
16561
|
+
if (back && back !== "") {
|
|
16562
|
+
logger.debug(LOG_PREFIX, "Back camera id: " + back);
|
|
16563
|
+
videoFilter.push(back);
|
|
16564
|
+
}
|
|
16565
|
+
|
|
16566
|
+
try {
|
|
16567
|
+
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
16568
|
+
if (stream) {
|
|
16569
|
+
const mediaDevices = await navigator.mediaDevices.enumerateDevices();
|
|
16570
|
+
if (mediaDevices) {
|
|
16571
|
+
logger.debug(LOG_PREFIX, "mediaDevices: " + JSON.stringify(mediaDevices));
|
|
16572
|
+
list = getList(mediaDevices, kind, videoFilter);
|
|
16573
|
+
}
|
|
16574
|
+
stream.getTracks().forEach(function (track) {
|
|
16575
|
+
track.stop();
|
|
16576
|
+
});
|
|
16577
|
+
}
|
|
16578
|
+
} catch (error) {
|
|
16579
|
+
logger.error(LOG_PREFIX, "Can't get device access with constraints " + JSON.stringify(constraints) + ", error " + error);
|
|
16580
|
+
}
|
|
16581
|
+
|
|
16582
|
+
return list;
|
|
16583
|
+
}
|
|
16436
16584
|
|
|
16437
16585
|
function normalizeConstraints(constraints) {
|
|
16438
16586
|
//WCS-2010. fixed TypeError after publish->stop->publish
|
|
@@ -16554,6 +16702,7 @@ module.exports = {
|
|
|
16554
16702
|
getMediaAccess: getMediaAccess,
|
|
16555
16703
|
releaseMedia: releaseMedia,
|
|
16556
16704
|
listDevices: listDevices,
|
|
16705
|
+
getMobileDevices: getMobileDevices,
|
|
16557
16706
|
playFirstSound: playFirstSound,
|
|
16558
16707
|
playFirstVideo: playFirstVideo,
|
|
16559
16708
|
available: available,
|