@flashphoner/websdk 2.0.260 → 2.0.261

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flashphoner/websdk",
3
- "version": "2.0.260",
3
+ "version": "2.0.261",
4
4
  "description": "Official Flashphoner WebCallServer WebSDK package",
5
5
  "main": "./src/flashphoner-core.js",
6
6
  "types": "./src/flashphoner-core.d.ts",
@@ -645,6 +645,12 @@ export const ERROR_INFO: Readonly<{
645
645
  * @memberOf Flashphoner.constants.ERROR_INFO
646
646
  */
647
647
  CAN_NOT_SET_RESOLUTION: string;
648
+ /**
649
+ * Error if cannot get peer connection stats
650
+ * @event CAN_NOT_GET_STATS
651
+ * @memberOf Flashphoner.constants.ERROR_INFO
652
+ */
653
+ CAN_NOT_GET_STATS: string;
648
654
  /**
649
655
  * Local browser error detected
650
656
  * @event LOCAL_ERROR
package/src/constants.js CHANGED
@@ -654,6 +654,12 @@ const ERROR_INFO = Object.freeze({
654
654
  * @memberOf Flashphoner.constants.ERROR_INFO
655
655
  */
656
656
  CAN_NOT_SET_RESOLUTION: 'Cannot switch a published stream resolution',
657
+ /**
658
+ * Error if cannot get peer connection stats
659
+ * @event CAN_NOT_GET_STATS
660
+ * @memberOf Flashphoner.constants.ERROR_INFO
661
+ */
662
+ CAN_NOT_GET_STATS: 'Cannot get PeerConnection stats',
657
663
  /**
658
664
  * Local browser error detected
659
665
  * @event LOCAL_ERROR
@@ -5,6 +5,7 @@ const constants = require("./constants");
5
5
  const util = require('./util');
6
6
  const LoggerObject = require('./util').logger;
7
7
  const clientInfo = require('./client-info');
8
+ const StatsCollector = require('./stats-collector');
8
9
  const Promise = require('promise-polyfill');
9
10
  const KalmanFilter = require('kalmanjs');
10
11
  const browserDetails = require('webrtc-adapter').default.browserDetails;
@@ -519,6 +520,9 @@ var createSession = function (options) {
519
520
 
520
521
  var wsConnection;
521
522
 
523
+ // WebRTC metrics sending description
524
+ let webRTCMetricsServerDescription;
525
+
522
526
  if (lbUrl) {
523
527
  requestURL(lbUrl);
524
528
  } else {
@@ -621,6 +625,7 @@ var createSession = function (options) {
621
625
  case 'getUserData':
622
626
  authToken = obj.authToken;
623
627
  cConfig = obj;
628
+ webRTCMetricsServerDescription = obj.webRTCMetricsServerDescription;
624
629
  onSessionStatusChange(SESSION_STATUS.ESTABLISHED, obj);
625
630
  break;
626
631
  case 'setRemoteSDP':
@@ -725,6 +730,34 @@ var createSession = function (options) {
725
730
  streamRefreshHandlers[obj.mediaSessionId](obj);
726
731
  }
727
732
  break;
733
+ case `webRTCMetricsDescriptionUpdate`:
734
+ if (obj.ids) {
735
+ obj.ids.forEach((id) => {
736
+ if (streamRefreshHandlers[id]) {
737
+ streamRefreshHandlers[id](obj);
738
+ }
739
+ });
740
+ } else {
741
+ if (obj.compression) {
742
+ webRTCMetricsServerDescription.compression = obj.compression;
743
+ }
744
+ if (obj.batchSize) {
745
+ webRTCMetricsServerDescription.batchSize = obj.batchSize;
746
+ }
747
+ if (obj.sampling) {
748
+ webRTCMetricsServerDescription.sampling = obj.sampling;
749
+ }
750
+ if (obj.types) {
751
+ webRTCMetricsServerDescription.types = obj.types;
752
+ }
753
+ if (obj.collect) {
754
+ webRTCMetricsServerDescription.collect = obj.collect;
755
+ }
756
+ for (const [id, handler] of Object.entries(streamRefreshHandlers)) {
757
+ handler(obj);
758
+ }
759
+ }
760
+ break;
728
761
  default:
729
762
  logger.info(LOG_PREFIX, "Unknown server message " + data.message);
730
763
  }
@@ -736,10 +769,12 @@ var createSession = function (options) {
736
769
 
737
770
  //WebSocket send helper
738
771
  function send(message, data) {
739
- wsConnection.send(JSON.stringify({
740
- message: message,
741
- data: [data]
742
- }));
772
+ if (wsConnection.readyState === WebSocket.OPEN) {
773
+ wsConnection.send(JSON.stringify({
774
+ message: message,
775
+ data: [data]
776
+ }));
777
+ }
743
778
  }
744
779
 
745
780
  //Session status update helper
@@ -1773,6 +1808,8 @@ var createSession = function (options) {
1773
1808
 
1774
1809
  var videoBytes = 0;
1775
1810
 
1811
+ var statsCollector = null;
1812
+
1776
1813
  /**
1777
1814
  * Represents media stream.
1778
1815
  *
@@ -1807,7 +1844,7 @@ var createSession = function (options) {
1807
1844
  return;
1808
1845
  }
1809
1846
 
1810
- if (streamInfo.available != undefined) {
1847
+ if (streamInfo.available !== undefined) {
1811
1848
  for (var i = 0; i < availableCallbacks.length; i++) {
1812
1849
  info_ = streamInfo.reason;
1813
1850
  if (streamInfo.available == "true") {
@@ -1866,10 +1903,28 @@ var createSession = function (options) {
1866
1903
  if (mediaConnection) {
1867
1904
  mediaConnection.close(cacheLocalResources);
1868
1905
  }
1906
+ if (statsCollector) {
1907
+ statsCollector.stop();
1908
+ statsCollector = null;
1909
+ }
1869
1910
  }
1870
1911
  if (record_ && typeof streamInfo.recordName !== 'undefined') {
1871
1912
  recordFileName = streamInfo.recordName;
1872
1913
  }
1914
+
1915
+ // Set up metrics collection
1916
+ if (event === STREAM_STATUS.PUBLISHING || event === STREAM_STATUS.PLAYING) {
1917
+ if (webRTCMetricsServerDescription && !statsCollector) {
1918
+ statsCollector = StatsCollector.StreamStatsCollector(webRTCMetricsServerDescription, id_, mediaConnection, wsConnection, logger);
1919
+ statsCollector.start();
1920
+ }
1921
+ }
1922
+
1923
+ // Pause or resume metrics collection
1924
+ if (!streamInfo.status && streamInfo.collect !== undefined && statsCollector) {
1925
+ statsCollector.update(streamInfo);
1926
+ }
1927
+
1873
1928
  //fire stream event
1874
1929
  if (callbacks[event]) {
1875
1930
  callbacks[event](stream);
@@ -0,0 +1,318 @@
1
+ 'use strict';
2
+
3
+ const util = require('./util');
4
+ const LOG_PREFIX = "stats-collector";
5
+
6
+ // Collect and send WebRTC statistics periodically
7
+ const StreamStatsCollector = function(description, id, mediaConnection, wsConnection, logger) {
8
+ let statCollector = {
9
+ description: description,
10
+ id: id,
11
+ mediaConnection: mediaConnection,
12
+ wsConnection: wsConnection,
13
+ logger: getLogger(logger),
14
+ headers: "",
15
+ compression: "none",
16
+ metricsBatch: null,
17
+ timer: null,
18
+ batchCount: 0,
19
+ start: async function() {
20
+ let error = "Can't collect WebRTC stats to send: ";
21
+ if (!statCollector.description.types) {
22
+ throw new Error(error + "no report types defined");
23
+ }
24
+ if (!statCollector.description.sampling) {
25
+ throw new Error(error + "no sampling interval defined");
26
+ }
27
+ if (!statCollector.description.batchSize) {
28
+ throw new Error(error + "no metrics batch size defined");
29
+ }
30
+ if (!statCollector.mediaConnection) {
31
+ throw new Error(error + "no media connection available");
32
+ }
33
+ if (!statCollector.wsConnection) {
34
+ throw new Error(error + "no websocket connection available");
35
+ }
36
+
37
+ await statCollector.updateHeaders();
38
+ await statCollector.updateCompression();
39
+ statCollector.sendHeaders();
40
+ if (statCollector.description.collect === "on") {
41
+ statCollector.collect(true);
42
+ }
43
+ },
44
+ collect: function(enable) {
45
+ if (enable) {
46
+ statCollector.startTimer();
47
+ } else {
48
+ statCollector.stopTimer();
49
+ }
50
+ },
51
+ stop: function() {
52
+ statCollector.stopTimer();
53
+ statCollector.headers = "";
54
+ },
55
+ update: async function(description) {
56
+ if (!description) {
57
+ if (statCollector.logger) {
58
+ statCollector.logger.error(LOG_PREFIX, "Can't update WebRTC metrics sending: no parameters passed");
59
+ return;
60
+ }
61
+ }
62
+ if (description.types || description.compression) {
63
+ statCollector.stop();
64
+ if (description.types) {
65
+ statCollector.description.types = description.types;
66
+ await statCollector.updateHeaders();
67
+ }
68
+ if (description.compression) {
69
+ statCollector.description.compression = description.compression;
70
+ await statCollector.updateCompression();
71
+ }
72
+ statCollector.sendHeaders();
73
+ } else {
74
+ statCollector.collect(false);
75
+ }
76
+ if (description.batchSize) {
77
+ statCollector.description.batchSize = description.batchSize;
78
+ }
79
+ if (description.sampling) {
80
+ statCollector.description.sampling = description.sampling;
81
+ }
82
+ if (description.collect) {
83
+ statCollector.description.collect = description.collect;
84
+ }
85
+ switch(statCollector.description.collect) {
86
+ case "on":
87
+ statCollector.collect(true);
88
+ break;
89
+ case "off":
90
+ statCollector.collect(false);
91
+ break;
92
+ }
93
+ },
94
+ updateHeaders: async function() {
95
+ let stats = await statCollector.mediaConnection.getWebRTCStats();
96
+ Object.keys(statCollector.description.types).forEach((type) => {
97
+ let typeDescriptor = statCollector.description.types[type];
98
+ let metricsString = "";
99
+ let contentFilters = null;
100
+ if (typeDescriptor.metrics) {
101
+ metricsString = typeDescriptor.metrics;
102
+ }
103
+ if (typeDescriptor.contains) {
104
+ contentFilters = typeDescriptor.contains;
105
+ }
106
+ if (stats[type]) {
107
+ stats[type].forEach((report) => {
108
+ statCollector.logger.debug(LOG_PREFIX, type + " report: " + JSON.stringify(report));
109
+ if (contentFilters) {
110
+ let filtersMatched = true;
111
+ for (const filter in contentFilters) {
112
+ statCollector.logger.debug(LOG_PREFIX, type + " filter by " + filter + ": " + JSON.stringify(contentFilters[filter]));
113
+ let filterMatched = false;
114
+ if (report[filter]) {
115
+ for (const value of contentFilters[filter]) {
116
+ statCollector.logger.debug(LOG_PREFIX, filter + ": " + value + " <> " + report[filter]);
117
+ if (report[filter] === value) {
118
+ filterMatched = true;
119
+ break;
120
+ }
121
+ }
122
+ }
123
+ filtersMatched = filtersMatched && filterMatched;
124
+ if (!filterMatched) {
125
+ break;
126
+ }
127
+ }
128
+ if (filtersMatched) {
129
+ statCollector.addHeaders(report, metricsString);
130
+ }
131
+ } else {
132
+ statCollector.addHeaders(report, metricsString);
133
+ }
134
+ });
135
+ } else {
136
+ statCollector.logger.warn(LOG_PREFIX, "No report type found in RTC stats: '" + type + "'");
137
+ }
138
+ });
139
+ },
140
+ addHeaders: function(report, metricsString) {
141
+ if (metricsString) {
142
+ let metrics = metricsString.split(",");
143
+ metrics.forEach((metric) => {
144
+ let metricFound = false;
145
+ for (const key of Object.keys(report)) {
146
+ if (metric === key) {
147
+ statCollector.headers = util.addFieldToCsvString(statCollector.headers, report.type + "." + report.id + "." + metric, ",");
148
+ metricFound = true;
149
+ break;
150
+ }
151
+ }
152
+ if (!metricFound) {
153
+ statCollector.logger.warn(LOG_PREFIX, "No metric found in RTC stats report '" + report.type + "': '" + metric + "'");
154
+ }
155
+ });
156
+ }
157
+ },
158
+ updateCompression: async function() {
159
+ if (statCollector.description.compression) {
160
+ if (statCollector.description.compression.indexOf("gzip") >= 0) {
161
+ await statCollector.checkForCompression("gzip");
162
+ } else if (statCollector.description.compression.indexOf("deflate") >= 0) {
163
+ await statCollector.checkForCompression("deflate");
164
+ }
165
+ }
166
+ },
167
+ checkForCompression: async function(compression) {
168
+ try {
169
+ await util.compress(compression, "test", false);
170
+ statCollector.compression = compression;
171
+ } catch (e) {
172
+ statCollector.logger.warn(LOG_PREFIX, "Can't compress metrics data using " + compression + ": " + e);
173
+ statCollector.compression = "none";
174
+ }
175
+ },
176
+ sendHeaders: function() {
177
+ let data = {
178
+ mediaSessionId: statCollector.id,
179
+ compression: statCollector.compression,
180
+ headers: statCollector.headers
181
+ };
182
+ statCollector.send("webRTCMetricsClientDescription", data);
183
+ },
184
+ send: function(message, data) {
185
+ statCollector.logger.debug(LOG_PREFIX, data);
186
+ if (statCollector.wsConnection.readyState === WebSocket.OPEN) {
187
+ statCollector.wsConnection.send(JSON.stringify({
188
+ message: message,
189
+ data: [data]
190
+ }));
191
+ }
192
+ },
193
+ startTimer: function() {
194
+ if (!statCollector.timer && statCollector.headers) {
195
+ statCollector.batchCount = statCollector.description.batchSize;
196
+ statCollector.timer = setInterval(statCollector.collectMetrics, statCollector.description.sampling);
197
+ }
198
+ },
199
+ stopTimer: function() {
200
+ if (statCollector.timer) {
201
+ clearInterval(statCollector.timer);
202
+ statCollector.timer = null;
203
+ statCollector.metricsBatch = null;
204
+ }
205
+ },
206
+ collectMetrics: async function() {
207
+ if (statCollector.timer) {
208
+ let stats = await statCollector.mediaConnection.getWebRTCStats();
209
+
210
+ if (!statCollector.metricsBatch) {
211
+ statCollector.metricsBatch = [];
212
+ }
213
+
214
+ let metrics = [];
215
+ statCollector.headers.split(",").forEach((header) => {
216
+ let components = header.split(".");
217
+ let descriptor = {
218
+ type: components[0],
219
+ id: components[1],
220
+ name: components[2]
221
+ }
222
+ let value = "undefined";
223
+
224
+ if (stats[descriptor.type]) {
225
+ for (const report of stats[descriptor.type]) {
226
+ if (report.id === descriptor.id) {
227
+ value = report[descriptor.name];
228
+ break;
229
+ }
230
+ }
231
+ }
232
+ metrics.push(value);
233
+ });
234
+ statCollector.metricsBatch.push(metrics);
235
+ statCollector.batchCount--;
236
+ if (statCollector.batchCount === 0) {
237
+ await statCollector.sendMetrics();
238
+ }
239
+ }
240
+ },
241
+ sendMetrics: async function() {
242
+ let previous;
243
+ let metricsToSend = [];
244
+ let metricsData;
245
+
246
+ for (let i = 0; i < statCollector.metricsBatch.length; i++) {
247
+ let metricsString = "";
248
+ for (let j = 0; j < statCollector.metricsBatch[i].length; j++) {
249
+ let valueString = valueToString(statCollector.metricsBatch[i][j]);
250
+ let previousString = "";
251
+ let separator = ";";
252
+ if (previous) {
253
+ previousString = valueToString(previous[j]);
254
+ }
255
+ if (valueString === previousString) {
256
+ valueString = "";
257
+ }
258
+ metricsString = util.addFieldToCsvString(metricsString, valueString, separator);
259
+ }
260
+ previous = statCollector.metricsBatch[i];
261
+ metricsToSend.push(metricsString);
262
+ }
263
+ if (statCollector.compression !== "none") {
264
+ try {
265
+ metricsData = await util.compress(statCollector.compression, JSON.stringify(metricsToSend), true);
266
+ } catch(e) {
267
+ statCollector.logger.warn(LOG_PREFIX, "Can't send metrics data using" + statCollector.compression + ": " + e);
268
+ metricsData = null;
269
+ }
270
+ } else {
271
+ metricsData = metricsToSend;
272
+ }
273
+ if (metricsData) {
274
+ let data = {
275
+ mediaSessionId: statCollector.id,
276
+ metrics: metricsData
277
+ };
278
+ statCollector.send("webRTCMetricsBatch", data);
279
+ }
280
+ statCollector.metricsBatch = null;
281
+ statCollector.batchCount = statCollector.description.batchSize;
282
+ }
283
+ }
284
+ return statCollector;
285
+ }
286
+
287
+ // Helper function to stringify a value
288
+ const valueToString = function(value) {
289
+ let valueString = "undefined";
290
+ if (typeof value === "object") {
291
+ valueString = JSON.stringify(value);
292
+ } else {
293
+ valueString = value.toString();
294
+ }
295
+ return valueString;
296
+ }
297
+
298
+ // Helper function to get logger object
299
+ const getLogger = function(logger) {
300
+ if (logger) {
301
+ if (logger.info !== undefined &&
302
+ logger.warn !== undefined &&
303
+ logger.error !== undefined &&
304
+ logger.debug !== undefined) {
305
+ return logger;
306
+ }
307
+ }
308
+ return {
309
+ info: function() {},
310
+ warn: function() {},
311
+ error: function() {},
312
+ debug: function() {}
313
+ };
314
+ }
315
+
316
+ module.exports = {
317
+ StreamStatsCollector: StreamStatsCollector
318
+ }
package/src/util.js CHANGED
@@ -490,6 +490,64 @@ const setPublishingBitrate = function(sdp, mediaConnection, minBitrate, maxBitra
490
490
  return sdp;
491
491
  };
492
492
 
493
+ const addFieldToCsvString = function(csvString, field, separator) {
494
+ if (field !== "" && field.indexOf(separator) >= 0 ) {
495
+ field = '"' + field + '"';
496
+ }
497
+ if (csvString === "" && field !== "") {
498
+ csvString = field;
499
+ } else {
500
+ csvString = csvString + separator + field;
501
+ }
502
+ return csvString;
503
+ }
504
+
505
+ const compress = async function(compression, data, base64) {
506
+ // Throw exception if CompessionStream is not available
507
+ if (typeof CompressionStream === "undefined") {
508
+ throw new Error("Compression is not available");
509
+ }
510
+
511
+ // Convert incoming string to a stream
512
+ let stream;
513
+ if(typeof data == "string") {
514
+ stream = new Blob([data], {
515
+ type: 'text/plain',
516
+ }).stream();
517
+ } else {
518
+ // Assume blog
519
+ stream = data.stream();
520
+ }
521
+
522
+ // gzip stream
523
+ const compressedReadableStream = stream.pipeThrough(
524
+ new CompressionStream(compression)
525
+ );
526
+
527
+ // create Response
528
+ const compressedResponse = await new Response(compressedReadableStream);
529
+
530
+ // Get response Blob
531
+ const blob = await compressedResponse.blob();
532
+
533
+ if(base64) {
534
+ // Get the ArrayBuffer
535
+ const buffer = await blob.arrayBuffer();
536
+
537
+ // convert ArrayBuffer to base64 encoded string
538
+ const compressedBase64 = btoa(
539
+ String.fromCharCode(
540
+ ...new Uint8Array(buffer)
541
+ )
542
+ );
543
+
544
+ return compressedBase64;
545
+
546
+ } else {
547
+ return blob;
548
+ }
549
+ }
550
+
493
551
  module.exports = {
494
552
  isEmptyObject,
495
553
  copyObjectToArray,
@@ -501,5 +559,7 @@ module.exports = {
501
559
  stripCodecs,
502
560
  getCurrentCodecAndSampleRate,
503
561
  isPromise,
504
- setPublishingBitrate
562
+ setPublishingBitrate,
563
+ addFieldToCsvString,
564
+ compress
505
565
  };