agora-appbuilder-core 4.1.19-beta.4 → 4.1.20

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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/template/bridge/rtc/webNg/RtcEngine.ts +320 -83
  3. package/template/customization-api/atoms.ts +0 -2
  4. package/template/customization-api/customEvents.ts +1 -4
  5. package/template/customization-api/index.ts +0 -1
  6. package/template/customization-api/typeDefinition.ts +1 -6
  7. package/template/defaultConfig.js +2 -2
  8. package/template/src/components/Controls.tsx +3 -1
  9. package/template/src/components/CustomSidePanel.tsx +13 -13
  10. package/template/src/components/RTMConfigure.tsx +0 -13
  11. package/template/src/components/livestream/LiveStreamContext.tsx +6 -2
  12. package/template/src/components/participants/UserActionMenuOptions.tsx +2 -29
  13. package/template/src/pages/Create.tsx +19 -31
  14. package/template/src/pages/video-call/ActionSheet.native.tsx +0 -2
  15. package/template/src/pages/video-call/ActionSheet.tsx +0 -2
  16. package/template/src/pages/video-call/ActionSheetHandle.tsx +0 -1
  17. package/template/src/pages/video-call/SidePanelHeader.tsx +1 -3
  18. package/template/src/pages/video-call/VideoCallScreen.tsx +0 -3
  19. package/template/src/pages/video-call/VideoRenderer.tsx +0 -4
  20. package/template/src/subComponents/LocalEndCall.tsx +1 -1
  21. package/template/src/subComponents/ScreenShareNotice.tsx +1 -1
  22. package/template/src/subComponents/SidePanelHeader.tsx +2 -16
  23. package/template/src/subComponents/recording/__tests__/recordingJourney.test.ts +25 -0
  24. package/template/src/subComponents/recording/__tests__/recordingLayoutResponse.test.ts +56 -0
  25. package/template/src/subComponents/recording/recordingJourney.ts +12 -0
  26. package/template/src/subComponents/recording/recordingLayoutResponse.ts +67 -0
  27. package/template/src/subComponents/recording/useRecording.tsx +28 -12
  28. package/template/src/subComponents/recording/useRecordingLayoutQuery.tsx +35 -45
  29. package/template/src/subComponents/screenshare/ScreenshareButton.tsx +1 -1
  30. package/template/src/subComponents/screenshare/ScreenshareConfigure.tsx +483 -31
  31. package/template/src/subComponents/screenshare/__tests__/screenshareJourney.test.ts +63 -0
  32. package/template/src/subComponents/screenshare/screenshareJourney.ts +43 -0
  33. package/template/src/subComponents/screenshare/useScreenshare.tsx +18 -2
  34. package/template/customization-api/ai-agent.ts +0 -4
@@ -958,19 +958,6 @@ const RtmConfigure = (props: any) => {
958
958
  );
959
959
  return;
960
960
  }
961
- if (
962
- !parsedValue ||
963
- typeof parsedValue !== 'object' ||
964
- Array.isArray(parsedValue)
965
- ) {
966
- logger.debug(
967
- LogSource.Events,
968
- 'CUSTOM_EVENTS',
969
- 'Ignoring non custom-event RTM payload',
970
- {evt, sender, ts},
971
- );
972
- return;
973
- }
974
961
  const {payload, persistLevel, source} = parsedValue;
975
962
  // Step 1: Set local attributes
976
963
  if (persistLevel === PersistanceLevel.Session) {
@@ -543,7 +543,9 @@ export const LiveStreamContextProvider: React.FC<
543
543
  raiseHandApprovedRequestRecallToastHeading?.current(),
544
544
  null,
545
545
  );
546
- screenshareContextInstanceRef?.current?.stopScreenshare(); // This will not exist on ios
546
+ screenshareContextInstanceRef?.current?.stopScreenshare(
547
+ 'livestream_demotion',
548
+ ); // This will not exist on ios
547
549
 
