agora-appbuilder-core 4.1.23 → 4.1.24-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.
@@ -3,6 +3,8 @@ import protoRoot from './proto/ptoto';
3
3
  import PQueue from 'p-queue';
4
4
  import type {CaptionObj, TranscriptItem} from './useCaption';
5
5
  import type {LanguageType} from './utils';
6
+ import {logger, LogSource} from '../../logger/AppBuilderLogger';
7
+ import {TRANSCRIPT_JOURNEY} from './transcriptJourney';
6
8
 
7
9
  type StreamMessageCallback = (args: [number, Uint8Array]) => void;
8
10
  type FinalListType = {
@@ -42,8 +44,29 @@ const useStreamMessageUtils = ({
42
44
  const finalTranscriptListRef = React.useRef<FinalListType>({});
43
45
  const finalTranslationListRef = React.useRef<FinalTranslationListType>({});
44
46
  const queue = React.useMemo(() => new PQueue({concurrency: 1}), []);
47
+ const messagesReceivedRef = React.useRef(0);
48
+ const messagesProcessedRef = React.useRef(0);
49
+ const finalizedSegmentsRef = React.useRef(0);
50
+ const speakersWithFinalizedSegmentsRef = React.useRef(new Set<string>());
45
51
 
46
52
  const streamMessageCallback: StreamMessageCallback = args => {
53
+ const [receivedBotUid, receivedPayload] = args;
54
+ const messageNumber = ++messagesReceivedRef.current;
55
+ if (messageNumber === 1 || messageNumber % 50 === 0) {
56
+ logger.log(
57
+ LogSource.Internals,
58
+ 'TRANSCRIPT',
59
+ `${TRANSCRIPT_JOURNEY} stream message received`,
60
+ {
61
+ stage: 'stream_message_received',
62
+ outcome: 'success',
63
+ botUid: receivedBotUid ?? null,
64
+ payloadByteLength: receivedPayload?.byteLength || 0,
65
+ messagesReceived: messageNumber,
66
+ messagesProcessed: messagesProcessedRef.current,
67
+ },
68
+ );
69
+ }
47
70
  const queueCallback = (args1: [number, Uint8Array]) => {
48
71
  const finalList = finalListRef.current;
49
72
  const finalTranscriptList = finalTranscriptListRef.current;
@@ -221,11 +244,29 @@ const useStreamMessageUtils = ({
221
244
  finalList[textstream.uid].push(finalText);
222
245
  finalTranscriptList[textstream.uid].push(finalText);
223
246
  currentFinalText = finalText;
224
- // log info to show measure the duration of passes in which a sentence gets finalized
247
+ const speakerUid = textstream.uid ?? null;
248
+ const speakerKey = String(speakerUid ?? 'unknown');
249
+ const isFirstFinalizedSegmentForSpeaker =
250
+ !speakersWithFinalizedSegmentsRef.current.has(speakerKey);
251
+ finalizedSegmentsRef.current += 1;
225
252
  const duration = performance.now() - captionStartTimeRef.current;
226
- console.log(
227
- `stt-Time taken to finalize caption ${currentFinalText}: ${duration}ms`,
228
- );
253
+ if (isFirstFinalizedSegmentForSpeaker) {
254
+ speakersWithFinalizedSegmentsRef.current.add(speakerKey);
255
+ logger.log(
256
+ LogSource.Internals,
257
+ 'TRANSCRIPT',
258
+ `${TRANSCRIPT_JOURNEY} first transcript segment finalized for speaker`,
259
+ {
260
+ stage: 'segment_finalized',
261
+ outcome: 'success',
262
+ botUid: botUid ?? null,
263
+ speakerUid,
264
+ finalCharacterCount: currentFinalText.length,
265
+ finalizeDurationMs: duration,
266
+ finalizedSegments: finalizedSegmentsRef.current,
267
+ },
268
+ );
269
+ }
229
270
  captionStartTimeRef.current = 0; // Reset start time
230
271
  }
231
272
 
@@ -364,14 +405,74 @@ const useStreamMessageUtils = ({
364
405
  };
365
406
  void queue
366
407
  .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
- );
408
+ .then(() => {
409
+ const processedCount = ++messagesProcessedRef.current;
410
+ if (processedCount === 1 || processedCount % 50 === 0) {
411
+ logger.log(
412
+ LogSource.Internals,
413
+ 'TRANSCRIPT',
414
+ `${TRANSCRIPT_JOURNEY} stream message processed`,
415
+ {
416
+ stage: 'stream_message_processed',
417
+ outcome: 'success',
418
+ botUid: receivedBotUid ?? null,
419
+ messagesReceived: messagesReceivedRef.current,
420
+ messagesProcessed: processedCount,
421
+ queueSize: queue.size,
422
+ },
423
+ );
424
+ }
425
+ })
426
+ .catch(error => {
427
+ logger.error(
428
+ LogSource.Internals,
429
+ 'TRANSCRIPT',
430
+ `${TRANSCRIPT_JOURNEY} failed to process stream message`,
431
+ {
432
+ stage: 'stream_message_processing',
433
+ outcome: 'failure',
434
+ botUid: receivedBotUid ?? null,
435
+ messageNumber,
436
+ messagesReceived: messagesReceivedRef.current,
437
+ messagesProcessed: messagesProcessedRef.current,
438
+ error: error ?? null,
439
+ },
440
+ );
441
+ });
371
442
  };
372
443
 
373
444
  const flushStreamMessageQueue = React.useCallback(async () => {
445
+ logger.log(
446
+ LogSource.Internals,
447
+ 'TRANSCRIPT',
448
+ `${TRANSCRIPT_JOURNEY} stream queue flush requested`,
449
+ {
450
+ stage: 'stream_queue_flush',
451
+ outcome: 'started',
452
+ messagesReceived: messagesReceivedRef.current,
453
+ messagesProcessed: messagesProcessedRef.current,
454
+ finalizedSegments: finalizedSegmentsRef.current,
455
+ speakersWithFinalizedSegments:
456
+ speakersWithFinalizedSegmentsRef.current.size,
457
+ queueSize: queue.size,
458
+ },
459
+ );
374
460
  await queue.onIdle();
461
+ logger.log(
462
+ LogSource.Internals,
463
+ 'TRANSCRIPT',
464
+ `${TRANSCRIPT_JOURNEY} stream queue flush completed`,
465
+ {
466
+ stage: 'stream_queue_flush',
467
+ outcome: 'success',
468
+ messagesReceived: messagesReceivedRef.current,
469
+ messagesProcessed: messagesProcessedRef.current,
470
+ finalizedSegments: finalizedSegmentsRef.current,
471
+ speakersWithFinalizedSegments:
472
+ speakersWithFinalizedSegmentsRef.current.size,
473
+ queueSize: queue.size,
474
+ },
475
+ );
375
476
  }, [queue]);
376
477
 
377
478
  return {
@@ -52,6 +52,11 @@ import {timeNow} from '../../rtm/utils';
52
52
  import LocalEventEmitter, {
53
53
  LocalEventsEnum,
54
54
  } from '../../rtm-events-api/LocalEvents';
55
+ import {
56
+ captureScreenshareRecoveryCandidate,
57
+ getScreenshareRecoveryDecision,
58
+ ScreenshareRecoveryCandidate,
59
+ } from './screenshareInterruptionRecovery';
55
60
 
56
61
  export const ScreenshareContextConsumer = ScreenshareContext.Consumer;
57
62
 
@@ -82,6 +87,14 @@ export const ScreenshareConfigure = (props: {children: React.ReactNode}) => {
82
87
  secondaryPinnedUid: secondaryPinnedUid,
83
88
  });
84
89
  const screenShareDataRef = useRef({screenShareData: screenShareData});
90
+ const recoveryCandidateRef = useRef<ScreenshareRecoveryCandidate | null>(
91
+ null,
92
+ );
93
+ const previousLayoutStateRef = useRef({
94
+ activeUids,
95
+ pinnedUid,
96
+ currentLayout,
97
+ });
85
98
  const localMute = useMuteToggleLocal();
86
99
  const {video} = useLocalUserInfo();
87
100
  useEffect(() => {
@@ -163,6 +176,71 @@ export const ScreenshareConfigure = (props: {children: React.ReactNode}) => {
163
176
  }
164
177
  }, [activeUids, screenShareData, triggerChangeLayout]);
165
178
 
179
+ useEffect(() => {
180
+ const previousLayoutState = previousLayoutStateRef.current;
181
+ const existingCandidate = recoveryCandidateRef.current;
182
+ const capturedCandidate = captureScreenshareRecoveryCandidate({
183
+ previousActiveUids: previousLayoutState.activeUids,
184
+ currentActiveUids: activeUids,
185
+ previousPinnedUid: previousLayoutState.pinnedUid,
186
+ previousLayout: previousLayoutState.currentLayout,
187
+ screenShareData,
188
+ detectedAt: Date.now(),
189
+ });
190
+
191
+ if (!existingCandidate && capturedCandidate) {
192
+ recoveryCandidateRef.current = capturedCandidate;
193
+ }
194
+
195
+ const candidate = recoveryCandidateRef.current;
196
+ if (candidate) {
197
+ const decision = getScreenshareRecoveryDecision({
198
+ candidate,
199
+ activeUids,
200
+ pinnedUid,
201
+ isVideoPublished: defaultContent?.[candidate.uid]?.video === 1,
202
+ screenShareData,
203
+ });
204
+
205
+ if (decision === 'waiting_for_video') {
206
+ candidate.joinedLogged = true;
207
+ } else if (decision === 'restore') {
208
+ isPinned.current = candidate.uid;
209
+ dispatch({type: 'UserPin', value: [candidate.uid]});
210
+ if (
211
+ candidate.previousLayout === getPinnedLayoutName() &&
212
+ currentLayout !== getPinnedLayoutName()
213
+ ) {
214
+ setPinnedLayout();
215
+ }
216
+ recoveryCandidateRef.current = null;
217
+ } else if (decision.startsWith('cancel_')) {
218
+ recoveryCandidateRef.current = null;
219
+ }
220
+ }
221
+
222
+ previousLayoutStateRef.current = {
223
+ activeUids,
224
+ pinnedUid,
225
+ currentLayout,
226
+ };
227
+ }, [
228
+ activeUids,
229
+ currentLayout,
230
+ defaultContent,
231
+ dispatch,
232
+ pinnedUid,
233
+ screenShareData,
234
+ setPinnedLayout,
235
+ ]);
236
+
237
+ useEffect(
238
+ () => () => {
239
+ recoveryCandidateRef.current = null;
240
+ },
241
+ [],
242
+ );
243
+
166
244
  useEffect(() => {
167
245
  /**
168
246
  * When user kicked off by remote host then stop the screenshare
@@ -170,7 +248,8 @@ export const ScreenshareConfigure = (props: {children: React.ReactNode}) => {
170
248
  const unsubKickUser = LocalEventEmitter.on(
171
249
  LocalEventsEnum.USER_KICKED_OFF_BY_REMOTE_HOST,
172
250
  () => {
173
- stopScreenshare(false, true);
251
+ // Force native capture cleanup even if React state has not caught up.
252
+ forceStopScreenshare();
174
253
  },
175
254
  );
176
255
  /**
@@ -179,7 +258,8 @@ export const ScreenshareConfigure = (props: {children: React.ReactNode}) => {
179
258
  const unsubKickScreenshare = events.on(
180
259
  controlMessageEnum.kickScreenshare,
181
260
  () => {
182
- stopScreenshare(false, true);
261
+ // A host-initiated removal must not depend on local active state.
262
+ forceStopScreenshare();
183
263
  },
184
264
  );
185
265
  const unsubScreenShareAttribute = events.on(
@@ -428,34 +508,39 @@ export const ScreenshareConfigure = (props: {children: React.ReactNode}) => {
428
508
  unpublishScreenshare,
429
509
  ]);
430
510
 
431
- const stopScreenshare = useCallback(
432
- async (enableVideo: boolean = false, forceStop: boolean = false) => {
433
- if (isScreenshareActive || forceStop) {
434
- logger.log(
435
- LogSource.Internals,
436
- 'SCREENSHARE',
437
- 'Trying to stop native screenshare',
438
- );
439
- try {
440
- engine?.current?.stopScreenCapture();
441
- } catch (error) {
442
- logger.error(
443
- LogSource.Internals,
444
- 'SCREENSHARE',
445
- 'native screenshare error on -> stopScreenCapture',
446
- error,
447
- );
448
- }
449
- } else {
450
- logger.debug(
451
- LogSource.Internals,
452
- 'SCREENSHARE',
453
- 'native screenshare -> no screenshare is active',
454
- );
455
- }
456
- },
457
- [engine, isScreenshareActive],
458
- );
511
+ const stopNativeScreenCapture = useCallback(async () => {
512
+ logger.log(
513
+ LogSource.Internals,
514
+ 'SCREENSHARE',
515
+ 'Trying to stop native screenshare',
516
+ );
517
+ try {
518
+ engine?.current?.stopScreenCapture();
519
+ } catch (error) {
520
+ logger.error(
521
+ LogSource.Internals,
522
+ 'SCREENSHARE',
523
+ 'native screenshare error on -> stopScreenCapture',
524
+ error,
525
+ );
526
+ }
527
+ }, [engine]);
528
+
529
+ const stopScreenshare = useCallback(async () => {
530
+ if (isScreenshareActive) {
531
+ await stopNativeScreenCapture();
532
+ } else {
533
+ logger.debug(
534
+ LogSource.Internals,
535
+ 'SCREENSHARE',
536
+ 'native screenshare -> no screenshare is active',
537
+ );
538
+ }
539
+ }, [isScreenshareActive, stopNativeScreenCapture]);
540
+
541
+ const forceStopScreenshare = useCallback(async () => {
542
+ await stopNativeScreenCapture();
543
+ }, [stopNativeScreenCapture]);
459
544
 
460
545
  const onLocalVideoStateChanged = useCallback(
461
546
  (source: VideoSourceType, state: LocalVideoStreamState, error) => {