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 +1 -1
- package/template/customization-implementation/__tests__/useCustomization.test.tsx +69 -0
- package/template/customization-implementation/useCustomization.tsx +2 -1
- package/template/defaultConfig.js +2 -2
- package/template/src/pages/video-call/SidePanelHeader.tsx +7 -4
- package/template/src/subComponents/caption/Caption.tsx +1 -38
- package/template/src/subComponents/caption/CaptionIcon.tsx +11 -10
- package/template/src/subComponents/caption/DownloadTranscriptBtn.tsx +5 -4
- package/template/src/subComponents/caption/Transcript.tsx +1 -32
- package/template/src/subComponents/caption/TranscriptIcon.tsx +10 -18
- package/template/src/subComponents/caption/useCaption.tsx +101 -7
- package/template/src/subComponents/caption/useSTTAPI.tsx +99 -14
- package/template/src/subComponents/caption/useStreamMessageUtils.native.ts +35 -20
- package/template/src/subComponents/caption/useStreamMessageUtils.ts +42 -25
- package/template/src/subComponents/caption/useTranscriptDownload.native.ts +59 -69
- package/template/src/subComponents/caption/useTranscriptDownload.ts +54 -43
- package/template/src/utils/useSpeechToText.ts +3 -29
package/package.json
CHANGED
|
@@ -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
|
|
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.
|
|
81
|
-
CORE_VERSION: "4.1.
|
|
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:
|
|
367
|
-
|
|
368
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
37
|
-
|
|
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 {
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
814
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 (!
|
|
180
|
-
|
|
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() -
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 (!
|
|
205
|
-
|
|
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() -
|
|
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
|
-
|
|
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
|
-
|
|
356
|
-
|
|
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 {
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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 {
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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,
|