@100mslive/roomkit-react 0.3.8-alpha.1 → 0.3.8-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.
@@ -1,4 +1,5 @@
1
1
  import React, { useEffect, useRef, useState } from 'react';
2
+ import { useMedia } from 'react-use';
2
3
  import { DefaultConferencingScreen_Elements } from '@100mslive/types-prebuilt';
3
4
  import { v4 as uuid } from 'uuid';
4
5
  import {
@@ -18,6 +19,7 @@ import { ActivatedPIP } from './PIP/PIPComponent';
18
19
  import { PictureInPicture } from './PIP/PIPManager';
19
20
  import { RoleChangeRequestModal } from './RoleChangeRequest/RoleChangeRequestModal';
20
21
  import { Box, Flex } from '../../Layout';
22
+ import { config } from '../../Theme';
21
23
  import { useHMSPrebuiltContext } from '../AppContext';
22
24
  import { VideoStreamingSection } from '../layouts/VideoStreamingSection';
23
25
  // @ts-ignore: No implicit Any
@@ -26,17 +28,21 @@ import FullPageProgress from './FullPageProgress';
26
28
  import { Header } from './Header';
27
29
  import { PreviousRoleInMetadata } from './PreviousRoleInMetadata';
28
30
  import { RaiseHand } from './RaiseHand';
31
+ import { CaptionsViewer } from '../plugins/CaptionsViewer';
29
32
  import {
30
33
  useRoomLayoutConferencingScreen,
31
34
  useRoomLayoutPreviewScreen,
32
35
  } from '../provider/roomLayoutProvider/hooks/useRoomLayoutScreen';
33
36
  // @ts-ignore: No implicit Any
34
- import { useAuthToken, useSetAppDataByKey } from './AppData/useUISettings';
37
+ import { useIsSidepaneTypeOpen } from './AppData/useSidepane';
38
+ // @ts-ignore: No implicit Any
39
+ import { useAuthToken, useIsCaptionEnabled, useSetAppDataByKey } from './AppData/useUISettings';
35
40
  import { useLandscapeHLSStream, useMobileHLSStream } from '../common/hooks';
36
- import { APP_DATA, isAndroid, isIOS, isIPadOS } from '../common/constants';
41
+ import { APP_DATA, isAndroid, isIOS, isIPadOS, SIDE_PANE_OPTIONS } from '../common/constants';
37
42
 
38
43
  export const ConferenceScreen = () => {
39
44
  const { userName, endpoints, onJoin: onJoinFunc } = useHMSPrebuiltContext();
45
+ const isMobile = useMedia(config.media.md);
40
46
  const screenProps = useRoomLayoutConferencingScreen();
41
47
  const { isPreviewScreenEnabled } = useRoomLayoutPreviewScreen();
42
48
  const roomState = useHMSStore(selectRoomState);
@@ -57,6 +63,10 @@ export const ConferenceScreen = () => {
57
63
  const isMobileHLSStream = useMobileHLSStream();
58
64
  const isLandscapeHLSStream = useLandscapeHLSStream();
59
65
  const isMwebHLSStream = isMobileHLSStream || isLandscapeHLSStream;
66
+ const isCaptionEnabled = useIsCaptionEnabled();
67
+ const isChatOpen = useIsSidepaneTypeOpen(SIDE_PANE_OPTIONS.CHAT);
68
+
69
+ const showCaptionAtTop = screenProps.elements?.chat?.is_overlay && isChatOpen;
60
70
 
61
71
  const toggleControls = () => {
62
72
  if (dropdownListRef.current?.length === 0 && isMobileDevice && !isMwebHLSStream) {
@@ -124,6 +134,28 @@ export const ConferenceScreen = () => {
124
134
  <FullPageProgress text="Starting live stream..." css={{ opacity: 0.8, bg: '$background_dim' }} />
125
135
  </Box>
126
136
  ) : null}
137
+ {isCaptionEnabled && screenProps.screenType !== 'hls_live_streaming' && (
138
+ <Box
139
+ css={{
140
+ position: 'fixed',
141
+ maxWidth: isMobile ? '100%' : '40%',
142
+ bottom: showCaptionAtTop ? '' : hideControlsForStreaming ? '5%' : '10%',
143
+ top: showCaptionAtTop ? (hideControlsForStreaming ? '5%' : '10%') : '',
144
+ left: isMobile ? 0 : '50%',
145
+ transform: isMobile ? '' : 'translateX(-50%)',
146
+ background: '#000000A3',
147
+ overflow: 'clip',
148
+ zIndex: 10,
149
+ height: 'fit-content',
150
+ r: '$1',
151
+ p: '$6',
152
+ transition: 'bottom 0.3s ease-in-out',
153
+ '&:empty': { display: 'none' },
154
+ }}
155
+ >
156
+ <CaptionsViewer />
157
+ </Box>
158
+ )}
127
159
  <Flex css={{ size: '100%', overflow: 'hidden' }} direction="column">
128
160
  {!(screenProps.hideSections.includes('header') || isMwebHLSStream) && (
129
161
  <Box
@@ -5,6 +5,7 @@ import { Chat_ChatState } from '@100mslive/types-prebuilt/elements/chat';
5
5
  import { config as cssConfig, Footer as AppFooter } from '../../..';
6
6
  // @ts-ignore: No implicit Any
7
7
  import { AudioVideoToggle } from '../AudioVideoToggle';
8
+ import { CaptionIcon } from '../CaptionIcon';
8
9
  // @ts-ignore: No implicit Any
9
10
  import { EmojiReaction } from '../EmojiReaction';
10
11
  // @ts-ignore: No implicit Any
@@ -96,6 +97,7 @@ export const Footer = ({
96
97
  <>
97
98
  <ScreenshareToggle />
98
99
  <RaiseHand />
100
+ {screenType !== 'hls_live_streaming' && <CaptionIcon />}
99
101
  {elements?.emoji_reactions && <EmojiReaction />}
100
102
  <LeaveRoom screenType={screenType} />
101
103
  </>
@@ -109,7 +109,7 @@ export const RoleAccordion = ({
109
109
  },
110
110
  }}
111
111
  >
112
- <Flex justify="between" css={{ flexGrow: 1, pr: '$6' }}>
112
+ <Flex justify="between" align="center" css={{ flexGrow: 1, pr: '$6' }}>
113
113
  <Text
114
114
  variant="sm"
115
115
  css={{ fontWeight: '$semiBold', textTransform: 'capitalize', color: '$on_surface_medium' }}
@@ -5,6 +5,7 @@ import { match } from 'ts-pattern';
5
5
  import {
6
6
  selectIsConnectedToRoom,
7
7
  selectIsLocalVideoEnabled,
8
+ selectIsTranscriptionEnabled,
8
9
  selectPeerCount,
9
10
  selectPermissions,
10
11
  useHMSActions,
@@ -13,12 +14,14 @@ import {
13
14
  } from '@100mslive/react-sdk';
14
15
  import {
15
16
  BrbIcon,
17
+ ClosedCaptionIcon,
16
18
  CrossIcon,
17
19
  EmojiIcon,
18
20
  HamburgerMenuIcon,
19
21
  HandIcon,
20
22
  HandRaiseSlashedIcon,
21
23
  InfoIcon,
24
+ OpenCaptionIcon,
22
25
  PeopleIcon,
23
26
  QuizActiveIcon,
24
27
  QuizIcon,
@@ -49,7 +52,7 @@ import { useSheetToggle } from '../../AppData/useSheet';
49
52
  // @ts-ignore: No implicit any
50
53
  import { usePollViewToggle, useSidepaneToggle } from '../../AppData/useSidepane';
51
54
  // @ts-ignore: No implicit Any
52
- import { useShowPolls } from '../../AppData/useUISettings';
55
+ import { useSetIsCaptionEnabled, useShowPolls } from '../../AppData/useUISettings';
53
56
  // @ts-ignore: No implicit any
54
57
  import { useDropdownList } from '../../hooks/useDropdownList';
55
58
  import { useMyMetadata } from '../../hooks/useMetadata';
@@ -102,6 +105,10 @@ export const MwebOptions = ({
102
105
  const toggleVB = useSidepaneToggle(SIDE_PANE_OPTIONS.VB);
103
106
  const isLocalVideoEnabled = useHMSStore(selectIsLocalVideoEnabled);
104
107
  const { startRecording, isRecordingLoading } = useRecordingHandler();
108
+
109
+ const isCaptionPresent = useHMSStore(selectIsTranscriptionEnabled);
110
+
111
+ const [isCaptionEnabled, setIsCaptionEnabled] = useSetIsCaptionEnabled();
105
112
  useDropdownList({ open: openModals.size > 0 || openOptionsSheet || openSettingsSheet, name: 'MoreSettings' });
106
113
 
107
114
  const updateState = (modalName: string, value: boolean) => {
@@ -186,6 +193,20 @@ export const MwebOptions = ({
186
193
  <ActionTile.Title>{isHandRaised ? 'Lower' : 'Raise'} Hand</ActionTile.Title>
187
194
  </ActionTile.Root>
188
195
  ) : null}
196
+ {isCaptionPresent && screenType !== 'hls_live_streaming' ? (
197
+ <ActionTile.Root
198
+ onClick={() => {
199
+ setIsCaptionEnabled(!isCaptionEnabled);
200
+ }}
201
+ >
202
+ {isCaptionEnabled ? (
203
+ <ClosedCaptionIcon width="20" height="20px" />
204
+ ) : (
205
+ <OpenCaptionIcon width="20" height="20px" />
206
+ )}
207
+ <ActionTile.Title>{isCaptionEnabled ? 'Hide Captions' : 'Captions Disabled'}</ActionTile.Title>
208
+ </ActionTile.Root>
209
+ ) : null}
189
210
 
190
211
  {isLocalVideoEnabled && !!elements?.virtual_background ? (
191
212
  <ActionTile.Root
@@ -105,7 +105,6 @@ export const VideoStreamingSection = ({
105
105
  // @ts-ignore
106
106
  return <GridLayout {...(elements as DefaultConferencingScreen_Elements)?.video_tile_layout?.grid} />;
107
107
  })}
108
-
109
108
  <Box
110
109
  css={{
111
110
  flex: match({ isLandscapeHLSStream, isMobileHLSStream })
@@ -0,0 +1,191 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { HMSTranscript, selectPeerNameByID, useHMSStore, useTranscript } from '@100mslive/react-sdk';
3
+ import { Flex } from '../../Layout';
4
+ import { Text } from '../../Text';
5
+
6
+ interface CaptionQueueData extends HMSTranscript {
7
+ transcriptQueue: SimpleQueue;
8
+ }
9
+
10
+ interface TranscriptData extends HMSTranscript {
11
+ timeout?: NodeJS.Timeout | undefined;
12
+ }
13
+ class SimpleQueue {
14
+ private storage: TranscriptData[] = [];
15
+ constructor(private capacity: number = 3, private MAX_STORAGE_TIME: number = 5000) {}
16
+ enqueue(data: TranscriptData): void {
17
+ if (this.size() === this.capacity && this.storage[this.size() - 1].final) {
18
+ this.dequeue(this.storage[this.size() - 1]);
19
+ }
20
+ if (this.size() === 0) {
21
+ this.storage.push(data);
22
+ this.addTimeout(this.storage[this.size() - 1], data.final);
23
+ return;
24
+ }
25
+ if (this.size() > 0 && this.storage[this.size() - 1]?.final === true) {
26
+ this.storage.push(data);
27
+ this.addTimeout(this.storage[this.size() - 1], data.final);
28
+ return;
29
+ }
30
+ this.storage[this.size() - 1].transcript = data.transcript;
31
+ this.storage[this.size() - 1].final = data.final;
32
+ this.storage[this.size() - 1].end = data.end;
33
+ this.addTimeout(this.storage[this.size() - 1], data.final);
34
+ }
35
+ addTimeout(item: TranscriptData, isFinal: boolean) {
36
+ if (!isFinal) {
37
+ return;
38
+ }
39
+ item.timeout = setTimeout(() => {
40
+ this.dequeue(item);
41
+ }, this.MAX_STORAGE_TIME);
42
+ }
43
+ dequeue(item: TranscriptData): TranscriptData | undefined {
44
+ const index = this.storage.indexOf(item);
45
+ if (index === -1) {
46
+ return undefined;
47
+ }
48
+ const removedItem = this.storage.splice(index, 1);
49
+ if (removedItem.length <= 0) {
50
+ return undefined;
51
+ }
52
+ this.clearTimeout(removedItem[0]);
53
+ return item;
54
+ }
55
+ clearTimeout(item: TranscriptData) {
56
+ if (!item.timeout) {
57
+ return;
58
+ }
59
+ clearTimeout(item.timeout);
60
+ }
61
+ peek(): TranscriptData | undefined {
62
+ if (this.size() <= 0) {
63
+ return undefined;
64
+ }
65
+ return this.storage[0];
66
+ }
67
+ getTranscription(): string {
68
+ let script = '';
69
+ this.storage.forEach((value: TranscriptData) => (script += value.transcript + ' '));
70
+ return script;
71
+ }
72
+ reset() {
73
+ this.storage.length = 0;
74
+ }
75
+ size(): number {
76
+ return this.storage.length;
77
+ }
78
+ }
79
+ class Queue {
80
+ private storage: Record<string, CaptionQueueData> = {};
81
+ constructor(private capacity: number = 3) {}
82
+
83
+ enqueue(data: HMSTranscript): void {
84
+ if (this.size() === this.capacity) {
85
+ this.dequeue();
86
+ }
87
+ if (!this.storage[data.peer_id]) {
88
+ this.storage[data.peer_id] = {
89
+ peer_id: data.peer_id,
90
+ transcript: data.transcript,
91
+ final: data.final,
92
+ transcriptQueue: new SimpleQueue(),
93
+ start: data.start,
94
+ end: data.end,
95
+ };
96
+ this.storage[data.peer_id].transcriptQueue.enqueue(data as TranscriptData);
97
+ return;
98
+ }
99
+ this.storage[data.peer_id].transcriptQueue.enqueue(data as TranscriptData);
100
+ }
101
+ dequeue(): CaptionQueueData {
102
+ const key: string = Object.keys(this.storage).shift() || '';
103
+ const captionData = this.storage[key];
104
+ captionData.transcriptQueue.reset();
105
+ delete this.storage[key];
106
+ return captionData;
107
+ }
108
+
109
+ peek(): CaptionQueueData | undefined {
110
+ if (this.size() <= 0) return undefined;
111
+ const key: string = Object.keys(this.storage).shift() || '';
112
+ return this.storage[key];
113
+ }
114
+
115
+ findPeerData(): { [key: string]: string }[] {
116
+ const keys = Object.keys(this.storage);
117
+ const data = keys.map((key: string) => {
118
+ const data = this.storage[key];
119
+ const word = data.transcriptQueue.getTranscription();
120
+ return { [key]: word };
121
+ });
122
+ return data;
123
+ }
124
+ size(): number {
125
+ return Object.keys(this.storage).length;
126
+ }
127
+ }
128
+
129
+ class CaptionMaintainerQueue {
130
+ captionData: Queue = new Queue();
131
+ push(data: HMSTranscript[] = []) {
132
+ data.forEach((value: HMSTranscript) => {
133
+ this.captionData.enqueue(value);
134
+ });
135
+ }
136
+ }
137
+ const TranscriptView = ({ peer_id, data }: { peer_id: string; data: string }) => {
138
+ const peerName = useHMSStore(selectPeerNameByID(peer_id));
139
+ data = data.trim();
140
+ if (!data) return null;
141
+ return (
142
+ <Text
143
+ variant="body2"
144
+ css={{
145
+ fontWeight: '$normal',
146
+ }}
147
+ >
148
+ {`${peerName}: ${data}`}
149
+ </Text>
150
+ );
151
+ };
152
+
153
+ export const CaptionsViewer = () => {
154
+ const [captionQueue] = useState<CaptionMaintainerQueue>(new CaptionMaintainerQueue());
155
+ const [currentData, setCurrentData] = useState<{ [key: string]: string }[]>([]);
156
+
157
+ useEffect(() => {
158
+ const timeInterval = setInterval(() => {
159
+ if (!captionQueue) {
160
+ return;
161
+ }
162
+ const data = captionQueue.captionData?.findPeerData();
163
+ setCurrentData(data);
164
+ }, 1000);
165
+ return () => clearInterval(timeInterval);
166
+ }, [captionQueue]);
167
+
168
+ useTranscript({
169
+ onTranscript: (data: HMSTranscript[]) => {
170
+ captionQueue && captionQueue.push(data as HMSTranscript[]);
171
+ },
172
+ });
173
+ const dataToShow = currentData.filter((data: { [key: string]: string }) => {
174
+ const key = Object.keys(data)[0];
175
+ if (data[key]) {
176
+ return true;
177
+ }
178
+ return false;
179
+ });
180
+ if (dataToShow.length <= 0) {
181
+ return null;
182
+ }
183
+ return (
184
+ <Flex direction="column" gap={1}>
185
+ {dataToShow.map((data: { [key: string]: string }, index: number) => {
186
+ const key = Object.keys(data)[0];
187
+ return <TranscriptView key={index} peer_id={key} data={data[key]} />;
188
+ })}
189
+ </Flex>
190
+ );
191
+ };