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.
- package/package.json +1 -1
- package/template/bridge/rtc/webNg/RtcEngine.ts +86 -49
- package/template/bridge/rtc/webNg/__tests__/screenshareLifecycle.test.ts +50 -21
- package/template/defaultConfig.js +2 -2
- package/template/src/components/PinnedVideo.tsx +43 -23
- package/template/src/components/__tests__/pinnedLayoutState.test.ts +47 -0
- package/template/src/components/participants/UserActionMenuOptions.tsx +15 -2
- package/template/src/components/pinnedLayoutState.ts +43 -0
- package/template/src/pages/video-call/VideoRenderer.tsx +2 -0
- package/template/src/subComponents/caption/Transcript.tsx +36 -0
- package/template/src/subComponents/caption/__tests__/transcriptJourney.test.ts +7 -0
- package/template/src/subComponents/caption/transcriptJourney.ts +1 -0
- package/template/src/subComponents/caption/useCaption.tsx +215 -46
- package/template/src/subComponents/caption/useSTTAPI.tsx +13 -12
- package/template/src/subComponents/caption/useStreamMessageUtils.ts +109 -8
- package/template/src/subComponents/screenshare/ScreenshareConfigure.native.tsx +115 -30
- package/template/src/subComponents/screenshare/ScreenshareConfigure.tsx +489 -147
- package/template/src/subComponents/screenshare/__tests__/ScreenshareConfigure.test.tsx +245 -0
- package/template/src/subComponents/screenshare/__tests__/screenshareInterruptionRecovery.test.ts +119 -0
- package/template/src/subComponents/screenshare/screenshareInterruptionRecovery.ts +118 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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(() =>
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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) => {
|