@100mslive/roomkit-react 0.3.8-alpha.1 → 0.3.8-alpha.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 && isMobile;
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
+ w: 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,192 @@
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)) || 'Participant';
139
+ data = data.trim();
140
+ if (!data) return null;
141
+ return (
142
+ <Text
143
+ variant="body2"
144
+ css={{
145
+ fontWeight: '$normal',
146
+ }}
147
+ >
148
+ <b>{peerName}: </b>
149
+ {data}
150
+ </Text>
151
+ );
152
+ };
153
+
154
+ export const CaptionsViewer = () => {
155
+ const [captionQueue] = useState<CaptionMaintainerQueue>(new CaptionMaintainerQueue());
156
+ const [currentData, setCurrentData] = useState<{ [key: string]: string }[]>([]);
157
+
158
+ useEffect(() => {
159
+ const timeInterval = setInterval(() => {
160
+ if (!captionQueue) {
161
+ return;
162
+ }
163
+ const data = captionQueue.captionData?.findPeerData();
164
+ setCurrentData(data);
165
+ }, 1000);
166
+ return () => clearInterval(timeInterval);
167
+ }, [captionQueue]);
168
+
169
+ useTranscript({
170
+ onTranscript: (data: HMSTranscript[]) => {
171
+ captionQueue && captionQueue.push(data as HMSTranscript[]);
172
+ },
173
+ });
174
+ const dataToShow = currentData.filter((data: { [key: string]: string }) => {
175
+ const key = Object.keys(data)[0];
176
+ if (data[key]) {
177
+ return true;
178
+ }
179
+ return false;
180
+ });
181
+ if (dataToShow.length <= 0) {
182
+ return null;
183
+ }
184
+ return (
185
+ <Flex direction="column">
186
+ {dataToShow.map((data: { [key: string]: string }, index: number) => {
187
+ const key = Object.keys(data)[0];
188
+ return <TranscriptView key={index} peer_id={key} data={data[key]} />;
189
+ })}
190
+ </Flex>
191
+ );
192
+ };