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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agora-appbuilder-core",
3
- "version": "4.1.24",
3
+ "version": "4.1.25-beta.1",
4
4
  "description": "React Native template for RTE app builder",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -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.24",
81
- CORE_VERSION: "4.1.24",
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 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
- );
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
- transcriptListenerSubscriptionRef.current
351
+ transcriptListenerRegistrationRef.current
347
352
  ) {
348
353
  return;
349
354
  }
350
355
 
351
- transcriptListenerSubscriptionRef.current = RtcEngineUnsafe.addListener(
352
- 'onStreamMessage',
353
- handleStreamMessageCallback,
356
+ logger.log(
357
+ LogSource.Internals,
358
+ 'TRANSCRIPT',
359
+ `${TRANSCRIPT_JOURNEY} stream listener registration requested`,
360
+ {stage: 'listener_registration', outcome: 'started'},
354
361
  );
355
- setIsSTTListenerAdded(true);
356
- }, [RtcEngineUnsafe, callActive, handleStreamMessageCallback]);
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
- transcriptListenerSubscriptionRef.current?.remove();
361
- transcriptListenerSubscriptionRef.current = null;
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
- !!localUid &&
398
- !!localBotUid &&
399
- !!hasUserJoinedRTM &&
400
- !!callActive &&
401
- !!(roomId?.host || roomId?.attendee);
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;