agora-appbuilder-core 4.1.24 → 4.1.25-beta.1
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/defaultConfig.js +2 -2
- package/template/src/subComponents/caption/__tests__/transcriptStreamListener.test.ts +47 -0
- package/template/src/subComponents/caption/transcriptJourney.ts +1 -0
- package/template/src/subComponents/caption/transcriptStreamListener.ts +50 -0
- package/template/src/subComponents/caption/useCaption.tsx +130 -31
package/package.json
CHANGED
|
@@ -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.25-beta.1",
|
|
81
|
+
CORE_VERSION: "4.1.25-beta.1",
|
|
82
82
|
DISABLE_LANDSCAPE_MODE: false,
|
|
83
83
|
STT_AUTO_START: false,
|
|
84
84
|
CLOUD_RECORDING_AUTO_START: false,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
registerTranscriptStreamListener,
|
|
3
|
+
removeTranscriptStreamListener,
|
|
4
|
+
TranscriptStreamEngine,
|
|
5
|
+
} from '../transcriptStreamListener';
|
|
6
|
+
|
|
7
|
+
describe('transcript stream listener lifecycle', () => {
|
|
8
|
+
it('tracks and removes a native listener when addListener returns void', () => {
|
|
9
|
+
const listener = jest.fn();
|
|
10
|
+
const engine: TranscriptStreamEngine = {
|
|
11
|
+
addListener: jest.fn(),
|
|
12
|
+
removeListener: jest.fn(),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const registration = registerTranscriptStreamListener(engine, listener);
|
|
16
|
+
|
|
17
|
+
expect(registration).toEqual({subscription: null});
|
|
18
|
+
expect(engine.addListener).toHaveBeenCalledTimes(1);
|
|
19
|
+
expect(engine.addListener).toHaveBeenCalledWith(
|
|
20
|
+
'onStreamMessage',
|
|
21
|
+
listener,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
removeTranscriptStreamListener(engine, listener, registration, false);
|
|
25
|
+
|
|
26
|
+
expect(engine.removeListener).toHaveBeenCalledTimes(1);
|
|
27
|
+
expect(engine.removeListener).toHaveBeenCalledWith(
|
|
28
|
+
'onStreamMessage',
|
|
29
|
+
listener,
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('uses the subscription returned by the web bridge', () => {
|
|
34
|
+
const listener = jest.fn();
|
|
35
|
+
const remove = jest.fn();
|
|
36
|
+
const engine: TranscriptStreamEngine = {
|
|
37
|
+
addListener: jest.fn(() => ({remove})),
|
|
38
|
+
removeListener: jest.fn(),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const registration = registerTranscriptStreamListener(engine, listener);
|
|
42
|
+
removeTranscriptStreamListener(engine, listener, registration, true);
|
|
43
|
+
|
|
44
|
+
expect(remove).toHaveBeenCalledTimes(1);
|
|
45
|
+
expect(engine.removeListener).not.toHaveBeenCalled();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const TRANSCRIPT_JOURNEY = '[TRANSCRIPT_JOURNEY]';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type TranscriptStreamListener = (...args: any[]) => void;
|
|
2
|
+
|
|
3
|
+
type TranscriptStreamSubscription = {
|
|
4
|
+
remove: () => void;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type TranscriptStreamEngine = {
|
|
8
|
+
addListener: (
|
|
9
|
+
event: 'onStreamMessage',
|
|
10
|
+
listener: TranscriptStreamListener,
|
|
11
|
+
) => unknown;
|
|
12
|
+
removeListener: (
|
|
13
|
+
event: 'onStreamMessage',
|
|
14
|
+
listener: TranscriptStreamListener,
|
|
15
|
+
) => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type TranscriptStreamListenerRegistration = {
|
|
19
|
+
subscription: TranscriptStreamSubscription | null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const registerTranscriptStreamListener = (
|
|
23
|
+
engine: TranscriptStreamEngine,
|
|
24
|
+
listener: TranscriptStreamListener,
|
|
25
|
+
): TranscriptStreamListenerRegistration => {
|
|
26
|
+
const result = engine.addListener('onStreamMessage', listener);
|
|
27
|
+
const subscription =
|
|
28
|
+
result &&
|
|
29
|
+
typeof (result as TranscriptStreamSubscription).remove === 'function'
|
|
30
|
+
? (result as TranscriptStreamSubscription)
|
|
31
|
+
: null;
|
|
32
|
+
|
|
33
|
+
// Native Agora returns void from addListener. The registration object is our
|
|
34
|
+
// platform-independent proof that registration completed successfully.
|
|
35
|
+
return {subscription};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const removeTranscriptStreamListener = (
|
|
39
|
+
engine: TranscriptStreamEngine,
|
|
40
|
+
listener: TranscriptStreamListener,
|
|
41
|
+
registration: TranscriptStreamListenerRegistration,
|
|
42
|
+
web: boolean,
|
|
43
|
+
) => {
|
|
44
|
+
if (web) {
|
|
45
|
+
registration.subscription?.remove();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
engine.removeListener('onStreamMessage', listener);
|
|
50
|
+
};
|
|
@@ -21,6 +21,13 @@ import {useRoomInfo} from '../../components/room-info/useRoomInfo';
|
|
|
21
21
|
import {useContent, useRtc} from 'customization-api';
|
|
22
22
|
import useStreamMessageUtils from './useStreamMessageUtils';
|
|
23
23
|
import {isWebInternal} from '../../utils/common';
|
|
24
|
+
import {TRANSCRIPT_JOURNEY} from './transcriptJourney';
|
|
25
|
+
import {
|
|
26
|
+
registerTranscriptStreamListener,
|
|
27
|
+
removeTranscriptStreamListener,
|
|
28
|
+
TranscriptStreamEngine,
|
|
29
|
+
TranscriptStreamListenerRegistration,
|
|
30
|
+
} from './transcriptStreamListener';
|
|
24
31
|
|
|
25
32
|
// Types
|
|
26
33
|
type GlobalSttState = {
|
|
@@ -320,22 +327,20 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
|
|
|
320
327
|
prevSpeakerRef,
|
|
321
328
|
selectedTranslationLanguageRef,
|
|
322
329
|
});
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
[streamMessageCallback],
|
|
338
|
-
);
|
|
330
|
+
const streamMessageCallbackRef = React.useRef(streamMessageCallback);
|
|
331
|
+
streamMessageCallbackRef.current = streamMessageCallback;
|
|
332
|
+
const transcriptListenerRegistrationRef =
|
|
333
|
+
React.useRef<TranscriptStreamListenerRegistration | null>(null);
|
|
334
|
+
|
|
335
|
+
const handleStreamMessageCallback = React.useCallback((...args: any[]) => {
|
|
336
|
+
if (isWebInternal()) {
|
|
337
|
+
const [uid, data] = args;
|
|
338
|
+
streamMessageCallbackRef.current([uid, data]);
|
|
339
|
+
} else {
|
|
340
|
+
const [, uid, , data] = args;
|
|
341
|
+
streamMessageCallbackRef.current([uid, data]);
|
|
342
|
+
}
|
|
343
|
+
}, []);
|
|
339
344
|
|
|
340
345
|
// Register once for the lifetime of the active call. Keeping collection in
|
|
341
346
|
// the provider makes it independent of whether caption/transcript UI is open.
|
|
@@ -343,24 +348,73 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
|
|
|
343
348
|
if (
|
|
344
349
|
!$config.ENABLE_STT ||
|
|
345
350
|
!callActive ||
|
|
346
|
-
|
|
351
|
+
transcriptListenerRegistrationRef.current
|
|
347
352
|
) {
|
|
348
353
|
return;
|
|
349
354
|
}
|
|
350
355
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
356
|
+
logger.log(
|
|
357
|
+
LogSource.Internals,
|
|
358
|
+
'TRANSCRIPT',
|
|
359
|
+
`${TRANSCRIPT_JOURNEY} stream listener registration requested`,
|
|
360
|
+
{stage: 'listener_registration', outcome: 'started'},
|
|
354
361
|
);
|
|
355
|
-
|
|
356
|
-
|
|
362
|
+
try {
|
|
363
|
+
transcriptListenerRegistrationRef.current =
|
|
364
|
+
registerTranscriptStreamListener(
|
|
365
|
+
RtcEngineUnsafe as unknown as TranscriptStreamEngine,
|
|
366
|
+
handleStreamMessageCallback,
|
|
367
|
+
);
|
|
368
|
+
setIsSTTListenerAdded(true);
|
|
369
|
+
logger.log(
|
|
370
|
+
LogSource.Internals,
|
|
371
|
+
'TRANSCRIPT',
|
|
372
|
+
`${TRANSCRIPT_JOURNEY} stream listener registered`,
|
|
373
|
+
{stage: 'listener_registration', outcome: 'success'},
|
|
374
|
+
);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
logger.error(
|
|
377
|
+
LogSource.Internals,
|
|
378
|
+
'TRANSCRIPT',
|
|
379
|
+
`${TRANSCRIPT_JOURNEY} stream listener registration failed`,
|
|
380
|
+
{
|
|
381
|
+
stage: 'listener_registration',
|
|
382
|
+
outcome: 'failure',
|
|
383
|
+
error: error ?? null,
|
|
384
|
+
},
|
|
385
|
+
);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
357
388
|
|
|
358
|
-
React.useEffect(() => {
|
|
359
389
|
return () => {
|
|
360
|
-
|
|
361
|
-
|
|
390
|
+
logger.log(
|
|
391
|
+
LogSource.Internals,
|
|
392
|
+
'TRANSCRIPT',
|
|
393
|
+
`${TRANSCRIPT_JOURNEY} stream listener cleanup requested`,
|
|
394
|
+
{
|
|
395
|
+
stage: 'listener_cleanup',
|
|
396
|
+
outcome: 'started',
|
|
397
|
+
listenerRegistered: !!transcriptListenerRegistrationRef.current,
|
|
398
|
+
},
|
|
399
|
+
);
|
|
400
|
+
if (transcriptListenerRegistrationRef.current) {
|
|
401
|
+
removeTranscriptStreamListener(
|
|
402
|
+
RtcEngineUnsafe as unknown as TranscriptStreamEngine,
|
|
403
|
+
handleStreamMessageCallback,
|
|
404
|
+
transcriptListenerRegistrationRef.current,
|
|
405
|
+
isWebInternal(),
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
transcriptListenerRegistrationRef.current = null;
|
|
409
|
+
setIsSTTListenerAdded(false);
|
|
410
|
+
logger.log(
|
|
411
|
+
LogSource.Internals,
|
|
412
|
+
'TRANSCRIPT',
|
|
413
|
+
`${TRANSCRIPT_JOURNEY} stream listener removed and local references cleared`,
|
|
414
|
+
{stage: 'listener_cleanup', outcome: 'completed'},
|
|
415
|
+
);
|
|
362
416
|
};
|
|
363
|
-
}, []);
|
|
417
|
+
}, [RtcEngineUnsafe, callActive, handleStreamMessageCallback]);
|
|
364
418
|
|
|
365
419
|
const flushPendingTranscript = React.useCallback(async () => {
|
|
366
420
|
await flushStreamMessageQueue();
|
|
@@ -393,12 +447,17 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
|
|
|
393
447
|
const updateErrorLabel = useString(sttUpdateError)();
|
|
394
448
|
|
|
395
449
|
// --- Derived readiness flag for STT ---
|
|
450
|
+
const localUidReady = !!localUid;
|
|
451
|
+
const localBotUidReady = !!localBotUid;
|
|
452
|
+
const rtmJoined = !!hasUserJoinedRTM;
|
|
453
|
+
const activeCallReady = !!callActive;
|
|
454
|
+
const roomInfoReady = !!(roomId?.host || roomId?.attendee);
|
|
396
455
|
const sttDepsReady =
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
456
|
+
localUidReady &&
|
|
457
|
+
localBotUidReady &&
|
|
458
|
+
rtmJoined &&
|
|
459
|
+
activeCallReady &&
|
|
460
|
+
roomInfoReady;
|
|
402
461
|
|
|
403
462
|
const sttStartGuardRef = React.useRef(false);
|
|
404
463
|
const sttAutoStartGuardRef = React.useRef(false);
|
|
@@ -409,6 +468,46 @@ const CaptionProvider: React.FC<CaptionProviderProps> = ({
|
|
|
409
468
|
sttDepsReadyRef.current = sttDepsReady;
|
|
410
469
|
}, [sttDepsReady]);
|
|
411
470
|
|
|
471
|
+
React.useEffect(() => {
|
|
472
|
+
const missingDependencies = [
|
|
473
|
+
!localUidReady && 'localUid',
|
|
474
|
+
!localBotUidReady && 'localBotUid',
|
|
475
|
+
!rtmJoined && 'RTM',
|
|
476
|
+
!activeCallReady && 'activeCall',
|
|
477
|
+
!roomInfoReady && 'roomInfo',
|
|
478
|
+
!isSTTListenerAdded && 'streamListener',
|
|
479
|
+
].filter(Boolean);
|
|
480
|
+
const transcriptReady = sttDepsReady && isSTTListenerAdded;
|
|
481
|
+
|
|
482
|
+
logger.log(
|
|
483
|
+
LogSource.Internals,
|
|
484
|
+
'TRANSCRIPT',
|
|
485
|
+
`${TRANSCRIPT_JOURNEY} STT readiness: ${
|
|
486
|
+
transcriptReady
|
|
487
|
+
? 'READY'
|
|
488
|
+
: `WAITING (missing: ${missingDependencies.join(', ')})`
|
|
489
|
+
}`,
|
|
490
|
+
{
|
|
491
|
+
stage: 'stt_readiness',
|
|
492
|
+
outcome: transcriptReady ? 'ready' : 'waiting',
|
|
493
|
+
localUidReady,
|
|
494
|
+
localBotUidReady,
|
|
495
|
+
rtmJoined,
|
|
496
|
+
callActive: activeCallReady,
|
|
497
|
+
roomInfoReady,
|
|
498
|
+
listenerRegistered: isSTTListenerAdded,
|
|
499
|
+
},
|
|
500
|
+
);
|
|
501
|
+
}, [
|
|
502
|
+
activeCallReady,
|
|
503
|
+
isSTTListenerAdded,
|
|
504
|
+
localBotUidReady,
|
|
505
|
+
localUidReady,
|
|
506
|
+
roomInfoReady,
|
|
507
|
+
rtmJoined,
|
|
508
|
+
sttDepsReady,
|
|
509
|
+
]);
|
|
510
|
+
|
|
412
511
|
React.useEffect(() => {
|
|
413
512
|
if (sttDepsReadyRef.current && !hasFlushedSttQueueRef.current) {
|
|
414
513
|
hasFlushedSttQueueRef.current = true;
|