548
550
  // Demote user's privileges to audience
549
551
  changeClientRoleTo(ClientRoleType.ClientRoleAudience);
@@ -716,7 +718,9 @@ export const LiveStreamContextProvider: React.FC<
716
718
  ClientRoleType.ClientRoleBroadcaster &&
717
719
  raiseHandList[localUidRef.current]?.raised === RaiseHandValue.TRUE
718
720
  ) {
719
- screenshareContextInstanceRef?.current?.stopScreenshare(); // This will not exist on ios
721
+ screenshareContextInstanceRef?.current?.stopScreenshare(
722
+ 'livestream_recall',
723
+ ); // This will not exist on ios
720
724
  // Change role
721
725
  changeClientRoleTo(ClientRoleType.ClientRoleAudience);
722
726
  }
@@ -87,7 +87,6 @@ interface UserActionMenuOptionsOptionsProps {
87
87
  spotlightUid?: UidType;
88
88
  setSpotlightUid?: (uid: UidType) => void;
89
89
  items?: UserActionMenuItemsConfig;
90
- extraMenuItems?: ActionMenuItem[];
91
90
  }
92
91
 
93
92
  export default function UserActionMenuOptionsOptions(
@@ -103,13 +102,7 @@ export default function UserActionMenuOptionsOptions(
103
102
  useState(false);
104
103
  const [actionMenuitems, setActionMenuitems] = useState<ActionMenuItem[]>([]);
105
104
  const {setSidePanel} = useSidePanel();
106
- const {
107
- user,
108
- actionMenuVisible,
109
- setActionMenuVisible,
110
- spotlightUid,
111
- extraMenuItems,
112
- } = props;
105
+ const {user, actionMenuVisible, setActionMenuVisible, spotlightUid} = props;
113
106
  const {currentLayout} = useLayout();
114
107
  const {pinnedUid, activeUids, customContent, secondaryPinnedUid} =
115
108
  useContent();
@@ -681,7 +674,7 @@ export default function UserActionMenuOptionsOptions(
681
674
  removeScreenshareConfig.onAction?.(user.parentUid);
682
675
  //for local user directly stop the screenshare
683
676
  if (localuid === user.parentUid) {
684
- stopScreenshare();
677
+ stopScreenshare('participant_menu');
685
678
  }
686
679
  //for remote user show popup and then user will use cta to stop screenshare
687
680
  else {
@@ -723,24 +716,6 @@ export default function UserActionMenuOptionsOptions(
723
716
  items.push(...customItems);
724
717
  }
725
718
 
726
- if (extraMenuItems?.length) {
727
- items.push(
728
- ...extraMenuItems.map(item => ({
729
- ...item,
730
- closeActionMenu: () => {
731
- item.closeActionMenu?.();
732
- setActionMenuVisible(false);
733
- },
734
- onPress: item.onPress
735
- ? () => {
736
- item.onPress?.();
737
- setActionMenuVisible(false);
738
- }
739
- : item.onPress,
740
- })),
741
- );
742
- }
743
-
744
719
  items.sort((a, b) => (a.order ?? 999) - (b.order ?? 999));
745
720
 
746
721
  setActionMenuitems(items);
@@ -754,8 +729,6 @@ export default function UserActionMenuOptionsOptions(
754
729
  secondaryPinnedUid,
755
730
  currentLayout,
756
731
  spotlightUid,
757
- extraMenuItems,
758
- setActionMenuVisible,
759
732
  ]);
760
733
 
761
734
  const {width: globalWidth, height: globalHeight} = useWindowDimensions();
@@ -29,6 +29,7 @@ import {useCustomization} from 'customization-implementation';
29
29
  import {useString} from '../utils/useString';
30
30
  import useCreateRoom from '../utils/useCreateRoom';
31
31
  import {CreateProvider} from './create/useCreate';
32
+ import useJoinRoom from '../utils/useJoinRoom';
32
33
  import {
33
34
  RoomInfoDefaultValue,
34
35
  useRoomInfo,
@@ -38,10 +39,12 @@ import Toggle from '../atoms/Toggle';
38
39
  import Card from '../atoms/Card';
39
40
  import Spacer from '../atoms/Spacer';
40
41
  import LinkButton from '../atoms/LinkButton';
42
+ import StorageContext from '../components/StorageContext';
41
43
  import ThemeConfig from '../theme';
42
44
  import Tooltip from '../atoms/Tooltip';
43
45
  import ImageIcon from '../atoms/ImageIcon';
44
46
  import hexadecimalTransparency from '../utils/hexadecimalTransparency';
47
+ import {randomNameGenerator} from '../utils';
45
48
  import {useSetRoomInfo} from '../components/room-info/useSetRoomInfo';
46
49
  import IDPLogoutComponent from '../auth/IDPLogoutComponent';
47
50
  import isSDK from '../utils/isSDK';
@@ -64,35 +67,25 @@ import {LogSource, logger} from '../logger/AppBuilderLogger';
64
67
  import SDKEvents from '../utils/SdkEvents';
65
68
 
66
69
  const Create = () => {
67
- const {CreateComponent, CreateHeaderRightSlotComponent} = useCustomization(
68
- data => {
69
- let components: {
70
- CreateComponent?: React.ElementType;
71
- CreateHeaderRightSlotComponent?: React.ElementType;
72
- } = {};
70
+ const {CreateComponent} = useCustomization(data => {
71
+ let components: {
72
+ CreateComponent?: React.ElementType;
73
+ } = {};
74
+ if (
75
+ data?.components?.create &&
76
+ typeof data?.components?.create !== 'object'
77
+ ) {
73
78
  if (
74
79
  data?.components?.create &&
75
- typeof data?.components?.create !== 'object'
76
- ) {
77
- if (
78
- data?.components?.create &&
79
- isValidReactComponent(data?.components?.create)
80
- )
81
- components.CreateComponent = data?.components?.create;
82
- }
83
- if (
84
- data?.components?.create &&
85
- typeof data?.components?.create === 'object' &&
86
- data?.components?.create?.headerRightSlot &&
87
- isValidReactComponent(data?.components?.create?.headerRightSlot)
88
- ) {
89
- components.CreateHeaderRightSlotComponent =
90
- data?.components?.create?.headerRightSlot;
91
- }
92
- return components;
93
- },
94
- );
80
+ isValidReactComponent(data?.components?.create)
81
+ )
82
+ components.CreateComponent = data?.components?.create;
83
+ }
84
+ return components;
85
+ });
95
86
 
87
+ const useJoin = useJoinRoom();
88
+ const {setStore} = useContext(StorageContext);
96
89
  const {setGlobalErrorMessage} = useContext(ErrorContext);
97
90
  const history = useHistory();
98
91
  const [loading, setLoading] = useState(false);
@@ -320,11 +313,6 @@ const Create = () => {
320
313
  <View>
321
314
  <View style={style.logoContainerStyle}>
322
315
  <Logo />
323
- {CreateHeaderRightSlotComponent ? (
324
- <CreateHeaderRightSlotComponent />
325
- ) : (
326
- <></>
327
- )}
328
316
  {isMobileUA() ? (
329
317
  <IDPLogoutComponent
330
318
  containerStyle={{marginTop: 0, marginRight: 0}}
@@ -291,8 +291,6 @@ const ActionSheet = props => {
291
291
  isCustomSidePanel={true}
292
292
  customSidePanelProps={{
293
293
  title: sidePanelArray[customSidePanelIndex]?.title,
294
- headerRightSlot:
295
- sidePanelArray[customSidePanelIndex]?.headerRightSlot,
296
294
  onClose: sidePanelArray[customSidePanelIndex]?.onClose,
297
295
  name: sidePanelArray[customSidePanelIndex]?.name,
298
296
  }}
@@ -326,8 +326,6 @@ const ActionSheet = props => {
326
326
  isCustomSidePanel={true}
327
327
  customSidePanelProps={{
328
328
  title: sidePanelArray[customSidePanelIndex]?.title,
329
- headerRightSlot:
330
- sidePanelArray[customSidePanelIndex]?.headerRightSlot,
331
329
  onClose: sidePanelArray[customSidePanelIndex]?.onClose,
332
330
  name: sidePanelArray[customSidePanelIndex]?.name,
333
331
  }}
@@ -16,7 +16,6 @@ const ActionSheetHandle = (props: {
16
16
  isCustomSidePanel?: boolean;
17
17
  customSidePanelProps?: {
18
18
  title: string;
19
- headerRightSlot?: React.ReactNode;
20
19
  onClose?: () => void;
21
20
  name: string;
22
21
  };
@@ -208,7 +208,6 @@ export const VBHeader = () => {
208
208
 
209
209
  export const CustomSidePanelHeader = (props: {
210
210
  title: string;
211
- headerRightSlot?: React.ReactNode;
212
211
  onClose?: () => void;
213
212
  name: string;
214
213
  }) => {
@@ -218,7 +217,6 @@ export const CustomSidePanelHeader = (props: {
218
217
  centerComponent={
219
218
  <Text style={SidePanelStyles.heading}>{props?.title}</Text>
220
219
  }
221
- trailingComponent={props?.headerRightSlot}
222
220
  trailingIconName="close"
223
221
  trailingIconOnPress={() => {
224
222
  setSidePanel(SidePanelType.None);
@@ -226,7 +224,7 @@ export const CustomSidePanelHeader = (props: {
226
224
  props?.onClose && props?.onClose();
227
225
  } catch (error) {
228
226
  console.error(
229
- `Error on calling onClose in custom side panel ${props.name}`,
227
+ `Error on calling onClose in custom side panel ${name}`,
230
228
  );
231
229
  }
232
230
  }}
@@ -400,9 +400,6 @@ const VideoCallScreen = () => {
400
400
  <CustomSidePanelView
401
401
  content={SidePanelArray[customSidePanelIndex]?.component}
402
402
  title={SidePanelArray[customSidePanelIndex]?.title}
403
- headerRightSlot={
404
- SidePanelArray[customSidePanelIndex]?.headerRightSlot
405
- }
406
403
  name={SidePanelArray[customSidePanelIndex]?.name}
407
404
  onClose={SidePanelArray[customSidePanelIndex]?.onClose}
408
405
  />
@@ -25,7 +25,6 @@ import {
25
25
  } from './DefaultLayouts';
26
26
  import IconButton from '../../atoms/IconButton';
27
27
  import UserActionMenuOptionsOptions from '../../components/participants/UserActionMenuOptions';
28
- import type {ActionMenuItem} from '../../atoms/ActionMenu';
29
28
  import {
30
29
  isMobileUA,
31
30
  isValidReactComponent,
@@ -61,7 +60,6 @@ export interface VideoRendererProps {
61
60
  CustomChild?: React.ComponentType;
62
61
  avatarRadius?: number;
63
62
  hideMenuOptions?: boolean;
64
- extraMenuItems?: ActionMenuItem[];
65
63
  containerStyle?: ViewStyle;
66
64
  innerContainerStyle?: ViewStyle;
67
65
  }
@@ -71,7 +69,6 @@ const VideoRenderer: React.FC<VideoRendererProps> = ({
71
69
  CustomChild,
72
70
  avatarRadius = 100,
73
71
  hideMenuOptions = false,
74
- extraMenuItems,
75
72
  containerStyle = {},
76
73
  innerContainerStyle = {},
77
74
  }) => {
@@ -217,7 +214,6 @@ const VideoRenderer: React.FC<VideoRendererProps> = ({
217
214
  from={'video-tile'}
218
215
  setSpotlightUid={setSpotlightUid}
219
216
  spotlightUid={spotlightUid}
220
- extraMenuItems={extraMenuItems}
221
217
  />
222
218
  <PlatformWrapper isHovered={isHovered} setIsHovered={setIsHovered}>
223
219
  <View
@@ -57,7 +57,7 @@ const LocalEndcall = (props: LocalEndcallProps) => {
57
57
  props.customExit();
58
58
  } else {
59
59
  if ((isAndroid() || isIOS()) && isScreenshareActive) {
60
- stopScreenshare();
60
+ stopScreenshare('end_call_cleanup');
61
61
  setEndCallState(true);
62
62
  } else {
63
63
  executeEndCall();
@@ -59,7 +59,7 @@ function ScreenShareNotice({uid, isMax}: {uid: UidType; isMax: boolean}) {
59
59
  ) : (
60
60
  <TouchableOpacity
61
61
  style={styles.btnContainer}
62
- onPress={() => stopScreenshare()}>
62
+ onPress={() => stopScreenshare('overlay_control')}>
63
63
  <View style={styles.iconContainer}>
64
64
  <ImageIcon
65
65
  iconType="plain"
@@ -13,7 +13,6 @@ export interface SidePanelHeaderProps {
13
13
  leadingIconOnPress?: () => void;
14
14
  trailingIconName?: keyof IconsInterface;
15
15
  trailingIconOnPress?: () => void;
16
- trailingComponent?: React.ReactNode;
17
16
  trailingIconName2?: keyof IconsInterface;
18
17
  trailingIconOnPress2?: () => void;
19
18
  isChat?: boolean;
@@ -57,21 +56,9 @@ const SidePanelHeader = React.forwardRef<View, SidePanelHeaderProps>(
57
56
  <View style={{width: 30, height: 'auto'}}></View>
58
57
  ) : null}
59
58
  {props?.centerComponent ? props.centerComponent : null}
60
- <View
61
- style={
62
- (props?.trailingIconName2 || props?.trailingComponent) &&
63
- SidePanelStyles.row
64
- }>
65
- {props?.trailingComponent ? props.trailingComponent : null}
59
+ <View style={props?.trailingIconName2 && SidePanelStyles.row}>
66
60
  {props?.trailingIconName ? (
67
- <View
68
- ref={ref}
69
- collapsable={false}
70
- style={
71
- props?.trailingIconName2 || props?.trailingComponent
72
- ? {}
73
- : {flex: 1}
74
- }>
61
+ <View ref={ref} collapsable={false} style={{flex: 1}}>
75
62
  <IconButton
76
63
  hoverEffect={true}
77
64
  hoverEffectStyle={{
@@ -129,7 +116,6 @@ export const SidePanelStyles = StyleSheet.create({
129
116
  borderBottomWidth: 1,
130
117
  borderBottomColor: $config.CARD_LAYER_3_COLOR,
131
118
  position: 'relative',
132
- zIndex: 1000,
133
119
  },
134
120
  chatPadding: {
135
121
  paddingHorizontal: 16,
@@ -0,0 +1,25 @@
1
+ import {runPostStartRecordingLayoutUpdate} from '../recordingJourney';
2
+
3
+ describe('recording journey', () => {
4
+ it('does not reject recording start when the post-start layout update fails', async () => {
5
+ const layoutError = new Error('layout update failed');
6
+ const onFailure = jest.fn();
7
+
8
+ await expect(
9
+ runPostStartRecordingLayoutUpdate(
10
+ () => Promise.reject(layoutError),
11
+ onFailure,
12
+ ),
13
+ ).resolves.toBe(false);
14
+ expect(onFailure).toHaveBeenCalledWith(layoutError);
15
+ });
16
+
17
+ it('reports a successful post-start layout update', async () => {
18
+ const onFailure = jest.fn();
19
+
20
+ await expect(
21
+ runPostStartRecordingLayoutUpdate(() => Promise.resolve(), onFailure),
22
+ ).resolves.toBe(true);
23
+ expect(onFailure).not.toHaveBeenCalled();
24
+ });
25
+ });
@@ -0,0 +1,56 @@
1
+ import {parseRecordingLayoutResponse} from '../recordingLayoutResponse';
2
+
3
+ const response = (
4
+ body: string,
5
+ options: {ok?: boolean; status?: number; statusText?: string} = {},
6
+ ) =>
7
+ ({
8
+ ok: options.ok ?? true,
9
+ status: options.status ?? 200,
10
+ statusText: options.statusText ?? 'OK',
11
+ text: jest.fn().mockResolvedValue(body),
12
+ } as Pick<Response, 'ok' | 'status' | 'statusText' | 'text'>);
13
+
14
+ describe('parseRecordingLayoutResponse', () => {
15
+ it('accepts an empty successful response', async () => {
16
+ await expect(
17
+ parseRecordingLayoutResponse(
18
+ response('', {status: 204, statusText: 'No Content'}),
19
+ ),
20
+ ).resolves.toBeUndefined();
21
+ });
22
+
23
+ it('returns a successful JSON response', async () => {
24
+ await expect(
25
+ parseRecordingLayoutResponse(response('{"success":true}')),
26
+ ).resolves.toEqual({success: true});
27
+ });
28
+
29
+ it('rejects an empty HTTP error with status details', async () => {
30
+ await expect(
31
+ parseRecordingLayoutResponse(
32
+ response('', {
33
+ ok: false,
34
+ status: 500,
35
+ statusText: 'Internal Server Error',
36
+ }),
37
+ ),
38
+ ).rejects.toMatchObject({
39
+ name: 'Error',
40
+ code: 'HTTP_500',
41
+ httpStatus: 500,
42
+ message:
43
+ 'Recording layout API failed with HTTP 500 Internal Server Error',
44
+ });
45
+ });
46
+
47
+ it('rejects invalid non-empty JSON with the response details', async () => {
48
+ await expect(
49
+ parseRecordingLayoutResponse(response('<html>')),
50
+ ).rejects.toMatchObject({
51
+ name: 'Error',
52
+ code: 'HTTP_200',
53
+ responseBody: '<html>',
54
+ });
55
+ });
56
+ });
@@ -0,0 +1,12 @@
1
+ export const runPostStartRecordingLayoutUpdate = async (
2
+ updateLayout: () => Promise<void>,
3
+ onFailure: (error: unknown) => void,
4
+ ) => {
5
+ try {
6
+ await updateLayout();
7
+ return true;
8
+ } catch (error) {
9
+ onFailure(error);
10
+ return false;
11
+ }
12
+ };
@@ -0,0 +1,67 @@
1
+ type RecordingLayoutResponse = Pick<
2
+ Response,
3
+ 'ok' | 'status' | 'statusText' | 'text'
4
+ >;
5
+
6
+ const createResponseError = (
7
+ message: string,
8
+ response: RecordingLayoutResponse,
9
+ responseBody: string,
10
+ ) =>
11
+ Object.assign(new Error(message), {
12
+ code: `HTTP_${response.status}`,
13
+ httpStatus: response.status,
14
+ httpStatusText: response.statusText,
15
+ responseBody,
16
+ });
17
+
18
+ export const parseRecordingLayoutResponse = async (
19
+ response: RecordingLayoutResponse,
20
+ ): Promise<unknown> => {
21
+ const responseBody = await response.text();
22
+ let responseData: any;
23
+
24
+ if (responseBody.trim()) {
25
+ try {
26
+ responseData = JSON.parse(responseBody);
27
+ } catch (error) {
28
+ throw createResponseError(
29
+ `Recording layout API returned invalid JSON: ${
30
+ error instanceof Error ? error.message : String(error)
31
+ }`,
32
+ response,
33
+ responseBody,
34
+ );
35
+ }
36
+ }
37
+
38
+ if (!response.ok) {
39
+ const backendMessage =
40
+ responseData?.error?.message ||
41
+ responseData?.error ||
42
+ responseData?.message ||
43
+ responseBody;
44
+ throw createResponseError(
45
+ `Recording layout API failed with HTTP ${response.status}${
46
+ response.statusText ? ` ${response.statusText}` : ''
47
+ }${backendMessage ? `: ${String(backendMessage)}` : ''}`,
48
+ response,
49
+ responseBody,
50
+ );
51
+ }
52
+
53
+ if (responseData?.error) {
54
+ const backendMessage =
55
+ responseData.error?.message ||
56
+ (typeof responseData.error === 'string'
57
+ ? responseData.error
58
+ : JSON.stringify(responseData.error));
59
+ throw createResponseError(
60
+ `Recording layout API returned an error: ${backendMessage}`,
61
+ response,
62
+ responseBody,
63
+ );
64
+ }
65
+
66
+ return responseData;
67
+ };
@@ -50,6 +50,7 @@ import {useScreenContext} from '../../components/contexts/ScreenShareContext';
50
50
  import {useLiveStreamDataContext} from '../../components/contexts/LiveStreamDataContext';
51
51
  import {fetchRetry} from '../../utils/fetch-retry';
52
52
  import {LogSource, logger} from '../../logger/AppBuilderLogger';
53
+ import {runPostStartRecordingLayoutUpdate} from './recordingJourney';
53
54
  import getUniqueID from '../../utils/getUniqueID';
54
55
  import {useControlPermissionMatrix} from '../../components/controls/useControlPermissionMatrix';
55
56
 
@@ -316,7 +317,7 @@ const RecordingProvider = (props: RecordingProviderProps) => {
316
317
  mode: recordingMode.toLowerCase(),
317
318
  }),
318
319
  })
319
- .then((res: any) => {
320
+ .then(async (res: any) => {
320
321
  const endRequestTs = Date.now();
321
322
  const latency = endRequestTs - startReqTs;
322
323
  if (res.status === 200) {
@@ -361,17 +362,32 @@ const RecordingProvider = (props: RecordingProviderProps) => {
361
362
  .sort((a, b) => b[1].ts - a[1].ts);
362
363
 
363
364
  const activeScreenshareUid = sorted.length > 0 ? sorted[0][0] : 0;
364
- if (activeScreenshareUid) {
365
- logger.debug(
366
- LogSource.Internals,
367
- 'RECORDING',
368
- 'screenshare: Executing presenter query for screenuid',
369
- activeScreenshareUid,
370
- );
371
- executePresenterQuery(parseInt(activeScreenshareUid));
372
- } else {
373
- executeNormalQuery();
374
- }
365
+ await runPostStartRecordingLayoutUpdate(
366
+ () => {
367
+ if (activeScreenshareUid) {
368
+ logger.debug(
369
+ LogSource.Internals,
370
+ 'RECORDING',
371
+ 'screenshare: Executing presenter query for screenuid',
372
+ activeScreenshareUid,
373
+ );
374
+ return executePresenterQuery(parseInt(activeScreenshareUid));
375
+ }
376
+ return executeNormalQuery();
377
+ },
378
+ layoutError => {
379
+ logger.error(
380
+ LogSource.NetworkRest,
381
+ 'recording_start',
382
+ 'Recording started successfully, but the initial recording layout update failed; recording remains active',
383
+ layoutError,
384
+ {
385
+ requestId,
386
+ activeScreenshareUid,
387
+ },
388
+ );
389
+ },
390
+ );
375
391
  }
376
392
  } else if (res.status === 500) {
377
393
  showErrorToast(headingStartError, subheadingError);