agora-appbuilder-core 4.1.22-beta.2 → 4.1.22-beta.3

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 (43) hide show
  1. package/package.json +2 -2
  2. package/template/.eslintrc.js +20 -0
  3. package/template/.prettierrc.js +18 -0
  4. package/template/.watchmanconfig +1 -0
  5. package/template/agora-rn-uikit/src/Views/MaxVideoView.tsx +2 -0
  6. package/template/agora-rn-uikit/src/Views/MinVideoView.tsx +12 -2
  7. package/template/bridge/rtc/webNg/RtcEngine.ts +639 -251
  8. package/template/bridge/rtc/webNg/RtcSurfaceView.tsx +203 -16
  9. package/template/bridge/rtc/webNg/__tests__/screenshareLifecycle.test.ts +358 -0
  10. package/template/bridge/rtc/webNg/__tests__/screenshareMediaHealth.test.ts +49 -0
  11. package/template/bridge/rtc/webNg/screenshareMediaHealth.ts +49 -0
  12. package/template/bridge/rtm/web/index.ts +22 -31
  13. package/template/config.json +101 -0
  14. package/template/customization-api/atoms.ts +0 -2
  15. package/template/customization-api/customEvents.ts +1 -4
  16. package/template/customization-api/index.ts +0 -1
  17. package/template/customization-api/typeDefinition.ts +1 -6
  18. package/template/defaultConfig.js +2 -2
  19. package/template/src/components/Controls.tsx +12 -6
  20. package/template/src/components/CustomSidePanel.tsx +13 -13
  21. package/template/src/components/RTMConfigure.tsx +0 -14
  22. package/template/src/components/participants/UserActionMenuOptions.tsx +1 -28
  23. package/template/src/logger/transports/__tests__/agora-transport.test.ts +48 -0
  24. package/template/src/logger/transports/agora-transport.ts +18 -6
  25. package/template/src/pages/Create.tsx +19 -31
  26. package/template/src/pages/video-call/ActionSheet.native.tsx +0 -2
  27. package/template/src/pages/video-call/ActionSheet.tsx +0 -2
  28. package/template/src/pages/video-call/ActionSheetHandle.tsx +0 -1
  29. package/template/src/pages/video-call/SidePanelHeader.tsx +1 -3
  30. package/template/src/pages/video-call/VideoCallScreen.tsx +0 -3
  31. package/template/src/pages/video-call/VideoRenderer.tsx +0 -4
  32. package/template/src/rtm-events-api/Events.ts +3 -1
  33. package/template/src/rtm-events-api/__tests__/Events.test.ts +55 -0
  34. package/template/src/subComponents/SidePanelHeader.tsx +2 -16
  35. package/template/src/subComponents/screenshare/ScreenshareButton.tsx +38 -4
  36. package/template/src/subComponents/screenshare/ScreenshareConfigure.tsx +280 -34
  37. package/template/src/subComponents/screenshare/__tests__/screenshareJourney.test.ts +10 -0
  38. package/template/src/subComponents/screenshare/__tests__/screenshareOperation.test.ts +31 -0
  39. package/template/src/subComponents/screenshare/screenshareJourney.ts +6 -0
  40. package/template/src/subComponents/screenshare/screenshareOperation.ts +21 -0
  41. package/template/src/subComponents/screenshare/useScreenshare.tsx +8 -0
  42. package/template/theme.json +91 -0
  43. package/template/customization-api/ai-agent.ts +0 -4
@@ -45,7 +45,18 @@ import {
45
45
  type ScreenEncoderConfigurationPreset,
46
46
  type VideoEncoderConfiguration,
47
47
  } from '../../../src/app-state/useVideoQuality';
48
- import {getScreenshareReleaseOrigin} from '../../../src/subComponents/screenshare/screenshareJourney';
48
+ import {
49
+ getScreenshareReleaseOrigin,
50
+ getScreenshareSessionBoundaryMessage,
51
+ } from '../../../src/subComponents/screenshare/screenshareJourney';
52
+
53
+ export type ScreenshareMediaDiagnostics = {
54
+ role: 'sender' | 'viewer';
55
+ screenshareSessionId: string;
56
+ screenShareUid: UID;
57
+ stats: Record<string, unknown> | null;
58
+ mediaTrack: MediaStreamTrack | null;
59
+ };
49
60
 
