agora-appbuilder-core 4.1.23-beta.4 → 4.1.23-beta.5

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.23-beta.4",
3
+ "version": "4.1.23-beta.5",
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.23-beta.4",
81
- CORE_VERSION: "4.1.23-beta.4",
80
+ CLI_VERSION: "3.1.23-beta.5",
81
+ CORE_VERSION: "4.1.23-beta.5",
82
82
  DISABLE_LANDSCAPE_MODE: false,
83
83
  STT_AUTO_START: false,
84
84
  CLOUD_RECORDING_AUTO_START: false,
@@ -54,6 +54,7 @@ import {
54
54
  getScreenshareStartDecision,
55
55
  getScreenshareStopDecision,
56
56
  } from './screenshareOperation';
57
+ import {createScreenshareRecordingLayoutReconciler} from './screenshareRecordingLayoutReconciler';
57
58
 
58
59
  type ScreenshareAction = 'start' | 'stop';
59
60
 
@@ -99,6 +100,13 @@ export const ScreenshareConfigure = (props: {
99
100
  };
100
101
 
101
102
  const {executeNormalQuery, executePresenterQuery} = useRecordingLayoutQuery();
103
+ const recordingActiveRef = useRef(props.isRecordingActive);
104
+ const executePresenterQueryRef = useRef(executePresenterQuery);
105
+ const recordingLayoutReconcilerRef = useRef(
106
+ createScreenshareRecordingLayoutReconciler(),
107
+ );
108
+ recordingActiveRef.current = props.isRecordingActive;
109
+ executePresenterQueryRef.current = executePresenterQuery;
102
110
 
103
111
  const {channel, appId, screenShareUid, screenShareToken, encryption} =
104
112
  useContext(PropsContext).rtcProps;
@@ -471,6 +479,10 @@ export const ScreenshareConfigure = (props: {
471
479
  },
472
480
  );
473
481
  await executePresenterQuery(screenShareUid);
482
+ recordingLayoutReconcilerRef.current.markApplied(
483
+ screenshareSessionId,
484
+ recordingActiveRef.current,
485
+ );
474
486
  } else {
475
487
  logger.log(
476
488
  LogSource.Internals,
@@ -509,6 +521,112 @@ export const ScreenshareConfigure = (props: {
509
521
  );
510
522
  };
511
523
 
524
+ useEffect(() => {
525
+ const screenshareSessionId = activeScreenshareSessionIdRef.current;
526
+ const reconciliationAttemptId = getUniqueID();
527
+ const logContext = {
528
+ action: 'start' as ScreenshareAction,
529
+ stage: 'recording_layout_reconciliation',
530
+ screenshareAttemptId: reconciliationAttemptId,
531
+ screenshareSessionId: screenshareSessionId || 'unknown-session',
532
+ recordingActive: props.isRecordingActive,
533
+ screenshareActive: isScreenshareActive,
534
+ screenShareUid,
535
+ operationState: operationStateRef.current,
536
+ };
537
+
538
+ recordingLayoutReconcilerRef.current
539
+ .reconcile({
540
+ screenshareSessionId,
541
+ isRecordingActive: props.isRecordingActive,
542
+ isScreenshareActive,
543
+ executePresenterQuery: () => {
544
+ logger.log(
545
+ LogSource.Internals,
546
+ 'SCREENSHARE',
547
+ `${SCREENSHARE_JOURNEY} screen share recording presenter layout reconciliation started`,
548
+ {
549
+ ...logContext,
550
+ outcome: 'started',
551
+ reconciliationReason:
552
+ 'recording_and_published_screenshare_are_active',
553
+ },
554
+ );
555
+ return executePresenterQueryRef.current(screenShareUid);
556
+ },
557
+ })
558
+ .then(result => {
559
+ if (result === 'applied') {
560
+ logger.log(
561
+ LogSource.Internals,
562
+ 'SCREENSHARE',
563
+ `${SCREENSHARE_JOURNEY} screen share recording presenter layout reconciliation completed successfully`,
564
+ {
565
+ ...logContext,
566
+ outcome: 'success',
567
+ recordingActiveNow: recordingActiveRef.current,
568
+ activeScreenshareSessionIdNow:
569
+ activeScreenshareSessionIdRef.current,
570
+ },
571
+ );
572
+ return;
573
+ }
574
+
575
+ if (result === 'already_applied' || result === 'in_progress') {
576
+ logger.log(
577
+ LogSource.Internals,
578
+ 'SCREENSHARE',
579
+ `${SCREENSHARE_JOURNEY} screen share recording presenter layout reconciliation skipped`,
580
+ {
581
+ ...logContext,
582
+ outcome: 'skipped',
583
+ duplicateReason:
584
+ result === 'already_applied'
585
+ ? 'presenter_layout_already_applied'
586
+ : 'presenter_layout_query_in_progress',
587
+ },
588
+ );
589
+ return;
590
+ }
591
+
592
+ if (
593
+ isScreenshareActive ||
594
+ (props.isRecordingActive && operationStateRef.current === 'starting')
595
+ ) {
596
+ logger.log(
597
+ LogSource.Internals,
598
+ 'SCREENSHARE',
599
+ `${SCREENSHARE_JOURNEY} screen share recording presenter layout reconciliation waiting for both states`,
600
+ {
601
+ ...logContext,
602
+ outcome: 'waiting',
603
+ waitingReason: !screenshareSessionId
604
+ ? 'missing_active_screenshare_session'
605
+ : !props.isRecordingActive
606
+ ? 'recording_inactive'
607
+ : 'screenshare_not_published',
608
+ },
609
+ );
610
+ }
611
+ })
612
+ .catch(recordingError => {
613
+ logger.error(
614
+ LogSource.Internals,
615
+ 'SCREENSHARE',
616
+ `${SCREENSHARE_JOURNEY} screen share recording presenter layout reconciliation failed; screen share remains active`,
617
+ recordingError,
618
+ {
619
+ ...logContext,
620
+ outcome: 'failure',
621
+ recordingActiveNow: recordingActiveRef.current,
622
+ activeScreenshareSessionIdNow:
623
+ activeScreenshareSessionIdRef.current,
624
+ ...getScreenshareError(recordingError),
625
+ },
626
+ );
627
+ });
628
+ }, [isScreenshareActive, props.isRecordingActive, screenShareUid]);
629
+
512
630
  const stopScreenshare = async (
513
631
  stopOrigin: ScreenshareStopOrigin = 'unknown',
514
632
  stopActorUid?: UidType,
@@ -1038,17 +1156,15 @@ export const ScreenshareConfigure = (props: {
1038
1156
  ...getScreenshareError(e),
1039
1157
  },
1040
1158
  );
1041
- if (!userCancelOrPermissionDenied) {
1042
- Toast.show({
1043
- leadingIconName: 'alert',
1044
- type: 'error',
1045
- text1: toastHeading,
1046
- text2: toastSubHeading,
1047
- visibilityTime: 1000 * 10,
1048
- primaryBtn: null,
1049
- secondaryBtn: null,
1050
- });
1051
- }
1159
+ Toast.show({
1160
+ leadingIconName: 'alert',
1161
+ type: 'error',
1162
+ text1: toastHeading,
1163
+ text2: toastSubHeading,
1164
+ visibilityTime: 1000 * 10,
1165
+ primaryBtn: null,
1166
+ secondaryBtn: null,
1167
+ });
1052
1168
  if (isActive && !rtcOperationSucceeded) {
1053
1169
  pendingScreenshareSessionIdRef.current = null;
1054
1170
  activeScreenshareSessionIdRef.current = null;
@@ -0,0 +1,164 @@
1
+ import {createScreenshareRecordingLayoutReconciler} from '../screenshareRecordingLayoutReconciler';
2
+
3
+ describe('screen-share recording layout reconciler', () => {
4
+ it('waits until recording and a published screen share are both active', async () => {
5
+ const executePresenterQuery = jest.fn().mockResolvedValue(undefined);
6
+ const reconciler = createScreenshareRecordingLayoutReconciler();
7
+
8
+ await expect(
9
+ reconciler.reconcile({
10
+ screenshareSessionId: 'session-1',
11
+ isRecordingActive: true,
12
+ isScreenshareActive: false,
13
+ executePresenterQuery,
14
+ }),
15
+ ).resolves.toBe('not_ready');
16
+ expect(executePresenterQuery).not.toHaveBeenCalled();
17
+ });
18
+
19
+ it('applies presenter layout once both states are active', async () => {
20
+ const executePresenterQuery = jest.fn().mockResolvedValue(undefined);
21
+ const reconciler = createScreenshareRecordingLayoutReconciler();
22
+
23
+ await expect(
24
+ reconciler.reconcile({
25
+ screenshareSessionId: 'session-1',
26
+ isRecordingActive: true,
27
+ isScreenshareActive: true,
28
+ executePresenterQuery,
29
+ }),
30
+ ).resolves.toBe('applied');
31
+ expect(executePresenterQuery).toHaveBeenCalledTimes(1);
32
+ });
33
+
34
+ it('reconciles when recording becomes active while screen-share publishing is still pending', async () => {
35
+ const executePresenterQuery = jest.fn().mockResolvedValue(undefined);
36
+ const reconciler = createScreenshareRecordingLayoutReconciler();
37
+
38
+ await expect(
39
+ reconciler.reconcile({
40
+ screenshareSessionId: 'session-1',
41
+ isRecordingActive: false,
42
+ isScreenshareActive: false,
43
+ executePresenterQuery,
44
+ }),
45
+ ).resolves.toBe('not_ready');
46
+ await expect(
47
+ reconciler.reconcile({
48
+ screenshareSessionId: 'session-1',
49
+ isRecordingActive: true,
50
+ isScreenshareActive: false,
51
+ executePresenterQuery,
52
+ }),
53
+ ).resolves.toBe('not_ready');
54
+ await expect(
55
+ reconciler.reconcile({
56
+ screenshareSessionId: 'session-1',
57
+ isRecordingActive: true,
58
+ isScreenshareActive: true,
59
+ executePresenterQuery,
60
+ }),
61
+ ).resolves.toBe('applied');
62
+ expect(executePresenterQuery).toHaveBeenCalledTimes(1);
63
+ });
64
+
65
+ it('does not duplicate a layout query for the same recording and screen-share session', async () => {
66
+ const executePresenterQuery = jest.fn().mockResolvedValue(undefined);
67
+ const reconciler = createScreenshareRecordingLayoutReconciler();
68
+ const args = {
69
+ screenshareSessionId: 'session-1',
70
+ isRecordingActive: true,
71
+ isScreenshareActive: true,
72
+ executePresenterQuery,
73
+ };
74
+
75
+ await expect(reconciler.reconcile(args)).resolves.toBe('applied');
76
+ await expect(reconciler.reconcile(args)).resolves.toBe('already_applied');
77
+ expect(executePresenterQuery).toHaveBeenCalledTimes(1);
78
+ });
79
+
80
+ it('deduplicates a query that is already in progress', async () => {
81
+ let resolveQuery: () => void = () => {};
82
+ const executePresenterQuery = jest.fn(
83
+ () =>
84
+ new Promise<void>(resolve => {
85
+ resolveQuery = resolve;
86
+ }),
87
+ );
88
+ const reconciler = createScreenshareRecordingLayoutReconciler();
89
+ const args = {
90
+ screenshareSessionId: 'session-1',
91
+ isRecordingActive: true,
92
+ isScreenshareActive: true,
93
+ executePresenterQuery,
94
+ };
95
+
96
+ const firstReconciliation = reconciler.reconcile(args);
97
+ await expect(reconciler.reconcile(args)).resolves.toBe('in_progress');
98
+ resolveQuery();
99
+ await expect(firstReconciliation).resolves.toBe('applied');
100
+ expect(executePresenterQuery).toHaveBeenCalledTimes(1);
101
+ });
102
+
103
+ it('applies layout again when recording restarts during the same screen-share session', async () => {
104
+ const executePresenterQuery = jest.fn().mockResolvedValue(undefined);
105
+ const reconciler = createScreenshareRecordingLayoutReconciler();
106
+
107
+ await reconciler.reconcile({
108
+ screenshareSessionId: 'session-1',
109
+ isRecordingActive: true,
110
+ isScreenshareActive: true,
111
+ executePresenterQuery,
112
+ });
113
+ await reconciler.reconcile({
114
+ screenshareSessionId: 'session-1',
115
+ isRecordingActive: false,
116
+ isScreenshareActive: true,
117
+ executePresenterQuery,
118
+ });
119
+ await expect(
120
+ reconciler.reconcile({
121
+ screenshareSessionId: 'session-1',
122
+ isRecordingActive: true,
123
+ isScreenshareActive: true,
124
+ executePresenterQuery,
125
+ }),
126
+ ).resolves.toBe('applied');
127
+ expect(executePresenterQuery).toHaveBeenCalledTimes(2);
128
+ });
129
+
130
+ it('retries after a failed layout query', async () => {
131
+ const layoutError = new Error('layout update failed');
132
+ const executePresenterQuery = jest
133
+ .fn()
134
+ .mockRejectedValueOnce(layoutError)
135
+ .mockResolvedValueOnce(undefined);
136
+ const reconciler = createScreenshareRecordingLayoutReconciler();
137
+ const args = {
138
+ screenshareSessionId: 'session-1',
139
+ isRecordingActive: true,
140
+ isScreenshareActive: true,
141
+ executePresenterQuery,
142
+ };
143
+
144
+ await expect(reconciler.reconcile(args)).rejects.toBe(layoutError);
145
+ await expect(reconciler.reconcile(args)).resolves.toBe('applied');
146
+ expect(executePresenterQuery).toHaveBeenCalledTimes(2);
147
+ });
148
+
149
+ it('honours a presenter layout already applied by the normal start path', async () => {
150
+ const executePresenterQuery = jest.fn().mockResolvedValue(undefined);
151
+ const reconciler = createScreenshareRecordingLayoutReconciler();
152
+
153
+ reconciler.markApplied('session-1', true);
154
+ await expect(
155
+ reconciler.reconcile({
156
+ screenshareSessionId: 'session-1',
157
+ isRecordingActive: true,
158
+ isScreenshareActive: true,
159
+ executePresenterQuery,
160
+ }),
161
+ ).resolves.toBe('already_applied');
162
+ expect(executePresenterQuery).not.toHaveBeenCalled();
163
+ });
164
+ });
@@ -0,0 +1,72 @@
1
+ export type RecordingLayoutReconciliationResult =
2
+ | 'applied'
3
+ | 'already_applied'
4
+ | 'in_progress'
5
+ | 'not_ready';
6
+
7
+ type ReconcileRecordingLayoutArgs = {
8
+ screenshareSessionId: string | null;
9
+ isRecordingActive: boolean;
10
+ isScreenshareActive: boolean;
11
+ executePresenterQuery: () => Promise<unknown>;
12
+ };
13
+
14
+ export const createScreenshareRecordingLayoutReconciler = () => {
15
+ let recordingActivation = 0;
16
+ let wasRecordingActive = false;
17
+ const appliedKeys = new Set<string>();
18
+ const inProgressKeys = new Set<string>();
19
+
20
+ const observeRecordingState = (isRecordingActive: boolean) => {
21
+ if (isRecordingActive && !wasRecordingActive) {
22
+ recordingActivation += 1;
23
+ }
24
+ wasRecordingActive = isRecordingActive;
25
+ return recordingActivation;
26
+ };
27
+
28
+ const getKey = (screenshareSessionId: string, activation: number) =>
29
+ `${screenshareSessionId}:${activation}`;
30
+
31
+ const markApplied = (
32
+ screenshareSessionId: string,
33
+ isRecordingActive: boolean,
34
+ ) => {
35
+ const activation = observeRecordingState(isRecordingActive);
36
+ if (!isRecordingActive) {
37
+ return;
38
+ }
39
+ appliedKeys.add(getKey(screenshareSessionId, activation));
40
+ };
41
+
42
+ const reconcile = async ({
43
+ screenshareSessionId,
44
+ isRecordingActive,
45
+ isScreenshareActive,
46
+ executePresenterQuery,
47
+ }: ReconcileRecordingLayoutArgs): Promise<RecordingLayoutReconciliationResult> => {
48
+ const activation = observeRecordingState(isRecordingActive);
49
+ if (!screenshareSessionId || !isRecordingActive || !isScreenshareActive) {
50
+ return 'not_ready';
51
+ }
52
+
53
+ const key = getKey(screenshareSessionId, activation);
54
+ if (appliedKeys.has(key)) {
55
+ return 'already_applied';
56
+ }
57
+ if (inProgressKeys.has(key)) {
58
+ return 'in_progress';
59
+ }
60
+
61
+ inProgressKeys.add(key);
62
+ try {
63
+ await executePresenterQuery();
64
+ appliedKeys.add(key);
65
+ return 'applied';
66
+ } finally {
67
+ inProgressKeys.delete(key);
68
+ }
69
+ };
70
+
71
+ return {markApplied, reconcile};
72
+ };