@100mslive/roomkit-react 0.2.8-alpha.8 → 0.3.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.
Files changed (33) hide show
  1. package/dist/{HLSView-6KPQ2KD6.js → HLSView-LZOTIUWE.js} +152 -96
  2. package/dist/HLSView-LZOTIUWE.js.map +7 -0
  3. package/dist/Prebuilt/components/Chat/Chat.d.ts +1 -1
  4. package/dist/Prebuilt/components/HMSVideo/VideoProgress.d.ts +3 -2
  5. package/dist/Prebuilt/components/HMSVideo/index.d.ts +3 -2
  6. package/dist/Prebuilt/components/SidePaneTabs.d.ts +0 -1
  7. package/dist/Prebuilt/layouts/SidePane.d.ts +1 -1
  8. package/dist/{chunk-JQCSGJIR.js → chunk-LCECN6XD.js} +297 -238
  9. package/dist/chunk-LCECN6XD.js.map +7 -0
  10. package/dist/index.cjs.js +446 -332
  11. package/dist/index.cjs.js.map +3 -3
  12. package/dist/index.js +1 -1
  13. package/dist/meta.cjs.json +96 -61
  14. package/dist/meta.esbuild.json +109 -73
  15. package/package.json +6 -6
  16. package/src/Prebuilt/components/Chat/Chat.tsx +23 -4
  17. package/src/Prebuilt/components/Chat/ChatFooter.tsx +2 -2
  18. package/src/Prebuilt/components/Chat/EmptyChat.tsx +5 -1
  19. package/src/Prebuilt/components/ConferenceScreen.tsx +13 -1
  20. package/src/Prebuilt/components/EmojiReaction.jsx +2 -2
  21. package/src/Prebuilt/components/Footer/RoleOptions.tsx +125 -126
  22. package/src/Prebuilt/components/HMSVideo/HMSVideo.jsx +13 -10
  23. package/src/Prebuilt/components/HMSVideo/MwebHLSViewTitle.tsx +6 -4
  24. package/src/Prebuilt/components/HMSVideo/VideoProgress.tsx +38 -25
  25. package/src/Prebuilt/components/MoreSettings/SplitComponents/MwebOptions.tsx +1 -1
  26. package/src/Prebuilt/components/MwebLandscapePrompt.tsx +9 -3
  27. package/src/Prebuilt/components/Polls/common/utils.ts +1 -1
  28. package/src/Prebuilt/components/SidePaneTabs.tsx +1 -4
  29. package/src/Prebuilt/layouts/HLSView.jsx +293 -239
  30. package/src/Prebuilt/layouts/SidePane.tsx +73 -49
  31. package/src/Prebuilt/layouts/VideoStreamingSection.tsx +10 -2
  32. package/dist/HLSView-6KPQ2KD6.js.map +0 -7
  33. package/dist/chunk-JQCSGJIR.js.map +0 -7
