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

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
@@ -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