@100mslive/react-sdk 0.0.17 → 0.0.18-alpha.2

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.
@@ -0,0 +1,466 @@
1
+ /* eslint-disable complexity */
2
+ /* eslint-disable no-plusplus */
3
+ /* eslint-disable @typescript-eslint/no-shadow */
4
+ import { HMSPeer, HMSTrack, HMSTrackID } from '@100mslive/hms-video-store';
5
+
6
+ export const chunk = <T>(elements: T[], chunkSize: number, onlyOnePage: boolean) =>
7
+ elements.reduce((resultArray: T[][], tile: T, index: number) => {
8
+ const chunkIndex = Math.floor(index / chunkSize);
9
+ if (chunkIndex > 0 && onlyOnePage) {
10
+ return resultArray;
11
+ }
12
+ if (!resultArray[chunkIndex]) {
13
+ resultArray[chunkIndex] = []; // start a new chunk
14
+ }
15
+
16
+ resultArray[chunkIndex].push(tile);
17
+ return resultArray;
18
+ }, []);
19
+
20
+ interface ChunkElements<T> {
21
+ elements: T[];
22
+ tilesInFirstPage: number;
23
+ onlyOnePage: boolean;
24
+ isLastPageDifferentFromFirstPage: boolean;
25
+ defaultWidth: number;
26
+ defaultHeight: number;
27
+ lastPageWidth: number;
28
+ lastPageHeight: number;
29
+ }
30
+
31
+ /**
32
+ * Given a list of tracks/elements and some constraints, group the tracks in separate pages.
33
+ * @return 2D list for every page which has the original element and height and width
34
+ * for its tile.
35
+ */
36
+ export const chunkElements = <T>({
37
+ elements,
38
+ tilesInFirstPage,
39
+ onlyOnePage,
40
+ isLastPageDifferentFromFirstPage,
41
+ defaultWidth,
42
+ defaultHeight,
43
+ lastPageWidth,
44
+ lastPageHeight,
45
+ }: ChunkElements<T>): (T & { width: number; height: number })[][] => {
46
+ const chunks: T[][] = chunk<T>(elements, tilesInFirstPage, onlyOnePage);
47
+ return chunks.map((ch, page) =>
48
+ ch.map(element => {
49
+ const isLastPage = page === chunks.length - 1;
50
+ const width = isLastPageDifferentFromFirstPage && isLastPage ? lastPageWidth : defaultWidth;
51
+ const height = isLastPageDifferentFromFirstPage && isLastPage ? lastPageHeight : defaultHeight;
52
+ return { ...element, height, width };
53
+ }),
54
+ );
55
+ };
56
+
57
+ /**
58
+ * Mathematical mode - the element with the highest occurrence in an array
59
+ * @param array
60
+ */
61
+ export function mode(array: number[]): number | null {
62
+ if (array.length === 0) {
63
+ return null;
64
+ }
65
+ const modeMap: Record<number, number> = {};
66
+ let maxEl = array[0];
67
+ let maxCount = 1;
68
+ for (let i = 0; i < array.length; i++) {
69
+ const el = array[i];
70
+ if (modeMap[el] === null) {
71
+ modeMap[el] = 1;
72
+ } else {
73
+ modeMap[el]++;
74
+ }
75
+ if (modeMap[el] > maxCount) {
76
+ maxEl = el;
77
+ maxCount = modeMap[el];
78
+ }
79
+ }
80
+ return maxEl;
81
+ }
82
+
83
+ export type TrackWithPeer = { track?: HMSTrack; peer: HMSPeer };
84
+
85
+ /**
86
+ * get the aspect ration occurring with the highest frequency
87
+ * @param tracks - video tracks to infer aspect ratios from
88
+ */
89
+ export const getModeAspectRatio = (tracks: TrackWithPeer[]): number | null =>
90
+ mode(
91
+ tracks
92
+ .filter(track => track.track?.width && track.track?.height)
93
+ .map(track => {
94
+ const width = track.track?.width;
95
+ const height = track.track?.height;
96
+ // Default to 1 if there are no video tracks
97
+ return (width || 1) / (height || 1);
98
+ }),
99
+ );
100
+
101
+ interface GetTileSizesInList {
102
+ count: number;
103
+ parentWidth: number;
104
+ parentHeight: number;
105
+ maxTileCount?: number;
106
+ maxRowCount?: number;
107
+ maxColCount?: number;
108
+ aspectRatio: {
109
+ width: number;
110
+ height: number;
111
+ };
112
+ }
113
+
114
+ interface GetTileSizes {
115
+ parentWidth: number;
116
+ parentHeight: number;
117
+ count: number;
118
+ maxCount: number;
119
+ aspectRatio: { width: number; height: number };
120
+ }
121
+
122
+ /**
123
+ * Finds the largest rectangle area when trying to place N rectangle into a containing
124
+ * rectangle without rotation.
125
+ *
126
+ * @param {Number} containerWidth The width of the container.
127
+ * @param {Number} containerHeight The height of the container.
128
+ * @param {Number} numRects How many rectangles must fit within.
129
+ * @param {Number} width The unscaled width of the rectangles to be placed.
130
+ * @param {Number} height The unscaled height of the rectangles to be placed.
131
+ * @return {Object} The area and number of rows and columns that fit.
132
+ */
133
+ export const largestRect = (
134
+ containerWidth: number,
135
+ containerHeight: number,
136
+ numRects: number,
137
+ width: number | undefined,
138
+ height: number | undefined,
139
+ ) => {
140
+ if (containerWidth < 0 || containerHeight < 0) {
141
+ throw new Error('Container must have a non-negative area');
142
+ }
143
+ if (numRects < 1 || !Number.isInteger(numRects)) {
144
+ throw new Error('Number of shapes to place must be a positive integer');
145
+ }
146
+ const aspectRatio = width && height && width / height;
147
+ if (aspectRatio !== undefined && isNaN(aspectRatio)) {
148
+ throw new Error('Aspect ratio must be a number');
149
+ }
150
+
151
+ let best = { area: 0, cols: 0, rows: 0, width: 0, height: 0 };
152
+
153
+ // TODO: Don't start with obviously-`ba`d candidates.
154
+ const startCols = numRects;
155
+ const colDelta = -1;
156
+
157
+ // For each combination of rows + cols that can fit the number of rectangles,
158
+ // place them and see the area.
159
+ if (aspectRatio !== undefined) {
160
+ for (let cols = startCols; cols > 0; cols += colDelta) {
161
+ const rows = Math.ceil(numRects / cols);
162
+ const hScale = containerWidth / (cols * aspectRatio);
163
+ const vScale = containerHeight / rows;
164
+ let width;
165
+ let height;
166
+ // Determine which axis is the constraint.
167
+ if (hScale <= vScale) {
168
+ width = containerWidth / cols;
169
+ height = width / aspectRatio;
170
+ } else {
171
+ height = containerHeight / rows;
172
+ width = height * aspectRatio;
173
+ }
174
+ const area = width * height;
175
+ if (area > best.area) {
176
+ best = { area, width, height, rows, cols };
177
+ }
178
+ }
179
+ }
180
+ return best;
181
+ };
182
+
183
+ export const getTileSizesWithColConstraint = ({
184
+ parentWidth,
185
+ parentHeight,
186
+ count,
187
+ maxCount,
188
+ aspectRatio,
189
+ }: GetTileSizes) => {
190
+ let defaultWidth = 0;
191
+ let defaultHeight = 0;
192
+ let lastPageWidth = 0;
193
+ let lastPageHeight = 0;
194
+ let isLastPageDifferentFromFirstPage = false;
195
+ let tilesInFirstPage = 0;
196
+ let tilesinLastPage = 0;
197
+ const cols = Math.min(
198
+ Math.ceil(Math.sqrt((count * (parentWidth / parentHeight)) / (aspectRatio.width / aspectRatio.height))),
199
+ maxCount,
200
+ );
201
+ let width = parentWidth / cols;
202
+ let height = width / (aspectRatio.width / aspectRatio.height);
203
+ if (height > parentHeight) {
204
+ height = parentHeight;
205
+ width = height / (aspectRatio.height / aspectRatio.width);
206
+ }
207
+ const rows = Math.floor(parentHeight / height);
208
+ defaultHeight = height;
209
+ defaultWidth = width;
210
+ tilesInFirstPage = Math.min(count, rows * cols);
211
+ tilesinLastPage = count % (rows * cols);
212
+ isLastPageDifferentFromFirstPage = tilesinLastPage > 0 && count > rows * cols;
213
+ if (isLastPageDifferentFromFirstPage) {
214
+ const cols = Math.min(
215
+ Math.ceil(Math.sqrt((tilesinLastPage * (parentWidth / parentHeight)) / (aspectRatio.width / aspectRatio.height))),
216
+ maxCount,
217
+ );
218
+ let width = parentWidth / cols;
219
+ let height = width / (aspectRatio.width / aspectRatio.height);
220
+ if (height > parentHeight) {
221
+ height = parentHeight;
222
+ width = height / (aspectRatio.height / aspectRatio.width);
223
+ }
224
+ lastPageHeight = height;
225
+ lastPageWidth = width;
226
+ }
227
+ return {
228
+ tilesInFirstPage,
229
+ defaultWidth,
230
+ defaultHeight,
231
+ lastPageWidth,
232
+ lastPageHeight,
233
+ isLastPageDifferentFromFirstPage,
234
+ };
235
+ };
236
+
237
+ export const getTileSizesWithPageConstraint = ({
238
+ parentWidth,
239
+ parentHeight,
240
+ count,
241
+ maxCount,
242
+ aspectRatio,
243
+ }: GetTileSizes) => {
244
+ let defaultWidth = 0;
245
+ let defaultHeight = 0;
246
+ let lastPageWidth = 0;
247
+ let lastPageHeight = 0;
248
+ let isLastPageDifferentFromFirstPage = false;
249
+ let tilesInFirstPage = 0;
250
+ let tilesinLastPage = 0;
251
+ const { width: initialWidth, height: initialHeight } = largestRect(
252
+ parentWidth,
253
+ parentHeight,
254
+ Math.min(count, maxCount),
255
+ aspectRatio.width,
256
+ aspectRatio.height,
257
+ );
258
+ defaultWidth = initialWidth;
259
+ defaultHeight = initialHeight;
260
+ tilesInFirstPage = Math.min(count, maxCount);
261
+ tilesinLastPage = count % maxCount;
262
+ isLastPageDifferentFromFirstPage = tilesinLastPage > 0 && count > maxCount;
263
+ if (isLastPageDifferentFromFirstPage) {
264
+ const { width: remWidth, height: remHeight } = largestRect(
265
+ parentWidth,
266
+ parentHeight,
267
+ tilesinLastPage,
268
+ aspectRatio.width,
269
+ aspectRatio.height,
270
+ );
271
+ lastPageWidth = remWidth;
272
+ lastPageHeight = remHeight;
273
+ }
274
+ return {
275
+ tilesInFirstPage,
276
+ defaultWidth,
277
+ defaultHeight,
278
+ lastPageWidth,
279
+ lastPageHeight,
280
+ isLastPageDifferentFromFirstPage,
281
+ };
282
+ };
283
+
284
+ export const getTileSizesWithRowConstraint = ({
285
+ parentWidth,
286
+ parentHeight,
287
+ count,
288
+ maxCount,
289
+ aspectRatio,
290
+ }: GetTileSizes) => {
291
+ let defaultWidth = 0;
292
+ let defaultHeight = 0;
293
+ let lastPageWidth = 0;
294
+ let lastPageHeight = 0;
295
+ let isLastPageDifferentFromFirstPage = false;
296
+ let tilesInFirstPage = 0;
297
+ let tilesinLastPage = 0;
298
+ const rows = Math.min(
299
+ Math.ceil(Math.sqrt((count * (aspectRatio.width / aspectRatio.height)) / (parentWidth / parentHeight))),
300
+ maxCount,
301
+ );
302
+ const height = parentHeight / rows;
303
+ const width = height * (aspectRatio.width / aspectRatio.height);
304
+ const cols = Math.floor(parentWidth / width);
305
+ defaultWidth = width;
306
+ defaultHeight = height;
307
+ tilesInFirstPage = Math.min(count, rows * cols);
308
+ tilesinLastPage = count % (rows * cols);
309
+ isLastPageDifferentFromFirstPage = tilesinLastPage > 0 && count > rows * cols;
310
+ if (isLastPageDifferentFromFirstPage) {
311
+ const rows = Math.min(
312
+ Math.ceil(Math.sqrt((tilesinLastPage * (aspectRatio.width / aspectRatio.height)) / (parentWidth / parentHeight))),
313
+ maxCount,
314
+ );
315
+ const height = parentHeight / rows;
316
+ const width = height * (aspectRatio.width / aspectRatio.height);
317
+ lastPageHeight = height;
318
+ lastPageWidth = width;
319
+ }
320
+ return {
321
+ tilesInFirstPage,
322
+ defaultWidth,
323
+ defaultHeight,
324
+ lastPageWidth,
325
+ lastPageHeight,
326
+ isLastPageDifferentFromFirstPage,
327
+ };
328
+ };
329
+
330
+ export function calculateLayoutSizes({
331
+ count,
332
+ parentWidth,
333
+ parentHeight,
334
+ maxTileCount,
335
+ maxRowCount,
336
+ maxColCount,
337
+ aspectRatio,
338
+ }: GetTileSizesInList) {
339
+ let defaultWidth = 0;
340
+ let defaultHeight = 0;
341
+ let lastPageWidth = 0;
342
+ let lastPageHeight = 0;
343
+ let isLastPageDifferentFromFirstPage = false;
344
+ let tilesInFirstPage = 0;
345
+
346
+ if (count === 0) {
347
+ // no tracks to show
348
+ return {
349
+ tilesInFirstPage,
350
+ defaultWidth,
351
+ defaultHeight,
352
+ lastPageWidth,
353
+ lastPageHeight,
354
+ isLastPageDifferentFromFirstPage,
355
+ };
356
+ }
357
+
358
+ if (maxTileCount) {
359
+ ({
360
+ tilesInFirstPage,
361
+ defaultWidth,
362
+ defaultHeight,
363
+ lastPageWidth,
364
+ lastPageHeight,
365
+ isLastPageDifferentFromFirstPage,
366
+ } = getTileSizesWithPageConstraint({
367
+ parentWidth,
368
+ parentHeight,
369
+ count,
370
+ maxCount: maxTileCount,
371
+ aspectRatio,
372
+ }));
373
+ } else if (maxRowCount) {
374
+ ({
375
+ tilesInFirstPage,
376
+ defaultWidth,
377
+ defaultHeight,
378
+ lastPageWidth,
379
+ lastPageHeight,
380
+ isLastPageDifferentFromFirstPage,
381
+ } = getTileSizesWithRowConstraint({
382
+ parentWidth,
383
+ parentHeight,
384
+ count,
385
+ maxCount: maxRowCount,
386
+ aspectRatio,
387
+ }));
388
+ } else if (maxColCount) {
389
+ ({
390
+ tilesInFirstPage,
391
+ defaultWidth,
392
+ defaultHeight,
393
+ lastPageWidth,
394
+ lastPageHeight,
395
+ isLastPageDifferentFromFirstPage,
396
+ } = getTileSizesWithColConstraint({
397
+ parentWidth,
398
+ parentHeight,
399
+ count,
400
+ maxCount: maxColCount,
401
+ aspectRatio,
402
+ }));
403
+ } else {
404
+ const { width, height } = largestRect(parentWidth, parentHeight, count, aspectRatio.width, aspectRatio.height);
405
+ defaultWidth = width;
406
+ defaultHeight = height;
407
+ tilesInFirstPage = count;
408
+ }
409
+ return {
410
+ tilesInFirstPage,
411
+ defaultWidth,
412
+ defaultHeight,
413
+ lastPageWidth,
414
+ lastPageHeight,
415
+ isLastPageDifferentFromFirstPage,
416
+ };
417
+ }
418
+
419
+ /**
420
+ * given list of peers and all tracks in the room, get a list of tile objects to show in the UI
421
+ * @param peers
422
+ * @param tracks
423
+ * @param includeScreenShareForPeer - fn will be called to check whether to include screenShare for the peer in returned tiles
424
+ * @param filterNonPublishingPeers - by default a peer with no tracks won't be counted towards final tiles
425
+ */
426
+ export const getVideoTracksFromPeers = (
427
+ peers: HMSPeer[],
428
+ tracks: Record<HMSTrackID, HMSTrack>,
429
+ includeScreenShareForPeer: (peer: HMSPeer) => boolean,
430
+ filterNonPublishingPeers = true,
431
+ ) => {
432
+ if (!peers || !tracks || !includeScreenShareForPeer) {
433
+ return [];
434
+ }
435
+ const peerTiles: TrackWithPeer[] = [];
436
+ for (const peer of peers) {
437
+ const onlyAudioTrack = peer.videoTrack === undefined && peer.audioTrack && tracks[peer.audioTrack];
438
+ if (onlyAudioTrack) {
439
+ peerTiles.push({ peer: peer });
440
+ } else if (peer.videoTrack && tracks[peer.videoTrack]) {
441
+ peerTiles.push({ track: tracks[peer.videoTrack], peer: peer });
442
+ } else if (!filterNonPublishingPeers) {
443
+ peerTiles.push({ peer: peer });
444
+ }
445
+ // Handle video tracks in auxiliary tracks as well.
446
+ if (peer.auxiliaryTracks.length > 0) {
447
+ peer.auxiliaryTracks.forEach(trackId => {
448
+ const track = tracks[trackId];
449
+ if (track?.type === 'video' && track?.source === 'regular') {
450
+ peerTiles.push({ track, peer });
451
+ }
452
+ });
453
+ }
454
+ if (includeScreenShareForPeer(peer) && peer.auxiliaryTracks.length > 0) {
455
+ const screenShareTrackID = peer.auxiliaryTracks.find(trackID => {
456
+ const track = tracks[trackID];
457
+ return track?.type === 'video' && track?.source === 'screen';
458
+ });
459
+ // Don't show tile if screenshare only has audio
460
+ if (screenShareTrackID) {
461
+ peerTiles.push({ track: tracks[screenShareTrackID], peer: peer });
462
+ }
463
+ }
464
+ }
465
+ return peerTiles;
466
+ };
@@ -0,0 +1,62 @@
1
+ export enum HMSLogLevel {
2
+ VERBOSE,
3
+ DEBUG,
4
+ INFO,
5
+ WARN,
6
+ ERROR,
7
+ NONE,
8
+ }
9
+
10
+ export default class HMSLogger {
11
+ static level: HMSLogLevel = HMSLogLevel.VERBOSE;
12
+
13
+ static v(tag: string, ...data: any[]) {
14
+ this.log(HMSLogLevel.VERBOSE, tag, ...data);
15
+ }
16
+
17
+ static d(tag: string, ...data: any[]) {
18
+ this.log(HMSLogLevel.DEBUG, tag, ...data);
19
+ }
20
+
21
+ static i(tag: string, ...data: any[]) {
22
+ this.log(HMSLogLevel.INFO, tag, ...data);
23
+ }
24
+
25
+ static w(tag: string, ...data: any[]) {
26
+ this.log(HMSLogLevel.WARN, tag, ...data);
27
+ }
28
+
29
+ static e(tag: string, ...data: any[]) {
30
+ this.log(HMSLogLevel.ERROR, tag, ...data);
31
+ }
32
+
33
+ // eslint-disable-next-line complexity
34
+ private static log(level: HMSLogLevel, tag: string, ...data: any[]) {
35
+ if (this.level.valueOf() > level.valueOf()) {
36
+ return;
37
+ }
38
+
39
+ switch (level) {
40
+ case HMSLogLevel.VERBOSE: {
41
+ console.log('HMSui-components: ', tag, ...data);
42
+ break;
43
+ }
44
+ case HMSLogLevel.DEBUG: {
45
+ console.debug('HMSui-components: ', tag, ...data);
46
+ break;
47
+ }
48
+ case HMSLogLevel.INFO: {
49
+ console.info('HMSui-components: ', tag, ...data);
50
+ break;
51
+ }
52
+ case HMSLogLevel.WARN: {
53
+ console.warn('HMSui-components: ', tag, ...data);
54
+ break;
55
+ }
56
+ case HMSLogLevel.ERROR: {
57
+ console.error('HMSui-components: ', tag, ...data);
58
+ break;
59
+ }
60
+ }
61
+ }
62
+ }