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

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. package/dist/{HLSView-A6GH22QT.js → HLSView-524T6OTY.js} +109 -120
  2. package/dist/{HLSView-A6GH22QT.js.map → HLSView-524T6OTY.js.map} +2 -2
  3. package/dist/HLSView-VL3DXGRO.css +2767 -0
  4. package/dist/HLSView-VL3DXGRO.css.map +7 -0
  5. package/dist/Prebuilt/common/constants.d.ts +1 -0
  6. package/dist/Prebuilt/components/AppData/useSidepaneResetOnLayoutUpdate.d.ts +3 -0
  7. package/dist/Prebuilt/components/Chat/Chat.d.ts +1 -1
  8. package/dist/Prebuilt/components/PIP/PIPComponent.d.ts +14 -0
  9. package/dist/Prebuilt/components/PIP/PIPManager.d.ts +100 -0
  10. package/dist/Prebuilt/components/RoleChangeModal.d.ts +5 -0
  11. package/dist/Prebuilt/components/TileMenu/TileMenuContent.d.ts +2 -1
  12. package/dist/Prebuilt/components/VideoLayouts/WhiteboardLayout.d.ts +1 -0
  13. package/dist/{chunk-5KG27WWA.js → chunk-IOHV3H2B.js} +1861 -2103
  14. package/dist/chunk-IOHV3H2B.js.map +7 -0
  15. package/dist/index.cjs.css +2767 -0
  16. package/dist/index.cjs.css.map +7 -0
  17. package/dist/index.cjs.js +3352 -3573
  18. package/dist/index.cjs.js.map +4 -4
  19. package/dist/index.css +2767 -0
  20. package/dist/index.css.map +7 -0
  21. package/dist/index.js +1 -1
  22. package/dist/meta.cjs.json +470 -808
  23. package/dist/meta.esbuild.json +549 -821
  24. package/package.json +9 -16
  25. package/src/Modal/DialogContent.tsx +1 -1
  26. package/src/Prebuilt/common/constants.ts +2 -0
  27. package/src/Prebuilt/components/AppData/useSidepaneResetOnLayoutUpdate.tsx +22 -0
  28. package/src/Prebuilt/components/AudioVideoToggle.tsx +1 -1
  29. package/src/Prebuilt/components/Chat/Chat.tsx +3 -4
  30. package/src/Prebuilt/components/Footer/ParticipantList.tsx +56 -37
  31. package/src/Prebuilt/components/MoreSettings/SplitComponents/DesktopOptions.tsx +2 -2
  32. package/src/Prebuilt/components/PIP/{PIPComponent.jsx → PIPComponent.tsx} +31 -18
  33. package/src/Prebuilt/components/PIP/{PIPManager.js → PIPManager.ts} +60 -35
  34. package/src/Prebuilt/components/RoleChangeModal.tsx +187 -0
  35. package/src/Prebuilt/components/SecondaryTiles.tsx +7 -0
  36. package/src/Prebuilt/components/TileMenu/TileMenu.tsx +5 -0
  37. package/src/Prebuilt/components/TileMenu/TileMenuContent.tsx +23 -1
  38. package/src/Prebuilt/components/VideoLayouts/ScreenshareLayout.tsx +0 -2
  39. package/src/Prebuilt/components/VideoLayouts/WhiteboardLayout.tsx +7 -13
  40. package/src/Prebuilt/components/VirtualBackground/VBPicker.tsx +3 -0
  41. package/src/Prebuilt/layouts/PDFView.jsx +6 -1
  42. package/src/Prebuilt/layouts/SidePane.tsx +9 -9
  43. package/src/Prebuilt/plugins/FlyingEmoji.jsx +2 -4
  44. package/dist/chunk-5KG27WWA.js.map +0 -7
  45. package/src/Prebuilt/components/RoleChangeModal.jsx +0 -185
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.1",
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.1",
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.1",
80
+ "@100mslive/hms-whiteboard": "0.0.0-alpha.2",
81
+ "@100mslive/react-icons": "0.10.10-alpha.1",
82
+ "@100mslive/react-sdk": "0.10.10-alpha.1",
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": "6d26007639e3d5088d8f47681590b58698e934f4"
126
119
  }
@@ -27,7 +27,7 @@ export const StyledDialogPortal = styled(DialogPrimitive.Portal, {});
27
27
 