50
61
  interface MediaDeviceInfo {
51
62
  readonly deviceId: string;
@@ -190,6 +201,14 @@ interface ScreenStream {
190
201
  audio?: ILocalAudioTrack;
191
202
  video?: ILocalVideoTrack;
192
203
  }
204
+ interface ActiveScreenshareLifecycle {
205
+ audio?: ILocalAudioTrack;
206
+ video: ILocalVideoTrack;
207
+ joined: boolean;
208
+ cleaned: boolean;
209
+ cleanupPromise?: Promise<void>;
210
+ journeyData: Record<string, unknown>;
211
+ }
193
212
  interface RemoteStream {
194
213
  audio?: IRemoteAudioTrack;
195
214
  video?: IRemoteVideoTrack;
@@ -253,13 +272,122 @@ export default class RtcEngine {
253
272
  public screenStream: ScreenStream = {};
254
273
  public remoteStreams = new Map<UID, RemoteStream>();
255
274
  private inScreenshare: Boolean = false;
256
- private isScreenshareCleanupInProgress = false;
275
+ private screenshareOperationState:
276
+ | 'inactive'
277
+ | 'starting'
278
+ | 'active'
279
+ | 'stopping' = 'inactive';
280
+ private screenshareOperationGeneration = 0;
281
+ private activeScreenshareLifecycle: ActiveScreenshareLifecycle | null = null;
257
282
  private activeScreenshareJourneyContext: {
258
283
  screenshareSessionId?: string;
259
284
  recordingActive?: boolean;
260
285
  screenShareUid?: UID;
261
286
  stopActorUid?: UID;
262
287
  } | null = null;
288
+ private remoteScreenshareUids = new Set<UID>();
289
+
290
+ registerRemoteScreenshareUid(uid: UID) {
291
+ if (uid === undefined || uid === null) {
292
+ return;
293
+ }
294
+ this.remoteScreenshareUids.add(uid);
295
+ }
296
+
297
+ private logRemoteScreenshareTermination(
298
+ uid: UID,
299
+ sdkEvent: 'user-unpublished' | 'user-left',
300
+ mediaType?: 'audio' | 'video',
301
+ ) {
302
+ if (!this.remoteScreenshareUids.has(uid)) {
303
+ return;
304
+ }
305
+ const videoTrack = this.remoteStreams?.get?.(uid)?.video;
306
+ let mediaTrack: MediaStreamTrack | null = null;
307
+ try {
308
+ mediaTrack = videoTrack?.getMediaStreamTrack?.() || null;
309
+ } catch (_) {}
310
+ logger.log(
311
+ LogSource.AgoraSDK,
312
+ 'Event',
313
+ `[SCREENSHARE_JOURNEY] receiver detected screen share stopped through RTC ${sdkEvent}`,
314
+ {
315
+ action: 'stop',
316
+ stage: 'receiver_rtc_termination',
317
+ outcome: 'detected',
318
+ role: 'viewer',
319
+ screenshareSessionId: 'remote-session-by-screen-uid',
320
+ screenShareUid: uid,
321
+ stopOrigin: 'remote_sender_or_connection',
322
+ sdkEvent,
323
+ mediaType,
324
+ mediaTrack: mediaTrack
325
+ ? {
326
+ readyState: mediaTrack.readyState,
327
+ enabled: mediaTrack.enabled,
328
+ muted: mediaTrack.muted,
329
+ }
330
+ : null,
331
+ },
332
+ );
333
+ this.remoteScreenshareUids.delete(uid);
334
+ }
335
+
336
+ getScreenshareMediaDiagnostics(uid: UID): ScreenshareMediaDiagnostics {
337
+ const isSender = uid === 1;
338
+ if (!isSender) {
339
+ this.registerRemoteScreenshareUid(uid);
340
+ }
341
+ const videoTrack = isSender
342
+ ? this.screenStream?.video
343
+ : this.remoteStreams?.get?.(uid)?.video;
344
+ let stats: Record<string, unknown> | null = null;
345
+ try {
346
+ stats = isSender
347
+ ? this.screenClient?.getLocalVideoStats?.() || null
348
+ : this.client?.getRemoteVideoStats?.()?.[uid] || null;
349
+ } catch (error) {
350
+ logger.error(
351
+ LogSource.AgoraSDK,
352
+ 'API',
353
+ '[SCREENSHARE_JOURNEY] screen share media health stats collection failed',
354
+ {
355
+ action: 'monitor',
356
+ stage: 'media_health',
357
+ outcome: 'stats_unavailable',
358
+ role: isSender ? 'sender' : 'viewer',
359
+ screenshareSessionId: isSender
360
+ ? this.activeScreenshareJourneyContext?.screenshareSessionId ||
361
+ 'unknown-session'
362
+ : 'remote-session-by-screen-uid',
363
+ screenShareUid: isSender
364
+ ? this.activeScreenshareJourneyContext?.screenShareUid ||
365
+ this.screenClient?.uid
366
+ : uid,
367
+ ...getScreenshareErrorDetails(error),
368
+ },
369
+ );
370
+ }
371
+ return {
372
+ role: isSender ? 'sender' : 'viewer',
373
+ screenshareSessionId: isSender
374
+ ? this.activeScreenshareJourneyContext?.screenshareSessionId ||
375
+ 'unknown-session'
376
+ : 'remote-session-by-screen-uid',
377
+ screenShareUid: isSender
378
+ ? this.activeScreenshareJourneyContext?.screenShareUid ||
379
+ this.screenClient?.uid
380
+ : uid,
381
+ stats,
382
+ mediaTrack: (() => {
383
+ try {
384
+ return videoTrack?.getMediaStreamTrack?.() || null;
385
+ } catch (_) {
386
+ return null;
387
+ }
388
+ })(),
389
+ };
390
+ }
263
391
  private videoProfile:
264
392
  | VideoEncoderConfigurationPreset
265
393
  | VideoEncoderConfiguration;
@@ -739,6 +867,7 @@ export default class RtcEngine {
739
867
  this.client.on('user-left', user => {
740
868
  logger.log(LogSource.AgoraSDK, 'Event', 'RTC [user-left]', user);
741
869
  const uid = user.uid;
870
+ this.logRemoteScreenshareTermination(uid, 'user-left');
742
871
  if (this.remoteStreams.has(uid)) {
743
872
  this.remoteStreams.delete(uid);
744
873
  }
@@ -819,6 +948,13 @@ export default class RtcEngine {
819
948
  user,
820
949
  mediaType,
821
950
  );
951
+ if (mediaType === 'video') {
952
+ this.logRemoteScreenshareTermination(
953
+ user.uid,
954
+ 'user-unpublished',
955
+ mediaType,
956
+ );
957
+ }
822
958
  if (mediaType === 'audio') {
823
959
  const data = this.remoteStreams.get(user.uid);
824
960
  try {
@@ -1537,6 +1673,167 @@ export default class RtcEngine {
1537
1673
  console.error('Please use enableEncryption instead');
1538
1674
  }
1539
1675
 
1676
+ private async cleanupActiveScreenshare(
1677
+ stopOrigin: string,
1678
+ notifyScreenshareStopped: boolean,
1679
+ screenshareAttemptId: string,
1680
+ expectedLifecycle?: ActiveScreenshareLifecycle,
1681
+ ): Promise<void> {
1682
+ const lifecycle = expectedLifecycle || this.activeScreenshareLifecycle;
1683
+ const journeyData = {
1684
+ ...(lifecycle?.journeyData || this.activeScreenshareJourneyContext || {}),
1685
+ action: 'stop',
1686
+ screenshareAttemptId,
1687
+ stopOrigin,
1688
+ };
1689
+
1690
+ if (!lifecycle) {
1691
+ logger.log(
1692
+ LogSource.AgoraSDK,
1693
+ 'API',
1694
+ `[SCREENSHARE_JOURNEY] screen share stop cleanup skipped because there is no active RTC lifecycle`,
1695
+ {
1696
+ ...journeyData,
1697
+ stage: 'cleanup',
1698
+ outcome: 'skipped',
1699
+ operationState: this.screenshareOperationState,
1700
+ duplicateReason: 'no_active_rtc_lifecycle',
1701
+ },
1702
+ );
1703
+ if (notifyScreenshareStopped) {
1704
+ await (this.eventsMap.get('onScreenshareStopped') as callbackType)(
1705
+ stopOrigin,
1706
+ screenshareAttemptId,
1707
+ journeyData.screenshareSessionId || 'unknown-session',
1708
+ journeyData.stopActorUid,
1709
+ );
1710
+ }
1711
+ this.inScreenshare = false;
1712
+ this.screenshareOperationState = 'inactive';
1713
+ return;
1714
+ }
1715
+
1716
+ if (
1717
+ expectedLifecycle &&
1718
+ this.activeScreenshareLifecycle !== expectedLifecycle
1719
+ ) {
1720
+ logger.log(
1721
+ LogSource.AgoraSDK,
1722
+ 'API',
1723
+ `[SCREENSHARE_JOURNEY] screen share stop ignored stale track-ended callback`,
1724
+ {
1725
+ ...journeyData,
1726
+ stage: 'track_ended',
1727
+ outcome: 'skipped',
1728
+ operationState: this.screenshareOperationState,
1729
+ duplicateReason: 'stale_screenshare_lifecycle',
1730
+ },
1731
+ );
1732
+ return;
1733
+ }
1734
+
1735
+ if (lifecycle.cleanupPromise) {
1736
+ logger.log(
1737
+ LogSource.AgoraSDK,
1738
+ 'API',
1739
+ `[SCREENSHARE_JOURNEY] screen share stop joined cleanup already in progress`,
1740
+ {
1741
+ ...journeyData,
1742
+ stage: 'cleanup',
1743
+ outcome: 'skipped',
1744
+ operationState: this.screenshareOperationState,
1745
+ duplicateReason: 'cleanup_already_in_progress',
1746
+ },
1747
+ );
1748
+ return lifecycle.cleanupPromise;
1749
+ }
1750
+
1751
+ this.screenshareOperationState = 'stopping';
1752
+ lifecycle.cleanupPromise = Promise.resolve().then(async () => {
1753
+ let cleanupError: unknown;
1754
+ const attemptCleanup = async (operation: () => unknown) => {
1755
+ try {
1756
+ await operation();
1757
+ } catch (error) {
1758
+ cleanupError = cleanupError || error;
1759
+ }
1760
+ };
1761
+ try {
1762
+ await attemptCleanup(() =>
1763
+ (this.eventsMap.get('onUserOffline') as callbackType)(
1764
+ {},
1765
+ this.screenClient.uid,
1766
+ ),
1767
+ );
1768
+ await attemptCleanup(() => lifecycle.audio?.stop());
1769
+ await attemptCleanup(() => lifecycle.video?.stop());
1770
+ await attemptCleanup(() => lifecycle.audio?.close());
1771
+ await attemptCleanup(() => lifecycle.video?.close());
1772
+ if (lifecycle.joined) {
1773
+ await attemptCleanup(() => this.screenClient.leave());
1774
+ }
1775
+ if (cleanupError) {
1776
+ logger.error(
1777
+ LogSource.AgoraSDK,
1778
+ 'API',
1779
+ `[SCREENSHARE_JOURNEY] screen share stop RTC cleanup completed with errors`,
1780
+ {
1781
+ ...journeyData,
1782
+ stage: 'cleanup',
1783
+ outcome: 'partial_failure',
1784
+ operationState: 'inactive',
1785
+ ...getScreenshareErrorDetails(cleanupError),
1786
+ },
1787
+ );
1788
+ } else {
1789
+ logger.log(
1790
+ LogSource.AgoraSDK,
1791
+ 'API',
1792
+ `[SCREENSHARE_JOURNEY] screen share stop RTC cleanup completed`,
1793
+ {
1794
+ ...journeyData,
1795
+ stage: 'cleanup',
1796
+ outcome: 'success',
1797
+ operationState: 'inactive',
1798
+ },
1799
+ );
1800
+ }
1801
+ } catch (error) {
1802
+ logger.error(
1803
+ LogSource.AgoraSDK,
1804
+ 'API',
1805
+ `[SCREENSHARE_JOURNEY] screen share stop RTC cleanup failed`,
1806
+ {
1807
+ ...journeyData,
1808
+ stage: 'cleanup',
1809
+ outcome: 'failure',
1810
+ operationState: this.screenshareOperationState,
1811
+ ...getScreenshareErrorDetails(error),
1812
+ },
1813
+ );
1814
+ throw error;
1815
+ } finally {
1816
+ lifecycle.cleaned = true;
1817
+ if (this.activeScreenshareLifecycle === lifecycle) {
1818
+ this.activeScreenshareLifecycle = null;
1819
+ this.screenStream = {};
1820
+ this.inScreenshare = false;
1821
+ this.screenshareOperationState = 'inactive';
1822
+ this.activeScreenshareJourneyContext = null;
1823
+ if (notifyScreenshareStopped) {
1824
+ await (this.eventsMap.get('onScreenshareStopped') as callbackType)(
1825
+ stopOrigin,
1826
+ screenshareAttemptId,
1827
+ journeyData.screenshareSessionId || 'unknown-session',
1828
+ journeyData.stopActorUid,
1829
+ );
1830
+ }
1831
+ }
1832
+ }
1833
+ });
1834
+ return await lifecycle.cleanupPromise;
1835
+ }
1836
+
1540
1837
  // async removeAllListeners<EventType extends keyof RtcEngineEvents>(event: EventType) {
1541
1838
  // this.client.removeAllListeners(eventName);
1542
1839
  // }
@@ -1548,7 +1845,23 @@ export default class RtcEngine {
1548
1845
  requestedStopOrigin,
1549
1846
  typeof document !== 'undefined' ? document.visibilityState : undefined,
1550
1847
  );
1551
- if (this.inScreenshare) {
1848
+ if (this.screenshareOperationState === 'starting') {
1849
+ this.screenshareOperationGeneration += 1;
1850
+ this.screenshareOperationState = 'inactive';
1851
+ logger.log(
1852
+ LogSource.AgoraSDK,
1853
+ 'API',
1854
+ `[SCREENSHARE_JOURNEY] screen share start cancelled by RTC release from ${stopOrigin}`,
1855
+ {
1856
+ action: 'start',
1857
+ stage: 'release',
1858
+ outcome: 'cancelled',
1859
+ stopOrigin,
1860
+ operationState: 'inactive',
1861
+ },
1862
+ );
1863
+ }
1864
+ if (this.activeScreenshareLifecycle || this.inScreenshare) {
1552
1865
  const screenshareAttemptId = `release-${Date.now()}`;
1553
1866
  const journeyData = {
1554
1867
  action: 'stop',
@@ -1574,21 +1887,10 @@ export default class RtcEngine {
1574
1887
  journeyData,
1575
1888
  );
1576
1889
  try {
1577
- (this.eventsMap.get('onUserOffline') as callbackType)(
1578
- {},
1579
- this.screenClient.uid,
1580
- );
1581
- this.screenStream.audio?.stop();
1582
- this.screenStream.video?.stop();
1583
- this.screenStream.audio?.close();
1584
- this.screenStream.video?.close();
1585
- await this.screenClient.leave();
1586
- this.inScreenshare = false;
1587
- (this.eventsMap.get('onScreenshareStopped') as callbackType)(
1890
+ await this.cleanupActiveScreenshare(
1588
1891
  stopOrigin,
1892
+ true,
1589
1893
  screenshareAttemptId,
1590
- journeyData.screenshareSessionId,
1591
- journeyData.stopActorUid,
1592
1894
  );
1593
1895
  logger.log(
1594
1896
  LogSource.AgoraSDK,
@@ -1608,6 +1910,19 @@ export default class RtcEngine {
1608
1910
  },
1609
1911
  );
1610
1912
  } finally {
1913
+ logger.log(
1914
+ LogSource.AgoraSDK,
1915
+ 'API',
1916
+ getScreenshareSessionBoundaryMessage(
1917
+ 'end',
1918
+ journeyData.screenshareSessionId,
1919
+ ),
1920
+ {
1921
+ ...journeyData,
1922
+ stage: 'session_boundary',
1923
+ outcome: 'ended',
1924
+ },
1925
+ );
1611
1926
  this.activeScreenshareJourneyContext = null;
1612
1927
  }
1613
1928
  }
@@ -1696,280 +2011,353 @@ export default class RtcEngine {
1696
2011
  ...screenShareConfig,
1697
2012
  encoderConfig: this.screenShareProfile,
1698
2013
  };
1699
- let joined = false;
1700
- let cleanupCompleted = false;
1701
- const cleanupScreenshare = async (
1702
- stopOrigin: string,
1703
- notifyScreenshareStopped: boolean,
1704
- screenshareAttemptId = journeyData.screenshareAttemptId,
1705
- ) => {
1706
- const cleanupJourneyData = notifyScreenshareStopped
1707
- ? {
1708
- ...journeyData,
1709
- action: 'stop',
1710
- screenshareAttemptId,
1711
- stopOrigin,
1712
- }
1713
- : journeyData;
1714
- if (this.isScreenshareCleanupInProgress || cleanupCompleted) {
1715
- logger.log(
1716
- LogSource.AgoraSDK,
1717
- 'API',
1718
- `[SCREENSHARE_JOURNEY] screen share ${cleanupJourneyData.action} cleanup skipped because cleanup is already running or completed`,
1719
- {
1720
- ...cleanupJourneyData,
1721
- stage: 'cleanup',
1722
- outcome: 'skipped',
1723
- stopOrigin,
1724
- },
2014
+ if (journeyData.action === 'stop') {
2015
+ logger.log(
2016
+ LogSource.AgoraSDK,
2017
+ 'API',
2018
+ `[SCREENSHARE_JOURNEY] screen share stop entered RTC engine from ${journeyData.stopOrigin}`,
2019
+ {
2020
+ ...journeyData,
2021
+ stage: 'rtc_stop',
2022
+ outcome: 'started',
2023
+ requestedAction: 'stop',
2024
+ executedAction: 'stop',
2025
+ operationState: this.screenshareOperationState,
2026
+ },
2027
+ );
2028
+ await this.cleanupActiveScreenshare(
2029
+ journeyData.stopOrigin,
2030
+ true,
2031
+ journeyData.screenshareAttemptId,
2032
+ );
2033
+ return;
2034
+ }
2035
+
2036
+ if (this.screenshareOperationState !== 'inactive' || this.inScreenshare) {
2037
+ const duplicateStartError = Object.assign(
2038
+ new Error(
2039
+ `Screen share start rejected while RTC state is ${this.screenshareOperationState}`,
2040
+ ),
2041
+ {code: 'SCREENSHARE_OPERATION_IN_PROGRESS'},
2042
+ );
2043
+ logger.log(
2044
+ LogSource.AgoraSDK,
2045
+ 'API',
2046
+ `[SCREENSHARE_JOURNEY] screen share start rejected because RTC screen share is ${this.screenshareOperationState}`,
2047
+ {
2048
+ ...journeyData,
2049
+ stage: 'precondition',
2050
+ outcome: 'skipped',
2051
+ requestedAction: 'start',
2052
+ executedAction: 'none',
2053
+ operationState: this.screenshareOperationState,
2054
+ duplicateReason: `start_requested_while_${this.screenshareOperationState}`,
2055
+ },
2056
+ );
2057
+ throw duplicateStartError;
2058
+ }
2059
+
2060
+ this.screenshareOperationState = 'starting';
2061
+ const operationGeneration = ++this.screenshareOperationGeneration;
2062
+ const ensureStartIsCurrent = () => {
2063
+ if (operationGeneration !== this.screenshareOperationGeneration) {
2064
+ throw Object.assign(
2065
+ new Error('Screen share start was cancelled by cleanup'),
2066
+ {code: 'SCREENSHARE_START_CANCELLED'},
1725
2067
  );
1726
- return;
1727
2068
  }
1728
- this.isScreenshareCleanupInProgress = true;
1729
- try {
1730
- if (joined || this.inScreenshare) {
1731
- (this.eventsMap.get('onUserOffline') as callbackType)(
1732
- {},
1733
- this.screenClient.uid,
2069
+ };
2070
+ let stage = 'encryption';
2071
+ let joined = false;
2072
+ let localVideoTrack: ILocalVideoTrack | undefined;
2073
+ let localAudioTrack: ILocalAudioTrack | undefined;
2074
+ try {
2075
+ logger.debug(
2076
+ LogSource.AgoraSDK,
2077
+ 'Log',
2078
+ '[SCREENSHARE_JOURNEY] screen share start entered RTC engine',
2079
+ {
2080
+ ...journeyData,
2081
+ stage: 'rtc_start',
2082
+ outcome: 'started',
2083
+ requestedAction: 'start',
2084
+ executedAction: 'start',
2085
+ operationState: this.screenshareOperationState,
2086
+ },
2087
+ );
2088
+ if (encryption && encryption.screenKey && encryption.mode) {
2089
+ let mode: EncryptionMode;
2090
+ mode = this.getEncryptionMode(true, encryption?.mode);
2091
+ try {
2092
+ /**
2093
+ * Since version 4.7.0, if client leaves a call
2094
+ * and joins again the encryption needs to be
2095
+ * set again
2096
+ */
2097
+ logger.log(
2098
+ LogSource.AgoraSDK,
2099
+ 'Log',
2100
+ '[SCREENSHARE_JOURNEY] screen share start configuring RTC screen-client encryption',
2101
+ {...journeyData, stage, outcome: 'started'},
1734
2102
  );
1735
- }
1736
- this.screenStream.audio?.stop();
1737
- this.screenStream.video?.stop();
1738
- this.screenStream.audio?.close();
1739
- this.screenStream.video?.close();
1740
- if (joined || this.inScreenshare) {
1741
- await this.screenClient.leave();
1742
- }
1743
- this.screenStream = {};
1744
- this.inScreenshare = false;
1745
- this.activeScreenshareJourneyContext = null;
1746
- if (notifyScreenshareStopped) {
1747
- (this.eventsMap.get('onScreenshareStopped') as callbackType)(
1748
- stopOrigin,
1749
- screenshareAttemptId,
1750
- cleanupJourneyData.screenshareSessionId,
1751
- cleanupJourneyData.stopActorUid,
2103
+ await this.screenClient.setEncryptionConfig(
2104
+ mode,
2105
+ encryption.screenKey,
2106
+ encryption.salt,
2107
+ true, // encryptDataStream
1752
2108
  );
2109
+ ensureStartIsCurrent();
2110
+ logger.log(
2111
+ LogSource.AgoraSDK,
2112
+ 'Log',
2113
+ '[SCREENSHARE_JOURNEY] screen share start RTC screen-client encryption configured successfully',
2114
+ {...journeyData, stage, outcome: 'success'},
2115
+ );
2116
+ } catch (e) {
2117
+ logger.error(
2118
+ LogSource.AgoraSDK,
2119
+ 'Log',
2120
+ '[SCREENSHARE_JOURNEY] screen share start RTC screen-client encryption configuration failed',
2121
+ {
2122
+ ...journeyData,
2123
+ stage,
2124
+ outcome: 'failure',
2125
+ ...getScreenshareErrorDetails(e),
2126
+ },
2127
+ );
2128
+ throw e;
1753
2129
  }
2130
+ } else {
1754
2131
  logger.log(
1755
- LogSource.AgoraSDK,
1756
- 'API',
1757
- `[SCREENSHARE_JOURNEY] screen share ${cleanupJourneyData.action} RTC cleanup completed`,
1758
- {
1759
- ...cleanupJourneyData,
1760
- stage: 'cleanup',
1761
- outcome: 'success',
1762
- stopOrigin,
1763
- },
1764
- );
1765
- cleanupCompleted = true;
1766
- } finally {
1767
- this.isScreenshareCleanupInProgress = false;
1768
- }
1769
- };
1770
- if (!this.inScreenshare) {
1771
- let stage = 'encryption';
1772
- try {
1773
- logger.debug(
1774
2132
  LogSource.AgoraSDK,
1775
2133
  'Log',
1776
- '[SCREENSHARE_JOURNEY] screen share start entered RTC engine',
1777
- {...journeyData, stage: 'rtc_start', outcome: 'started'},
2134
+ '[SCREENSHARE_JOURNEY] screen share start RTC screen-client encryption skipped because encryption is not configured',
2135
+ {...journeyData, stage, outcome: 'skipped'},
1778
2136
  );
1779
- if (encryption && encryption.screenKey && encryption.mode) {
1780
- let mode: EncryptionMode;
1781
- mode = this.getEncryptionMode(true, encryption?.mode);
1782
- try {
1783
- /**
1784
- * Since version 4.7.0, if client leaves a call
1785
- * and joins again the encryption needs to be
1786
- * set again
1787
- */
1788
- logger.log(
1789
- LogSource.AgoraSDK,
1790
- 'Log',
1791
- '[SCREENSHARE_JOURNEY] screen share start configuring RTC screen-client encryption',
1792
- {...journeyData, stage, outcome: 'started'},
1793
- );
1794
- await this.screenClient.setEncryptionConfig(
1795
- mode,
1796
- encryption.screenKey,
1797
- encryption.salt,
1798
- true, // encryptDataStream
1799
- );
1800
- logger.log(
1801
- LogSource.AgoraSDK,
1802
- 'Log',
1803
- '[SCREENSHARE_JOURNEY] screen share start RTC screen-client encryption configured successfully',
1804
- {...journeyData, stage, outcome: 'success'},
1805
- );
1806
- } catch (e) {
1807
- logger.error(
1808
- LogSource.AgoraSDK,
1809
- 'Log',
1810
- '[SCREENSHARE_JOURNEY] screen share start RTC screen-client encryption configuration failed',
1811
- {
1812
- ...journeyData,
1813
- stage,
1814
- outcome: 'failure',
1815
- ...getScreenshareErrorDetails(e),
1816
- },
1817
- );
1818
- throw e;
1819
- }
1820
- } else {
2137
+ }
2138
+
2139
+ stage = 'create_screen_video_track';
2140
+ logger.log(
2141
+ LogSource.AgoraSDK,
2142
+ 'API',
2143
+ '[SCREENSHARE_JOURNEY] screen share start calling AgoraRTC.createScreenVideoTrack',
2144
+ {
2145
+ ...journeyData,
2146
+ stage,
2147
+ outcome: 'started',
2148
+ config,
2149
+ },
2150
+ );
2151
+ const screenTracks = await AgoraRTC.createScreenVideoTrack(config, audio);
2152
+ const isSingleScreenTrack = this.isSingleTrack(screenTracks);
2153
+ localVideoTrack = isSingleScreenTrack ? screenTracks : screenTracks[0];
2154
+ localAudioTrack = isSingleScreenTrack ? undefined : screenTracks[1];
2155
+ ensureStartIsCurrent();
2156
+ logger.log(
2157
+ LogSource.AgoraSDK,
2158
+ 'API',
2159
+ '[SCREENSHARE_JOURNEY] screen share start AgoraRTC.createScreenVideoTrack completed successfully',
2160
+ {
2161
+ ...journeyData,
2162
+ stage,
2163
+ outcome: 'success',
2164
+ hasVideoTrack: Boolean(
2165
+ isSingleScreenTrack ? screenTracks : screenTracks[0],
2166
+ ),
2167
+ hasAudioTrack: Boolean(isSingleScreenTrack ? false : screenTracks[1]),
2168
+ },
2169
+ );
2170
+ stage = 'rtc_join';
2171
+ logger.log(
2172
+ LogSource.AgoraSDK,
2173
+ 'API',
2174
+ '[SCREENSHARE_JOURNEY] screen share start calling screenClient.join',
2175
+ {...journeyData, stage, outcome: 'started', channelName, optionalUid},
2176
+ );
2177
+ await this.screenClient.join(
2178
+ this.appId,
2179
+ channelName,
2180
+ token || null,
2181
+ optionalUid || null,
2182
+ );
2183
+ joined = true;
2184
+ ensureStartIsCurrent();
2185
+ logger.log(
2186
+ LogSource.AgoraSDK,
2187
+ 'API',
2188
+ '[SCREENSHARE_JOURNEY] screen share start screenClient.join completed successfully',
2189
+ {...journeyData, stage, outcome: 'success'},
2190
+ );
2191
+ stage = 'rtc_publish';
2192
+ logger.log(
2193
+ LogSource.AgoraSDK,
2194
+ 'API',
2195
+ '[SCREENSHARE_JOURNEY] screen share start calling screenClient.publish',
2196
+ {...journeyData, stage, outcome: 'started'},
2197
+ );
2198
+ await this.screenClient.publish(
2199
+ localAudioTrack ? [localVideoTrack, localAudioTrack] : localVideoTrack,
2200
+ );
2201
+ ensureStartIsCurrent();
2202
+ logger.log(
2203
+ LogSource.AgoraSDK,
2204
+ 'API',
2205
+ '[SCREENSHARE_JOURNEY] screen share start screenClient.publish completed successfully',
2206
+ {...journeyData, stage, outcome: 'success'},
2207
+ );
2208
+ const lifecycle: ActiveScreenshareLifecycle = {
2209
+ video: localVideoTrack,
2210
+ audio: localAudioTrack,
2211
+ joined,
2212
+ cleaned: false,
2213
+ journeyData,
2214
+ };
2215
+ this.screenStream = {
2216
+ video: localVideoTrack,
2217
+ audio: localAudioTrack,
2218
+ };
2219
+ this.activeScreenshareLifecycle = lifecycle;
2220
+ this.inScreenshare = true;
2221
+ this.screenshareOperationState = 'active';
2222
+ this.activeScreenshareJourneyContext = {
2223
+ screenshareSessionId: journeyData.screenshareSessionId,
2224
+ recordingActive: journeyData.recordingActive,
2225
+ screenShareUid: journeyData.screenShareUid,
2226
+ stopActorUid: journeyData.stopActorUid,
2227
+ };
2228
+ localVideoTrack.on('track-ended', async () => {
2229
+ const nativeStopAttemptId = `${
2230
+ journeyData.screenshareAttemptId
2231
+ }-native-${Date.now()}`;
2232
+ const mediaTrack = localVideoTrack?.getMediaStreamTrack?.();
2233
+ if (this.activeScreenshareLifecycle !== lifecycle) {
1821
2234
  logger.log(
1822
2235
  LogSource.AgoraSDK,
1823
- 'Log',
1824
- '[SCREENSHARE_JOURNEY] screen share start RTC screen-client encryption skipped because encryption is not configured',
1825
- {...journeyData, stage, outcome: 'skipped'},
2236
+ 'API',
2237
+ '[SCREENSHARE_JOURNEY] screen share stop ignored stale video track-ended callback',
2238
+ {
2239
+ ...journeyData,
2240
+ action: 'stop',
2241
+ screenshareAttemptId: nativeStopAttemptId,
2242
+ stage: 'track_ended',
2243
+ outcome: 'skipped',
2244
+ stopOrigin: 'browser_native_control',
2245
+ operationState: this.screenshareOperationState,
2246
+ duplicateReason: 'stale_screenshare_lifecycle',
2247
+ },
1826
2248
  );
2249
+ return;
1827
2250
  }
1828
-
1829
- stage = 'create_screen_video_track';
1830
2251
  logger.log(
1831
2252
  LogSource.AgoraSDK,
1832
2253
  'API',
1833
- '[SCREENSHARE_JOURNEY] screen share start calling AgoraRTC.createScreenVideoTrack',
2254
+ '[SCREENSHARE_JOURNEY] screen share stop detected from browser native control through video track-ended',
1834
2255
  {
1835
2256
  ...journeyData,
1836
- stage,
2257
+ action: 'stop',
2258
+ screenshareAttemptId: nativeStopAttemptId,
2259
+ stage: 'track_ended',
1837
2260
  outcome: 'started',
1838
- config,
1839
- },
1840
- );
1841
- const screenTracks = await AgoraRTC.createScreenVideoTrack(
1842
- config,
1843
- audio,
1844
- );
1845
- const isSingleScreenTrack = this.isSingleTrack(screenTracks);
1846
- logger.log(
1847
- LogSource.AgoraSDK,
1848
- 'API',
1849
- '[SCREENSHARE_JOURNEY] screen share start AgoraRTC.createScreenVideoTrack completed successfully',
1850
- {
1851
- ...journeyData,
1852
- stage,
1853
- outcome: 'success',
1854
- hasVideoTrack: Boolean(
1855
- isSingleScreenTrack ? screenTracks : screenTracks[0],
1856
- ),
1857
- hasAudioTrack: Boolean(
1858
- isSingleScreenTrack ? false : screenTracks[1],
1859
- ),
2261
+ stopOrigin: 'browser_native_control',
2262
+ mediaTrack: mediaTrack
2263
+ ? {
2264
+ readyState: mediaTrack.readyState,
2265
+ enabled: mediaTrack.enabled,
2266
+ muted: mediaTrack.muted,
2267
+ label: mediaTrack.label,
2268
+ settings: mediaTrack.getSettings?.(),
2269
+ }
2270
+ : null,
1860
2271
  },
1861
2272
  );
1862
- if (isSingleScreenTrack) {
1863
- this.screenStream.video = screenTracks;
1864
- } else {
1865
- this.screenStream.video = screenTracks[0];
1866
- this.screenStream.audio = screenTracks[1];
1867
- }
1868
- stage = 'rtc_join';
1869
- logger.log(
1870
- LogSource.AgoraSDK,
1871
- 'API',
1872
- '[SCREENSHARE_JOURNEY] screen share start calling screenClient.join',
1873
- {...journeyData, stage, outcome: 'started', channelName, optionalUid},
1874
- );
1875
- await this.screenClient.join(
1876
- this.appId,
1877
- channelName,
1878
- token || null,
1879
- optionalUid || null,
1880
- );
1881
- joined = true;
1882
- this.inScreenshare = true;
1883
- logger.log(
1884
- LogSource.AgoraSDK,
1885
- 'API',
1886
- '[SCREENSHARE_JOURNEY] screen share start screenClient.join completed successfully',
1887
- {...journeyData, stage, outcome: 'success'},
1888
- );
1889
- stage = 'rtc_publish';
1890
- logger.log(
1891
- LogSource.AgoraSDK,
1892
- 'API',
1893
- '[SCREENSHARE_JOURNEY] screen share start calling screenClient.publish',
1894
- {...journeyData, stage, outcome: 'started'},
1895
- );
1896
- await this.screenClient.publish(
1897
- this.screenStream.audio
1898
- ? [this.screenStream.video, this.screenStream.audio]
1899
- : this.screenStream.video,
1900
- );
1901
- logger.log(
1902
- LogSource.AgoraSDK,
1903
- 'API',
1904
- '[SCREENSHARE_JOURNEY] screen share start screenClient.publish completed successfully',
1905
- {...journeyData, stage, outcome: 'success'},
1906
- );
1907
- this.activeScreenshareJourneyContext = {
1908
- screenshareSessionId: journeyData.screenshareSessionId,
1909
- recordingActive: journeyData.recordingActive,
1910
- screenShareUid: journeyData.screenShareUid,
1911
- stopActorUid: journeyData.stopActorUid,
1912
- };
1913
- this.screenStream.video.on('track-ended', async () => {
1914
- const nativeStopAttemptId = `${
1915
- journeyData.screenshareAttemptId
1916
- }-native-${Date.now()}`;
1917
- const mediaTrack = this.screenStream.video?.getMediaStreamTrack?.();
1918
- logger.log(
2273
+ try {
2274
+ await this.cleanupActiveScreenshare(
2275
+ 'browser_native_control',
2276
+ true,
2277
+ nativeStopAttemptId,
2278
+ lifecycle,
2279
+ );
2280
+ } catch (error) {
2281
+ logger.error(
1919
2282
  LogSource.AgoraSDK,
1920
2283
  'API',
1921
- '[SCREENSHARE_JOURNEY] screen share stop detected from browser native control through video track-ended',
2284
+ '[SCREENSHARE_JOURNEY] screen share stop from browser native control completed with cleanup errors',
1922
2285
  {
1923
2286
  ...journeyData,
1924
2287
  action: 'stop',
1925
2288
  screenshareAttemptId: nativeStopAttemptId,
1926
- stage: 'track_ended',
1927
- outcome: 'started',
2289
+ stage: 'cleanup',
2290
+ outcome: 'failure',
1928
2291
  stopOrigin: 'browser_native_control',
1929
- mediaTrack: mediaTrack
1930
- ? {
1931
- readyState: mediaTrack.readyState,
1932
- enabled: mediaTrack.enabled,
1933
- muted: mediaTrack.muted,
1934
- label: mediaTrack.label,
1935
- settings: mediaTrack.getSettings?.(),
1936
- }
1937
- : null,
2292
+ ...getScreenshareErrorDetails(error),
1938
2293
  },
1939
2294
  );
1940
- await cleanupScreenshare(
1941
- 'browser_native_control',
1942
- true,
1943
- nativeStopAttemptId,
1944
- );
1945
- });
1946
- } catch (e) {
1947
- logger.error(
2295
+ }
2296
+ logger.log(
1948
2297
  LogSource.AgoraSDK,
1949
2298
  'API',
1950
- `[SCREENSHARE_JOURNEY] screen share start failed inside RTC engine at ${stage}`,
2299
+ getScreenshareSessionBoundaryMessage(
2300
+ 'end',
2301
+ journeyData.screenshareSessionId,
2302
+ ),
1951
2303
  {
1952
2304
  ...journeyData,
1953
- stage,
1954
- outcome: 'failure',
1955
- joined,
1956
- ...getScreenshareErrorDetails(e),
2305
+ action: 'stop',
2306
+ screenshareAttemptId: nativeStopAttemptId,
2307
+ stage: 'session_boundary',
2308
+ outcome: 'ended',
2309
+ stopOrigin: 'browser_native_control',
1957
2310
  },
1958
2311
  );
1959
- if (joined || this.screenStream.video || this.screenStream.audio) {
1960
- await cleanupScreenshare('startup_failure', false);
1961
- }
1962
- throw e;
1963
- }
1964
- } else {
1965
- logger.log(
2312
+ });
2313
+ } catch (e) {
2314
+ logger.error(
1966
2315
  LogSource.AgoraSDK,
1967
2316
  'API',
1968
- `[SCREENSHARE_JOURNEY] screen share stop entered RTC engine from ${journeyData.stopOrigin}`,
1969
- {...journeyData, stage: 'rtc_stop', outcome: 'started'},
2317
+ `[SCREENSHARE_JOURNEY] screen share start failed inside RTC engine at ${stage}`,
2318
+ {
2319
+ ...journeyData,
2320
+ stage,
2321
+ outcome: 'failure',
2322
+ joined,
2323
+ ...getScreenshareErrorDetails(e),
2324
+ },
1970
2325
  );
1971
- await cleanupScreenshare(journeyData.stopOrigin, true);
1972
- this.activeScreenshareJourneyContext = null;
2326
+ const cleanupOperations: Array<() => unknown> = [
2327
+ () => localAudioTrack?.stop(),
2328
+ () => localVideoTrack?.stop(),
2329
+ () => localAudioTrack?.close(),
2330
+ () => localVideoTrack?.close(),
2331
+ ];
2332
+ if (joined) {
2333
+ cleanupOperations.push(() => this.screenClient.leave());
2334
+ }
2335
+ try {
2336
+ for (const cleanupOperation of cleanupOperations) {
2337
+ try {
2338
+ await cleanupOperation();
2339
+ } catch (cleanupError) {
2340
+ logger.error(
2341
+ LogSource.AgoraSDK,
2342
+ 'API',
2343
+ `[SCREENSHARE_JOURNEY] screen share start failure cleanup operation failed`,
2344
+ {
2345
+ ...journeyData,
2346
+ stage: 'cleanup',
2347
+ outcome: 'failure',
2348
+ ...getScreenshareErrorDetails(cleanupError),
2349
+ },
2350
+ );
2351
+ }
2352
+ }
2353
+ } finally {
2354
+ this.screenStream = {};
2355
+ this.activeScreenshareLifecycle = null;
2356
+ this.inScreenshare = false;
2357
+ this.activeScreenshareJourneyContext = null;
2358
+ this.screenshareOperationState = 'inactive';
2359
+ }
2360
+ throw e;
1973
2361
  }
1974
2362
  }
1975
2363
  }