@100mslive/roomkit-react 0.3.9 → 0.3.10-alpha.0

Sign up to get free protection for your applications and to get access to all the features.
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "prebuilt",
11
11
  "roomkit"
12
12
  ],
13
- "version": "0.3.9",
13
+ "version": "0.3.10-alpha.0",
14
14
  "author": "100ms",
15
15
  "license": "MIT",
16
16
  "repository": {
@@ -27,7 +27,8 @@
27
27
  "require": "./dist/index.cjs.js",
28
28
  "import": "./dist/index.js",
29
29
  "default": "./dist/index.js"
30
- }
30
+ },
31
+ "./index.css": "./dist/index.css"
31
32
  },
32
33
  "sideEffects": false,
33
34
  "scripts": {
@@ -50,11 +51,6 @@
50
51
  "@babel/preset-env": "^7.22.5",
51
52
  "@babel/preset-react": "^7.22.5",
52
53
  "@babel/preset-typescript": "^7.22.5",
53
- "@rollup/plugin-commonjs": "^21.0.3",
54
- "@rollup/plugin-image": "^3.0.2",
55
- "@rollup/plugin-json": "^6.0.0",
56
- "@rollup/plugin-node-resolve": "^13.1.3",
57
- "@rollup/plugin-typescript": "^8.3.1",
58
54
  "@storybook/addon-a11y": "^7.0.27",
59
55
  "@storybook/addon-actions": "^7.0.27",
60
56
  "@storybook/addon-essentials": "^7.0.27",
@@ -72,21 +68,18 @@
72
68
  "babel-plugin-react-require": "3.1.3",
73
69
  "esbuild-loader": "^4.0.2",
74
70
  "react": "^18.1.0",
75
- "rollup": "^2.70.1",
76
- "rollup-plugin-esbuild": "^5.0.0",
77
- "rollup-plugin-import-css": "^3.3.1",
78
- "rollup-plugin-terser": "^7.0.2",
79
71
  "storybook-dark-mode": "^3.0.0"
80
72
  },
81
73
  "peerDependencies": {
82
74
  "react": ">=17.0.2 <19.0.0"
83
75
  },
84
76
  "dependencies": {
85
- "@100mslive/hls-player": "0.3.9",
77
+ "@100mslive/hls-player": "0.3.10-alpha.0",
86
78
  "@100mslive/hms-noise-cancellation": "0.0.1",
87
- "@100mslive/hms-virtual-background": "1.13.9",
88
- "@100mslive/react-icons": "0.10.9",
89
- "@100mslive/react-sdk": "0.10.9",
79
+ "@100mslive/hms-virtual-background": "1.13.10-alpha.0",
80
+ "@100mslive/hms-whiteboard": "0.0.0-alpha.1",
81
+ "@100mslive/react-icons": "0.10.10-alpha.0",
82
+ "@100mslive/react-sdk": "0.10.10-alpha.0",
90
83
  "@100mslive/types-prebuilt": "0.12.8",
91
84
  "@emoji-mart/data": "^1.0.6",
92
85
  "@emoji-mart/react": "^1.0.1",
@@ -122,5 +115,5 @@
122
115
  "uuid": "^8.3.2",
123
116
  "worker-timers": "^7.0.40"
124
117
  },
125
- "gitHead": "b0a949da447e2dae0ed46087699ca18e9cd3537e"
118
+ "gitHead": "c705b5b8ce729747f28c7aff242057ac8178d338"
126
119
  }
@@ -116,10 +116,10 @@ export const DesktopOptions = ({
116
116
  ) : null}
117
117
 
