agora-appbuilder-core 4.1.20 → 4.1.21
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/bridge/rtm/web/__tests__/storage.test.ts +130 -0
- package/template/bridge/rtm/web/index.ts +44 -19
- package/template/customization-api/customize.tsx +6 -0
- package/template/customization-api/index.ts +11 -0
- package/template/customization-api/typeDefinition.ts +6 -0
- package/template/defaultConfig.js +2 -2
- package/template/src/assets/live-reactions/1f440.gif +0 -0
- package/template/src/assets/live-reactions/1f44d_1f3fb.gif +0 -0
- package/template/src/assets/live-reactions/1f44d_1f3fc.gif +0 -0
- package/template/src/assets/live-reactions/1f44d_1f3fd.gif +0 -0
- package/template/src/assets/live-reactions/1f44d_1f3fe.gif +0 -0
- package/template/src/assets/live-reactions/1f44d_1f3ff.gif +0 -0
- package/template/src/assets/live-reactions/1f44f_1f3fb.gif +0 -0
- package/template/src/assets/live-reactions/1f44f_1f3fc.gif +0 -0
- package/template/src/assets/live-reactions/1f44f_1f3fd.gif +0 -0
- package/template/src/assets/live-reactions/1f44f_1f3fe.gif +0 -0
- package/template/src/assets/live-reactions/1f44f_1f3ff.gif +0 -0
- package/template/src/assets/live-reactions/1f4a1.gif +0 -0
- package/template/src/assets/live-reactions/1f4af.gif +0 -0
- package/template/src/assets/live-reactions/1f525.gif +0 -0
- package/template/src/assets/live-reactions/1f60e.gif +0 -0
- package/template/src/assets/live-reactions/1f643.gif +0 -0
- package/template/src/assets/live-reactions/1f64c.gif +0 -0
- package/template/src/assets/live-reactions/1f64c_1f3fb.gif +0 -0
- package/template/src/assets/live-reactions/1f64c_1f3fc.gif +0 -0
- package/template/src/assets/live-reactions/1f64c_1f3fd.gif +0 -0
- package/template/src/assets/live-reactions/1f64c_1f3fe.gif +0 -0
- package/template/src/assets/live-reactions/1f64c_1f3ff.gif +0 -0
- package/template/src/assets/live-reactions/1f64f.gif +0 -0
- package/template/src/assets/live-reactions/1f64f_1f3fb.gif +0 -0
- package/template/src/assets/live-reactions/1f64f_1f3fc.gif +0 -0
- package/template/src/assets/live-reactions/1f64f_1f3fd.gif +0 -0
- package/template/src/assets/live-reactions/1f64f_1f3fe.gif +0 -0
- package/template/src/assets/live-reactions/1f64f_1f3ff.gif +0 -0
- package/template/src/assets/live-reactions/1f680.gif +0 -0
- package/template/src/assets/live-reactions/2705.gif +0 -0
- package/template/src/assets/live-reactions/animated/1f440.json +1 -0
- package/template/src/assets/live-reactions/animated/1f44d_1f3fb.json +1 -0
- package/template/src/assets/live-reactions/animated/1f44d_1f3fc.json +1 -0
- package/template/src/assets/live-reactions/animated/1f44d_1f3fd.json +1 -0
- package/template/src/assets/live-reactions/animated/1f44d_1f3fe.json +1 -0
- package/template/src/assets/live-reactions/animated/1f44d_1f3ff.json +1 -0
- package/template/src/assets/live-reactions/animated/1f44f_1f3fb.json +1 -0
- package/template/src/assets/live-reactions/animated/1f44f_1f3fc.json +1 -0
- package/template/src/assets/live-reactions/animated/1f44f_1f3fd.json +1 -0
- package/template/src/assets/live-reactions/animated/1f44f_1f3fe.json +1 -0
- package/template/src/assets/live-reactions/animated/1f44f_1f3ff.json +1 -0
- package/template/src/assets/live-reactions/animated/1f4a1.json +1 -0
- package/template/src/assets/live-reactions/animated/1f4af.json +1 -0
- package/template/src/assets/live-reactions/animated/1f525.json +1 -0
- package/template/src/assets/live-reactions/animated/1f60e.json +1 -0
- package/template/src/assets/live-reactions/animated/1f643.json +1 -0
- package/template/src/assets/live-reactions/animated/1f64c.json +1 -0
- package/template/src/assets/live-reactions/animated/1f64c_1f3fb.json +1 -0
- package/template/src/assets/live-reactions/animated/1f64c_1f3fc.json +1 -0
- package/template/src/assets/live-reactions/animated/1f64c_1f3fd.json +1 -0
- package/template/src/assets/live-reactions/animated/1f64c_1f3fe.json +1 -0
- package/template/src/assets/live-reactions/animated/1f64c_1f3ff.json +1 -0
- package/template/src/assets/live-reactions/animated/1f64f.json +1 -0
- package/template/src/assets/live-reactions/animated/1f64f_1f3fb.json +1 -0
- package/template/src/assets/live-reactions/animated/1f64f_1f3fc.json +1 -0
- package/template/src/assets/live-reactions/animated/1f64f_1f3fd.json +1 -0
- package/template/src/assets/live-reactions/animated/1f64f_1f3fe.json +1 -0
- package/template/src/assets/live-reactions/animated/1f64f_1f3ff.json +1 -0
- package/template/src/assets/live-reactions/animated/1f680.json +1 -0
- package/template/src/assets/live-reactions/animated/2705.json +1 -0
- package/template/src/assets/live-reactions/custom/ty.png +0 -0
- package/template/src/components/RTMConfigure.tsx +15 -10
- package/template/src/components/StorageContext.tsx +3 -0
- package/template/src/components/reactions/LiveReactionBadge.tsx +20 -2
- package/template/src/components/reactions/LiveReactionButton.tsx +200 -44
- package/template/src/components/reactions/LiveReactionStageOverlay.native.tsx +34 -10
- package/template/src/components/reactions/LiveReactionStageOverlay.tsx +30 -7
- package/template/src/components/reactions/catalog.ts +241 -0
- package/template/src/components/useVideoCall.tsx +20 -2
- package/template/src/rtm/__tests__/isRemoteDeparture.test.ts +19 -0
- package/template/src/rtm/isRemoteDeparture.ts +4 -0
- package/template/src/subComponents/caption/__tests__/sttRequestBody.test.ts +61 -0
- package/template/src/subComponents/caption/__tests__/sttSessionId.native.test.ts +76 -0
- package/template/src/subComponents/caption/__tests__/sttSessionId.web.test.ts +190 -0
- package/template/src/subComponents/caption/__tests__/useSTTAPI.test.tsx +129 -0
- package/template/src/subComponents/caption/sttRequestBody.ts +54 -0
- package/template/src/subComponents/caption/sttSessionCoordinator.ts +315 -0
- package/template/src/subComponents/caption/sttSessionId.ts +21 -0
- package/template/src/subComponents/caption/sttSessionId.web.ts +21 -0
- package/template/src/subComponents/caption/useSTTAPI.tsx +13 -33
- package/template/src/utils/__tests__/useEndCall.test.tsx +282 -0
- package/template/src/utils/useEndCall.ts +63 -22
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import RTMEngine from '../../rtm/RTMEngine';
|
|
2
|
+
import getUniqueID from '../../utils/getUniqueID';
|
|
3
|
+
import {createSTTSessionCoordinator} from './sttSessionCoordinator';
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
createSTTSessionCoordinator,
|
|
7
|
+
STT_SESSION_ID_KEY,
|
|
8
|
+
} from './sttSessionCoordinator';
|
|
9
|
+
|
|
10
|
+
const coordinator = createSTTSessionCoordinator({
|
|
11
|
+
getClient: () => RTMEngine.getInstance().engine,
|
|
12
|
+
createId: getUniqueID,
|
|
13
|
+
wait: milliseconds =>
|
|
14
|
+
new Promise(resolve => setTimeout(resolve, milliseconds)),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const ensureSTTSessionId = coordinator.ensureSTTSessionId;
|
|
18
|
+
export const isOnlyLocalRTMParticipant = coordinator.isOnlyLocalRTMParticipant;
|
|
19
|
+
export const clearSTTSessionIdIfLast = coordinator.clearSTTSessionIdIfLast;
|
|
20
|
+
export const cleanupSTTSessionOnEnd = coordinator.cleanupSTTSessionOnEnd;
|
|
21
|
+
export const resetSTTSessionIdCache = coordinator.resetSTTSessionIdCache;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import React, {useContext} from 'react';
|
|
2
2
|
import StorageContext from '../../components/StorageContext';
|
|
3
3
|
import {useRoomInfo} from '../../components/room-info/useRoomInfo';
|
|
4
|
-
import {LanguageTranslationConfig} from './useCaption';
|
|
4
|
+
import {type LanguageTranslationConfig} from './useCaption';
|
|
5
5
|
import {PropsContext, useLocalUid} from '../../../agora-rn-uikit';
|
|
6
6
|
import {logger, LogSource} from '../../logger/AppBuilderLogger';
|
|
7
7
|
import getUniqueID from '../../utils/getUniqueID';
|
|
8
|
+
import {buildSTTRequestBody, type STTMethod} from './sttRequestBody';
|
|
9
|
+
import {ensureSTTSessionId} from './sttSessionId';
|
|
8
10
|
|
|
9
11
|
export interface STTAPIResponse {
|
|
10
12
|
success: boolean;
|
|
@@ -57,7 +59,7 @@ const useSTTAPI = (): IuseSTTAPI => {
|
|
|
57
59
|
}, [rtcProps]);
|
|
58
60
|
|
|
59
61
|
const apiCall = async (
|
|
60
|
-
method:
|
|
62
|
+
method: STTMethod,
|
|
61
63
|
botUid: number,
|
|
62
64
|
translationConfig?: LanguageTranslationConfig,
|
|
63
65
|
): Promise<STTAPIResponse> => {
|
|
@@ -68,14 +70,19 @@ const useSTTAPI = (): IuseSTTAPI => {
|
|
|
68
70
|
// Calculate which user this bot belongs to
|
|
69
71
|
const ownerUid = botUid - 900000000;
|
|
70
72
|
|
|
71
|
-
|
|
73
|
+
const requestBody = await buildSTTRequestBody({
|
|
74
|
+
method,
|
|
75
|
+
botUid,
|
|
72
76
|
passphrase:
|
|
73
77
|
roomIdRef?.current?.host || roomIdRef?.current?.attendee || '',
|
|
74
|
-
|
|
75
|
-
encryption_mode: $config.ENCRYPTION_ENABLED
|
|
78
|
+
encryptionMode: $config.ENCRYPTION_ENABLED
|
|
76
79
|
? rtcPropsRef?.current.encryption.mode
|
|
77
80
|
: null,
|
|
78
|
-
|
|
81
|
+
localUid: localUidRef.current,
|
|
82
|
+
channelName: rtcPropsRef?.current?.channel || '',
|
|
83
|
+
translationConfig,
|
|
84
|
+
resolveSessionId: ensureSTTSessionId,
|
|
85
|
+
});
|
|
79
86
|
|
|
80
87
|
console.log(
|
|
81
88
|
`[STT_BOT_SUBSCRIPTION] ${method.toUpperCase()} - Bot UID: ${botUid} will subscribe to User UID: ${ownerUid}`,
|
|
@@ -86,33 +93,6 @@ const useSTTAPI = (): IuseSTTAPI => {
|
|
|
86
93
|
translationConfig,
|
|
87
94
|
},
|
|
88
95
|
);
|
|
89
|
-
// Add translate_config only for start/update methods
|
|
90
|
-
if (translationConfig?.source?.[0]) {
|
|
91
|
-
requestBody.lang = translationConfig.source;
|
|
92
|
-
// Sanitize payload: remove source language from targets to avoid API errors
|
|
93
|
-
const sanitizedTargets =
|
|
94
|
-
translationConfig?.targets?.filter(
|
|
95
|
-
target => target !== translationConfig?.source[0],
|
|
96
|
-
) || [];
|
|
97
|
-
const shouldTranslate = sanitizedTargets.length > 0;
|
|
98
|
-
// Add translate_config payload only if targets exist
|
|
99
|
-
if (shouldTranslate) {
|
|
100
|
-
requestBody.translate_config = [
|
|
101
|
-
{
|
|
102
|
-
source_lang: translationConfig.source[0],
|
|
103
|
-
target_lang: sanitizedTargets,
|
|
104
|
-
},
|
|
105
|
-
];
|
|
106
|
-
if (method === 'update') {
|
|
107
|
-
requestBody.translate = true;
|
|
108
|
-
}
|
|
109
|
-
} else if (method === 'update') {
|
|
110
|
-
// If method is update and no targets are passed
|
|
111
|
-
requestBody.translate = false;
|
|
112
|
-
}
|
|
113
|
-
requestBody.subscribeAudioUids = [`${localUidRef.current}`];
|
|
114
|
-
}
|
|
115
|
-
|
|
116
96
|
const response = await fetch(`${STT_API_URL}/${method}`, {
|
|
117
97
|
method: 'POST',
|
|
118
98
|
headers: {
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import TestRenderer, {act} from 'react-test-renderer';
|
|
3
|
+
import {afterEach, beforeEach, describe, expect, it, jest} from '@jest/globals';
|
|
4
|
+
|
|
5
|
+
jest.mock('customization-implementation', () => ({
|
|
6
|
+
useCustomization: () => undefined,
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
jest.mock('customization-api', () => ({
|
|
10
|
+
useCaption: jest.fn(),
|
|
11
|
+
useContent: jest.fn(),
|
|
12
|
+
useRoomInfo: jest.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
jest.mock('../../../agora-rn-uikit', () => {
|
|
16
|
+
const ReactModule = require('react');
|
|
17
|
+
return {
|
|
18
|
+
PropsContext: ReactModule.createContext({}),
|
|
19
|
+
DispatchContext: ReactModule.createContext({}),
|
|
20
|
+
useLocalUid: jest.fn(() => 123),
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
jest.mock('../../components/Router', () => ({
|
|
25
|
+
useHistory: () => ({}),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
jest.mock('../../subComponents/LocalEndCall', () => ({
|
|
29
|
+
stopForegroundService: jest.fn(),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
jest.mock('../../rtm/RTMEngine', () => {
|
|
33
|
+
const unsubscribe = jest.fn();
|
|
34
|
+
return {
|
|
35
|
+
__esModule: true,
|
|
36
|
+
default: {
|
|
37
|
+
getInstance: () => ({engine: {unsubscribe}}),
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
jest.mock('../../auth/config', () => ({ENABLE_AUTH: true}));
|
|
43
|
+
|
|
44
|
+
jest.mock('../../auth/AuthProvider', () => ({
|
|
45
|
+
useAuth: () => ({authLogin: jest.fn()}),
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
jest.mock('../../components/chat/chatConfigure', () => ({
|
|
49
|
+
useChatConfigure: () => ({deleteChatUser: jest.fn()}),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
jest.mock('../common', () => ({
|
|
53
|
+
isWebInternal: jest.fn(),
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
jest.mock('../../subComponents/caption/sttSessionId', () => ({
|
|
57
|
+
cleanupSTTSessionOnEnd: jest.fn(),
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
import {useCaption, useContent, useRoomInfo} from 'customization-api';
|
|
61
|
+
import {DispatchContext, PropsContext} from '../../../agora-rn-uikit';
|
|
62
|
+
import RTMEngine from '../../rtm/RTMEngine';
|
|
63
|
+
import {cleanupSTTSessionOnEnd} from '../../subComponents/caption/sttSessionId';
|
|
64
|
+
import {isWebInternal} from '../common';
|
|
65
|
+
import useEndCall from '../useEndCall';
|
|
66
|
+
|
|
67
|
+
const mockUseCaption = useCaption as jest.Mock<any>;
|
|
68
|
+
const mockUseContent = useContent as jest.Mock<any>;
|
|
69
|
+
const mockUseRoomInfo = useRoomInfo as jest.Mock<any>;
|
|
70
|
+
const mockCleanupSTTSessionOnEnd = cleanupSTTSessionOnEnd as jest.Mock<any>;
|
|
71
|
+
const mockIsWebInternal = isWebInternal as jest.Mock<any>;
|
|
72
|
+
const mockUnsubscribe = RTMEngine.getInstance().engine
|
|
73
|
+
.unsubscribe as jest.Mock<any>;
|
|
74
|
+
|
|
75
|
+
const renderHook = (dispatch: (action: any) => void) => {
|
|
76
|
+
let endCall!: ReturnType<typeof useEndCall>;
|
|
77
|
+
let renderer!: TestRenderer.ReactTestRenderer;
|
|
78
|
+
|
|
79
|
+
act(() => {
|
|
80
|
+
renderer = TestRenderer.create(
|
|
81
|
+
<PropsContext.Provider value={{rtcProps: {channel: 'room'}} as any}>
|
|
82
|
+
<DispatchContext.Provider value={{dispatch} as any}>
|
|
83
|
+
{React.createElement(() => {
|
|
84
|
+
endCall = useEndCall();
|
|
85
|
+
return null;
|
|
86
|
+
})}
|
|
87
|
+
</DispatchContext.Provider>
|
|
88
|
+
</PropsContext.Provider>,
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return {endCall, renderer};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
describe('useEndCall STT session cleanup', () => {
|
|
96
|
+
const mockStopSTT = jest.fn<any>();
|
|
97
|
+
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
jest.useFakeTimers();
|
|
100
|
+
jest.clearAllMocks();
|
|
101
|
+
mockUseCaption.mockReturnValue({
|
|
102
|
+
isSTTActive: true,
|
|
103
|
+
stopSTTBotSession: mockStopSTT,
|
|
104
|
+
});
|
|
105
|
+
mockUseContent.mockReturnValue({
|
|
106
|
+
defaultContent: {
|
|
107
|
+
123: {
|
|
108
|
+
type: 'rtc',
|
|
109
|
+
isHost: 'true',
|
|
110
|
+
offline: false,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
mockUseRoomInfo.mockReturnValue({data: {isHost: true}});
|
|
115
|
+
mockStopSTT.mockResolvedValue(undefined);
|
|
116
|
+
mockUnsubscribe.mockResolvedValue(undefined);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
afterEach(() => {
|
|
120
|
+
jest.useRealTimers();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('awaits web cleanup before RTM unsubscribe and end-call dispatch', async () => {
|
|
124
|
+
const calls: string[] = [];
|
|
125
|
+
mockIsWebInternal.mockReturnValue(true);
|
|
126
|
+
mockStopSTT.mockImplementation(async () => {
|
|
127
|
+
calls.push('stop');
|
|
128
|
+
});
|
|
129
|
+
mockCleanupSTTSessionOnEnd.mockImplementation(
|
|
130
|
+
async (_channel, _uid, _active, stop) => {
|
|
131
|
+
calls.push('cleanup-start');
|
|
132
|
+
await stop();
|
|
133
|
+
calls.push('cleanup-end');
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
mockUnsubscribe.mockImplementation(async () => {
|
|
137
|
+
calls.push('unsubscribe');
|
|
138
|
+
});
|
|
139
|
+
const {endCall, renderer} = renderHook(() => calls.push('dispatch'));
|
|
140
|
+
|
|
141
|
+
await act(async () => {
|
|
142
|
+
await endCall();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(mockCleanupSTTSessionOnEnd).toHaveBeenCalledWith(
|
|
146
|
+
'room',
|
|
147
|
+
'123',
|
|
148
|
+
true,
|
|
149
|
+
mockStopSTT,
|
|
150
|
+
);
|
|
151
|
+
expect(calls).toEqual([
|
|
152
|
+
'cleanup-start',
|
|
153
|
+
'stop',
|
|
154
|
+
'cleanup-end',
|
|
155
|
+
'unsubscribe',
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
act(() => jest.runOnlyPendingTimers());
|
|
159
|
+
expect(calls).toEqual([
|
|
160
|
+
'cleanup-start',
|
|
161
|
+
'stop',
|
|
162
|
+
'cleanup-end',
|
|
163
|
+
'unsubscribe',
|
|
164
|
+
'dispatch',
|
|
165
|
+
]);
|
|
166
|
+
renderer.unmount();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('awaits native cleanup before RTM unsubscribe and end-call dispatch', async () => {
|
|
170
|
+
const calls: string[] = [];
|
|
171
|
+
mockIsWebInternal.mockReturnValue(false);
|
|
172
|
+
mockStopSTT.mockImplementation(async () => {
|
|
173
|
+
calls.push('stop');
|
|
174
|
+
});
|
|
175
|
+
mockCleanupSTTSessionOnEnd.mockImplementation(
|
|
176
|
+
async (_channel, _uid, _active, stop) => {
|
|
177
|
+
calls.push('cleanup-start');
|
|
178
|
+
await stop();
|
|
179
|
+
calls.push('cleanup-end');
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
mockUnsubscribe.mockImplementation(async () => {
|
|
183
|
+
calls.push('unsubscribe');
|
|
184
|
+
});
|
|
185
|
+
const {endCall, renderer} = renderHook(() => calls.push('dispatch'));
|
|
186
|
+
|
|
187
|
+
await act(async () => {
|
|
188
|
+
await endCall();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(mockCleanupSTTSessionOnEnd).toHaveBeenCalledWith(
|
|
192
|
+
'room',
|
|
193
|
+
'123',
|
|
194
|
+
true,
|
|
195
|
+
expect.any(Function),
|
|
196
|
+
);
|
|
197
|
+
expect(calls).toEqual([
|
|
198
|
+
'cleanup-start',
|
|
199
|
+
'stop',
|
|
200
|
+
'cleanup-end',
|
|
201
|
+
'unsubscribe',
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
act(() => jest.runOnlyPendingTimers());
|
|
205
|
+
expect(calls).toEqual([
|
|
206
|
+
'cleanup-start',
|
|
207
|
+
'stop',
|
|
208
|
+
'cleanup-end',
|
|
209
|
+
'unsubscribe',
|
|
210
|
+
'dispatch',
|
|
211
|
+
]);
|
|
212
|
+
renderer.unmount();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('falls back to the native last-host stop when RTM cleanup fails', async () => {
|
|
216
|
+
const calls: string[] = [];
|
|
217
|
+
mockIsWebInternal.mockReturnValue(false);
|
|
218
|
+
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
219
|
+
mockCleanupSTTSessionOnEnd.mockImplementation(async () => {
|
|
220
|
+
calls.push('cleanup');
|
|
221
|
+
throw new Error('RTM unavailable');
|
|
222
|
+
});
|
|
223
|
+
mockStopSTT.mockImplementation(async () => {
|
|
224
|
+
calls.push('stop');
|
|
225
|
+
});
|
|
226
|
+
mockUnsubscribe.mockImplementation(async () => {
|
|
227
|
+
calls.push('unsubscribe');
|
|
228
|
+
});
|
|
229
|
+
const {endCall, renderer} = renderHook(() => calls.push('dispatch'));
|
|
230
|
+
|
|
231
|
+
await act(async () => {
|
|
232
|
+
await endCall();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(mockStopSTT).toHaveBeenCalledTimes(1);
|
|
236
|
+
expect(calls).toEqual(['cleanup', 'stop', 'unsubscribe']);
|
|
237
|
+
|
|
238
|
+
act(() => jest.runOnlyPendingTimers());
|
|
239
|
+
expect(calls).toEqual(['cleanup', 'stop', 'unsubscribe', 'dispatch']);
|
|
240
|
+
renderer.unmount();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('does not stop native STT twice when metadata removal fails after stop', async () => {
|
|
244
|
+
mockIsWebInternal.mockReturnValue(false);
|
|
245
|
+
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
246
|
+
mockCleanupSTTSessionOnEnd.mockImplementation(
|
|
247
|
+
async (_channel, _uid, _active, stop) => {
|
|
248
|
+
await stop();
|
|
249
|
+
throw new Error('metadata removal failed');
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
const {endCall, renderer} = renderHook(() => {});
|
|
253
|
+
|
|
254
|
+
await act(async () => {
|
|
255
|
+
await endCall();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
expect(mockStopSTT).toHaveBeenCalledTimes(1);
|
|
259
|
+
renderer.unmount();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('continues native end-call when RTM cleanup and fallback stop both fail', async () => {
|
|
263
|
+
const calls: string[] = [];
|
|
264
|
+
mockIsWebInternal.mockReturnValue(false);
|
|
265
|
+
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
266
|
+
mockCleanupSTTSessionOnEnd.mockRejectedValue(new Error('RTM unavailable'));
|
|
267
|
+
mockStopSTT.mockRejectedValue(new Error('stop unavailable'));
|
|
268
|
+
mockUnsubscribe.mockImplementation(async () => {
|
|
269
|
+
calls.push('unsubscribe');
|
|
270
|
+
});
|
|
271
|
+
const {endCall, renderer} = renderHook(() => calls.push('dispatch'));
|
|
272
|
+
|
|
273
|
+
await act(async () => {
|
|
274
|
+
await endCall();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
expect(calls).toEqual(['unsubscribe']);
|
|
278
|
+
act(() => jest.runOnlyPendingTimers());
|
|
279
|
+
expect(calls).toEqual(['unsubscribe', 'dispatch']);
|
|
280
|
+
renderer.unmount();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import {useContext} from 'react';
|
|
2
2
|
import {useCustomization} from 'customization-implementation';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
useContent,
|
|
6
|
-
useRoomInfo,
|
|
7
|
-
} from 'customization-api';
|
|
8
|
-
import {PropsContext, DispatchContext} from '../../agora-rn-uikit';
|
|
3
|
+
import {useCaption, useContent, useRoomInfo} from 'customization-api';
|
|
4
|
+
import {PropsContext, DispatchContext, useLocalUid} from '../../agora-rn-uikit';
|
|
9
5
|
import {useHistory} from '../components/Router';
|
|
10
6
|
import {stopForegroundService} from '../subComponents/LocalEndCall';
|
|
11
7
|
import RTMEngine from '../rtm/RTMEngine';
|
|
12
8
|
import {ENABLE_AUTH} from '../auth/config';
|
|
13
9
|
import {useAuth} from '../auth/AuthProvider';
|
|
14
10
|
import {useChatConfigure} from '../components/chat/chatConfigure';
|
|
11
|
+
import {isWebInternal} from './common';
|
|
12
|
+
import {cleanupSTTSessionOnEnd} from '../subComponents/caption/sttSessionId';
|
|
15
13
|
|
|
16
14
|
const useEndCall = () => {
|
|
17
15
|
const history = useHistory();
|
|
@@ -25,6 +23,7 @@ const useEndCall = () => {
|
|
|
25
23
|
|
|
26
24
|
const {rtcProps} = useContext(PropsContext);
|
|
27
25
|
const {dispatch} = useContext(DispatchContext);
|
|
26
|
+
const localUid = useLocalUid();
|
|
28
27
|
|
|
29
28
|
const beforeEndCall = useCustomization(
|
|
30
29
|
data =>
|
|
@@ -43,31 +42,73 @@ const useEndCall = () => {
|
|
|
43
42
|
console.log('debugging error on beforeEndCall', error);
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
const scheduleEndCall = () => {
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
dispatch({
|
|
48
|
+
type: 'EndCall',
|
|
49
|
+
value: [],
|
|
50
|
+
});
|
|
50
51
|
});
|
|
51
|
-
}
|
|
52
|
+
};
|
|
53
|
+
const isWebCall = isWebInternal();
|
|
54
|
+
|
|
52
55
|
// stopping foreground servie on end call
|
|
53
56
|
stopForegroundService();
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
|
|
58
|
+
let nativeStopPromise: Promise<void> | undefined;
|
|
59
|
+
const stopNativeSTTOnce = (): Promise<void> => {
|
|
60
|
+
if (!nativeStopPromise) {
|
|
61
|
+
nativeStopPromise = stopSTTBotSession().catch(error => {
|
|
62
|
+
nativeStopPromise = undefined;
|
|
63
|
+
throw error;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return nativeStopPromise;
|
|
67
|
+
};
|
|
68
|
+
const stopSTT = isWebCall ? stopSTTBotSession : stopNativeSTTOnce;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
await cleanupSTTSessionOnEnd(
|
|
72
|
+
rtcProps.channel,
|
|
73
|
+
String(localUid),
|
|
74
|
+
isSTTActive,
|
|
75
|
+
stopSTT,
|
|
76
|
+
);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error(
|
|
79
|
+
`Failed to clean up the ${isWebCall ? 'web' : 'native'} STT session ID`,
|
|
80
|
+
error,
|
|
81
|
+
);
|
|
82
|
+
if (!isWebCall && isSTTActive) {
|
|
83
|
+
const usersInCall = Object.entries(defaultContent).filter(
|
|
84
|
+
item =>
|
|
85
|
+
item[1].type === 'rtc' &&
|
|
86
|
+
item[1].isHost === 'true' &&
|
|
87
|
+
!item[1].offline,
|
|
88
|
+
);
|
|
89
|
+
if (usersInCall.length === 1) {
|
|
90
|
+
try {
|
|
91
|
+
await stopNativeSTTOnce();
|
|
92
|
+
} catch (stopError) {
|
|
93
|
+
console.error(
|
|
94
|
+
'Failed to stop native STT during fallback',
|
|
95
|
+
stopError,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
64
100
|
}
|
|
65
101
|
|
|
66
102
|
// removing user from chat server
|
|
67
103
|
if ($config.CHAT) {
|
|
68
104
|
deleteChatUser();
|
|
69
105
|
}
|
|
70
|
-
|
|
106
|
+
try {
|
|
107
|
+
await RTMEngine.getInstance().engine.unsubscribe(rtcProps.channel);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error('Failed to unsubscribe from the RTM channel', error);
|
|
110
|
+
}
|
|
111
|
+
scheduleEndCall();
|
|
71
112
|
if (!ENABLE_AUTH) {
|
|
72
113
|
// await authLogout();
|
|
73
114
|
await authLogin();
|