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
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import TestRenderer, {act} from 'react-test-renderer';
|
|
3
|
+
import {beforeEach, describe, expect, it, jest} from '@jest/globals';
|
|
4
|
+
|
|
5
|
+
let mockContent: any;
|
|
6
|
+
let mockLayout: any;
|
|
7
|
+
let mockScreenContext: any;
|
|
8
|
+
|
|
9
|
+
const mockDispatch = jest.fn();
|
|
10
|
+
const mockSetPinnedLayout = jest.fn();
|
|
11
|
+
const mockChangeLayout = jest.fn();
|
|
12
|
+
|
|
13
|
+
jest.mock('../../../../agora-rn-uikit', () => {
|
|
14
|
+
const ReactModule = require('react');
|
|
15
|
+
return {
|
|
16
|
+
DispatchContext: ReactModule.createContext({dispatch: jest.fn()}),
|
|
17
|
+
PropsContext: ReactModule.createContext({
|
|
18
|
+
rtcProps: {
|
|
19
|
+
channel: 'channel',
|
|
20
|
+
appId: 'app-id',
|
|
21
|
+
screenShareUid: 101,
|
|
22
|
+
screenShareToken: 'screen-token',
|
|
23
|
+
encryption: {},
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
jest.mock('../useScreenshare', () => {
|
|
30
|
+
const ReactModule = require('react');
|
|
31
|
+
return {
|
|
32
|
+
ScreenshareContext: ReactModule.createContext({
|
|
33
|
+
isScreenshareActive: false,
|
|
34
|
+
operationState: 'inactive',
|
|
35
|
+
startScreenshare: jest.fn(),
|
|
36
|
+
stopScreenshare: jest.fn(),
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
jest.mock('customization-api', () => ({
|
|
42
|
+
controlMessageEnum: {kickScreenshare: 'kickScreenshare'},
|
|
43
|
+
useContent: () => mockContent,
|
|
44
|
+
useLayout: () => mockLayout,
|
|
45
|
+
useRtc: () => ({
|
|
46
|
+
RtcEngineUnsafe: {
|
|
47
|
+
addListener: jest.fn(),
|
|
48
|
+
registerScreenShareStoppedCallback: jest.fn(),
|
|
49
|
+
startScreenshare: jest.fn(),
|
|
50
|
+
stopScreenshare: jest.fn(),
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
jest.mock('../../../pages/video-call/DefaultLayouts', () => ({
|
|
56
|
+
getGridLayoutName: () => 'grid',
|
|
57
|
+
getPinnedLayoutName: () => 'pinned',
|
|
58
|
+
useChangeDefaultLayout: () => mockChangeLayout,
|
|
59
|
+
useSetPinnedLayout: () => mockSetPinnedLayout,
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
jest.mock('../../../components/contexts/ScreenShareContext', () => ({
|
|
63
|
+
useScreenContext: () => mockScreenContext,
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
jest.mock('../../../rtm-events-api', () => ({
|
|
67
|
+
__esModule: true,
|
|
68
|
+
default: {
|
|
69
|
+
on: jest.fn(() => jest.fn()),
|
|
70
|
+
send: jest.fn(async () => true),
|
|
71
|
+
},
|
|
72
|
+
PersistanceLevel: {Sender: 'sender'},
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
jest.mock('../../../rtm-events', () => ({
|
|
76
|
+
EventActions: {
|
|
77
|
+
SCREENSHARE_STARTED: 'screenshare-started',
|
|
78
|
+
SCREENSHARE_STOPPED: 'screenshare-stopped',
|
|
79
|
+
},
|
|
80
|
+
EventNames: {SCREENSHARE_ATTRIBUTE: 'screenshare-attribute'},
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
jest.mock('../../recording/useRecordingLayoutQuery', () => ({
|
|
84
|
+
__esModule: true,
|
|
85
|
+
default: () => ({
|
|
86
|
+
executeNormalQuery: jest.fn(),
|
|
87
|
+
executePresenterQuery: jest.fn(),
|
|
88
|
+
}),
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
jest.mock('../../../logger/AppBuilderLogger', () => ({
|
|
92
|
+
LogSource: {Internals: 'Internals'},
|
|
93
|
+
logger: {
|
|
94
|
+
debug: jest.fn(),
|
|
95
|
+
error: jest.fn(),
|
|
96
|
+
log: jest.fn(),
|
|
97
|
+
},
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
jest.mock('../../../utils/useString', () => ({
|
|
101
|
+
useString: () => () => 'Screen share error',
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
jest.mock('../../../../react-native-toast-message', () => ({
|
|
105
|
+
__esModule: true,
|
|
106
|
+
default: {show: jest.fn()},
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
jest.mock('../../../utils/getUniqueID', () => ({
|
|
110
|
+
__esModule: true,
|
|
111
|
+
default: () => 'test-id',
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
jest.mock('../../../utils', () => ({
|
|
115
|
+
filterObject: (value: Record<string, any>, predicate: Function) =>
|
|
116
|
+
Object.fromEntries(Object.entries(value).filter(entry => predicate(entry))),
|
|
117
|
+
}));
|
|
118
|
+
|
|
119
|
+
import {DispatchContext} from '../../../../agora-rn-uikit';
|
|
120
|
+
import {ScreenshareConfigure} from '../ScreenshareConfigure.tsx';
|
|
121
|
+
|
|
122
|
+
const activeScreenShare = {
|
|
123
|
+
101: {name: 'Screen 101', isActive: true, ts: 1000},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const renderConfigurator = (version: number) => (
|
|
127
|
+
<DispatchContext.Provider value={{dispatch: mockDispatch} as any}>
|
|
128
|
+
<ScreenshareConfigure isRecordingActive={false}>
|
|
129
|
+
<React.Fragment key={version} />
|
|
130
|
+
</ScreenshareConfigure>
|
|
131
|
+
</DispatchContext.Provider>
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const updateConfigurator = async (
|
|
135
|
+
renderer: TestRenderer.ReactTestRenderer,
|
|
136
|
+
version: number,
|
|
137
|
+
) => {
|
|
138
|
+
await act(async () => {
|
|
139
|
+
renderer.update(renderConfigurator(version));
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
describe('ScreenshareConfigure interruption recovery', () => {
|
|
144
|
+
beforeEach(() => {
|
|
145
|
+
jest.clearAllMocks();
|
|
146
|
+
mockContent = {
|
|
147
|
+
activeUids: [101, 100, 200],
|
|
148
|
+
pinnedUid: 101,
|
|
149
|
+
secondaryPinnedUid: 100,
|
|
150
|
+
defaultContent: {
|
|
151
|
+
101: {name: 'Screen 101', parentUid: 100, video: 1},
|
|
152
|
+
100: {name: 'User 100', video: 1},
|
|
153
|
+
200: {name: 'User 200', video: 1},
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
mockLayout = {currentLayout: 'pinned'};
|
|
157
|
+
mockScreenContext = {
|
|
158
|
+
screenShareData: activeScreenShare,
|
|
159
|
+
setScreenShareData: jest.fn(),
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('restores the primary pin only after the interrupted screen republishes video', async () => {
|
|
164
|
+
let renderer!: TestRenderer.ReactTestRenderer;
|
|
165
|
+
await act(async () => {
|
|
166
|
+
renderer = TestRenderer.create(renderConfigurator(0));
|
|
167
|
+
});
|
|
168
|
+
mockDispatch.mockClear();
|
|
169
|
+
mockSetPinnedLayout.mockClear();
|
|
170
|
+
|
|
171
|
+
mockContent = {...mockContent, activeUids: [100, 200]};
|
|
172
|
+
mockLayout = {currentLayout: 'grid'};
|
|
173
|
+
await updateConfigurator(renderer, 1);
|
|
174
|
+
expect(mockDispatch).not.toHaveBeenCalledWith({
|
|
175
|
+
type: 'UserPin',
|
|
176
|
+
value: [101],
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
mockContent = {
|
|
180
|
+
...mockContent,
|
|
181
|
+
activeUids: [100, 200, 101],
|
|
182
|
+
defaultContent: {
|
|
183
|
+
...mockContent.defaultContent,
|
|
184
|
+
101: {...mockContent.defaultContent[101], video: 0},
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
await updateConfigurator(renderer, 2);
|
|
188
|
+
expect(mockDispatch).not.toHaveBeenCalledWith({
|
|
189
|
+
type: 'UserPin',
|
|
190
|
+
value: [101],
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
mockContent = {
|
|
194
|
+
...mockContent,
|
|
195
|
+
defaultContent: {
|
|
196
|
+
...mockContent.defaultContent,
|
|
197
|
+
101: {...mockContent.defaultContent[101], video: 1},
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
await updateConfigurator(renderer, 3);
|
|
201
|
+
|
|
202
|
+
expect(mockDispatch).toHaveBeenCalledWith({
|
|
203
|
+
type: 'UserPin',
|
|
204
|
+
value: [101],
|
|
205
|
+
});
|
|
206
|
+
expect(mockDispatch).not.toHaveBeenCalledWith(
|
|
207
|
+
expect.objectContaining({type: 'UserSecondaryPin'}),
|
|
208
|
+
);
|
|
209
|
+
expect(mockSetPinnedLayout).toHaveBeenCalledTimes(1);
|
|
210
|
+
|
|
211
|
+
renderer.unmount();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('does not restore the interrupted screen after the user pins another tile', async () => {
|
|
215
|
+
let renderer!: TestRenderer.ReactTestRenderer;
|
|
216
|
+
await act(async () => {
|
|
217
|
+
renderer = TestRenderer.create(renderConfigurator(0));
|
|
218
|
+
});
|
|
219
|
+
mockDispatch.mockClear();
|
|
220
|
+
mockSetPinnedLayout.mockClear();
|
|
221
|
+
|
|
222
|
+
mockContent = {...mockContent, activeUids: [100, 200]};
|
|
223
|
+
mockLayout = {currentLayout: 'grid'};
|
|
224
|
+
await updateConfigurator(renderer, 1);
|
|
225
|
+
|
|
226
|
+
mockContent = {
|
|
227
|
+
...mockContent,
|
|
228
|
+
activeUids: [100, 200, 101],
|
|
229
|
+
pinnedUid: 200,
|
|
230
|
+
defaultContent: {
|
|
231
|
+
...mockContent.defaultContent,
|
|
232
|
+
101: {...mockContent.defaultContent[101], video: 1},
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
await updateConfigurator(renderer, 2);
|
|
236
|
+
|
|
237
|
+
expect(mockDispatch).not.toHaveBeenCalledWith({
|
|
238
|
+
type: 'UserPin',
|
|
239
|
+
value: [101],
|
|
240
|
+
});
|
|
241
|
+
expect(mockSetPinnedLayout).not.toHaveBeenCalled();
|
|
242
|
+
|
|
243
|
+
renderer.unmount();
|
|
244
|
+
});
|
|
245
|
+
});
|
package/template/src/subComponents/screenshare/__tests__/screenshareInterruptionRecovery.test.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
captureScreenshareRecoveryCandidate,
|
|
3
|
+
getScreenshareRecoveryDecision,
|
|
4
|
+
} from '../screenshareInterruptionRecovery';
|
|
5
|
+
|
|
6
|
+
const activeScreen = {
|
|
7
|
+
101: {name: 'Screen', isActive: true, ts: 1000},
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
describe('screen-share RTC interruption recovery', () => {
|
|
11
|
+
it('captures a candidate only when the active pinned screen UID disappears', () => {
|
|
12
|
+
expect(
|
|
13
|
+
captureScreenshareRecoveryCandidate({
|
|
14
|
+
previousActiveUids: [101, 100, 200],
|
|
15
|
+
currentActiveUids: [100, 200],
|
|
16
|
+
previousPinnedUid: 101,
|
|
17
|
+
previousLayout: 'pinned',
|
|
18
|
+
screenShareData: activeScreen,
|
|
19
|
+
detectedAt: 2000,
|
|
20
|
+
}),
|
|
21
|
+
).toEqual({
|
|
22
|
+
uid: 101,
|
|
23
|
+
previousPinnedUid: 101,
|
|
24
|
+
previousLayout: 'pinned',
|
|
25
|
+
screenshareStartedAt: 1000,
|
|
26
|
+
activeScreenshareUidsAtDetection: [101],
|
|
27
|
+
detectedAt: 2000,
|
|
28
|
+
joinedLogged: false,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('does not capture an unpinned screen interruption', () => {
|
|
33
|
+
expect(
|
|
34
|
+
captureScreenshareRecoveryCandidate({
|
|
35
|
+
previousActiveUids: [200, 101, 100],
|
|
36
|
+
currentActiveUids: [200, 100],
|
|
37
|
+
previousPinnedUid: 200,
|
|
38
|
+
previousLayout: 'pinned',
|
|
39
|
+
screenShareData: activeScreen,
|
|
40
|
+
detectedAt: 2000,
|
|
41
|
+
}),
|
|
42
|
+
).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('waits for both same-UID join and video publication before restore', () => {
|
|
46
|
+
const candidate = {
|
|
47
|
+
uid: 101,
|
|
48
|
+
previousPinnedUid: 101,
|
|
49
|
+
previousLayout: 'pinned',
|
|
50
|
+
screenshareStartedAt: 1000,
|
|
51
|
+
activeScreenshareUidsAtDetection: [101],
|
|
52
|
+
detectedAt: 2000,
|
|
53
|
+
joinedLogged: false,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
expect(
|
|
57
|
+
getScreenshareRecoveryDecision({
|
|
58
|
+
candidate,
|
|
59
|
+
activeUids: [100, 200],
|
|
60
|
+
isVideoPublished: false,
|
|
61
|
+
screenShareData: activeScreen,
|
|
62
|
+
}),
|
|
63
|
+
).toBe('waiting_for_join');
|
|
64
|
+
expect(
|
|
65
|
+
getScreenshareRecoveryDecision({
|
|
66
|
+
candidate,
|
|
67
|
+
activeUids: [100, 200, 101],
|
|
68
|
+
isVideoPublished: false,
|
|
69
|
+
screenShareData: activeScreen,
|
|
70
|
+
}),
|
|
71
|
+
).toBe('waiting_for_video');
|
|
72
|
+
expect(
|
|
73
|
+
getScreenshareRecoveryDecision({
|
|
74
|
+
candidate,
|
|
75
|
+
activeUids: [100, 200, 101],
|
|
76
|
+
isVideoPublished: true,
|
|
77
|
+
screenShareData: activeScreen,
|
|
78
|
+
}),
|
|
79
|
+
).toBe('restore');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it.each([
|
|
83
|
+
[
|
|
84
|
+
'cancel_stopped',
|
|
85
|
+
{101: {name: 'Screen', isActive: false, ts: 1000}},
|
|
86
|
+
undefined,
|
|
87
|
+
],
|
|
88
|
+
[
|
|
89
|
+
'cancel_replaced',
|
|
90
|
+
{
|
|
91
|
+
...activeScreen,
|
|
92
|
+
201: {name: 'New screen', isActive: true, ts: 1001},
|
|
93
|
+
},
|
|
94
|
+
undefined,
|
|
95
|
+
],
|
|
96
|
+
['cancel_user_override', activeScreen, 200],
|
|
97
|
+
])(
|
|
98
|
+
'returns %s when recovery is no longer authoritative',
|
|
99
|
+
(expected, screenShareData, pinnedUid) => {
|
|
100
|
+
expect(
|
|
101
|
+
getScreenshareRecoveryDecision({
|
|
102
|
+
candidate: {
|
|
103
|
+
uid: 101,
|
|
104
|
+
previousPinnedUid: 101,
|
|
105
|
+
previousLayout: 'pinned',
|
|
106
|
+
screenshareStartedAt: 1000,
|
|
107
|
+
activeScreenshareUidsAtDetection: [101],
|
|
108
|
+
detectedAt: 2000,
|
|
109
|
+
joinedLogged: false,
|
|
110
|
+
},
|
|
111
|
+
activeUids: [100, 101, 200],
|
|
112
|
+
pinnedUid,
|
|
113
|
+
isVideoPublished: true,
|
|
114
|
+
screenShareData,
|
|
115
|
+
}),
|
|
116
|
+
).toBe(expected);
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {UidType} from '../../../agora-rn-uikit';
|
|
2
|
+
import {ScreenShareObjectInterface} from '../../components/contexts/ScreenShareContext';
|
|
3
|
+
|
|
4
|
+
export interface ScreenshareRecoveryCandidate {
|
|
5
|
+
uid: UidType;
|
|
6
|
+
previousPinnedUid: UidType;
|
|
7
|
+
previousLayout: string;
|
|
8
|
+
screenshareStartedAt: number;
|
|
9
|
+
activeScreenshareUidsAtDetection: UidType[];
|
|
10
|
+
detectedAt: number;
|
|
11
|
+
joinedLogged: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface CaptureRecoveryCandidateArgs {
|
|
15
|
+
previousActiveUids: UidType[];
|
|
16
|
+
currentActiveUids: UidType[];
|
|
17
|
+
previousPinnedUid?: UidType;
|
|
18
|
+
previousLayout: string;
|
|
19
|
+
screenShareData: ScreenShareObjectInterface;
|
|
20
|
+
detectedAt: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const captureScreenshareRecoveryCandidate = ({
|
|
24
|
+
previousActiveUids,
|
|
25
|
+
currentActiveUids,
|
|
26
|
+
previousPinnedUid,
|
|
27
|
+
previousLayout,
|
|
28
|
+
screenShareData,
|
|
29
|
+
detectedAt,
|
|
30
|
+
}: CaptureRecoveryCandidateArgs): ScreenshareRecoveryCandidate | null => {
|
|
31
|
+
// Recovery applies only when the tile that disappeared was the pinned tile,
|
|
32
|
+
// was present in the previous RTC render list, and is still logically active
|
|
33
|
+
// according to screen-share signaling. Intentional stops therefore do not
|
|
34
|
+
// create recovery candidates.
|
|
35
|
+
if (
|
|
36
|
+
!previousPinnedUid ||
|
|
37
|
+
!previousActiveUids.includes(previousPinnedUid) ||
|
|
38
|
+
currentActiveUids.includes(previousPinnedUid) ||
|
|
39
|
+
!screenShareData?.[previousPinnedUid]?.isActive
|
|
40
|
+
) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
uid: previousPinnedUid,
|
|
46
|
+
previousPinnedUid,
|
|
47
|
+
previousLayout,
|
|
48
|
+
screenshareStartedAt: screenShareData[previousPinnedUid]?.ts || 0,
|
|
49
|
+
// Remember every share that already existed so a newly started share can
|
|
50
|
+
// be distinguished from recovery of this interrupted UID.
|
|
51
|
+
activeScreenshareUidsAtDetection: Object.entries(screenShareData)
|
|
52
|
+
.filter(([, share]) => share?.isActive)
|
|
53
|
+
.map(([uid]) => Number(uid)),
|
|
54
|
+
detectedAt,
|
|
55
|
+
joinedLogged: false,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type ScreenshareRecoveryDecision =
|
|
60
|
+
| 'waiting_for_join'
|
|
61
|
+
| 'waiting_for_video'
|
|
62
|
+
| 'restore'
|
|
63
|
+
| 'cancel_stopped'
|
|
64
|
+
| 'cancel_replaced'
|
|
65
|
+
| 'cancel_user_override';
|
|
66
|
+
|
|
67
|
+
interface GetRecoveryDecisionArgs {
|
|
68
|
+
candidate: ScreenshareRecoveryCandidate;
|
|
69
|
+
activeUids: UidType[];
|
|
70
|
+
pinnedUid?: UidType;
|
|
71
|
+
isVideoPublished: boolean;
|
|
72
|
+
screenShareData: ScreenShareObjectInterface;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const getScreenshareRecoveryDecision = ({
|
|
76
|
+
candidate,
|
|
77
|
+
activeUids,
|
|
78
|
+
pinnedUid,
|
|
79
|
+
isVideoPublished,
|
|
80
|
+
screenShareData,
|
|
81
|
+
}: GetRecoveryDecisionArgs): ScreenshareRecoveryDecision => {
|
|
82
|
+
const recoveredShare = screenShareData?.[candidate.uid];
|
|
83
|
+
// RTM or local cleanup marked the share inactive, so RTC rejoin events must
|
|
84
|
+
// not resurrect a screen share that was intentionally stopped.
|
|
85
|
+
if (!recoveredShare?.isActive) {
|
|
86
|
+
return 'cancel_stopped';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// A screen UID that became active after interruption represents a newer
|
|
90
|
+
// share and takes precedence over restoring the old pinned tile.
|
|
91
|
+
const newerActiveShareExists = Object.entries(screenShareData || {}).some(
|
|
92
|
+
([uid, share]) =>
|
|
93
|
+
share?.isActive &&
|
|
94
|
+
!candidate.activeScreenshareUidsAtDetection.includes(Number(uid)),
|
|
95
|
+
);
|
|
96
|
+
if (newerActiveShareExists) {
|
|
97
|
+
return 'cancel_replaced';
|
|
98
|
+
}
|
|
99
|
+
// The same UID can be reused for a later share. Its start timestamp prevents
|
|
100
|
+
// that new session from being mistaken for recovery of the old one.
|
|
101
|
+
if (recoveredShare.ts !== candidate.screenshareStartedAt) {
|
|
102
|
+
return 'cancel_replaced';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Respect a pin selected by the user while recovery was pending.
|
|
106
|
+
if (pinnedUid && pinnedUid !== candidate.uid) {
|
|
107
|
+
return 'cancel_user_override';
|
|
108
|
+
}
|
|
109
|
+
// Joining and publishing are separate RTC events. Wait for both so the
|
|
110
|
+
// layout never restores a tile before its video is available.
|
|
111
|
+
if (!activeUids.includes(candidate.uid)) {
|
|
112
|
+
return 'waiting_for_join';
|
|
113
|
+
}
|
|
114
|
+
if (!isVideoPublished) {
|
|
115
|
+
return 'waiting_for_video';
|
|
116
|
+
}
|
|
117
|
+
return 'restore';
|
|
118
|
+
};
|