agora-appbuilder-core 4.1.23-beta.4 → 4.1.23
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/screenshare/ScreenshareConfigure.tsx +127 -11
- package/template/src/subComponents/screenshare/__tests__/screenshareRecordingLayoutReconciler.test.ts +164 -0
- package/template/src/subComponents/screenshare/screenshareRecordingLayoutReconciler.ts +72 -0
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.23
|
|
81
|
-
CORE_VERSION: "4.1.23
|
|
80
|
+
CLI_VERSION: "3.1.23",
|
|
81
|
+
CORE_VERSION: "4.1.23",
|
|
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
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
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
|
+
};
|