@@ -152,7 +152,7 @@ export const ChatFooter = ({ onSend, children }: { onSend: (count: number) => vo
152
152
  <Flex>
153
153
  <ChatSelectorContainer />
154
154
  {canDisableChat && isMobile && isOverlayChat ? (
155
- <Flex align="center" justify="end" css={{ mb: '$4' }}>
155
+ <Flex align="center" justify="end" css={{ mb: '$4' }} onClick={e => e.stopPropagation()}>
156
156
  <Popover.Root>
157
157
  <Popover.Trigger asChild>
158
158
  <IconButton css={{ border: '1px solid $border_bright' }}>
@@ -273,7 +273,7 @@ export const ChatFooter = ({ onSend, children }: { onSend: (count: number) => vo
273
273
  css={{
274
274
  alignItems: 'center',
275
275
  }}
276
- gap="1"
276
+ gap="2"
277
277
  >
278
278
  {noAVPermissions ? <RaiseHand css={{ bg: '$surface_default' }} /> : null}
279
279
  <MoreSettings elements={elements} screenType={screenType} />
@@ -7,6 +7,7 @@ import { config as cssConfig } from '../../../Theme';
7
7
  import emptyChat from '../../images/empty-chat.svg';
8
8
  import { useRoomLayoutConferencingScreen } from '../../provider/roomLayoutProvider/hooks/useRoomLayoutScreen';
9
9
  import { useIsPeerBlacklisted } from '../hooks/useChatBlacklist';
10
+ import { useLandscapeHLSStream, useMobileHLSStream } from '../../common/hooks';
10
11
 
11
12
  export const EmptyChat = () => {
12
13
  const { elements } = useRoomLayoutConferencingScreen();
@@ -18,8 +19,11 @@ export const EmptyChat = () => {
18
19
  elements.chat.private_chat_enabled ||
19
20
  (elements.chat.roles_whitelist && elements.chat.roles_whitelist.length)) &&
20
21
  !isLocalPeerBlacklisted;
22
+ const isMobileHLSStream = useMobileHLSStream();
23
+ const isLandscapeStream = useLandscapeHLSStream();
24
+ const streaming = isMobileHLSStream || isLandscapeStream;
21
25
 
22
- if (isMobile && elements?.chat?.is_overlay) return <></>;
26
+ if (isMobile && elements?.chat?.is_overlay && !streaming) return <></>;
23
27
 
24
28
  return (
25
29
  <Flex
@@ -22,6 +22,8 @@ import { Box, Flex } from '../../Layout';
22
22
  import { useHMSPrebuiltContext } from '../AppContext';
23
23
  import { VideoStreamingSection } from '../layouts/VideoStreamingSection';
24
24
  // @ts-ignore: No implicit Any
25
+ import { EmojiReaction } from './EmojiReaction';
26
+ // @ts-ignore: No implicit Any
25
27
  import FullPageProgress from './FullPageProgress';
26
28
  import { Header } from './Header';
27
29
  import { PreviousRoleInMetadata } from './PreviousRoleInMetadata';
@@ -195,12 +197,22 @@ export const ConferenceScreen = () => {
195
197
  alignItems: 'center',
196
198
  pr: '$4',
197
199
  pb: '$4',
200
+ position: 'relative',
198
201
  }}
199
202
  justify="end"
200
- gap="1"
203
+ gap="2"
201
204
  >
202
205
  {noAVPermissions ? <RaiseHand /> : null}
203
206
  <MoreSettings elements={screenProps.elements} screenType={screenProps.screenType} />
207
+ <Box
208
+ css={{
209
+ position: 'absolute',
210
+ bottom: '100%',
211
+ mb: '$4',
212
+ }}
213
+ >
214
+ <EmojiReaction />
215
+ </Box>
204
216
  </Flex>
205
217
  )}
206
218
  <RoleChangeRequestModal />
@@ -23,7 +23,7 @@ import { EMOJI_REACTION_TYPE } from '../common/constants';
23
23
 
24
24
  init({ data });
25
25
 
26
- export const EmojiReaction = () => {
26
+ export const EmojiReaction = ({ showCard = false }) => {
27
27
  const [open, setOpen] = useState(false);
28
28
  const isConnected = useHMSStore(selectIsConnectedToRoom);
29
29
  useDropdownList({ open: open, name: 'EmojiReaction' });
@@ -68,7 +68,7 @@ export const EmojiReaction = () => {
68
68
  return null;
69
69
  }
70
70
 
71
- if ((isMobile || isLandscape) && !(isLandscapeStream || isMobileHLSStream)) {
71
+ if (showCard) {
72
72
  return <EmojiCard sendReaction={sendReaction} />;
73
73
  }
74
74
  return (
@@ -1,12 +1,13 @@
1
1
  import React, { useState } from 'react';
2
2
  import { DefaultConferencingScreen_Elements } from '@100mslive/types-prebuilt';
3
+ import { match } from 'ts-pattern';
3
4
  import {
4
5
  HMSPeer,
5
6
  selectPermissions,
6
7
  selectRoleByRoleName,
8
+ selectTracksMap,
7
9
  useHMSActions,
8
10
  useHMSStore,
9
- useHMSVanillaStore,
10
11
  } from '@100mslive/react-sdk';
11
12
  import {
12
13
  MicOffIcon,
@@ -32,12 +33,59 @@ const optionTextCSS = {
32
33
  whiteSpace: 'nowrap',
33
34
  };
34
35
 
35
- const MuteUnmuteOption = ({ roleName, peerList }: { peerList: HMSPeer[]; roleName: string }) => {
36
- const vanillaStore = useHMSVanillaStore();
37
- const store = vanillaStore.getState();
38
- const hmsActions = useHMSActions();
36
+ const DropdownWrapper = ({ children }: { children: React.ReactNode }) => {
37
+ const [openOptions, setOpenOptions] = useState(false);
38
+ if (React.Children.toArray(children).length === 0) {
39
+ return null;
40
+ }
41
+ return (
42
+ <Dropdown.Root open={openOptions} onOpenChange={setOpenOptions}>
43
+ <Dropdown.Trigger
44
+ data-testid="role_group_options"
45
+ onClick={e => e.stopPropagation()}
46
+ className="role_actions"
47
+ asChild
48
+ css={{
49
+ p: '$1',
50
+ r: '$0',
51
+ c: '$on_surface_high',
52
+ visibility: openOptions ? 'visible' : 'hidden',
53
+ '&:hover': {
54
+ c: '$on_surface_medium',
55
+ },
56
+ '@md': {
57
+ visibility: 'visible',
58
+ },
59
+ }}
60
+ >
61
+ <Flex>
62
+ <VerticalMenuIcon />
63
+ </Flex>
64
+ </Dropdown.Trigger>
65
+ <Dropdown.Content
66
+ onClick={e => e.stopPropagation()}
67
+ css={{ w: 'max-content', bg: '$surface_default', py: 0 }}
68
+ align="end"
69
+ >
70
+ {children}
71
+ </Dropdown.Content>
72
+ </Dropdown.Root>
73
+ );
74
+ };
75
+
76
+ export const RoleOptions = ({ roleName, peerList }: { roleName: string; peerList: HMSPeer[] }) => {
39
77
  const permissions = useHMSStore(selectPermissions);
78
+ const hmsActions = useHMSActions();
79
+ const { elements } = useRoomLayoutConferencingScreen();
80
+ const { on_stage_role, off_stage_roles = [] } = (elements as DefaultConferencingScreen_Elements)?.on_stage_exp || {};
81
+ const canRemoveRoleFromStage = permissions?.changeRole && roleName === on_stage_role;
40
82
  const role = useHMSStore(selectRoleByRoleName(roleName));
83
+ const tracks = useHMSStore(selectTracksMap);
84
+ if (!role) {
85
+ return null;
86
+ }
87
+ const canPublishAudio = role.publishParams.allowed.includes('audio');
88
+ const canPublishVideo = role.publishParams.allowed.includes('video');
41
89
 
42
90
  let isVideoOnForSomePeers = false;
43
91
  let isAudioOnForSomePeers = false;
@@ -46,8 +94,8 @@ const MuteUnmuteOption = ({ roleName, peerList }: { peerList: HMSPeer[]; roleNam
46
94
  if (peer.isLocal) {
47
95
  return;
48
96
  }
49
- const isAudioOn = !!peer.audioTrack && store.tracks[peer.audioTrack]?.enabled;
50
- const isVideoOn = !!peer.videoTrack && store.tracks[peer.videoTrack]?.enabled;
97
+ const isAudioOn = !!peer.audioTrack && tracks[peer.audioTrack]?.enabled;
98
+ const isVideoOn = !!peer.videoTrack && tracks[peer.videoTrack]?.enabled;
51
99
  isAudioOnForSomePeers = isAudioOnForSomePeers || isAudioOn;
52
100
  isVideoOnForSomePeers = isVideoOnForSomePeers || isVideoOn;
53
101
  });
@@ -60,75 +108,11 @@ const MuteUnmuteOption = ({ roleName, peerList }: { peerList: HMSPeer[]; roleNam
60
108
  }
61
109
  };
62
110
 
63
- if (!role) {
64
- return null;
65
- }
66
-
67
- return (
68
- <>
69
- {role.publishParams.allowed.includes('audio') && (
70
- <>
71
- {isAudioOnForSomePeers && permissions?.mute ? (
72
- <Dropdown.Item css={dropdownItemCSS} onClick={() => setTrackEnabled('audio', false)}>
73
- <MicOffIcon />
74
- <Text variant="sm" css={optionTextCSS}>
75
- Mute Audio for All
76
- </Text>
77
- </Dropdown.Item>
78
- ) : null}
79
-
80
- {!isAudioOnForSomePeers && permissions?.unmute ? (
81
- <Dropdown.Item css={dropdownItemCSS} onClick={() => setTrackEnabled('audio', true)}>
82
- <MicOnIcon />
83
- <Text variant="sm" css={optionTextCSS}>
84
- Request to Unmute Audio for All
85
- </Text>
86
- </Dropdown.Item>
87
- ) : null}
88
- </>
89
- )}
90
-
91
- {role.publishParams.allowed.includes('video') && (
92
- <>
93
- {isVideoOnForSomePeers && permissions?.mute ? (
94
- <Dropdown.Item css={dropdownItemCSS} onClick={() => setTrackEnabled('video', false)}>
95
- <VideoOffIcon />
96
- <Text variant="sm" css={optionTextCSS}>
97
- Mute Video for All
98
- </Text>
99
- </Dropdown.Item>
100
- ) : null}
101
-
102
- {!isVideoOnForSomePeers && permissions?.unmute ? (
103
- <Dropdown.Item css={dropdownItemCSS} onClick={() => setTrackEnabled('video', true)}>
104
- <VideoOnIcon />
105
- <Text variant="sm" css={optionTextCSS}>
106
- Request to Unmute Video for All
107
- </Text>
108
- </Dropdown.Item>
109
- ) : null}
110
- </>
111
- )}
112
- </>
113
- );
114
- };
115
-
116
- export const RoleOptions = ({ roleName, peerList }: { roleName: string; peerList: HMSPeer[] }) => {
117
- const [openOptions, setOpenOptions] = useState(false);
118
- const permissions = useHMSStore(selectPermissions);
119
- const hmsActions = useHMSActions();
120
- const { elements } = useRoomLayoutConferencingScreen();
121
- const { on_stage_role, off_stage_roles = [] } = (elements as DefaultConferencingScreen_Elements)?.on_stage_exp || {};
122
- const canMuteOrUnmute = permissions?.mute || permissions?.unmute;
123
- const canRemoveRoleFromStage = permissions?.changeRole && roleName === on_stage_role;
124
- const role = useHMSStore(selectRoleByRoleName(roleName));
125
-
126
111
  // on stage and off stage roles
127
112
  const canRemoveRoleFromRoom =
128
113
  permissions?.removeOthers && (on_stage_role === roleName || off_stage_roles?.includes(roleName));
129
114
 
130
115
  if (
131
- !(canMuteOrUnmute || canRemoveRoleFromStage || canRemoveRoleFromRoom) ||
132
116
  peerList.length === 0 ||
133
117
  // if only local peer is present no need to show any options
134
118
  (peerList.length === 1 && peerList[0].isLocal) ||
@@ -157,60 +141,75 @@ export const RoleOptions = ({ roleName, peerList }: { roleName: string; peerList
157
141
  };
158
142
 
159
143
  return (
160
- <Dropdown.Root open={openOptions} onOpenChange={setOpenOptions}>
161
- <Dropdown.Trigger
162
- data-testid="role_group_options"
163
- onClick={e => e.stopPropagation()}
164
- className="role_actions"
165
- asChild
166
- css={{
167
- p: '$1',
168
- r: '$0',
169
- c: '$on_surface_high',
170
- visibility: openOptions ? 'visible' : 'hidden',
171
- '&:hover': {
172
- c: '$on_surface_medium',
173
- },
174
- '@md': {
175
- visibility: 'visible',
176
- },
177
- }}
178
- >
179
- <Flex>
180
- <VerticalMenuIcon />
181
- </Flex>
182
- </Dropdown.Trigger>
183
- <Dropdown.Content
184
- onClick={e => e.stopPropagation()}
185
- css={{ w: 'max-content', bg: '$surface_default', py: 0 }}
186
- align="end"
187
- >
188
- {canRemoveRoleFromStage && (
189
- <Dropdown.Item
190
- css={{ ...dropdownItemCSS, borderBottom: '1px solid $border_bright' }}
191
- onClick={removeAllFromStage}
192
- >
193
- <PersonRectangleIcon />
194
- <Text variant="sm" css={optionTextCSS}>
195
- Remove all from Stage
196
- </Text>
197
- </Dropdown.Item>
198
- )}
199
-
200
- {canMuteOrUnmute && <MuteUnmuteOption peerList={peerList} roleName={roleName} />}
201
-
202
- {canRemoveRoleFromRoom && (
203
- <Dropdown.Item
204
- css={{ ...dropdownItemCSS, borderTop: '1px solid $border_bright', color: '$alert_error_default' }}
205
- onClick={removePeersFromRoom}
206
- >
207
- <RemoveUserIcon />
208
- <Text variant="sm" css={{ ...optionTextCSS, color: 'inherit' }}>
209
- Remove all from Room
210
- </Text>
211
- </Dropdown.Item>
212
- )}
213
- </Dropdown.Content>
214
- </Dropdown.Root>
144
+ <DropdownWrapper>
145
+ {canRemoveRoleFromStage ? (
146
+ <Dropdown.Item
147
+ css={{ ...dropdownItemCSS, borderBottom: '1px solid $border_bright' }}
148
+ onClick={removeAllFromStage}
149
+ >
150
+ <PersonRectangleIcon />
151
+ <Text variant="sm" css={optionTextCSS}>
152
+ Remove all from Stage
153
+ </Text>
154
+ </Dropdown.Item>
155
+ ) : null}
156
+
157
+ {match({ canPublishAudio, isAudioOnForSomePeers, canMute: permissions?.mute, canUnmute: permissions?.unmute })
158
+ .with({ canPublishAudio: true, isAudioOnForSomePeers: true, canMute: true }, () => {
159
+ return (
160
+ <Dropdown.Item css={dropdownItemCSS} onClick={() => setTrackEnabled('audio', false)}>
161
+ <MicOffIcon />
162
+ <Text variant="sm" css={optionTextCSS}>
163
+ Mute Audio for All
164
+ </Text>
165
+ </Dropdown.Item>
166
+ );
167
+ })
168
+ .with({ canPublishAudio: true, isAudioOnForSomePeers: false, canUnmute: true }, () => {
169
+ return (
170
+ <Dropdown.Item css={dropdownItemCSS} onClick={() => setTrackEnabled('audio', true)}>
171
+ <MicOnIcon />
172
+ <Text variant="sm" css={optionTextCSS}>
173
+ Request to Unmute Audio for All
174
+ </Text>
175
+ </Dropdown.Item>
176
+ );
177
+ })
178
+ .otherwise(() => null)}
179
+ {match({ canPublishVideo, isVideoOnForSomePeers, canMute: permissions?.mute, canUnmute: permissions?.unmute })
180
+ .with({ canPublishVideo: true, isVideoOnForSomePeers: true, canMute: true }, () => {
181
+ return (
182
+ <Dropdown.Item css={dropdownItemCSS} onClick={() => setTrackEnabled('video', false)}>
183
+ <VideoOffIcon />
184
+ <Text variant="sm" css={optionTextCSS}>
185
+ Mute Video for All
186
+ </Text>
187
+ </Dropdown.Item>
188
+ );
189
+ })
190
+ .with({ canPublishVideo: true, isVideoOnForSomePeers: false, canUnmute: true }, () => {
191
+ return (
192
+ <Dropdown.Item css={dropdownItemCSS} onClick={() => setTrackEnabled('video', true)}>
193
+ <VideoOnIcon />
194
+ <Text variant="sm" css={optionTextCSS}>
195
+ Request to Unmute Video for All
196
+ </Text>
197
+ </Dropdown.Item>
198
+ );
199
+ })
200
+ .otherwise(() => null)}
201
+
202
+ {canRemoveRoleFromRoom ? (
203
+ <Dropdown.Item
204
+ css={{ ...dropdownItemCSS, borderTop: '1px solid $border_bright', color: '$alert_error_default' }}
205
+ onClick={removePeersFromRoom}
206
+ >
207
+ <RemoveUserIcon />
208
+ <Text variant="sm" css={{ ...optionTextCSS, color: 'inherit' }}>
209
+ Remove all from Room
210
+ </Text>
211
+ </Dropdown.Item>
212
+ ) : null}
213
+ </DropdownWrapper>
215
214
  );
216
215
  };
@@ -1,17 +1,20 @@
1
1
  import React, { forwardRef } from 'react';
2
- import { useMedia } from 'react-use';
3
- import { config, Flex } from '../../../';
4
- import { useIsLandscape } from '../../common/hooks';
2
+ import { Flex } from '../../../Layout';
5
3
 
6
4
  export const HMSVideo = forwardRef(({ children, ...props }, videoRef) => {
7
- const isLandscape = useIsLandscape();
8
- const isMobile = useMedia(config.media.md);
9
5
  return (
10
6
  <Flex
11
7
  data-testid="hms-video"
12
8
  css={{
13
9
  size: '100%',
14
10
  position: 'relative',
11
+ justifyContent: 'center',
12
+ '@md': {
13
+ height: 'auto',
14
+ '& video': {
15
+ height: '$60 !important',
16
+ },
17
+ },
15
18
  '& video::cue': {
16
19
  color: 'white',
17
20
  whiteSpace: 'pre-line',
@@ -34,16 +37,16 @@ export const HMSVideo = forwardRef(({ children, ...props }, videoRef) => {
34
37
  >
35
38
  <video
36
39
  style={{
37
- flex: '1 1 0',
38
40
  margin: '0 auto',
39
- minHeight: '0',
40
41
  objectFit: 'contain',
41
- width: 'inherit',
42
- height: isLandscape || isMobile ? '100%' : '',
43
- position: isLandscape || isMobile ? 'absolute' : '',
42
+ width: 'auto',
43
+ height: 'auto',
44
+ maxWidth: '100%',
45
+ maxHeight: '100%',
44
46
  }}
45
47
  ref={videoRef}
46
48
  playsInline
49
+ disablePictureInPicture
47
50
  />
48
51
  {children}
49
52
  </Flex>
@@ -17,7 +17,7 @@ import { SIDE_PANE_OPTIONS } from '../../common/constants';
17
17
  half page will have chat or participant view
18
18
  */
19
19
  export const HLSViewTitle = () => {
20
- const { title, details } = useRoomLayoutHeader();
20
+ const { title, details, description } = useRoomLayoutHeader();
21
21
  const toggleDetailsPane = useSidepaneToggle(SIDE_PANE_OPTIONS.ROOM_DETAILS);
22
22
  const isDetailSidepaneOpen = useIsSidepaneTypeOpen(SIDE_PANE_OPTIONS.ROOM_DETAILS);
23
23
 
@@ -74,9 +74,11 @@ export const HLSViewTitle = () => {
74
74
  ) : null}
75
75
  <Flex>
76
76
  <RoomDetailsRow details={details} />
77
- <Text variant="caption" css={{ color: '$on_surface_medium' }} onClick={toggleDetailsPane}>
78
- &nbsp;...more
79
- </Text>
77
+ {description ? (
78
+ <Text variant="caption" css={{ color: '$on_surface_medium' }} onClick={toggleDetailsPane}>
79
+ &nbsp;...more
80
+ </Text>
81
+ ) : null}
80
82
  </Flex>
81
83
  </Flex>
82
84
  </Flex>
@@ -1,54 +1,66 @@
1
- import React, { useEffect, useState } from 'react';
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
2
  import { Box, Flex, Slider } from '../../..';
3
3
  import { useHMSPlayerContext } from './PlayerContext';
4
4
  import { getPercentage } from './utils';
5
5
 
6
- export const VideoProgress = ({ isDvr = true }: { isDvr: boolean }) => {
6
+ export const VideoProgress = ({
7
+ seekProgress,
8
+ setSeekProgress,
9
+ }: {
10
+ seekProgress: boolean;
11
+ setSeekProgress: (value: boolean) => void;
12
+ }) => {
7
13
  const { hlsPlayer } = useHMSPlayerContext();
8
14
  const [videoProgress, setVideoProgress] = useState<number>(0);
9
15
  const [bufferProgress, setBufferProgress] = useState(0);
10
16
  const videoEl = hlsPlayer?.getVideoElement();
11
17
 
12
- const onValueChange = (time: number) => {
13
- hlsPlayer?.seekTo(time);
14
- };
18
+ const setProgress = useCallback(() => {
19
+ if (!videoEl) {
20
+ return;
21
+ }
22
+ const duration = isFinite(videoEl.duration) ? videoEl.duration : videoEl.seekable?.end(0) || 0;
23
+ const videoProgress = Math.floor(getPercentage(videoEl.currentTime, duration));
24
+ let bufferProgress = 0;
25
+ if (videoEl.buffered.length > 0) {
26
+ bufferProgress = Math.floor(getPercentage(videoEl.buffered?.end(0), duration));
27
+ }
28
+ setVideoProgress(isNaN(videoProgress) ? 0 : videoProgress);
29
+ setBufferProgress(isNaN(bufferProgress) ? 0 : bufferProgress);
30
+ }, [videoEl]);
31
+ const timeupdateHandler = useCallback(() => {
32
+ if (!videoEl || seekProgress) {
33
+ return;
34
+ }
35
+ setProgress();
36
+ }, [seekProgress, setProgress, videoEl]);
15
37
  useEffect(() => {
16
38
  if (!videoEl) {
17
39
  return;
18
40
  }
19
- const timeupdateHandler = () => {
20
- if (!videoEl) {
21
- return;
22
- }
23
- const videoProgress = Math.floor(getPercentage(videoEl.currentTime, videoEl.duration));
24
- let bufferProgress = 0;
25
- if (videoEl.buffered.length > 0) {
26
- bufferProgress = Math.floor(getPercentage(videoEl.buffered?.end(0), videoEl.duration));
27
- }
28
-
29
- setVideoProgress(isNaN(videoProgress) ? 0 : videoProgress);
30
- setBufferProgress(isNaN(bufferProgress) ? 0 : bufferProgress);
31
- };
32
41
  videoEl.addEventListener('timeupdate', timeupdateHandler);
33
42
  return function cleanup() {
34
43
  videoEl?.removeEventListener('timeupdate', timeupdateHandler);
35
44
  };
36
- }, [videoEl]);
45
+ }, [timeupdateHandler, videoEl]);
37
46
 
38
47
  const onProgress = (progress: number[]) => {
39
48
  const progress1 = Math.floor(getPercentage(progress[0], 100));
40
49
  const videoEl = hlsPlayer?.getVideoElement();
41
- const currentTime = (progress1 * (videoEl?.duration || 0)) / 100;
42
- if (onValueChange) {
43
- onValueChange(currentTime);
50
+ if (!videoEl) {
51
+ return;
44
52
  }
53
+ const duration = isFinite(videoEl.duration) ? videoEl.duration : videoEl.seekable?.end(0) || 0;
54
+ const currentTime = (progress1 * duration) / 100;
55
+ hlsPlayer?.seekTo(currentTime);
56
+ setProgress();
45
57
  };
46
58
 
47
59
  if (!videoEl) {
48
60
  return null;
49
61
  }
50
62
  return (
51
- <Flex align="center" css={{ cursor: 'pointer', h: '$2', alignSelf: 'stretch', pointerEvents: isDvr ? '' : 'none' }}>
63
+ <Flex align="center" css={{ cursor: 'pointer', h: '$2', alignSelf: 'stretch' }}>
52
64
  <Slider
53
65
  id="video-actual-rest"
54
66
  css={{
@@ -56,7 +68,6 @@ export const VideoProgress = ({ isDvr = true }: { isDvr: boolean }) => {
56
68
  h: '$2',
57
69
  zIndex: 1,
58
70
  transition: `all .2s ease .5s`,
59
- pointerEvents: isDvr ? '' : 'none',
60
71
  }}
61
72
  min={0}
62
73
  max={100}
@@ -64,7 +75,9 @@ export const VideoProgress = ({ isDvr = true }: { isDvr: boolean }) => {
64
75
  value={[videoProgress]}
65
76
  showTooltip={false}
66
77
  onValueChange={onProgress}
67
- thumbStyles={{ w: '$6', h: '$6', display: isDvr ? '' : 'none' }}
78
+ onPointerDown={() => setSeekProgress(true)}
79
+ onPointerUp={() => setSeekProgress(false)}
80
+ thumbStyles={{ w: '$6', h: '$6' }}
68
81
  />
69
82
  <Box
70
83
  id="video-buffer"
@@ -343,7 +343,7 @@ export const MwebOptions = ({
343
343
  mx: '$4',
344
344
  }}
345
345
  >
346
- <EmojiReaction />
346
+ <EmojiReaction showCard />
347
347
  </Box>
348
348
  )}
349
349
  {showRecordingOn && (
@@ -1,5 +1,6 @@
1
1
  import React, { useEffect, useState } from 'react';
2
2
  import { useMedia } from 'react-use';
3
+ import { match, P } from 'ts-pattern';
3
4
  import { RefreshIcon } from '@100mslive/react-icons';
4
5
  import { Button } from '../../Button';
5
6
  import { Box, Flex } from '../../Layout';
@@ -22,21 +23,26 @@ export const MwebLandscapePrompt = () => {
22
23
  }
23
24
 
24
25
  if (!window.screen?.orientation) {
25
- setShowMwebLandscapePrompt(isLandscape);
26
+ setShowMwebLandscapePrompt(isLandscape && !isLandscapeHLSStream);
26
27
  return;
27
28
  }
28
29
  const handleRotation = () => {
29
30
  const angle = window.screen.orientation.angle;
30
31
  const type = window.screen.orientation.type || '';
31
32
  // Angle check needed to diff bw mobile and desktop
32
- setShowMwebLandscapePrompt(angle ? angle >= 90 && type.includes('landscape') : isLandscape);
33
+ setShowMwebLandscapePrompt(
34
+ match({ angle, isLandscapeHLSStream, isLandscape, type })
35
+ .with({ isLandscapeHLSStream }, () => false)
36
+ .with({ angle: P.when(angle => angle && angle >= 90) }, ({ type }) => type.includes('landscape'))
37
+ .otherwise(() => isLandscape),
38
+ );
33
39
  };
34
40
  handleRotation();
35
41
  window.screen.orientation.addEventListener('change', handleRotation);
36
42
  return () => {
37
43
  window.screen.orientation.removeEventListener('change', handleRotation);
38
44
  };
39
- }, [isLandscape]);
45
+ }, [isLandscape, isLandscapeHLSStream]);
40
46
 
41
47
  if (isLandscapeHLSStream) {
42
48
  return null;
@@ -16,7 +16,7 @@ export const getFormattedTime = (milliseconds: number | undefined, precise = tru
16
16
  if (!precise && (hours || minutes)) {
17
17
  return formattedTime;
18
18
  }
19
- formattedTime += `${minutes >= 1 ? Math.floor(seconds) : seconds.toFixed(3)}s`;
19
+ formattedTime += `${precise ? seconds.toFixed(3) : Math.floor(seconds)}s`;
20
20
 
21
21
  return formattedTime;
22
22
  };
@@ -41,9 +41,8 @@ const ParticipantCount = ({ count }: { count: number }) => {
41
41
 
42
42
  export const SidePaneTabs = React.memo<{
43
43
  active: 'Participants | Chat';
44
- hideControls?: boolean;
45
44
  hideTab?: boolean;
46
- }>(({ active = SIDE_PANE_OPTIONS.CHAT, hideControls, hideTab = false }) => {
45
+ }>(({ active = SIDE_PANE_OPTIONS.CHAT, hideTab = false }) => {
47
46
  const toggleChat = useSidepaneToggle(SIDE_PANE_OPTIONS.CHAT);
48
47
  const toggleParticipants = useSidepaneToggle(SIDE_PANE_OPTIONS.PARTICIPANTS);
49
48
  const resetSidePane = useSidepaneReset();
@@ -85,7 +84,6 @@ export const SidePaneTabs = React.memo<{
85
84
  css={{
86
85
  color: '$on_primary_high',
87
86
  h: '100%',
88
- marginTop: hideControls && isOverlayChat ? '$17' : '0',
89
87
  transition: 'margin 0.3s ease-in-out',
90
88
  position: 'relative',
91
89
  }}
@@ -103,7 +101,6 @@ export const SidePaneTabs = React.memo<{
103
101
  css={{
104
102
  color: '$on_primary_high',
105
103
  h: '100%',
106
- marginTop: hideControls && isOverlayChat ? '$17' : '0',
107
104
  transition: 'margin 0.3s ease-in-out',
108
105
  }}
109
106
  >