118
118
  {screenType !== 'hls_live_streaming' ? (
119
- <Dropdown.Item css={{ '&:empty': { display: 'none' } }}>
119
+ <Dropdown.Item css={{ p: 0, '&:empty': { display: 'none' } }}>
120
120
  <PIP
121
121
  content={
122
- <Flex css={{ w: '100%' }}>
122
+ <Flex css={{ w: '100%', h: '100%', p: '$8' }}>
123
123
  <PipIcon />
124
124
  <Text variant="sm" css={{ ml: '$4' }}>
125
125
  {isPipOn ? 'Disable' : 'Enable'} Picture-in-Picture
@@ -1,10 +1,12 @@
1
1
  import React, { useCallback, useEffect, useState } from 'react';
2
- import { selectPeers, selectTracksMap, useHMSActions, useHMSStore, useHMSVanillaStore } from '@100mslive/react-sdk';
2
+ import { selectPeers, selectTracksMap, useHMSActions, useHMSVanillaStore } from '@100mslive/react-sdk';
3
3
  import { PipIcon } from '@100mslive/react-icons';
4
- import { Flex, Tooltip } from '../../../';
4
+ import { Flex, Tooltip } from '../../..';
5
5
  import IconButton from '../../IconButton';
6
6
  import { PictureInPicture } from './PIPManager';
7
+ // @ts-ignore: No implicit Any
7
8
  import { MediaSession } from './SetupMediaSession';
9
+ // @ts-ignore: No implicit Any
8
10
  import { usePinnedTrack } from '../AppData/useUISettings';
9
11
 
10
12
  /**
@@ -50,26 +52,37 @@ const PIPComponent = ({ content = null }) => {
50
52
  * the subscriptions to store are done only if required.
51
53
  */
52
54
  export const ActivatedPIP = () => {
53
- const tracksMap = useHMSStore(selectTracksMap);
54
- const storePeers = useHMSStore(selectPeers);
55
+ const store = useHMSVanillaStore();
55
56
  const pinnedTrack = usePinnedTrack();
56
57
 
57
58
  useEffect(() => {
58
- function updatePIP() {
59
- if (!PictureInPicture.isOn()) {
60
- return;
61
- }
62
- let pipPeers = storePeers;
63
- if (pinnedTrack) {
64
- pipPeers = storePeers.filter(peer => pinnedTrack.peerId === peer.id);
65
- }
66
- PictureInPicture.updatePeersAndTracks(pipPeers, tracksMap).catch(err => {
67
- console.error('error in updating pip', err);
68
- });
59
+ function subscribeToStore() {
60
+ return store.subscribe(tracksMap => {
61
+ let pipPeers = store.getState(selectPeers);
62
+ if (pinnedTrack) {
63
+ pipPeers = pipPeers.filter(peer => pinnedTrack.peerId === peer.id);
64
+ }
65
+ PictureInPicture.updatePeersAndTracks(pipPeers, tracksMap).catch(err => {
66
+ console.error('error in updating pip', err);
67
+ });
68
+ }, selectTracksMap);
69
69
  }
70
- PictureInPicture.listenToStateChange(updatePIP);
71
- updatePIP();
72
- }, [storePeers, tracksMap, pinnedTrack]);
70
+ let unsubscribe: (() => void) | undefined = PictureInPicture.isOn() ? subscribeToStore() : undefined;
71
+ PictureInPicture.listenToStateChange(isOn => {
72
+ if (isOn) {
73
+ if (!unsubscribe) {
74
+ unsubscribe = subscribeToStore();
75
+ }
76
+ } else {
77
+ unsubscribe?.();
78
+ unsubscribe = undefined;
79
+ }
80
+ });
81
+ return () => {
82
+ unsubscribe?.();
83
+ unsubscribe = undefined;
84
+ };
85
+ }, [pinnedTrack, store]);
73
86
 
74
87
  return <></>;
75
88
  };
@@ -1,4 +1,6 @@
1
1
  import * as workerTimers from 'worker-timers';
2
+ import { HMSActions, HMSPeer, HMSTrack, HMSVideoTrack } from '@100mslive/react-sdk';
3
+ // @ts-ignore: No implicit any
2
4
  import { drawVideoElementsOnCanvas, dummyChangeInCanvas, resetPIPCanvasColors } from './pipUtils';
3
5
  import { isIOS, isMacOS, isSafari } from '../../common/constants';
4
6
  const MAX_NUMBER_OF_TILES_IN_PIP = 4;
@@ -7,12 +9,12 @@ const DEFAULT_CANVAS_WIDTH = 480;
7
9
  const DEFAULT_CANVAS_HEIGHT = 320;
8
10
  const LEAVE_EVENT_NAME = 'leavepictureinpicture';
9
11
 
10
- const PIPStates = {
11
- starting: 'starting',
12
- started: 'started',
13
- stopping: 'stopping',
14
- stopped: 'stopped',
15
- };
12
+ enum PIPStates {
13
+ starting = 'starting',
14
+ started = 'started',
15
+ stopping = 'stopping',
16
+ stopped = 'stopped',
17
+ }
16
18
 
17
19
  /**
18
20
  * video elements are stitched together as a canvas which is converted to
@@ -24,12 +26,21 @@ const PIPStates = {
24
26
  * tracks which should be shown.
25
27
  */
26
28
  class PipManager {
27
- listeners = new Set();
29
+ private listeners = new Set<(value: boolean) => void>();
30
+ private canvas: HTMLCanvasElement | null = null;
31
+ private pipVideo: HTMLVideoElement | null = null;
32
+ private hmsActions: HMSActions | null = null;
33
+ private timeoutRef = 0;
34
+ private videoElements: HTMLVideoElement[] = [];
35
+ private onStateChange: ((value: boolean) => void) | null = null;
36
+ private tracksToShow: Array<string> = [];
37
+ private state: PIPStates = PIPStates.stopped;
38
+
28
39
  constructor() {
29
40
  this.reset();
30
41
  }
31
42
 
32
- listenToStateChange(cb) {
43
+ listenToStateChange(cb: (value: boolean) => void) {
33
44
  this.listeners.add(cb);
34
45
  }
35
46
 
@@ -41,12 +52,11 @@ class PipManager {
41
52
  resetPIPCanvasColors();
42
53
  this.canvas = null; // where stitching will take place
43
54
  this.pipVideo = null; // the element which will be sent in PIP
44
- this.timeoutRef = null; // setTimeout reference so it can be cancelled
55
+ this.timeoutRef = 0; // setTimeout reference so it can be cancelled
45
56
  this.hmsActions = null; // for attaching detaching
46
57
  this.videoElements = []; // for attaching tracks
47
58
  this.tracksToShow = [];
48
- // eslint-disable-next-line @typescript-eslint/no-empty-function
49
- this.onStateChange = () => {}; // for user of this class to listen to changes
59
+ this.onStateChange = null; // for user of this class to listen to changes
50
60
  this.state = PIPStates.stopped;
51
61
  }
52
62
 
@@ -70,7 +80,7 @@ class PipManager {
70
80
  * @param hmsActions
71
81
  * @param onStateChangeFn {function(bool):void} callback called to notify change in pip state
72
82
  */
73
- async start(hmsActions, onStateChangeFn) {
83
+ async start(hmsActions: HMSActions, onStateChangeFn: (value: boolean) => void): Promise<void> {
74
84
  if (!this.isSupported()) {
75
85
  throw new Error('pip is not supported on this browser');
76
86
  }
@@ -84,14 +94,14 @@ class PipManager {
84
94
  try {
85
95
  await this.init(hmsActions, onStateChangeFn);
86
96
  // when user closes pip, call internal stop
87
- this.pipVideo.addEventListener(LEAVE_EVENT_NAME, this.stop);
97
+ this.pipVideo?.addEventListener(LEAVE_EVENT_NAME, this.stop);
88
98
  this.renderLoop();
89
99
  if (!this.isOn()) {
90
100
  await this.requestPIP();
91
101
  }
92
102
  console.debug('pip started');
93
103
  this.state = PIPStates.started;
94
- this.onStateChange(true);
104
+ this.onStateChange?.(true);
95
105
  this.callListeners(true);
96
106
  } catch (err) {
97
107
  console.error('error in request pip', err);
@@ -107,14 +117,14 @@ class PipManager {
107
117
  this.pipVideo?.removeEventListener(LEAVE_EVENT_NAME, this.stop);
108
118
  if (this.timeoutRef) {
109
119
  workerTimers.clearTimeout(this.timeoutRef);
110
- this.timeoutRef = null;
120
+ this.timeoutRef = 0;
111
121
  }
112
122
  if (this.isOn()) {
113
123
  this.exitPIP();
114
124
  }
115
125
  // detach all to avoid bandwidth consumption
116
126
  await this.detachOldAttachNewTracks(this.tracksToShow, []);
117
- this.onStateChange(false); // notify parent about this
127
+ this.onStateChange?.(false); // notify parent about this
118
128
  this.callListeners(false);
119
129
  this.reset(); // cleanup
120
130
  this.state = PIPStates.stopped;
@@ -124,8 +134,9 @@ class PipManager {
124
134
  * @param peers {Array} All Remote Peers present in call.
125
135
  * @param tracksMap {Object} map of track id to track
126
136
  * */
127
- async updatePeersAndTracks(peers, tracksMap) {
137
+ async updatePeersAndTracks(peers: HMSPeer[], tracksMap: Record<string, HMSTrack>) {
128
138
  if (!this.canvas) {
139
+ console.log('no canvas to render video to');
129
140
  return;
130
141
  }
131
142
  const newTracksToShowUnordered = this.pickTracksToShow(peers, tracksMap);
@@ -142,14 +153,14 @@ class PipManager {
142
153
  /**
143
154
  * @private {boolean} on - whether pip is on/off
144
155
  */
145
- callListeners = on => {
156
+ callListeners = (on: boolean) => {
146
157
  this.listeners.forEach(listener => listener?.(on));
147
158
  };
148
159
 
149
160
  /**
150
161
  * @private
151
162
  */
152
- async init(hmsActions, onStateChangeFn) {
163
+ async init(hmsActions: HMSActions, onStateChangeFn: (value: boolean) => void) {
153
164
  await this.initMediaElements();
154
165
  this.hmsActions = hmsActions;
155
166
  this.onStateChange = onStateChangeFn;
@@ -180,11 +191,11 @@ class PipManager {
180
191
  }
181
192
 
182
193
  initializeVideoElements() {
183
- let videoElements = [];
194
+ const videoElements = [];
184
195
  for (let i = 0; i < MAX_NUMBER_OF_TILES_IN_PIP; i++) {
185
196
  const videoElement = document.createElement('video');
186
197
  videoElement.autoplay = true;
187
- videoElement.playsinline = true;
198
+ videoElement.playsInline = true;
188
199
  videoElements.push(videoElement);
189
200
  }
190
201
  return videoElements;
@@ -212,6 +223,9 @@ class PipManager {
212
223
  if (this.isOn()) {
213
224
  this.exitPIP(); // is this really needed?
214
225
  }
226
+ if (!this.pipVideo) {
227
+ return;
228
+ }
215
229
  await this.pipVideo.requestPictureInPicture();
216
230
  } catch (error) {
217
231
  console.error('error in requestpip', error, 'state', this.state);
@@ -228,16 +242,16 @@ class PipManager {
228
242
  * @param peers {Array<any>}
229
243
  * @param tracksMap {Record<string, any>}
230
244
  */
231
- pickTracksToShow(peers, tracksMap) {
232
- const tracksToShow = [];
245
+ pickTracksToShow(peers: HMSPeer[], tracksMap: Record<string, HMSTrack>) {
246
+ const tracksToShow = new Set<string>();
233
247
  for (const peer of peers) {
234
- if (tracksToShow.length === MAX_NUMBER_OF_TILES_IN_PIP) {
248
+ if (tracksToShow.size === MAX_NUMBER_OF_TILES_IN_PIP) {
235
249
  break;
236
- } else if (peer.videoTrack && tracksMap[peer.videoTrack]?.enabled) {
237
- tracksToShow.push(peer.videoTrack);
250
+ } else if (peer.videoTrack && this.showTrack(tracksMap[peer.videoTrack] as HMSVideoTrack)) {
251
+ tracksToShow.add(peer.videoTrack);
238
252
  }
239
253
  }
240
- return tracksToShow;
254
+ return Array.from(tracksToShow);
241
255
  }
242
256
 
243
257
  /**
@@ -252,9 +266,9 @@ class PipManager {
252
266
  * @param newTracks {Array}
253
267
  * @return {Array}
254
268
  */
255
- orderNewTracksToShow(newTracks, oldTracks) {
256
- const betterNewTracks = [];
257
- const leftOvers = [];
269
+ orderNewTracksToShow(newTracks: string[], oldTracks: string[]) {
270
+ const betterNewTracks: Array<string> = new Array(newTracks.length);
271
+ const leftOvers: Array<string> = [];
258
272
  // put the common ones in right position
259
273
  newTracks.forEach(track => {
260
274
  const oldPosition = oldTracks.indexOf(track);
@@ -268,10 +282,17 @@ class PipManager {
268
282
  // put the left overs in remaining empty positions
269
283
  for (let i = 0; i < newTracks.length; i++) {
270
284
  if (!betterNewTracks[i]) {
271
- betterNewTracks[i] = leftOvers.shift();
285
+ const newEntry = leftOvers.shift();
286
+ if (newEntry) {
287
+ betterNewTracks[i] = newEntry;
288
+ }
272
289
  }
273
290
  }
274
- return betterNewTracks;
291
+ return Array.from(new Set(betterNewTracks));
292
+ }
293
+
294
+ private showTrack(track: HMSVideoTrack | undefined) {
295
+ return track && track.enabled && !track.degraded;
275
296
  }
276
297
 
277
298
  /**
@@ -283,7 +304,11 @@ class PipManager {
283
304
  * @param tracksMap {Record<String, any>}
284
305
  */
285
306
  // eslint-disable-next-line complexity
286
- async detachOldAttachNewTracks(oldTracks, newTracks, tracksMap = null) {
307
+ async detachOldAttachNewTracks(
308
+ oldTracks: Array<string>,
309
+ newTracks: Array<string>,
310
+ tracksMap: Record<string, HMSTrack> | null = null,
311
+ ) {
287
312
  const numTracks = Math.max(oldTracks.length, newTracks.length);
288
313
  for (let i = 0; i < numTracks; i++) {
289
314
  if (oldTracks[i] === newTracks[i]) {
@@ -292,7 +317,7 @@ class PipManager {
292
317
  // old track is there but not equal to new track, detach
293
318
  // no need to call detach if we know for sure track is no longer in store
294
319
  if (!tracksMap || tracksMap[oldTracks[i]]) {
295
- await this.hmsActions.detachVideo(oldTracks[i], this.videoElements[i]);
320
+ await this.hmsActions?.detachVideo(oldTracks[i], this.videoElements[i]);
296
321
  }
297
322
  if (this.videoElements[i]) {
298
323
  // even if old track got removed from the room, element needs to be cleaned up
@@ -300,7 +325,7 @@ class PipManager {
300
325
  }
301
326
  }
302
327
  if (newTracks[i]) {
303
- await this.hmsActions.attachVideo(newTracks[i], this.videoElements[i]);
328
+ await this.hmsActions?.attachVideo(newTracks[i], this.videoElements[i]);
304
329
  }
305
330
  }
306
331
  }
@@ -42,6 +42,13 @@ export const SecondaryTiles = ({ peers, onPageChange, onPageSize, edgeToEdge, ha
42
42
  });
43
43
  const pageSize = pagesWithTiles[0]?.length || 0;
44
44
 
45
+ // Handles final peer leaving from the last page
46
+ useEffect(() => {
47
+ if (peers.length > 0 && !pagesWithTiles[page]?.length) {
48
+ setPage(Math.max(0, page - 1));
49
+ }
50
+ }, [peers, page, pagesWithTiles]);
51
+
45
52
  useEffect(() => {
46
53
  if (pageSize > 0) {
47
54
  onPageSize?.(pageSize);
@@ -55,8 +55,6 @@ export const ScreenshareLayout = ({ peers, onPageChange, onPageSize, edgeToEdge
55
55
  };
56
56
  }, [activeSharePeer?.id, isMobile, setActiveScreenSharePeer]);
57
57
 
58
- console.log({ activeSharePeer, secondaryPeers });
59
-
60
58
  return (
61
59
  <ProminenceLayout.Root edgeToEdge={edgeToEdge} hasSidebar={hasSidebar}>
62
60
  <ProminenceLayout.ProminentSection>
@@ -1,5 +1,6 @@
1
1
  import React, { useEffect, useMemo } from 'react';
2
2
  import { useMedia } from 'react-use';
3
+ import { Whiteboard } from '@100mslive/hms-whiteboard';
3
4
  import { selectPeerByCondition, selectWhiteboard, useHMSStore, useWhiteboard } from '@100mslive/react-sdk';
4
5
  import { Box } from '../../../Layout';
5
6
  import { config as cssConfig } from '../../../Theme';
@@ -11,10 +12,12 @@ import { ProminenceLayout } from './ProminenceLayout';
11
12
  // @ts-ignore: No implicit Any
12
13
  import { useSetUiSettings } from '../AppData/useUISettings';
13
14
  import { UI_SETTINGS } from '../../common/constants';
15
+ // eslint-disable-next-line import/no-unresolved
16
+ import '@100mslive/hms-whiteboard/index.css';
14
17
 
15
18
  const WhiteboardEmbed = () => {
16
19
  const isMobile = useMedia(cssConfig.media.md);
17
- const { iframeRef } = useWhiteboard(isMobile);
20
+ const { token, endpoint, zoomToContent } = useWhiteboard(isMobile);
18
21
 
19
22
  return (
20
23
  <Box
@@ -28,18 +31,9 @@ const WhiteboardEmbed = () => {
28
31
  },
29
32
  }}
30
33
  >
31
- <iframe
32
- title="Whiteboard View"
33
- ref={iframeRef}
34
- style={{
35
- width: '100%',
36
- height: '100%',
37
- border: 0,
38
- borderRadius: '0.75rem',
39
- }}
40
- allow="autoplay; clipboard-write;"
41
- referrerPolicy="no-referrer"
42
- />
34
+ <Box css={{ size: '100%' }}>
35
+ <Whiteboard token={token} endpoint={`https://${endpoint}`} zoomToContent={zoomToContent} />
36
+ </Box>
43
37
  </Box>
44
38
  );
45
39
  };
@@ -11,10 +11,15 @@ import { usePDFConfig, useResetPDFConfig } from '../components/AppData/useUISett
11
11
  export const PDFView = () => {
12
12
  const pdfConfig = usePDFConfig();
13
13
  const resetConfig = useResetPDFConfig();
14
-
15
14
  // need to send resetConfig to clear configuration, if stop screenshare occurs.
16
15
  const { iframeRef, startPDFShare, isPDFShareInProgress } = usePDFShare(resetConfig);
17
16
 
17
+ useEffect(() => {
18
+ // no working in other useEffect, as return is called multiple time on state change
19
+ return () => {
20
+ resetConfig();
21
+ };
22
+ }, []);
18
23
  useEffect(() => {
19
24
  (async () => {
20
25
  try {
@@ -119,12 +119,10 @@ export function FlyingEmoji() {
119
119
  emoji.wiggleType === 0 ? wiggleLeftRight() : wiggleRightLeft()
120
120
  } 1s ease-in-out infinite alternate`,
121
121
  }}
122
- onAnimationEnd={() => {
123
- setEmojis(emojis.filter(item => item.id !== emoji.id));
124
- }}
122
+ onAnimationEnd={() => setEmojis(emojis.filter(item => item.id !== emoji.id))}
125
123
  >
126
124
  <Box>
127
- <em-emoji id={emoji.emojiId} size="48px" set="apple"></em-emoji>
125
+ <em-emoji id={emoji.emojiId} size="48px" set="apple" />
128
126
  </Box>
129
127
  {emoji.senderName ? (
130
128
  <Box