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
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import React, {useContext, useEffect, useRef, useState} from 'react';
13
13
  import {DispatchContext, PropsContext, UidType} from '../../../agora-rn-uikit';
14
- import {ScreenshareContext} from './useScreenshare';
14
+ import {ScreenshareContext, ScreenshareStopOrigin} from './useScreenshare';
15
15
  import {
16
16
  getGridLayoutName,
17
17
  getPinnedLayoutName,
@@ -38,6 +38,15 @@ import {
38
38
  videoRoomScreenShareErrorToastSubHeading,
39
39
  } from '../../language/default-labels/videoCallScreenLabels';
40
40
  import {LogSource, logger} from '../../logger/AppBuilderLogger';
41
+ import getUniqueID from '../../utils/getUniqueID';
42
+ import {
43
+ getScreenshareSessionId,
44
+ getScreenshareError,
45
+ isUserCancelOrPermissionDenied,
46
+ SCREENSHARE_JOURNEY,
47
+ } from './screenshareJourney';
48
+
49
+ type ScreenshareAction = 'start' | 'stop';
41
50
 
42
51
  export const ScreenshareContextConsumer = ScreenshareContext.Consumer;
43
52
 
@@ -48,6 +57,13 @@ export const ScreenshareConfigure = (props: {
48
57
  const toastHeading = useString(videoRoomScreenShareErrorToastHeading)();
49
58
  const toastSubHeading = useString(videoRoomScreenShareErrorToastSubHeading)();
50
59
  const [isScreenshareActive, setScreenshareActive] = useState(false);
60
+ const activeScreenshareSessionIdRef = useRef<string | null>(null);
61
+ const stopScreenshareRef = useRef<
62
+ (
63
+ origin?: ScreenshareStopOrigin,
64
+ stopActorUid?: UidType,
65
+ ) => Promise<void> | void
66
+ >(() => {});
51
67
  const {dispatch} = useContext(DispatchContext);
52
68
  const rtc = useRtc();
53
69
  const {defaultContent, activeUids, pinnedUid, secondaryPinnedUid} =
@@ -157,10 +173,9 @@ export const ScreenshareConfigure = (props: {
157
173
  useEffect(() => {
158
174
  const unsubKickScreenshare = events.on(
159
175
  controlMessageEnum.kickScreenshare,
160
- () => {
176
+ data => {
161
177
  //if screenscreen already active. then below method will stop the screen share
162
- // @ts-ignore
163
- rtc.RtcEngineUnsafe.startScreenshare();
178
+ stopScreenshareRef.current('remote_host_removal', data?.sender);
164
179
  },
165
180
  );
166
181
  const unsubScreenshareAttribute = events.on(
@@ -232,9 +247,47 @@ export const ScreenshareConfigure = (props: {
232
247
  };
233
248
  }, []);
234
249
 
235
- const ScreenshareStoppedCallback = () => {
250
+ const ScreenshareStoppedCallback = (
251
+ stopOrigin: ScreenshareStopOrigin = 'unknown',
252
+ screenshareAttemptId = getUniqueID(),
253
+ screenshareSessionId = activeScreenshareSessionIdRef.current ||
254
+ 'unknown-session',
255
+ stopActorUid?: UidType,
256
+ ) => {
257
+ const callbackStartedAt = Date.now();
258
+ logger.log(
259
+ LogSource.Internals,
260
+ 'SCREENSHARE',
261
+ `${SCREENSHARE_JOURNEY} screen share stop callback received from RTC engine`,
262
+ {
263
+ action: 'stop',
264
+ stage: 'stop_callback',
265
+ outcome: 'started',
266
+ screenshareAttemptId,
267
+ screenshareSessionId,
268
+ recordingActive: props.isRecordingActive,
269
+ screenShareUid,
270
+ stopOrigin,
271
+ stopActorUid,
272
+ },
273
+ );
236
274
  setScreenshareActive(false);
237
- logger.debug(LogSource.Internals, 'SCREENSHARE', 'screenshare stopped.');
275
+ logger.log(
276
+ LogSource.Internals,
277
+ 'SCREENSHARE',
278
+ `${SCREENSHARE_JOURNEY} screen share stop local active state set to inactive`,
279
+ {
280
+ action: 'stop',
281
+ stage: 'local_active_state',
282
+ outcome: 'success',
283
+ screenshareAttemptId,
284
+ screenshareSessionId,
285
+ recordingActive: props.isRecordingActive,
286
+ screenShareUid,
287
+ stopOrigin,
288
+ stopActorUid,
289
+ },
290
+ );
238
291
  events.send(
239
292
  EventNames.SCREENSHARE_ATTRIBUTE,
240
293
  JSON.stringify({
@@ -243,6 +296,22 @@ export const ScreenshareConfigure = (props: {
243
296
  }),
244
297
  PersistanceLevel.Sender,
245
298
  );
299
+ logger.log(
300
+ LogSource.Internals,
301
+ 'SCREENSHARE',
302
+ `${SCREENSHARE_JOURNEY} screen share stop RTM event sent to remote users`,
303
+ {
304
+ action: 'stop',
305
+ stage: 'rtm_event',
306
+ outcome: 'success',
307
+ screenshareAttemptId,
308
+ screenshareSessionId,
309
+ recordingActive: props.isRecordingActive,
310
+ screenShareUid,
311
+ stopOrigin,
312
+ stopActorUid,
313
+ },
314
+ );
246
315
  setScreenShareData(prevState => {
247
316
  return {
248
317
  ...prevState,
@@ -253,6 +322,22 @@ export const ScreenshareConfigure = (props: {
253
322
  },
254
323
  };
255
324
  });
325
+ logger.log(
326
+ LogSource.Internals,
327
+ 'SCREENSHARE',
328
+ `${SCREENSHARE_JOURNEY} screen share stop local context set to inactive`,
329
+ {
330
+ action: 'stop',
331
+ stage: 'screenshare_context',
332
+ outcome: 'success',
333
+ screenshareAttemptId,
334
+ screenshareSessionId,
335
+ recordingActive: props.isRecordingActive,
336
+ screenShareUid,
337
+ stopOrigin,
338
+ stopActorUid,
339
+ },
340
+ );
256
341
  //if local user stopped the screenshare then change layout to grid
257
342
  //if user pinned somebody then don't triggerlayout change
258
343
  if (!pinnedUidRef.current.pinnedUid) {
@@ -265,6 +350,40 @@ export const ScreenshareConfigure = (props: {
265
350
  value: [0],
266
351
  });
267
352
  }
353
+ logger.log(
354
+ LogSource.Internals,
355
+ 'SCREENSHARE',
356
+ `${SCREENSHARE_JOURNEY} screen share stop layout update completed when applicable`,
357
+ {
358
+ action: 'stop',
359
+ stage: 'layout_update',
360
+ outcome: 'success',
361
+ screenshareAttemptId,
362
+ screenshareSessionId,
363
+ recordingActive: props.isRecordingActive,
364
+ screenShareUid,
365
+ stopOrigin,
366
+ stopActorUid,
367
+ },
368
+ );
369
+ logger.log(
370
+ LogSource.Internals,
371
+ 'SCREENSHARE',
372
+ `${SCREENSHARE_JOURNEY} screen share stop callback handling completed`,
373
+ {
374
+ action: 'stop',
375
+ stage: 'complete',
376
+ outcome: 'success',
377
+ screenshareAttemptId,
378
+ screenshareSessionId,
379
+ recordingActive: props.isRecordingActive,
380
+ screenShareUid,
381
+ stopOrigin,
382
+ stopActorUid,
383
+ elapsedMs: Date.now() - callbackStartedAt,
384
+ },
385
+ );
386
+ activeScreenshareSessionIdRef.current = null;
268
387
  };
269
388
 
270
389
  useEffect(() => {
@@ -275,54 +394,267 @@ export const ScreenshareConfigure = (props: {
275
394
  );
276
395
  }, []);
277
396
 
278
- const executeRecordingQuery = (isScreenActive: boolean) => {
397
+ const executeRecordingQuery = async (
398
+ isScreenActive: boolean,
399
+ screenshareAttemptId: string,
400
+ screenshareSessionId: string,
401
+ stopOrigin: ScreenshareStopOrigin,
402
+ stopActorUid?: UidType,
403
+ ) => {
404
+ const action: ScreenshareAction = isScreenActive ? 'start' : 'stop';
279
405
  if (isScreenActive) {
280
406
  // If recording is going on, set the presenter query
281
407
  logger.log(
282
408
  LogSource.Internals,
283
409
  'SCREENSHARE',
284
- 'Recording is going on set presenter query',
410
+ `${SCREENSHARE_JOURNEY} screen share start recording presenter layout query started`,
411
+ {
412
+ action,
413
+ stage: 'recording_layout',
414
+ outcome: 'started',
415
+ screenshareAttemptId,
416
+ screenshareSessionId,
417
+ recordingActive: props.isRecordingActive,
418
+ screenShareUid,
419
+ stopOrigin,
420
+ stopActorUid,
421
+ },
285
422
  );
286
- executePresenterQuery(screenShareUid);
423
+ await executePresenterQuery(screenShareUid);
287
424
  } else {
288
425
  logger.log(
289
426
  LogSource.Internals,
290
427
  'SCREENSHARE',
291
- 'Recording is NOT going on set normal query',
428
+ `${SCREENSHARE_JOURNEY} screen share stop recording normal layout query started`,
429
+ {
430
+ action,
431
+ stage: 'recording_layout',
432
+ outcome: 'started',
433
+ screenshareAttemptId,
434
+ screenshareSessionId,
435
+ recordingActive: props.isRecordingActive,
436
+ screenShareUid,
437
+ stopOrigin,
438
+ stopActorUid,
439
+ },
292
440
  );
293
441
  // If no recording is going on, set the normal query
294
- executeNormalQuery();
442
+ await executeNormalQuery();
295
443
  }
444
+ logger.log(
445
+ LogSource.Internals,
446
+ 'SCREENSHARE',
447
+ `${SCREENSHARE_JOURNEY} screen share ${action} recording layout query completed successfully`,
448
+ {
449
+ action,
450
+ stage: 'recording_layout',
451
+ outcome: 'success',
452
+ screenshareAttemptId,
453
+ screenshareSessionId,
454
+ recordingActive: props.isRecordingActive,
455
+ screenShareUid,
456
+ stopOrigin,
457
+ stopActorUid,
458
+ },
459
+ );
296
460
  };
297
461
 
298
- const stopScreenshare = () => {
462
+ const stopScreenshare = async (
463
+ stopOrigin: ScreenshareStopOrigin = 'unknown',
464
+ stopActorUid?: UidType,
465
+ ) => {
466
+ const screenshareAttemptId = getUniqueID();
467
+ const screenshareSessionId = getScreenshareSessionId(
468
+ 'stop',
469
+ activeScreenshareSessionIdRef.current,
470
+ getUniqueID,
471
+ );
472
+ logger.log(
473
+ LogSource.Internals,
474
+ 'SCREENSHARE',
475
+ `${SCREENSHARE_JOURNEY} screen share stop requested from ${stopOrigin}`,
476
+ {
477
+ action: 'stop',
478
+ stage: 'ui_request',
479
+ outcome: 'started',
480
+ screenshareAttemptId,
481
+ screenshareSessionId,
482
+ recordingActive: props.isRecordingActive,
483
+ screenShareUid,
484
+ stopOrigin,
485
+ stopActorUid,
486
+ },
487
+ );
299
488
  if (!isScreenshareActive) {
489
+ logger.log(
490
+ LogSource.Internals,
491
+ 'SCREENSHARE',
492
+ `${SCREENSHARE_JOURNEY} screen share stop skipped because screen share is already inactive`,
493
+ {
494
+ action: 'stop',
495
+ stage: 'precondition',
496
+ outcome: 'skipped',
497
+ screenshareAttemptId,
498
+ screenshareSessionId,
499
+ recordingActive: props.isRecordingActive,
500
+ screenShareUid,
501
+ stopOrigin,
502
+ stopActorUid,
503
+ },
504
+ );
300
505
  return;
301
506
  }
302
- userScreenshare(false);
507
+ await userScreenshare(
508
+ false,
509
+ screenshareAttemptId,
510
+ screenshareSessionId,
511
+ stopOrigin,
512
+ stopActorUid,
513
+ );
303
514
  };
304
- const startScreenshare = () => {
515
+ const startScreenshare = async () => {
516
+ const screenshareAttemptId = getUniqueID();
517
+ const screenshareSessionId = getScreenshareSessionId(
518
+ 'start',
519
+ activeScreenshareSessionIdRef.current,
520
+ getUniqueID,
521
+ );
522
+ logger.log(
523
+ LogSource.Internals,
524
+ 'SCREENSHARE',
525
+ `${SCREENSHARE_JOURNEY} screen share start requested from UI`,
526
+ {
527
+ action: 'start',
528
+ stage: 'ui_request',
529
+ outcome: 'started',
530
+ screenshareAttemptId,
531
+ screenshareSessionId,
532
+ recordingActive: props.isRecordingActive,
533
+ screenShareUid,
534
+ stopOrigin: 'unknown',
535
+ },
536
+ );
305
537
  if (isScreenshareActive) {
538
+ logger.log(
539
+ LogSource.Internals,
540
+ 'SCREENSHARE',
541
+ `${SCREENSHARE_JOURNEY} screen share start skipped because screen share is already active`,
542
+ {
543
+ action: 'start',
544
+ stage: 'precondition',
545
+ outcome: 'skipped',
546
+ screenshareAttemptId,
547
+ screenshareSessionId,
548
+ recordingActive: props.isRecordingActive,
549
+ screenShareUid,
550
+ stopOrigin: 'unknown',
551
+ },
552
+ );
306
553
  return;
307
554
  }
308
- userScreenshare(true);
555
+ await userScreenshare(
556
+ true,
557
+ screenshareAttemptId,
558
+ screenshareSessionId,
559
+ 'unknown',
560
+ );
309
561
  };
562
+ stopScreenshareRef.current = stopScreenshare;
310
563
 
311
- const userScreenshare = async (isActive: boolean) => {
564
+ const userScreenshare = async (
565
+ isActive: boolean,
566
+ screenshareAttemptId: string,
567
+ screenshareSessionId: string,
568
+ stopOrigin: ScreenshareStopOrigin,
569
+ stopActorUid?: UidType,
570
+ ) => {
571
+ const startedAt = Date.now();
572
+ const action: ScreenshareAction = isActive ? 'start' : 'stop';
573
+ let stage = 'recording_layout';
574
+ let recordingLayoutFailed = false;
312
575
  logger.log(
313
576
  LogSource.Internals,
314
577
  'SCREENSHARE',
315
- `${isActive ? 'starting' : 'stopping'} screenshare`,
578
+ `${SCREENSHARE_JOURNEY} screen share ${action} workflow started`,
316
579
  {
317
- channel,
580
+ action,
581
+ stage: 'journey',
582
+ outcome: 'started',
583
+ screenshareAttemptId,
584
+ screenshareSessionId,
585
+ recordingActive: props.isRecordingActive,
318
586
  screenShareUid,
319
- encryption,
587
+ stopOrigin,
588
+ stopActorUid,
589
+ channel,
320
590
  },
321
591
  );
322
592
  try {
323
593
  if (props.isRecordingActive) {
324
- executeRecordingQuery(isActive);
594
+ try {
595
+ await executeRecordingQuery(
596
+ isActive,
597
+ screenshareAttemptId,
598
+ screenshareSessionId,
599
+ stopOrigin,
600
+ stopActorUid,
601
+ );
602
+ } catch (recordingError) {
603
+ recordingLayoutFailed = true;
604
+ logger.error(
605
+ LogSource.Internals,
606
+ 'SCREENSHARE',
607
+ `${SCREENSHARE_JOURNEY} screen share ${action} recording layout query failed; continuing screen share ${action}`,
608
+ recordingError,
609
+ {
610
+ action,
611
+ stage,
612
+ outcome: 'partial_failure',
613
+ screenshareAttemptId,
614
+ screenshareSessionId,
615
+ recordingActive: props.isRecordingActive,
616
+ screenShareUid,
617
+ stopOrigin,
618
+ stopActorUid,
619
+ ...getScreenshareError(recordingError),
620
+ },
621
+ );
622
+ }
623
+ } else {
624
+ logger.log(
625
+ LogSource.Internals,
626
+ 'SCREENSHARE',
627
+ `${SCREENSHARE_JOURNEY} screen share ${action} recording layout query skipped because recording is inactive`,
628
+ {
629
+ action,
630
+ stage,
631
+ outcome: 'skipped',
632
+ screenshareAttemptId,
633
+ screenshareSessionId,
634
+ recordingActive: false,
635
+ screenShareUid,
636
+ stopOrigin,
637
+ stopActorUid,
638
+ },
639
+ );
325
640
  }
641
+ stage = 'rtc_operation';
642
+ logger.log(
643
+ LogSource.Internals,
644
+ 'SCREENSHARE',
645
+ `${SCREENSHARE_JOURNEY} screen share ${action} calling RtcEngine.startScreenshare`,
646
+ {
647
+ action,
648
+ stage,
649
+ outcome: 'started',
650
+ screenshareAttemptId,
651
+ screenshareSessionId,
652
+ recordingActive: props.isRecordingActive,
653
+ screenShareUid,
654
+ stopOrigin,
655
+ stopActorUid,
656
+ },
657
+ );
326
658
  // @ts-ignore
327
659
  await rtc.RtcEngineUnsafe.startScreenshare(
328
660
  screenShareToken,
@@ -333,11 +665,56 @@ export const ScreenshareConfigure = (props: {
333
665
  rtc.RtcEngineUnsafe as unknown as IAgoraRTC,
334
666
  encryption as unknown as any,
335
667
  {encoderConfig: '1080p_2', optimizationMode: 'detail'},
668
+ 'auto',
669
+ {
670
+ action,
671
+ screenshareAttemptId,
672
+ screenshareSessionId,
673
+ recordingActive: props.isRecordingActive,
674
+ screenShareUid,
675
+ stopOrigin,
676
+ stopActorUid,
677
+ },
678
+ );
679
+ logger.log(
680
+ LogSource.Internals,
681
+ 'SCREENSHARE',
682
+ `${SCREENSHARE_JOURNEY} screen share ${action} RtcEngine.startScreenshare completed successfully`,
683
+ {
684
+ action,
685
+ stage,
686
+ outcome: 'success',
687
+ screenshareAttemptId,
688
+ screenshareSessionId,
689
+ recordingActive: props.isRecordingActive,
690
+ screenShareUid,
691
+ stopOrigin,
692
+ stopActorUid,
693
+ },
336
694
  );
337
695
  isActive && setScreenshareActive(true);
338
696
 
339
697
  if (isActive) {
698
+ activeScreenshareSessionIdRef.current = screenshareSessionId;
699
+ stage = 'local_active_state';
700
+ logger.log(
701
+ LogSource.Internals,
702
+ 'SCREENSHARE',
703
+ `${SCREENSHARE_JOURNEY} screen share start local active state set to active`,
704
+ {
705
+ action,
706
+ stage,
707
+ outcome: 'success',
708
+ screenshareAttemptId,
709
+ screenshareSessionId,
710
+ recordingActive: props.isRecordingActive,
711
+ screenShareUid,
712
+ stopOrigin,
713
+ stopActorUid,
714
+ },
715
+ );
340
716
  // 1. Set local state
717
+ stage = 'screenshare_context';
341
718
  setScreenShareData(prevState => {
342
719
  return {
343
720
  ...prevState,
@@ -349,7 +726,24 @@ export const ScreenshareConfigure = (props: {
349
726
  },
350
727
  };
351
728
  });
729
+ logger.log(
730
+ LogSource.Internals,
731
+ 'SCREENSHARE',
732
+ `${SCREENSHARE_JOURNEY} screen share start local context set to active`,
733
+ {
734
+ action,
735
+ stage,
736
+ outcome: 'success',
737
+ screenshareAttemptId,
738
+ screenshareSessionId,
739
+ recordingActive: props.isRecordingActive,
740
+ screenShareUid,
741
+ stopOrigin,
742
+ stopActorUid,
743
+ },
744
+ );
352
745
  // 2. Inform everyone in the channel screenshare is actice
746
+ stage = 'rtm_event';
353
747
  events.send(
354
748
  EventNames.SCREENSHARE_ATTRIBUTE,
355
749
  JSON.stringify({
@@ -359,23 +753,81 @@ export const ScreenshareConfigure = (props: {
359
753
  }),
360
754
  PersistanceLevel.Sender,
361
755
  );
756
+ logger.log(
757
+ LogSource.Internals,
758
+ 'SCREENSHARE',
759
+ `${SCREENSHARE_JOURNEY} screen share start RTM event sent to remote users`,
760
+ {
761
+ action,
762
+ stage,
763
+ outcome: 'success',
764
+ screenshareAttemptId,
765
+ screenshareSessionId,
766
+ recordingActive: props.isRecordingActive,
767
+ screenShareUid,
768
+ stopOrigin,
769
+ stopActorUid,
770
+ },
771
+ );
362
772
  }
773
+ logger.log(
774
+ LogSource.Internals,
775
+ 'SCREENSHARE',
776
+ `${SCREENSHARE_JOURNEY} screen share ${action} workflow ${
777
+ recordingLayoutFailed
778
+ ? 'completed with recording layout failure'
779
+ : 'completed successfully'
780
+ }`,
781
+ {
782
+ action,
783
+ stage: 'complete',
784
+ outcome: recordingLayoutFailed ? 'partial_success' : 'success',
785
+ screenshareAttemptId,
786
+ screenshareSessionId,
787
+ recordingActive: props.isRecordingActive,
788
+ screenShareUid,
789
+ stopOrigin,
790
+ stopActorUid,
791
+ elapsedMs: Date.now() - startedAt,
792
+ },
793
+ );
363
794
  } catch (e) {
795
+ const userCancelOrPermissionDenied = isUserCancelOrPermissionDenied(e);
364
796
  logger.error(
365
797
  LogSource.Internals,
366
798
  'SCREENSHARE',
367
- 'failed to start screen share',
368
- e,
799
+ `${SCREENSHARE_JOURNEY} screen share ${action} workflow failed at ${stage}: ${
800
+ userCancelOrPermissionDenied
801
+ ? 'user cancelled picker or permission was denied'
802
+ : 'unexpected failure'
803
+ }`,
804
+ {
805
+ action,
806
+ stage,
807
+ outcome: userCancelOrPermissionDenied
808
+ ? 'user_cancel_or_permission_denied'
809
+ : 'failure',
810
+ screenshareAttemptId,
811
+ screenshareSessionId,
812
+ recordingActive: props.isRecordingActive,
813
+ screenShareUid,
814
+ stopOrigin,
815
+ stopActorUid,
816
+ elapsedMs: Date.now() - startedAt,
817
+ ...getScreenshareError(e),
818
+ },
369
819
  );
370
- Toast.show({
371
- leadingIconName: 'alert',
372
- type: 'error',
373
- text1: toastHeading,
374
- text2: toastSubHeading,
375
- visibilityTime: 1000 * 10,
376
- primaryBtn: null,
377
- secondaryBtn: null,
378
- });
820
+ if (!userCancelOrPermissionDenied) {
821
+ Toast.show({
822
+ leadingIconName: 'alert',
823
+ type: 'error',
824
+ text1: toastHeading,
825
+ text2: toastSubHeading,
826
+ visibilityTime: 1000 * 10,
827
+ primaryBtn: null,
828
+ secondaryBtn: null,
829
+ });
830
+ }
379
831
  }
380
832
  };
381
833
 
@@ -0,0 +1,63 @@
1
+ import {
2
+ getScreenshareError,
3
+ getScreenshareReleaseOrigin,
4
+ getScreenshareSessionId,
5
+ isUserCancelOrPermissionDenied,
6
+ SCREENSHARE_JOURNEY,
7
+ } from '../screenshareJourney';
8
+
9
+ describe('screenshare journey logging', () => {
10
+ it('uses the Datadog filter prefix', () => {
11
+ expect(SCREENSHARE_JOURNEY).toBe('[SCREENSHARE_JOURNEY]');
12
+ });
13
+
14
+ it.each([
15
+ [{code: 'PERMISSION_DENIED'}, true],
16
+ [{name: 'NotAllowedError'}, true],
17
+ [{message: 'AgoraRTCError PERMISSION_DENIED'}, true],
18
+ [{code: 'NETWORK_ERROR'}, false],
19
+ ])('classifies permission and cancellation errors', (error, expected) => {
20
+ expect(isUserCancelOrPermissionDenied(error)).toBe(expected);
21
+ });
22
+
23
+ it('normalizes SDK error details for structured logs', () => {
24
+ expect(
25
+ getScreenshareError({
26
+ code: 'PERMISSION_DENIED',
27
+ name: 'AgoraRTCError',
28
+ message: 'NotAllowedError: Permission denied by user',
29
+ }),
30
+ ).toEqual({
31
+ sdkErrorCode: 'PERMISSION_DENIED',
32
+ sdkErrorName: 'AgoraRTCError',
33
+ sdkErrorMessage: 'NotAllowedError: Permission denied by user',
34
+ });
35
+ });
36
+
37
+ it('reuses the active screen-share session ID when stopping', () => {
38
+ const createId = jest.fn(() => 'new-session-id');
39
+
40
+ expect(getScreenshareSessionId('stop', 'active-session-id', createId)).toBe(
41
+ 'active-session-id',
42
+ );
43
+ expect(createId).not.toHaveBeenCalled();
44
+ });
45
+
46
+ it('creates a new screen-share session ID when starting', () => {
47
+ const createId = jest.fn(() => 'new-session-id');
48
+
49
+ expect(getScreenshareSessionId('start', null, createId)).toBe(
50
+ 'new-session-id',
51
+ );
52
+ expect(createId).toHaveBeenCalledTimes(1);
53
+ });
54
+
55
+ it('classifies hidden-document release as page unload', () => {
56
+ expect(getScreenshareReleaseOrigin(undefined, 'hidden')).toBe(
57
+ 'page_unload',
58
+ );
59
+ expect(getScreenshareReleaseOrigin(undefined, 'visible')).toBe(
60
+ 'end_call_cleanup',
61
+ );
62
+ });
63
+ });