agora-appbuilder-core 4.1.22 → 4.1.23-beta.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agora-appbuilder-core",
3
- "version": "4.1.22",
3
+ "version": "4.1.23-beta.2",
4
4
  "description": "React Native template for RTE app builder",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -0,0 +1,69 @@
1
+ import React from 'react';
2
+ import TestRenderer, {act} from 'react-test-renderer';
3
+ import {describe, expect, it, jest} from '@jest/globals';
4
+
5
+ jest.mock('customization', () => ({
6
+ __esModule: true,
7
+ default: {
8
+ lifecycle: {
9
+ beforeEndCall: jest.fn(),
10
+ },
11
+ },
12
+ }));
13
+
14
+ jest.mock('../../src/components/SdkApiContext', () => {
15
+ const ReactModule = require('react');
16
+
17
+ return {
18
+ SdkApiContext: ReactModule.createContext({customize: {}}),
19
+ };
20
+ });
21
+
22
+ import customizationConfig from 'customization';
23
+ import {SdkApiContext} from '../../src/components/SdkApiContext';
24
+ import {CustomizationProvider, useCustomization} from '../useCustomization';
25
+
26
+ const Consumer = ({onRender}: {onRender: (value: unknown) => void}) => {
27
+ onRender(useCustomization());
28
+ return null;
29
+ };
30
+
31
+ describe('CustomizationProvider', () => {
32
+ it('uses the default customization until SDK customization is available', () => {
33
+ const onRender = jest.fn();
34
+ let renderer!: TestRenderer.ReactTestRenderer;
35
+
36
+ act(() => {
37
+ renderer = TestRenderer.create(
38
+ <CustomizationProvider>
39
+ <Consumer onRender={onRender} />
40
+ </CustomizationProvider>,
41
+ );
42
+ });
43
+
44
+ expect(onRender).toHaveBeenLastCalledWith(customizationConfig);
45
+ renderer.unmount();
46
+ });
47
+
48
+ it('uses customization supplied through the SDK', () => {
49
+ const onRender = jest.fn();
50
+ const customization = {lifecycle: {onDeepLink: jest.fn()}};
51
+ let renderer!: TestRenderer.ReactTestRenderer;
52
+
53
+ act(() => {
54
+ renderer = TestRenderer.create(
55
+ <SdkApiContext.Provider
56
+ value={{customize: {customization}} as React.ContextType<
57
+ typeof SdkApiContext
58
+ >}>
59
+ <CustomizationProvider>
60
+ <Consumer onRender={onRender} />
61
+ </CustomizationProvider>
62
+ </SdkApiContext.Provider>,
63
+ );
64
+ });
65
+
66
+ expect(onRender).toHaveBeenLastCalledWith(customization);
67
+ renderer.unmount();
68
+ });
69
+ });
@@ -22,7 +22,8 @@ const CustomizationProvider: React.FC = (props) => {
22
22
  const {customize: userCustomization} = useContext(SdkApiContext);
23
23
 
24
24
  return (
25
- <CustomizationContext.Provider value={userCustomization.customization}>
25
+ <CustomizationContext.Provider
26
+ value={userCustomization.customization ?? customizationConfig}>
26
27
  {props.children}
27
28
  </CustomizationContext.Provider>
28
29
  );
@@ -77,8 +77,8 @@ const DefaultConfig = {
77
77
  CHAT_ORG_NAME: "",
78
78
  CHAT_APP_NAME: "",
79
79
  CHAT_URL: "",
80
- CLI_VERSION: "3.1.22",
81
- CORE_VERSION: "4.1.22",
80
+ CLI_VERSION: "3.1.23-beta.2",
81
+ CORE_VERSION: "4.1.23-beta.2",
82
82
  DISABLE_LANDSCAPE_MODE: false,
83
83
  STT_AUTO_START: false,
84
84
  CLOUD_RECORDING_AUTO_START: false,
@@ -278,7 +278,7 @@ const TranscriptHeaderActionMenu = (props: TranscriptHeaderActionMenuProps) => {
278
278
  transcriptViewMode,
279
279
  setTranscriptViewMode,
280
280
  } = useCaption();
281
- const {downloadTranscript} = useTranscriptDownload();
281
+ const {downloadTranscript, isTranscriptPreparing} = useTranscriptDownload();
282
282
  const [modalPosition, setModalPosition] = React.useState({});
283
283
  const [isPosCalculated, setIsPosCalculated] = React.useState(false);
284
284
  const {width: globalWidth, height: globalHeight} = useWindowDimensions();
@@ -363,9 +363,12 @@ const TranscriptHeaderActionMenu = (props: TranscriptHeaderActionMenuProps) => {
363
363
  iconColor: $config.SECONDARY_ACTION_COLOR,
364
364
  textColor: $config.FONT_COLOR,
365
365
  title: downloadTranscriptLabel,
366
- disabled: meetingTranscript.length === 0 || isLangChangeInProgress,
367
- onPress: () => {
368
- downloadTranscript();
366
+ disabled:
367
+ meetingTranscript.length === 0 ||
368
+ isLangChangeInProgress ||
369
+ isTranscriptPreparing,
370
+ onPress: async () => {
371
+ await downloadTranscript();
369
372
  setActionMenuVisible(false);
370
373
  },
371
374
  });
@@ -1,11 +1,9 @@
1
1
  import {StyleSheet, TextStyle, View} from 'react-native';
2
2
  import React from 'react';
3
- import {useContent, useRtc} from 'customization-api';
3
+ import {useContent} from 'customization-api';
4
4
  import {useCaption} from './useCaption';
5
5
  import CaptionText from './CaptionText';
6
6
  import Loading from '../Loading';
7
- import {isWebInternal} from '../../utils/common';
8
- import useStreamMessageUtils from './useStreamMessageUtils';
9
7
  import hexadecimalTransparency from '../../utils/hexadecimalTransparency';
10
8
  import {useString} from '../../utils/useString';
11
9
  import {useLocalUid} from '../../../agora-rn-uikit';
@@ -14,17 +12,6 @@ import {
14
12
  sttSettingTranslationLanguageText,
15
13
  } from '../../language/default-labels/videoCallScreenLabels';
16
14
 
17
- export type WebStreamMessageArgs = [number, Uint8Array];
18
- export type NativeStreamMessageArgs = [
19
- {},
20
- number,
21
- number,
22
- Uint8Array,
23
- number,
24
- number,
25
- ];
26
- export type StreamMessageArgs = WebStreamMessageArgs | NativeStreamMessageArgs;
27
-
28
15
  interface CaptionProps {
29
16
  captionTextStyle?: TextStyle;
30
17
  captionUserStyle?: TextStyle;
@@ -34,12 +21,9 @@ const Caption: React.FC<CaptionProps> = ({
34
21
  captionTextStyle = {},
35
22
  captionUserStyle = {},
36
23
  }) => {
37
- const {RtcEngineUnsafe} = useRtc();
38
24
  const {
39
25
  isLangChangeInProgress,
40
26
  captionObj, //state for current live caption for all users
41
- isSTTListenerAdded,
42
- setIsSTTListenerAdded,
43
27
  activeSpeakerRef,
44
28
  prevSpeakerRef,
45
29
  getBotOwnerUid,
@@ -51,32 +35,11 @@ const Caption: React.FC<CaptionProps> = ({
51
35
  const stLabel = useString<boolean>(sttSettingTranslationLanguageText)(
52
36
  isSTTActive,
53
37
  );
54
- const {streamMessageCallback} = useStreamMessageUtils();
55
38
  const {defaultContent} = useContent();
56
39
 
57
40
  const [activelinesAvailable, setActiveLinesAvailable] = React.useState(0);
58
41
  const [inActiveLinesAvailable, setInActiveLinesAvaialble] = React.useState(0);
59
42
 
60
- const handleStreamMessageCallback = (...args: StreamMessageArgs) => {
61
- setIsSTTListenerAdded(true);
62
- if (isWebInternal()) {
63
- const [uid, data] = args as WebStreamMessageArgs;
64
- streamMessageCallback([uid, data]);
65
- } else {
66
- const [, uid, , data] = args as NativeStreamMessageArgs;
67
- streamMessageCallback([uid, data]);
68
- }
69
- };
70
-
71
- React.useEffect(() => {
72
- !isSTTListenerAdded &&
73
- RtcEngineUnsafe.addListener(
74
- 'onStreamMessage',
75
- handleStreamMessageCallback,
76
- );
77
- // eslint-disable-next-line react-hooks/exhaustive-deps
78
- }, []);
79
-
80
43
  if (isLangChangeInProgress) {
81
44
  return (
82
45
  <Loading
@@ -1,9 +1,8 @@
1
1
  import {View} from 'react-native';
2
2
  import React from 'react';
3
3
  import IconButton, {IconButtonProps} from '../../atoms/IconButton';
4
- import {LanguageTranslationConfig, useCaption} from './useCaption';
4
+ import {useCaption} from './useCaption';
5
5
  import LanguageSelectorPopup from './LanguageSelectorPopup';
6
- import useSTTAPI from './useSTTAPI';
7
6
  import {useString} from '../../utils/useString';
8
7
  import {toolbarItemCaptionText} from '../../language/default-labels/videoCallScreenLabels';
9
8
  import {useToolbarProps} from '../../atoms/ToolbarItem';
@@ -31,7 +30,6 @@ const CaptionIcon = (props: CaptionIconProps) => {
31
30
  isCaptionON,
32
31
  setIsCaptionON,
33
32
  isSTTActive,
34
- isSTTError,
35
33
  sttDepsReady,
36
34
  confirmSpokenLanguageChange,
37
35
  } = useCaption();
@@ -45,13 +43,16 @@ const CaptionIcon = (props: CaptionIconProps) => {
45
43
  const captionLabel = useString<boolean>(toolbarItemCaptionText);
46
44
  const label = captionLabel(isCaptionON);
47
45
  const onPress = () => {
48
- if (isSTTError || !isSTTActive) {
49
- // Show popup when error or STT not active
46
+ // Hiding an already-visible caption is a local UI action and must not be
47
+ // blocked by a failed or delayed STT API request.
48
+ if (isCaptionON) {
49
+ setIsCaptionON(false);
50
+ closeActionSheet?.();
51
+ } else if (!isSTTActive) {
50
52
  setLanguagePopup(true);
51
53
  } else {
52
- // STT is active and no error
53
- setIsCaptionON(prev => !prev);
54
- closeActionSheet();
54
+ setIsCaptionON(true);
55
+ closeActionSheet?.();
55
56
  }
56
57
  // if (isSTTActive) {
57
58
  // // is lang popup has been shown once for any user in meeting
@@ -91,9 +92,9 @@ const CaptionIcon = (props: CaptionIconProps) => {
91
92
 
92
93
  const onConfirm = async (newSpokenLang: LanguageType) => {
93
94
  try {
94
- closeActionSheet();
95
+ closeActionSheet?.();
95
96
  setLanguagePopup(false);
96
- setIsCaptionON(prev => !prev);
97
+ setIsCaptionON(true);
97
98
  await confirmSpokenLanguageChange(newSpokenLang);
98
99
  } catch (error) {
99
100
  setIsCaptionON(false);
@@ -23,8 +23,8 @@ const DownloadTranscriptBtn = (props: DownloadTranscriptBtn) => {
23
23
  iconName = 'download',
24
24
  text = downloadTranscriptLabel,
25
25
  } = props;
26
- const {isSTTActive} = useCaption();
27
- const {downloadTranscript} = useTranscriptDownload();
26
+ const {meetingTranscript} = useCaption();
27
+ const {downloadTranscript, isTranscriptPreparing} = useTranscriptDownload();
28
28
  return (
29
29
  <View style={styles.btnContainer}>
30
30
  <PrimaryButton
@@ -33,8 +33,9 @@ const DownloadTranscriptBtn = (props: DownloadTranscriptBtn) => {
33
33
  iconName={iconName}
34
34
  containerStyle={[styles.btnContainerStyle, containerStyle] as Object}
35
35
  textStyle={[styles.btnTxtStyle, textStyle] as Object}
36
- onPress={() => {
37
- downloadTranscript();
36
+ disabled={isTranscriptPreparing || meetingTranscript.length === 0}
37
+ onPress={async () => {
38
+ await downloadTranscript();
38
39
  }}
39
40
  text={text}
40
41
  />
@@ -21,7 +21,7 @@ import {
21
21
  useIsSmall,
22
22
  } from '../../utils/common';
23
23
  import {TranscriptHeader} from '../../pages/video-call/SidePanelHeader';
24
- import {useRtc, useContent} from 'customization-api';
24
+ import {useContent} from 'customization-api';
25
25
  import {TranscriptItem, useCaption} from './useCaption';
26
26
  import {TranscriptText} from './TranscriptText';
27
27
  import PrimaryButton from '../../atoms/PrimaryButton';
@@ -30,7 +30,6 @@ import Loading from '../Loading';
30
30
  import ImageIcon from '../../atoms/ImageIcon';
31
31
  import hexadecimalTransparency from '../../../src/utils/hexadecimalTransparency';
32
32
  import Spacer from '../../atoms/Spacer';
33
- import useStreamMessageUtils from './useStreamMessageUtils';
34
33
  import useCaptionWidth from './useCaptionWidth';
35
34
  import DownloadTranscriptBtn from './DownloadTranscriptBtn';
36
35
  import {useString} from '../../../src/utils/useString';
@@ -46,10 +45,6 @@ export interface TranscriptProps {
46
45
  showHeader?: boolean;
47
46
  }
48
47
 
49
- type WebStreamMessageArgs = [number, Uint8Array];
50
- type NativeStreamMessageArgs = [{}, number, number, Uint8Array, number, number];
51
- type StreamMessageArgs = WebStreamMessageArgs | NativeStreamMessageArgs;
52
-
53
48
  const Transcript = (props: TranscriptProps) => {
54
49
  const isSmall = useIsSmall();
55
50
  const {currentLayout} = useLayout();
@@ -57,8 +52,6 @@ const Transcript = (props: TranscriptProps) => {
57
52
  const {
58
53
  meetingTranscript,
59
54
  isLangChangeInProgress,
60
- isSTTListenerAdded,
61
- setIsSTTListenerAdded,
62
55
  getBotOwnerUid,
63
56
  isSTTActive,
64
57
  selectedTranslationLanguage,
@@ -82,9 +75,7 @@ const Transcript = (props: TranscriptProps) => {
82
75
  const flatListRef = React.useRef(null);
83
76
  const [searchQuery, setSearchQuery] = React.useState('');
84
77
  const [searchResults, setSearchResults] = React.useState([]);
85
- const {RtcEngineUnsafe} = useRtc();
86
78
  const {defaultContent} = useContent();
87
- const {streamMessageCallback} = useStreamMessageUtils();
88
79
 
89
80
  const [isFocused, setIsFocused] = React.useState(false);
90
81
 
@@ -257,28 +248,6 @@ const Transcript = (props: TranscriptProps) => {
257
248
  return <Text style={styles.emptyMsg}>{noresults}</Text>;
258
249
  };
259
250
 
260
- const handleStreamMessageCallback = (...args: StreamMessageArgs) => {
261
- console.log('[STT_GLOBAL] handleStreamMessageCallback', args);
262
- setIsSTTListenerAdded(true);
263
- if (isWebInternal()) {
264
- const [uid, data] = args as WebStreamMessageArgs;
265
- streamMessageCallback([uid, data]);
266
- } else {
267
- const [, uid, , data] = args as NativeStreamMessageArgs;
268
- streamMessageCallback([uid, data]);
269
- }
270
- };
271
-
272
- React.useEffect(() => {
273
- if (!isSTTListenerAdded) {
274
- RtcEngineUnsafe.addListener(
275
- 'onStreamMessage',
276
- handleStreamMessageCallback,
277
- );
278
- }
279
- // eslint-disable-next-line react-hooks/exhaustive-deps
280
- }, []);
281
-
282
251
  return (
283
252
  <View
284
253
  // onLayout={handleLayout}
@@ -1,4 +1,4 @@
1
- import {StyleSheet, View} from 'react-native';
1
+ import {View} from 'react-native';
2
2
  import React from 'react';
3
3
  import {SidePanelType, useSidePanel} from 'customization-api';
4
4
  import IconButton, {IconButtonProps} from '../../atoms/IconButton';
@@ -23,16 +23,13 @@ const TranscriptIcon = (props: TranscriptIconProps) => {
23
23
  useToolbarProps();
24
24
  const {setSidePanel, sidePanel} = useSidePanel();
25
25
  const {
26
- showToolTip = false,
27
26
  showLabel = $config.ICON_TEXT,
28
27
  disabled = false,
29
28
  isOnActionSheet = false,
30
- isMobileView = false,
31
29
  } = props;
32
30
 
33
31
  // const {start, restart, isAuthorizedTranscriptUser} = useSTTAPI();
34
- const {isSTTActive, isSTTError, sttDepsReady, confirmSpokenLanguageChange} =
35
- useCaption();
32
+ const {isSTTActive, sttDepsReady, confirmSpokenLanguageChange} = useCaption();
36
33
  // const isDisabled = !isAuthorizedTranscriptUser();
37
34
  const [isLanguagePopupOpen, setLanguagePopup] =
38
35
  React.useState<boolean>(false);
@@ -40,13 +37,14 @@ const TranscriptIcon = (props: TranscriptIconProps) => {
40
37
 
41
38
  const isTranscriptON = sidePanel === SidePanelType.Transcript;
42
39
  const onPress = () => {
43
- if (isSTTError || !isSTTActive) {
40
+ // Closing an open transcript is always allowed, even if this participant's
41
+ // STT bot is retrying or failed to start.
42
+ if (isTranscriptON) {
43
+ setSidePanel(SidePanelType.None);
44
+ } else if (!isSTTActive) {
44
45
  setLanguagePopup(true);
45
46
  } else {
46
- // isFirstTimePopupOpen.current = true;
47
- setSidePanel(
48
- isTranscriptON ? SidePanelType.None : SidePanelType.Transcript,
49
- );
47
+ setSidePanel(SidePanelType.Transcript);
50
48
  }
51
49
  };
52
50
 
@@ -62,7 +60,7 @@ const TranscriptIcon = (props: TranscriptIconProps) => {
62
60
  ? $config.PRIMARY_ACTION_TEXT_COLOR
63
61
  : $config.SECONDARY_ACTION_COLOR,
64
62
  },
65
- disabled: !sttDepsReady,
63
+ disabled: disabled || !sttDepsReady,
66
64
  btnTextProps: {
67
65
  text: showLabel
68
66
  ? isOnActionSheet
@@ -91,11 +89,7 @@ const TranscriptIcon = (props: TranscriptIconProps) => {
91
89
  // await restart(language, userOwnLanguages);
92
90
  // }
93
91
  await confirmSpokenLanguageChange(newSpokenLang);
94
- if (!isTranscriptON) {
95
- setSidePanel(SidePanelType.Transcript);
96
- } else {
97
- setSidePanel(SidePanelType.None);
98
- }
92
+ setSidePanel(SidePanelType.Transcript);
99
93
  } catch (error) {
100
94
  console.log('eror in starting stt', error);
101
95
  }
@@ -115,5 +109,3 @@ const TranscriptIcon = (props: TranscriptIconProps) => {
115
109
  };
116
110
 
117
111
  export default TranscriptIcon;
118
-
119
- const styles = StyleSheet.create({});
@@ -18,7 +18,9 @@ import {
18
18
  } from '../../language/default-labels/videoCallScreenLabels';
19
19
  import chatContext from '../../components/ChatContext';
20
20
  import {useRoomInfo} from '../../components/room-info/useRoomInfo';
21
- import {useContent} from 'customization-api';
21
+ import {useContent, useRtc} from 'customization-api';
22
+ import useStreamMessageUtils from './useStreamMessageUtils';
23
+ import {isWebInternal} from '../../utils/common';
22
24
 
23
25
  // Types
24
26
  type GlobalSttState = {
@@ -64,7 +66,7 @@ export type TranscriptItem = {
64
66
  selectedTranslationLanguage?: LanguageType;
65
67
  };
66
68
 
67
- type CaptionObj = {
69
+ export type CaptionObj = {
68
70
  [key: string]: {
69
71
  text: string;
70
72
  translations: TranslationItem[];
@@ -127,6 +129,8 @@ export const CaptionContext = React.createContext<{
127
129
  // holds meeting transcript
128
130
  meetingTranscript: TranscriptItem[];
129
131
  setMeetingTranscript: React.Dispatch<React.SetStateAction<TranscriptItem[]>>;
132
+ flushPendingTranscript: () => Promise<void>;
133
+ getMeetingTranscript: () => TranscriptItem[];
130
134
 
131
135
  // holds status of stt language change process
132
136
  isLangChangeInProgress: boolean;
@@ -180,6 +184,8 @@ export const CaptionContext = React.createContext<{
180
184
  setTranscriptViewMode: () => {},
181
185
  meetingTranscript: [],
182
186
  setMeetingTranscript: () => {},
187
+ flushPendingTranscript: async () => {},
188
+ getMeetingTranscript: () => [],
183
189
  isLangChangeInProgress: false,
184
190
  setIsLangChangeInProgress: () => {},
185
191
  captionObj: {},
@@ -236,9 +242,27 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
236
242
  React.useState<boolean>(false);
237
243
 
238
244
  const [captionObj, setCaptionObj] = React.useState<CaptionObj>({});
239
- const [meetingTranscript, setMeetingTranscript] = React.useState<
245
+ const [meetingTranscript, setMeetingTranscriptState] = React.useState<
240
246
  TranscriptItem[]
241
247
  >([]);
248
+ const meetingTranscriptRef = React.useRef<TranscriptItem[]>([]);
249
+ const setMeetingTranscript = React.useCallback<
250
+ React.Dispatch<React.SetStateAction<TranscriptItem[]>>
251
+ >(nextTranscript => {
252
+ const nextValue =
253
+ typeof nextTranscript === 'function'
254
+ ? nextTranscript(meetingTranscriptRef.current)
255
+ : nextTranscript;
256
+
257
+ // Keep an immediately readable canonical snapshot. React state can be
258
+ // committed after the stream-processing queue has already become idle.
259
+ meetingTranscriptRef.current = nextValue;
260
+ setMeetingTranscriptState(nextValue);
261
+ }, []);
262
+ const getMeetingTranscript = React.useCallback(
263
+ () => meetingTranscriptRef.current,
264
+ [],
265
+ );
242
266
 
243
267
  const [isSTTListenerAdded, setIsSTTListenerAdded] =
244
268
  React.useState<boolean>(false);
@@ -287,6 +311,61 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
287
311
  selectedTranslationLanguageRef.current = selectedTranslationLanguage;
288
312
  }, [selectedTranslationLanguage]);
289
313
 
314
+ const {RtcEngineUnsafe} = useRtc();
315
+ const {streamMessageCallback, flushStreamMessageQueue} =
316
+ useStreamMessageUtils({
317
+ setCaptionObj,
318
+ setMeetingTranscript,
319
+ activeSpeakerRef,
320
+ prevSpeakerRef,
321
+ selectedTranslationLanguageRef,
322
+ });
323
+ const transcriptListenerSubscriptionRef = React.useRef<{
324
+ remove: () => void;
325
+ } | null>(null);
326
+
327
+ const handleStreamMessageCallback = React.useCallback(
328
+ (...args: any[]) => {
329
+ if (isWebInternal()) {
330
+ const [uid, data] = args;
331
+ streamMessageCallback([uid, data]);
332
+ } else {
333
+ const [, uid, , data] = args;
334
+ streamMessageCallback([uid, data]);
335
+ }
336
+ },
337
+ [streamMessageCallback],
338
+ );
339
+
340
+ // Register once for the lifetime of the active call. Keeping collection in
341
+ // the provider makes it independent of whether caption/transcript UI is open.
342
+ React.useEffect(() => {
343
+ if (
344
+ !$config.ENABLE_STT ||
345
+ !callActive ||
346
+ transcriptListenerSubscriptionRef.current
347
+ ) {
348
+ return;
349
+ }
350
+
351
+ transcriptListenerSubscriptionRef.current = RtcEngineUnsafe.addListener(
352
+ 'onStreamMessage',
353
+ handleStreamMessageCallback,
354
+ );
355
+ setIsSTTListenerAdded(true);
356
+ }, [RtcEngineUnsafe, callActive, handleStreamMessageCallback]);
357
+
358
+ React.useEffect(() => {
359
+ return () => {
360
+ transcriptListenerSubscriptionRef.current?.remove();
361
+ transcriptListenerSubscriptionRef.current = null;
362
+ };
363
+ }, []);
364
+
365
+ const flushPendingTranscript = React.useCallback(async () => {
366
+ await flushStreamMessageQueue();
367
+ }, [flushStreamMessageQueue]);
368
+
290
369
  const isSTTActive = globalSttState.globalSttEnabled;
291
370
 
292
371
  // STT API methods
@@ -448,8 +527,7 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
448
527
  setIsLangChangeInProgress(true);
449
528
  const result = await start(localBotUidRef.current, newConfig);
450
529
  console.log('[STT] start result: ', result);
451
- if (result.success || result.error?.code === 610) {
452
- // Success or already started
530
+ if (result.success) {
453
531
  setIsSTTError(false);
454
532
  logger.log(
455
533
  LogSource.NetworkRest,
@@ -810,8 +888,18 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
810
888
  targetChange,
811
889
  );
812
890
  if (!ok) {
813
- console.warn('[STT] Skipping global state update because API failed.');
814
- continue;
891
+ if (isLocal) {
892
+ console.warn(
893
+ '[STT] Skipping local global state update because API failed.',
894
+ );
895
+ continue;
896
+ }
897
+ // A received RTM event is authoritative for the meeting-level state.
898
+ // Failure to provision this participant's bot must not make the UI
899
+ // report that meeting STT is disabled while captions are arriving.
900
+ console.warn(
901
+ '[STT] Applying remote global state after local STT API failure.',
902
+ );
815
903
  }
816
904
  // update global state AFTER processing
817
905
  setGlobalSttState(newState);
@@ -848,6 +936,7 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
848
936
  targets: newState.globalTranslationTargets,
849
937
  });
850
938
  if (!result.success) {
939
+ sttStartGuardRef.current = false;
851
940
  return false;
852
941
  }
853
942
  buildSttTranscriptForSourceChanged(
@@ -979,6 +1068,9 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
979
1068
  }
980
1069
  return true;
981
1070
  } catch (error) {
1071
+ if (isStartOperation) {
1072
+ sttStartGuardRef.current = false;
1073
+ }
982
1074
  logger.error(
983
1075
  LogSource.Internals,
984
1076
  'STT',
@@ -1033,6 +1125,8 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
1033
1125
  setTranscriptViewMode,
1034
1126
  meetingTranscript,
1035
1127
  setMeetingTranscript,
1128
+ flushPendingTranscript,
1129
+ getMeetingTranscript,
1036
1130
  isLangChangeInProgress,
1037
1131
  setIsLangChangeInProgress,
1038
1132
  captionObj,
@@ -11,12 +11,26 @@ import {ensureSTTSessionId} from './sttSessionId';
11
11
  export interface STTAPIResponse {
12
12
  success: boolean;
13
13
  data?: any;
14
+ httpStatus?: number;
14
15
  error?: {
15
16
  message: string;
16
17
  code?: number;
17
18
  };
18
19
  }
19
20
 
21
+ export const STT_START_MAX_ATTEMPTS = 5;
22
+ export const STT_START_RETRY_DELAY_MS = 500;
23
+ const STT_START_RETRY_SPREAD_MS = 200;
24
+
25
+ const wait = (delayMs: number) =>
26
+ new Promise<void>(resolve => setTimeout(resolve, delayMs));
27
+
28
+ const getStartRetryDelay = (botUid: number) =>
29
+ STT_START_RETRY_DELAY_MS + (Math.abs(botUid) % STT_START_RETRY_SPREAD_MS);
30
+
31
+ const shouldRetryStart = (result: STTAPIResponse) =>
32
+ result.httpStatus === 429 || result.error?.code === 610;
33
+
20
34
  interface IuseSTTAPI {
21
35
  start: (
22
36
  botUid: number,
@@ -104,16 +118,64 @@ const useSTTAPI = (): IuseSTTAPI => {
104
118
  body: JSON.stringify(requestBody),
105
119
  });
106
120
 
107
- const res = await response.json();
121
+ let res: any;
122
+ try {
123
+ res = await response.json();
124
+ } catch (error) {
125
+ // Some HTTP failures (including 429s) can have an empty/non-JSON body.
126
+ // Preserve the HTTP status so callers can still apply retry policy.
127
+ res = undefined;
128
+ }
108
129
  const endReqTs = Date.now();
109
130
  const latency = endReqTs - startReqTs;
110
131
 
132
+ const httpStatus = response.status;
133
+ const isHttpFailure =
134
+ response.ok === false ||
135
+ (typeof httpStatus === 'number' && httpStatus >= 400);
136
+ const responseError = res?.error;
137
+
138
+ if (isHttpFailure || responseError?.message) {
139
+ const errorPayload = responseError || res;
140
+ const message =
141
+ errorPayload?.message ||
142
+ (httpStatus
143
+ ? `STT request failed with status ${httpStatus}`
144
+ : 'STT request failed');
145
+
146
+ logger.error(
147
+ LogSource.NetworkRest,
148
+ 'stt',
149
+ `STT API Failure - Called ${method}`,
150
+ errorPayload,
151
+ {
152
+ responseData: res,
153
+ httpStatus,
154
+ requestId,
155
+ startReqTs,
156
+ endReqTs,
157
+ latency,
158
+ },
159
+ );
160
+
161
+ return {
162
+ success: false,
163
+ httpStatus,
164
+ error: {
165
+ message,
166
+ code: errorPayload?.code,
167
+ },
168
+ data: res,
169
+ };
170
+ }
171
+
111
172
  logger.log(
112
173
  LogSource.NetworkRest,
113
174
  'stt',
114
175
  `STT API Success - Called ${method}`,
115
176
  {
116
177
  responseData: res,
178
+ httpStatus,
117
179
  requestId,
118
180
  startReqTs,
119
181
  endReqTs,
@@ -121,20 +183,9 @@ const useSTTAPI = (): IuseSTTAPI => {
121
183
  },
122
184
  );
123
185
 
124
- // Check if response has error
125
- if (res?.error?.message) {
126
- return {
127
- success: false,
128
- error: {
129
- message: res.error.message,
130
- code: res.error.code,
131
- },
132
- data: res,
133
- };
134
- }
135
-
136
186
  return {
137
187
  success: true,
188
+ httpStatus,
138
189
  data: res,
139
190
  };
140
191
  } catch (error) {
@@ -167,7 +218,41 @@ const useSTTAPI = (): IuseSTTAPI => {
167
218
  botUid: number,
168
219
  translationConfig: LanguageTranslationConfig,
169
220
  ): Promise<STTAPIResponse> => {
170
- return await apiCall('startv7', botUid, translationConfig);
221
+ const retryDelayMs = getStartRetryDelay(botUid);
222
+ let result: STTAPIResponse;
223
+
224
+ for (let attempt = 1; attempt <= STT_START_MAX_ATTEMPTS; attempt += 1) {
225
+ result = await apiCall('startv7', botUid, translationConfig);
226
+
227
+ if (
228
+ result.success ||
229
+ !shouldRetryStart(result) ||
230
+ attempt === STT_START_MAX_ATTEMPTS
231
+ ) {
232
+ return result;
233
+ }
234
+
235
+ logger.log(
236
+ LogSource.NetworkRest,
237
+ 'stt',
238
+ 'Retrying STT start after transient failure',
239
+ {
240
+ attempt,
241
+ maxAttempts: STT_START_MAX_ATTEMPTS,
242
+ retryDelayMs,
243
+ botUid,
244
+ httpStatus: result.httpStatus,
245
+ errorCode: result.error?.code,
246
+ },
247
+ );
248
+ await wait(retryDelayMs);
249
+ }
250
+
251
+ // The loop always returns, but keep the fallback explicit for type safety.
252
+ return {
253
+ success: false,
254
+ error: {message: 'STT start failed after retrying'},
255
+ };
171
256
  };
172
257
 
173
258
  const update = async (
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
- import {useCaption} from './useCaption';
3
2
  import protoRoot from './proto/ptoto';
3
+ import type {CaptionObj, TranscriptItem} from './useCaption';
4
+ import type {LanguageType} from './utils';
4
5
 
5
6
  type StreamMessageCallback = (args: [number, Uint8Array]) => void;
6
7
  type FinalListType = {
@@ -17,25 +18,33 @@ type FinalTranslationListType = {
17
18
  };
18
19
  };
19
20
 
20
- const useStreamMessageUtils = (): {
21
+ type StreamMessageUtilsOptions = {
22
+ setCaptionObj: React.Dispatch<React.SetStateAction<CaptionObj>>;
23
+ setMeetingTranscript: React.Dispatch<React.SetStateAction<TranscriptItem[]>>;
24
+ activeSpeakerRef: React.MutableRefObject<string>;
25
+ prevSpeakerRef: React.MutableRefObject<string>;
26
+ selectedTranslationLanguageRef: React.MutableRefObject<LanguageType | null>;
27
+ };
28
+
29
+ const useStreamMessageUtils = ({
30
+ setCaptionObj,
31
+ setMeetingTranscript,
32
+ activeSpeakerRef,
33
+ prevSpeakerRef,
34
+ selectedTranslationLanguageRef,
35
+ }: StreamMessageUtilsOptions): {
21
36
  streamMessageCallback: StreamMessageCallback;
37
+ flushStreamMessageQueue: () => Promise<void>;
22
38
  } => {
23
- const {
24
- setCaptionObj,
25
- setMeetingTranscript,
26
- activeSpeakerRef,
27
- prevSpeakerRef,
28
- // Use ref instead of state to avoid stale closure issues
29
- // The ref always has the current value, even in callbacks created at mount time
30
- selectedTranslationLanguageRef,
31
- } = useCaption();
32
-
33
- let captionStartTime: number = 0;
34
- const finalList: FinalListType = {};
35
- const finalTranscriptList: FinalListType = {};
36
- const finalTranslationList: FinalTranslationListType = {};
39
+ const captionStartTimeRef = React.useRef<number>(0);
40
+ const finalListRef = React.useRef<FinalListType>({});
41
+ const finalTranscriptListRef = React.useRef<FinalListType>({});
42
+ const finalTranslationListRef = React.useRef<FinalTranslationListType>({});
37
43
 
38
44
  const streamMessageCallback: StreamMessageCallback = args => {
45
+ const finalList = finalListRef.current;
46
+ const finalTranscriptList = finalTranscriptListRef.current;
47
+ const finalTranslationList = finalTranslationListRef.current;
39
48
  /* uid - bot which sends stream message in channel
40
49
  payload - stream message in Uint8Array format
41
50
  */
@@ -176,8 +185,8 @@ const useStreamMessageUtils = (): {
176
185
  } else {
177
186
  nonFinalText =
178
187
  word.text !== '.' ? nonFinalText + word.text : nonFinalText;
179
- if (!captionStartTime) {
180
- captionStartTime = performance.now();
188
+ if (!captionStartTimeRef.current) {
189
+ captionStartTimeRef.current = performance.now();
181
190
  }
182
191
  }
183
192
  }
@@ -186,11 +195,11 @@ const useStreamMessageUtils = (): {
186
195
  finalTranscriptList[textstream.uid].push(finalText);
187
196
  currentFinalText = finalText;
188
197
  // log info to show measure the duration of passes in which a sentence gets finalized
189
- const duration = performance.now() - captionStartTime;
198
+ const duration = performance.now() - captionStartTimeRef.current;
190
199
  console.log(
191
200
  `stt-Time taken to finalize caption ${currentFinalText}: ${duration}ms`,
192
201
  );
193
- captionStartTime = null; // Reset start time
202
+ captionStartTimeRef.current = 0; // Reset start time
194
203
  }
195
204
 
196
205
  /* Updating Meeting Transcript */
@@ -322,8 +331,14 @@ const useStreamMessageUtils = (): {
322
331
  console.groupEnd();
323
332
  };
324
333
 
334
+ const flushStreamMessageQueue = React.useCallback(async () => {
335
+ // Native stream-message processing is synchronous, so there is no queue
336
+ // left to drain when this callback returns.
337
+ }, []);
338
+
325
339
  return {
326
340
  streamMessageCallback,
341
+ flushStreamMessageQueue,
327
342
  };
328
343
  };
329
344
 
@@ -1,6 +1,8 @@
1
- import {useCaption} from './useCaption';
1
+ import React from 'react';
2
2
  import protoRoot from './proto/ptoto';
3
3
  import PQueue from 'p-queue';
4
+ import type {CaptionObj, TranscriptItem} from './useCaption';
5
+ import type {LanguageType} from './utils';
4
6
 
5
7
  type StreamMessageCallback = (args: [number, Uint8Array]) => void;
6
8
  type FinalListType = {
@@ -17,27 +19,35 @@ type FinalTranslationListType = {
17
19
  };
18
20
  };
19
21
 
20
- const useStreamMessageUtils = (): {
22
+ type StreamMessageUtilsOptions = {
23
+ setCaptionObj: React.Dispatch<React.SetStateAction<CaptionObj>>;
24
+ setMeetingTranscript: React.Dispatch<React.SetStateAction<TranscriptItem[]>>;
25
+ activeSpeakerRef: React.MutableRefObject<string>;
26
+ prevSpeakerRef: React.MutableRefObject<string>;
27
+ selectedTranslationLanguageRef: React.MutableRefObject<LanguageType | null>;
28
+ };
29
+
30
+ const useStreamMessageUtils = ({
31
+ setCaptionObj,
32
+ setMeetingTranscript,
33
+ activeSpeakerRef,
34
+ prevSpeakerRef,
35
+ selectedTranslationLanguageRef,
36
+ }: StreamMessageUtilsOptions): {
21
37
  streamMessageCallback: StreamMessageCallback;
38
+ flushStreamMessageQueue: () => Promise<void>;
22
39
  } => {
23
- const {
24
- setCaptionObj,
25
- setMeetingTranscript,
26
- activeSpeakerRef,
27
- prevSpeakerRef,
28
- // Use ref instead of state to avoid stale closure issues
29
- // The ref always has the current value, even in callbacks created at mount time
30
- selectedTranslationLanguageRef,
31
- } = useCaption();
32
-
33
- let captionStartTime: number = 0;
34
- const finalList: FinalListType = {};
35
- const finalTranscriptList: FinalListType = {};
36
- const finalTranslationList: FinalTranslationListType = {};
37
- const queue = new PQueue({concurrency: 1});
40
+ const captionStartTimeRef = React.useRef<number>(0);
41
+ const finalListRef = React.useRef<FinalListType>({});
42
+ const finalTranscriptListRef = React.useRef<FinalListType>({});
43
+ const finalTranslationListRef = React.useRef<FinalTranslationListType>({});
44
+ const queue = React.useMemo(() => new PQueue({concurrency: 1}), []);
38
45
 
39
46
  const streamMessageCallback: StreamMessageCallback = args => {
40
47
  const queueCallback = (args1: [number, Uint8Array]) => {
48
+ const finalList = finalListRef.current;
49
+ const finalTranscriptList = finalTranscriptListRef.current;
50
+ const finalTranslationList = finalTranslationListRef.current;
41
51
  /* uid - bot which sends stream message in channel
42
52
  payload - stream message in Uint8Array format
43
53
  */
@@ -201,8 +211,8 @@ const useStreamMessageUtils = (): {
201
211
  } else {
202
212
  nonFinalText =
203
213
  word.text !== '.' ? nonFinalText + word.text : nonFinalText;
204
- if (!captionStartTime) {
205
- captionStartTime = performance.now();
214
+ if (!captionStartTimeRef.current) {
215
+ captionStartTimeRef.current = performance.now();
206
216
  }
207
217
  }
208
218
  }
@@ -212,11 +222,11 @@ const useStreamMessageUtils = (): {
212
222
  finalTranscriptList[textstream.uid].push(finalText);
213
223
  currentFinalText = finalText;
214
224
  // log info to show measure the duration of passes in which a sentence gets finalized
215
- const duration = performance.now() - captionStartTime;
225
+ const duration = performance.now() - captionStartTimeRef.current;
216
226
  console.log(
217
227
  `stt-Time taken to finalize caption ${currentFinalText}: ${duration}ms`,
218
228
  );
219
- captionStartTime = null; // Reset start time
229
+ captionStartTimeRef.current = 0; // Reset start time
220
230
  }
221
231
 
222
232
  /* Updating Meeting Transcript */
@@ -352,14 +362,21 @@ const useStreamMessageUtils = (): {
352
362
  // console.log('final List =>', finalList);
353
363
  // console.groupEnd();
354
364
  };
355
- (async () => {
356
- await queue.add(() => queueCallback(args));
357
- console.log('stt- using pq queue');
358
- })();
365
+ void queue
366
+ .add(() => queueCallback(args))
367
+ .then(() => console.log('stt- using pq queue'))
368
+ .catch(error =>
369
+ console.error('Failed to process STT stream message', error),
370
+ );
359
371
  };
360
372
 
373
+ const flushStreamMessageQueue = React.useCallback(async () => {
374
+ await queue.onIdle();
375
+ }, [queue]);
376
+
361
377
  return {
362
378
  streamMessageCallback,
379
+ flushStreamMessageQueue,
363
380
  };
364
381
  };
365
382
 
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  import {formatTranscriptContent} from './utils';
2
3
  import {useCaption} from './useCaption';
3
4
  import RNFetchBlob from 'rn-fetch-blob';
@@ -7,87 +8,76 @@ import {LogSource, logger} from '../../logger/AppBuilderLogger';
7
8
 
8
9
  const useTranscriptDownload = (): {
9
10
  downloadTranscript: () => Promise<string | null>;
11
+ isTranscriptPreparing: boolean;
10
12
  } => {
11
- const {meetingTranscript} = useCaption();
13
+ const {flushPendingTranscript, getMeetingTranscript} = useCaption();
14
+ const [isTranscriptPreparing, setIsTranscriptPreparing] =
15
+ React.useState(false);
12
16
  const {defaultContent} = useContent();
13
17
  const {
14
18
  data: {meetingTitle},
15
19
  } = useRoomInfo();
16
20
 
17
- const downloadTranscript = (): Promise<string | null> => {
18
- return new Promise((resolve, reject) => {
19
- try {
20
- logger.log(
21
- LogSource.Internals,
22
- 'TRANSCRIPT',
23
- 'Trying to download transcript',
24
- );
25
- const [finalContent, fileName] = formatTranscriptContent(
26
- meetingTranscript,
27
- meetingTitle,
28
- defaultContent,
29
- );
21
+ const downloadTranscript = async (): Promise<string | null> => {
22
+ if (isTranscriptPreparing) {
23
+ return null;
24
+ }
30
25
 
31
- // get path to the Documents directory, don't have access to Downloads folder so saving in documents 1
32
- const documentsDir = RNFetchBlob.fs.dirs.DocumentDir;
26
+ setIsTranscriptPreparing(true);
27
+ try {
28
+ logger.log(
29
+ LogSource.Internals,
30
+ 'TRANSCRIPT',
31
+ 'Trying to download transcript',
32
+ );
33
+ await flushPendingTranscript();
34
+ const transcriptSnapshot = getMeetingTranscript();
35
+ const [finalContent, fileName] = formatTranscriptContent(
36
+ transcriptSnapshot,
37
+ meetingTitle,
38
+ defaultContent,
39
+ );
33
40
 
34
- // setting file path
35
- const filePath = `${documentsDir}/${fileName}`;
41
+ // get path to the Documents directory, don't have access to Downloads folder so saving in documents 1
42
+ const documentsDir = RNFetchBlob.fs.dirs.DocumentDir;
36
43
 
37
- // Writing content to the file
38
- RNFetchBlob.fs
39
- .writeFile(filePath, finalContent, 'utf8')
40
- .then(() => {
41
- logger.warn(
42
- LogSource.Internals,
43
- 'TRANSCRIPT',
44
- 'Content downloaded successfully on native',
45
- );
46
- // need to show the preview of downloaded file
47
- Share.open({url: `file://${filePath}`, type: 'text/plain'})
48
- .then(res => {
49
- logger.warn(
50
- LogSource.Internals,
51
- 'TRANSCRIPT',
52
- 'File shared successfully:',
53
- res,
54
- );
55
- resolve(filePath);
56
- })
57
- .catch(error => {
58
- logger.error(
59
- LogSource.Internals,
60
- 'TRANSCRIPT',
61
- 'Error sharing file:',
62
- error,
63
- );
64
- reject(error);
65
- });
44
+ // setting file path
45
+ const filePath = `${documentsDir}/${fileName}`;
66
46
 
67
- resolve(filePath);
68
- })
69
- .catch(error => {
70
- logger.error(
71
- LogSource.Internals,
72
- 'TRANSCRIPT',
73
- 'Error downloading content:',
74
- error,
75
- );
76
- reject(error);
77
- });
78
- } catch (error) {
79
- logger.error(
80
- LogSource.Internals,
81
- 'TRANSCRIPT',
82
- 'Error downloading content:',
83
- error,
84
- );
85
- reject(error);
86
- }
87
- });
47
+ // Writing content to the file
48
+ await RNFetchBlob.fs.writeFile(filePath, finalContent, 'utf8');
49
+ logger.warn(
50
+ LogSource.Internals,
51
+ 'TRANSCRIPT',
52
+ 'Content downloaded successfully on native',
53
+ );
54
+
55
+ // need to show the preview of downloaded file
56
+ const result = await Share.open({
57
+ url: `file://${filePath}`,
58
+ type: 'text/plain',
59
+ });
60
+ logger.warn(
61
+ LogSource.Internals,
62
+ 'TRANSCRIPT',
63
+ 'File shared successfully:',
64
+ result,
65
+ );
66
+ return filePath;
67
+ } catch (error) {
68
+ logger.error(
69
+ LogSource.Internals,
70
+ 'TRANSCRIPT',
71
+ 'Error downloading content:',
72
+ error,
73
+ );
74
+ throw error;
75
+ } finally {
76
+ setIsTranscriptPreparing(false);
77
+ }
88
78
  };
89
79
 
90
- return {downloadTranscript};
80
+ return {downloadTranscript, isTranscriptPreparing};
91
81
  };
92
82
 
93
83
  export default useTranscriptDownload;
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  import {formatTranscriptContent} from './utils';
2
3
  import {useCaption} from './useCaption';
3
4
  import {useRoomInfo, useContent} from 'customization-api';
@@ -5,58 +6,68 @@ import {LogSource, logger} from '../../logger/AppBuilderLogger';
5
6
 
6
7
  const useTranscriptDownload = (): {
7
8
  downloadTranscript: () => Promise<string | null>;
9
+ isTranscriptPreparing: boolean;
8
10
  } => {
9
- const {meetingTranscript} = useCaption();
11
+ const {flushPendingTranscript, getMeetingTranscript} = useCaption();
12
+ const [isTranscriptPreparing, setIsTranscriptPreparing] =
13
+ React.useState(false);
10
14
 
11
15
  const {defaultContent} = useContent();
12
16
  const {
13
17
  data: {meetingTitle},
14
18
  } = useRoomInfo();
15
19
 
16
- const downloadTranscript = (): Promise<string | null> => {
17
- return new Promise((resolve, reject) => {
18
- try {
19
- const [finalContent, fileName] = formatTranscriptContent(
20
- meetingTranscript,
21
- meetingTitle,
22
- defaultContent,
23
- );
24
-
25
- // blob with required content
26
- const blob = new Blob([finalContent], {type: 'text/plain'});
27
-
28
- // url to download content
29
- const downloadUrl = URL.createObjectURL(blob);
30
-
31
- // anchor ele to download
32
- const anchor = document.createElement('a');
33
- anchor.href = downloadUrl;
34
- anchor.download = fileName;
35
-
36
- // click to dowload the file
37
- anchor.click();
38
-
39
- // revoke download url
40
- URL.revokeObjectURL(downloadUrl);
41
- logger.debug(
42
- LogSource.Internals,
43
- 'TRANSCRIPT',
44
- 'content donwloaded successfully',
45
- );
46
- resolve(downloadUrl);
47
- } catch (error) {
48
- logger.error(
49
- LogSource.Internals,
50
- 'TRANSCRIPT',
51
- 'failed to download content',
52
- error,
53
- );
54
- reject(error);
55
- }
56
- });
20
+ const downloadTranscript = async (): Promise<string | null> => {
21
+ if (isTranscriptPreparing) {
22
+ return null;
23
+ }
24
+
25
+ setIsTranscriptPreparing(true);
26
+ try {
27
+ await flushPendingTranscript();
28
+ const transcriptSnapshot = getMeetingTranscript();
29
+ const [finalContent, fileName] = formatTranscriptContent(
30
+ transcriptSnapshot,
31
+ meetingTitle,
32
+ defaultContent,
33
+ );
34
+
35
+ // blob with required content
36
+ const blob = new Blob([finalContent], {type: 'text/plain'});
37
+
38
+ // url to download content
39
+ const downloadUrl = URL.createObjectURL(blob);
40
+
41
+ // anchor ele to download
42
+ const anchor = document.createElement('a');
43
+ anchor.href = downloadUrl;
44
+ anchor.download = fileName;
45
+
46
+ // click to dowload the file
47
+ anchor.click();
48
+
49
+ // revoke download url
50
+ URL.revokeObjectURL(downloadUrl);
51
+ logger.debug(
52
+ LogSource.Internals,
53
+ 'TRANSCRIPT',
54
+ 'content downloaded successfully',
55
+ );
56
+ return downloadUrl;
57
+ } catch (error) {
58
+ logger.error(
59
+ LogSource.Internals,
60
+ 'TRANSCRIPT',
61
+ 'failed to download content',
62
+ error,
63
+ );
64
+ throw error;
65
+ } finally {
66
+ setIsTranscriptPreparing(false);
67
+ }
57
68
  };
58
69
 
59
- return {downloadTranscript};
70
+ return {downloadTranscript, isTranscriptPreparing};
60
71
  };
61
72
 
62
73
  export default useTranscriptDownload;
@@ -3,7 +3,6 @@ import {
3
3
  SidePanelType,
4
4
  customEvents,
5
5
  useContent,
6
- useRtc,
7
6
  useSTTAPI,
8
7
  useSidePanel,
9
8
  } from 'customization-api';
@@ -13,13 +12,6 @@ import {
13
12
  useCaption,
14
13
  } from '../subComponents/caption/useCaption';
15
14
  import {LanguageType} from '../subComponents/caption/utils';
16
- import useStreamMessageUtils from '../subComponents/caption/useStreamMessageUtils';
17
- import {
18
- NativeStreamMessageArgs,
19
- StreamMessageArgs,
20
- WebStreamMessageArgs,
21
- } from '../subComponents/caption/Caption';
22
- import {isWebInternal} from './common';
23
15
 
24
16
  const useSpeechToText = () => {
25
17
  const {
@@ -29,8 +21,6 @@ const useSpeechToText = () => {
29
21
  captionObj: captionData,
30
22
  prevSpeakerRef,
31
23
  activeSpeakerRef,
32
- isSTTListenerAdded,
33
- setIsSTTListenerAdded,
34
24
  startSTTBotSession,
35
25
  updateSTTBotSession,
36
26
  stopSTTBotSession,
@@ -42,8 +32,6 @@ const useSpeechToText = () => {
42
32
 
43
33
  // const isAuthorizedSTTUserRef = useRef(isAuthorizedSTTUser);
44
34
  const defaultContentRef = useRef(defaultContent);
45
- const {RtcEngineUnsafe} = useRtc();
46
- const {streamMessageCallback} = useStreamMessageUtils();
47
35
 
48
36
  const showTranscriptPanel = (show: boolean) => {
49
37
  show
@@ -97,23 +85,9 @@ const useSpeechToText = () => {
97
85
  return await stopSTTBotSession();
98
86
  };
99
87
 
100
- const addStreamMessageListener = () => {
101
- !isSTTListenerAdded &&
102
- RtcEngineUnsafe.addListener(
103
- 'onStreamMessage',
104
- handleStreamMessageCallback,
105
- );
106
- };
107
- const handleStreamMessageCallback = (...args: StreamMessageArgs) => {
108
- setIsSTTListenerAdded(true);
109
- if (isWebInternal()) {
110
- const [uid, data] = args as WebStreamMessageArgs;
111
- streamMessageCallback([uid, data]);
112
- } else {
113
- const [, uid, , data] = args as NativeStreamMessageArgs;
114
- streamMessageCallback([uid, data]);
115
- }
116
- };
88
+ // Kept for customization API compatibility. CaptionProvider owns the single
89
+ // meeting-lifetime listener now, so callers no longer need to register one.
90
+ const addStreamMessageListener = () => {};
117
91
 
118
92
  const changeSpeakingLanguage = async (
119
93
  translateConfig: LanguageTranslationConfig,