28
28
  export const CustomDialogContent = styled(DialogPrimitive.Content, {
29
29
  color: '$on_surface_medium',
30
- backgroundColor: '$surface_default',
30
+ backgroundColor: '$surface_dim',
31
31
  borderRadius: '8px',
32
32
  position: 'absolute',
33
33
  top: '50%',
@@ -70,6 +70,8 @@ export const SIDE_PANE_OPTIONS = {
70
70
  ROOM_DETAILS: 'ROOM_DETAILS',
71
71
  };
72
72
 
73
+ export type SidePaneOption = (typeof SIDE_PANE_OPTIONS)[keyof typeof SIDE_PANE_OPTIONS];
74
+
73
75
  export const SHEET_OPTIONS = {
74
76
  ROOM_DETAILS: 'ROOM_DETAILS',
75
77
  };
@@ -0,0 +1,22 @@
1
+ import { useEffect } from 'react';
2
+ import { DefaultConferencingScreen_Elements } from '@100mslive/types-prebuilt';
3
+ import { selectAppData, useHMSStore } from '@100mslive/react-sdk';
4
+ import { useRoomLayoutConferencingScreen } from '../../provider/roomLayoutProvider/hooks/useRoomLayoutScreen';
5
+ // @ts-ignore
6
+ import { useSidepaneReset } from './useSidepane';
7
+ import { APP_DATA, SidePaneOption } from '../../common/constants';
8
+
9
+ // Closes the sidepane if an element is removed from the layout via the customiser
10
+ export const useSidepaneResetOnLayoutUpdate = (
11
+ layoutKey: keyof DefaultConferencingScreen_Elements,
12
+ sidepaneOption: SidePaneOption,
13
+ ) => {
14
+ const { elements } = useRoomLayoutConferencingScreen();
15
+ const sidepane = useHMSStore(selectAppData(APP_DATA.sidePane));
16
+ const resetSidePane = useSidepaneReset();
17
+ useEffect(() => {
18
+ if (sidepane === sidepaneOption && !elements?.[layoutKey]) {
19
+ resetSidePane();
20
+ }
21
+ }, [elements, elements?.[layoutKey], resetSidePane, sidepane, layoutKey, sidepaneOption]);
22
+ };
@@ -122,6 +122,7 @@ const NoiseCancellation = () => {
122
122
 
123
123
  return (
124
124
  <>
125
+ <Dropdown.ItemSeparator css={{ mx: 0 }} />
125
126
  <Dropdown.Item
126
127
  css={{
127
128
  p: '$4 $8',
@@ -256,7 +257,6 @@ export const AudioVideoToggle = ({ hideOptions = false }) => {
256
257
  </Dropdown.Group>
257
258
  </>
258
259
  )}
259
- <Dropdown.ItemSeparator css={{ mx: 0 }} />
260
260
  <NoiseCancellation />
261
261
  <AudioSettings onClick={() => setShowSettings(true)} />
262
262
  </IconButtonWithOptions>
@@ -15,8 +15,9 @@ import { ChatFooter } from './ChatFooter';
15
15
  import { ChatBlocked, ChatPaused } from './ChatStates';
16
16
  import { PinnedMessage } from './PinnedMessage';
17
17
  import { useRoomLayoutConferencingScreen } from '../../provider/roomLayoutProvider/hooks/useRoomLayoutScreen';
18
+ import { useSidepaneResetOnLayoutUpdate } from '../AppData/useSidepaneResetOnLayoutUpdate';
18
19
  import { useLandscapeHLSStream, useMobileHLSStream } from '../../common/hooks';
19
- import { SESSION_STORE_KEY } from '../../common/constants';
20
+ import { SESSION_STORE_KEY, SIDE_PANE_OPTIONS } from '../../common/constants';
20
21
 
21
22
  export const Chat = () => {
22
23
  const { elements } = useRoomLayoutConferencingScreen();
@@ -27,6 +28,7 @@ export const Chat = () => {
27
28
  const isMobile = useMedia(cssConfig.media.md);
28
29
  const isMobileHLSStream = useMobileHLSStream();
29
30
  const isLandscapeStream = useLandscapeHLSStream();
31
+ useSidepaneResetOnLayoutUpdate('chat', SIDE_PANE_OPTIONS.CHAT);
30
32
 
31
33
  const scrollToBottom = useCallback(
32
34
  (unreadCount = 0) => {
@@ -42,9 +44,6 @@ export const Chat = () => {
42
44
  [hmsActions, vanillaStore],
43
45
  );
44
46
 
45
- if (!elements?.chat) {
46
- return null;
47
- }
48
47
  const streaming = isMobileHLSStream || isLandscapeStream;
49
48
 
50
49
  return (
@@ -21,6 +21,7 @@ import {
21
21
  HandIcon,
22
22
  MicOffIcon,
23
23
  PeopleIcon,
24
+ PersonSettingsIcon,
24
25
  SearchIcon,
25
26
  VerticalMenuIcon,
26
27
  } from '@100mslive/react-icons';
@@ -29,10 +30,12 @@ import { Accordion, Box, Button, config as cssConfig, Dropdown, Flex, Input, Tex
29
30
  import IconButton from '../../IconButton';
30
31
  import { ConnectionIndicator } from '../Connection/ConnectionIndicator';
31
32
  import { RemoveParticipant } from '../RemoveParticipant';
33
+ import { RoleChangeModal } from '../RoleChangeModal';
32
34
  import { RoleAccordion } from './RoleAccordion';
33
35
  import { useRoomLayoutConferencingScreen } from '../../provider/roomLayoutProvider/hooks/useRoomLayoutScreen';
34
36
  // @ts-ignore: No implicit Any
35
37
  import { useIsSidepaneTypeOpen, useSidepaneToggle } from '../AppData/useSidepane';
38
+ import { useSidepaneResetOnLayoutUpdate } from '../AppData/useSidepaneResetOnLayoutUpdate';
36
39
  import { usePeerOnStageActions } from '../hooks/usePeerOnStageActions';
37
40
  import { useParticipants } from '../../common/hooks';
38
41
  // @ts-ignore: No implicit Any
@@ -71,6 +74,8 @@ export const ParticipantList = ({
71
74
  });
72
75
  }
73
76
 
77
+ useSidepaneResetOnLayoutUpdate('participant_list', SIDE_PANE_OPTIONS.PARTICIPANTS);
78
+
74
79
  const onSearch = useCallback((value: string) => {
75
80
  setFilter(filterValue => {
76
81
  if (!filterValue) {
@@ -365,45 +370,59 @@ const ParticipantMoreActions = ({ peerId, role }: { peerId: string; role: string
365
370
  isInStage,
366
371
  shouldShowStageRoleChange,
367
372
  } = usePeerOnStageActions({ peerId, role });
373
+ const canChangeRole = !!useHMSStore(selectPermissions)?.changeRole;
374
+ const [openRoleChangeModal, setOpenRoleChangeModal] = useState(false);
375
+
368
376
  return (
369
- <Dropdown.Root open={open} onOpenChange={value => setOpen(value)} modal={false}>
370
- <Dropdown.Trigger
371
- asChild
372
- data-testid="participant_more_actions"
373
- className="participant_item"
374
- css={{
375
- p: '$1',
376
- r: '$0',
377
- c: '$on_surface_high',
378
- display: open ? 'flex' : 'none',
379
- '&:hover': {
380
- bg: '$surface_bright',
381
- },
382
- '@md': {
383
- display: 'flex',
384
- },
385
- }}
386
- tabIndex={0}
387
- >
388
- <Box css={{ my: 'auto' }}>
389
- <VerticalMenuIcon />
390
- </Box>
391
- </Dropdown.Trigger>
392
- <Dropdown.Portal>
393
- <Dropdown.Content align="end" sideOffset={8} css={{ w: '$64', bg: '$surface_default' }}>
394
- {shouldShowStageRoleChange ? (
395
- <Dropdown.Item css={{ bg: '$surface_default' }} onClick={() => handleStageAction()}>
396
- <ChangeRoleIcon />
397
- <Text variant="sm" css={{ ml: '$4', fontWeight: '$semiBold', c: '$on_surface_high' }}>
398
- {isInStage ? remove_from_stage_label : bring_to_stage_label}
399
- </Text>
400
- </Dropdown.Item>
401
- ) : null}
377
+ <>
378
+ <Dropdown.Root open={open} onOpenChange={value => setOpen(value)} modal={false}>
379
+ <Dropdown.Trigger
380
+ asChild
381
+ data-testid="participant_more_actions"
382
+ className="participant_item"
383
+ css={{
384
+ p: '$1',
385
+ r: '$0',
386
+ c: '$on_surface_high',
387
+ display: open ? 'flex' : 'none',
388
+ '&:hover': {
389
+ bg: '$surface_bright',
390
+ },
391
+ '@md': {
392
+ display: 'flex',
393
+ },
394
+ }}
395
+ tabIndex={0}
396
+ >
397
+ <Box css={{ my: 'auto' }}>
398
+ <VerticalMenuIcon />
399
+ </Box>
400
+ </Dropdown.Trigger>
401
+ <Dropdown.Portal>
402
+ <Dropdown.Content align="end" sideOffset={8} css={{ w: '$64', bg: '$surface_default' }}>
403
+ {shouldShowStageRoleChange ? (
404
+ <Dropdown.Item css={{ bg: '$surface_default' }} onClick={() => handleStageAction()}>
405
+ <ChangeRoleIcon />
406
+ <Text variant="sm" css={{ ml: '$4', fontWeight: '$semiBold', c: '$on_surface_high' }}>
407
+ {isInStage ? remove_from_stage_label : bring_to_stage_label}
408
+ </Text>
409
+ </Dropdown.Item>
410
+ ) : null}
402
411
 
403
- <RemoveParticipant peerId={peerId} />
404
- </Dropdown.Content>
405
- </Dropdown.Portal>
406
- </Dropdown.Root>
412
+ {canChangeRole ? (
413
+ <Dropdown.Item css={{ bg: '$surface_default' }} onClick={() => setOpenRoleChangeModal(true)}>
414
+ <PersonSettingsIcon />
415
+ <Text variant="sm" css={{ ml: '$4', fontWeight: '$semiBold', c: '$on_surface_high' }}>
416
+ Switch Role
417
+ </Text>
418
+ </Dropdown.Item>
419
+ ) : null}
420
+ <RemoveParticipant peerId={peerId} />
421
+ </Dropdown.Content>
422
+ </Dropdown.Portal>
423
+ </Dropdown.Root>
424
+ {openRoleChangeModal && <RoleChangeModal peerId={peerId} onOpenChange={setOpenRoleChangeModal} />}
425
+ </>
407
426
  );
408
427
  };
409
428
 
@@ -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
  }