@stream-io/video-react-sdk 1.32.4 → 1.33.0

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 (119) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/css/embedded.css +3630 -0
  3. package/dist/css/embedded.css.map +1 -0
  4. package/dist/css/styles.css +13 -2
  5. package/dist/css/styles.css.map +1 -1
  6. package/dist/embedded-BackgroundFilters-RdXfNf6_.es.js +353 -0
  7. package/dist/embedded-BackgroundFilters-RdXfNf6_.es.js.map +1 -0
  8. package/dist/embedded-BackgroundFilters-Zu84SkRR.cjs.js +355 -0
  9. package/dist/embedded-BackgroundFilters-Zu84SkRR.cjs.js.map +1 -0
  10. package/dist/embedded-CallStatsLatencyChart-Bj5OSYzg.es.js +57 -0
  11. package/dist/embedded-CallStatsLatencyChart-Bj5OSYzg.es.js.map +1 -0
  12. package/dist/embedded-CallStatsLatencyChart-CpL1M_s0.cjs.js +59 -0
  13. package/dist/embedded-CallStatsLatencyChart-CpL1M_s0.cjs.js.map +1 -0
  14. package/dist/embedded.cjs.js +3410 -0
  15. package/dist/embedded.cjs.js.map +1 -0
  16. package/dist/embedded.d.ts +1 -0
  17. package/dist/embedded.es.js +3407 -0
  18. package/dist/embedded.es.js.map +1 -0
  19. package/dist/index.cjs.js +67 -202
  20. package/dist/index.cjs.js.map +1 -1
  21. package/dist/index.es.js +69 -204
  22. package/dist/index.es.js.map +1 -1
  23. package/dist/src/embedded/EmbeddedClientProvider.d.ts +21 -0
  24. package/dist/src/embedded/call/CallControls.d.ts +9 -0
  25. package/dist/src/embedded/call/CallHeader.d.ts +4 -0
  26. package/dist/src/embedded/call/CallLayout.d.ts +4 -0
  27. package/dist/src/embedded/call/CallStateRouter.d.ts +4 -0
  28. package/dist/src/embedded/call/EmbeddedCall.d.ts +6 -0
  29. package/dist/src/embedded/call/index.d.ts +1 -0
  30. package/dist/src/embedded/context/ConfigurationContext.d.ts +11 -0
  31. package/dist/src/embedded/context/index.d.ts +1 -0
  32. package/dist/src/embedded/hooks/index.d.ts +8 -0
  33. package/dist/src/embedded/hooks/useCallDuration.d.ts +7 -0
  34. package/dist/src/embedded/hooks/useEmbeddedClient.d.ts +22 -0
  35. package/dist/src/embedded/hooks/useInitializeCall.d.ts +11 -0
  36. package/dist/src/embedded/hooks/useInitializeVideoClient.d.ts +16 -0
  37. package/dist/src/embedded/hooks/useIsLivestreamPaused.d.ts +8 -0
  38. package/dist/src/embedded/hooks/useLayout.d.ts +9 -0
  39. package/dist/src/embedded/hooks/useNoiseCancellationLoader.d.ts +12 -0
  40. package/dist/src/embedded/hooks/useWakeLock.d.ts +5 -0
  41. package/dist/src/embedded/index.d.ts +3 -0
  42. package/dist/src/embedded/livestream/EmbeddedLivestream.d.ts +5 -0
  43. package/dist/src/embedded/livestream/LivestreamUI.d.ts +1 -0
  44. package/dist/src/embedded/livestream/host/HostLayout.d.ts +7 -0
  45. package/dist/src/embedded/livestream/host/HostStateRouter.d.ts +1 -0
  46. package/dist/src/embedded/livestream/index.d.ts +1 -0
  47. package/dist/src/embedded/livestream/viewer/ViewerLayout.d.ts +1 -0
  48. package/dist/src/embedded/livestream/viewer/ViewerLobby.d.ts +4 -0
  49. package/dist/src/embedded/livestream/viewer/ViewerStateRouter.d.ts +1 -0
  50. package/dist/src/embedded/shared/BlurToggleButton/BlurToggleButton.d.ts +2 -0
  51. package/dist/src/embedded/shared/CallFeedback/CallEndedScreen.d.ts +6 -0
  52. package/dist/src/embedded/shared/CallFeedback/CallFeedback.d.ts +4 -0
  53. package/dist/src/embedded/shared/CallFeedback/RatingScreen.d.ts +5 -0
  54. package/dist/src/embedded/shared/CallFeedback/StarRating.d.ts +6 -0
  55. package/dist/src/embedded/shared/CallFeedback/ThankYouScreen.d.ts +1 -0
  56. package/dist/src/embedded/shared/ConnectionNotification/ConnectionNotification.d.ts +1 -0
  57. package/dist/src/embedded/shared/EmbeddedParticipantViewUI/EmbeddedParticipantViewUI.d.ts +1 -0
  58. package/dist/src/embedded/shared/JoinError/JoinError.d.ts +5 -0
  59. package/dist/src/embedded/shared/Lobby/DeviceControls.d.ts +5 -0
  60. package/dist/src/embedded/shared/Lobby/DisabledDeviceButton.d.ts +6 -0
  61. package/dist/src/embedded/shared/Lobby/Lobby.d.ts +10 -0
  62. package/dist/src/embedded/shared/Lobby/ToggleCameraButton.d.ts +1 -0
  63. package/dist/src/embedded/shared/Lobby/ToggleMicButton.d.ts +1 -0
  64. package/dist/src/embedded/shared/Lobby/VideoPreviewFallbacks.d.ts +2 -0
  65. package/dist/src/embedded/shared/ViewersCount/ViewersCount.d.ts +5 -0
  66. package/dist/src/embedded/shared/index.d.ts +7 -0
  67. package/dist/src/embedded/types.d.ts +65 -0
  68. package/dist/src/hooks/usePersistedDevicePreferences.d.ts +3 -12
  69. package/dist/src/translations/index.d.ts +42 -1
  70. package/embedded.ts +1 -0
  71. package/package.json +18 -4
  72. package/src/core/components/CallLayout/LivestreamLayout.tsx +53 -41
  73. package/src/embedded/EmbeddedClientProvider.tsx +125 -0
  74. package/src/embedded/call/CallControls.tsx +124 -0
  75. package/src/embedded/call/CallHeader.tsx +30 -0
  76. package/src/embedded/call/CallLayout.tsx +66 -0
  77. package/src/embedded/call/CallStateRouter.tsx +56 -0
  78. package/src/embedded/call/EmbeddedCall.tsx +14 -0
  79. package/src/embedded/call/index.ts +1 -0
  80. package/src/embedded/context/ConfigurationContext.tsx +36 -0
  81. package/src/embedded/context/index.ts +1 -0
  82. package/src/embedded/hooks/index.ts +8 -0
  83. package/src/embedded/hooks/useCallDuration.ts +40 -0
  84. package/src/embedded/hooks/useEmbeddedClient.ts +64 -0
  85. package/src/embedded/hooks/useInitializeCall.ts +51 -0
  86. package/src/embedded/hooks/useInitializeVideoClient.ts +118 -0
  87. package/src/embedded/hooks/useIsLivestreamPaused.ts +44 -0
  88. package/src/embedded/hooks/useLayout.ts +100 -0
  89. package/src/embedded/hooks/useNoiseCancellationLoader.ts +62 -0
  90. package/src/embedded/hooks/useWakeLock.ts +33 -0
  91. package/src/embedded/index.ts +12 -0
  92. package/src/embedded/livestream/EmbeddedLivestream.tsx +16 -0
  93. package/src/embedded/livestream/LivestreamUI.tsx +17 -0
  94. package/src/embedded/livestream/host/HostLayout.tsx +210 -0
  95. package/src/embedded/livestream/host/HostStateRouter.tsx +100 -0
  96. package/src/embedded/livestream/index.ts +1 -0
  97. package/src/embedded/livestream/viewer/ViewerLayout.tsx +160 -0
  98. package/src/embedded/livestream/viewer/ViewerLobby.tsx +135 -0
  99. package/src/embedded/livestream/viewer/ViewerStateRouter.tsx +82 -0
  100. package/src/embedded/shared/BlurToggleButton/BlurToggleButton.tsx +75 -0
  101. package/src/embedded/shared/CallFeedback/CallEndedScreen.tsx +55 -0
  102. package/src/embedded/shared/CallFeedback/CallFeedback.tsx +51 -0
  103. package/src/embedded/shared/CallFeedback/RatingScreen.tsx +47 -0
  104. package/src/embedded/shared/CallFeedback/StarRating.tsx +46 -0
  105. package/src/embedded/shared/CallFeedback/ThankYouScreen.tsx +19 -0
  106. package/src/embedded/shared/ConnectionNotification/ConnectionNotification.tsx +59 -0
  107. package/src/embedded/shared/EmbeddedParticipantViewUI/EmbeddedParticipantViewUI.tsx +32 -0
  108. package/src/embedded/shared/JoinError/JoinError.tsx +27 -0
  109. package/src/embedded/shared/Lobby/DeviceControls.tsx +54 -0
  110. package/src/embedded/shared/Lobby/DisabledDeviceButton.tsx +21 -0
  111. package/src/embedded/shared/Lobby/Lobby.tsx +59 -0
  112. package/src/embedded/shared/Lobby/ToggleCameraButton.tsx +44 -0
  113. package/src/embedded/shared/Lobby/ToggleMicButton.tsx +48 -0
  114. package/src/embedded/shared/Lobby/VideoPreviewFallbacks.tsx +55 -0
  115. package/src/embedded/shared/ViewersCount/ViewersCount.tsx +18 -0
  116. package/src/embedded/shared/index.ts +7 -0
  117. package/src/embedded/types.ts +80 -0
  118. package/src/hooks/usePersistedDevicePreferences.ts +8 -307
  119. package/src/translations/en.json +44 -2
@@ -203,7 +203,12 @@ export const BackstageLayout = (props: BackstageLayoutProps) => {
203
203
  <span className="str-video__livestream-layout__starts-at">
204
204
  {startsAt.getTime() < Date.now()
205
205
  ? t('Livestream starts soon')
206
- : t('Livestream starts at {{ startsAt }}', { startsAt })}
206
+ : t('Livestream starts at {{ time }}', {
207
+ time: startsAt.toLocaleTimeString([], {
208
+ hour: '2-digit',
209
+ minute: '2-digit',
210
+ }),
211
+ })}
207
212
  </span>
208
213
  )}
209
214
  {showEarlyParticipantCount && (
@@ -259,49 +264,56 @@ const ParticipantOverlay = (props: {
259
264
  <div className="str-video__livestream-layout__overlay">
260
265
  {overlayBarVisible && (
261
266
  <div className="str-video__livestream-layout__overlay__bar">
262
- {showLiveBadge && (
263
- <span className="str-video__livestream-layout__live-badge">
264
- {t('Live')}
265
- </span>
266
- )}
267
- {showParticipantCount && (
268
- <span className="str-video__livestream-layout__viewers-count">
269
- {humanizeParticipantCount
270
- ? humanize(participantCount)
271
- : participantCount}
272
- </span>
273
- )}
274
- {showSpeakerName && (
275
- <span
276
- className="str-video__livestream-layout__speaker-name"
277
- title={participant.name || participant.userId || ''}
278
- >
279
- {participant.name || participant.userId || ''}
280
- </span>
281
- )}
282
- {showDuration && (
283
- <span className="str-video__livestream-layout__duration">
284
- {formatDuration(duration)}
285
- </span>
286
- )}
287
- {showMuteButton && (
288
- <span
289
- className={clsx(
290
- 'str-video__livestream-layout__mute-button',
291
- isSpeakerMuted &&
292
- 'str-video__livestream-layout__mute-button--muted',
293
- )}
294
- onClick={() => speaker.setVolume(isSpeakerMuted ? 1 : 0)}
295
- />
296
- )}
297
- {enableFullScreen &&
298
- participantViewElement &&
299
- typeof participantViewElement.requestFullscreen !== 'undefined' && (
267
+ <div className="str-video__livestream-layout__overlay__bar-left">
268
+ {showLiveBadge && (
269
+ <span className="str-video__livestream-layout__live-badge">
270
+ {t('Live')}
271
+ </span>
272
+ )}
273
+ {showParticipantCount && (
274
+ <span className="str-video__livestream-layout__viewers-count">
275
+ {humanizeParticipantCount
276
+ ? humanize(participantCount)
277
+ : participantCount}
278
+ </span>
279
+ )}
280
+ {showSpeakerName && (
281
+ <span
282
+ className="str-video__livestream-layout__speaker-name"
283
+ title={participant.name || participant.userId || ''}
284
+ >
285
+ {participant.name || participant.userId || ''}
286
+ </span>
287
+ )}
288
+ </div>
289
+ <div className="str-video__livestream-layout__overlay__bar-center">
290
+ {showDuration && (
291
+ <span className="str-video__livestream-layout__duration">
292
+ {formatDuration(duration)}
293
+ </span>
294
+ )}
295
+ </div>
296
+ <div className="str-video__livestream-layout__overlay__bar-right">
297
+ {showMuteButton && (
300
298
  <span
301
- className="str-video__livestream-layout__go-fullscreen"
302
- onClick={toggleFullScreen}
299
+ className={clsx(
300
+ 'str-video__livestream-layout__mute-button',
301
+ isSpeakerMuted &&
302
+ 'str-video__livestream-layout__mute-button--muted',
303
+ )}
304
+ onClick={() => speaker.setVolume(isSpeakerMuted ? 1 : 0)}
303
305
  />
304
306
  )}
307
+ {enableFullScreen &&
308
+ participantViewElement &&
309
+ typeof participantViewElement.requestFullscreen !==
310
+ 'undefined' && (
311
+ <span
312
+ className="str-video__livestream-layout__go-fullscreen"
313
+ onClick={toggleFullScreen}
314
+ />
315
+ )}
316
+ </div>
305
317
  </div>
306
318
  )}
307
319
  </div>
@@ -0,0 +1,125 @@
1
+ import { useCallback, useState, type ReactNode } from 'react';
2
+ import type { INoiseCancellation } from '@stream-io/audio-filters-web';
3
+ import { useEffectEvent as useEffectEventShim } from '@stream-io/video-react-bindings';
4
+
5
+ import { StreamCall, StreamVideo } from '../core';
6
+ import {
7
+ StreamTheme,
8
+ BackgroundFiltersProvider,
9
+ NoiseCancellationProvider,
10
+ } from '../components';
11
+ import { ConfigurationProvider } from './context';
12
+ import { useEmbeddedClient } from './hooks';
13
+ import type { LogLevel, TokenProvider } from '@stream-io/video-client';
14
+ import type { EmbeddedUser, LayoutOption } from './types';
15
+ import { LoadingIndicator } from '../components';
16
+
17
+ export interface EmbeddedClientProviderProps {
18
+ apiKey: string;
19
+ user: EmbeddedUser;
20
+ callId: string;
21
+ callType: string;
22
+ token?: string;
23
+ tokenProvider?: TokenProvider;
24
+ logLevel?: LogLevel;
25
+ onError?: (error: any) => void;
26
+ layout?: LayoutOption;
27
+ theme?: Record<string, string>;
28
+ children: ReactNode;
29
+ }
30
+
31
+ const NoiseCancellationWrapper = ({
32
+ noiseCancellation,
33
+ children,
34
+ }: {
35
+ noiseCancellation?: INoiseCancellation;
36
+ children: ReactNode;
37
+ }) => {
38
+ if (!noiseCancellation) {
39
+ return <>{children}</>;
40
+ }
41
+
42
+ return (
43
+ <NoiseCancellationProvider noiseCancellation={noiseCancellation}>
44
+ {children}
45
+ </NoiseCancellationProvider>
46
+ );
47
+ };
48
+
49
+ /**
50
+ * Shared provider wrapper for embedded components.
51
+ * Handles client/call initialization and wraps children with all necessary providers.
52
+ */
53
+ export const EmbeddedClientProvider = ({
54
+ apiKey,
55
+ user,
56
+ callId,
57
+ callType,
58
+ token,
59
+ tokenProvider,
60
+ logLevel,
61
+ onError,
62
+ layout,
63
+ theme,
64
+ children,
65
+ }: EmbeddedClientProviderProps) => {
66
+ const [showError, setShowError] = useState<boolean>(false);
67
+
68
+ const onErrorStable = useEffectEventShim(onError ?? console.error);
69
+ const handleError = useCallback(
70
+ (error: any) => {
71
+ setShowError(true);
72
+ onErrorStable(error);
73
+ },
74
+ [onErrorStable],
75
+ );
76
+
77
+ const { client, call, noiseCancellation, noiseCancellationReady } =
78
+ useEmbeddedClient({
79
+ apiKey,
80
+ user,
81
+ callId,
82
+ callType,
83
+ token,
84
+ tokenProvider,
85
+ logLevel,
86
+ handleError,
87
+ });
88
+
89
+ if (showError) {
90
+ return (
91
+ <StreamTheme className="str-video__embedded">
92
+ <div className="str-video__embedded-error">
93
+ <p className="str-video__embedded-error__message">
94
+ An error occurred while initializing the client. Please try again
95
+ later.
96
+ </p>
97
+ </div>
98
+ </StreamTheme>
99
+ );
100
+ }
101
+
102
+ if (!call || !client || !noiseCancellationReady) {
103
+ return (
104
+ <StreamTheme className="str-video__embedded">
105
+ <LoadingIndicator className="str-video__embedded-loading" />
106
+ </StreamTheme>
107
+ );
108
+ }
109
+
110
+ return (
111
+ <StreamVideo client={client}>
112
+ <StreamCall call={call}>
113
+ <ConfigurationProvider layout={layout} onError={onErrorStable}>
114
+ <BackgroundFiltersProvider>
115
+ <NoiseCancellationWrapper noiseCancellation={noiseCancellation}>
116
+ <StreamTheme className="str-video__embedded" style={theme}>
117
+ {children}
118
+ </StreamTheme>
119
+ </NoiseCancellationWrapper>
120
+ </BackgroundFiltersProvider>
121
+ </ConfigurationProvider>
122
+ </StreamCall>
123
+ </StreamVideo>
124
+ );
125
+ };
@@ -0,0 +1,124 @@
1
+ import { OwnCapability } from '@stream-io/video-client';
2
+ import {
3
+ Restricted,
4
+ useCallStateHooks,
5
+ useI18n,
6
+ } from '@stream-io/video-react-bindings';
7
+ import {
8
+ CancelCallConfirmButton,
9
+ CompositeButton,
10
+ DeviceSelectorAudioInput,
11
+ DeviceSelectorAudioOutput,
12
+ Icon,
13
+ MicCaptureErrorNotification,
14
+ ReactionsButton,
15
+ RecordCallConfirmationButton,
16
+ ScreenShareButton,
17
+ ToggleAudioPublishingButton,
18
+ ToggleVideoPublishingButton,
19
+ WithTooltip,
20
+ } from '../../components';
21
+
22
+ import { useCallDuration } from '../hooks';
23
+ import { CameraMenuWithBlur } from '../shared';
24
+
25
+ interface CallControlsProps {
26
+ showParticipants: boolean;
27
+ onToggleParticipants: () => void;
28
+ }
29
+
30
+ /**
31
+ * Renders the active call control bar
32
+ */
33
+ export const CallControls = ({
34
+ showParticipants,
35
+ onToggleParticipants,
36
+ }: CallControlsProps) => {
37
+ const { t } = useI18n();
38
+ const { useCallSession } = useCallStateHooks();
39
+ const session = useCallSession();
40
+ const startedAt = session?.started_at;
41
+ const { elapsed } = useCallDuration(startedAt);
42
+
43
+ return (
44
+ <div className="str-video__embedded-call-controls str-video__call-controls">
45
+ <div className="str-video__call-controls--group str-video__call-controls--options">
46
+ <Restricted requiredGrants={[OwnCapability.CREATE_REACTION]}>
47
+ <div className="str-video__embedded-mobile">
48
+ <ReactionsButton />
49
+ </div>
50
+ </Restricted>
51
+ {startedAt && (
52
+ <div className="str-video__embedded-call-duration str-video__embedded-desktop">
53
+ <Icon
54
+ icon="verified"
55
+ className="str-video__embedded-call-duration__icon"
56
+ />
57
+ <span className="str-video__embedded-call-duration__time">
58
+ {elapsed}
59
+ </span>
60
+ </div>
61
+ )}
62
+ </div>
63
+ <div className="str-video__call-controls--group str-video__call-controls--media">
64
+ <Restricted
65
+ requiredGrants={[OwnCapability.SEND_AUDIO]}
66
+ hasPermissionsOnly
67
+ >
68
+ <MicCaptureErrorNotification>
69
+ <ToggleAudioPublishingButton
70
+ Menu={
71
+ <>
72
+ <DeviceSelectorAudioInput
73
+ visualType="list"
74
+ title={t('Microphone')}
75
+ />
76
+ <DeviceSelectorAudioOutput
77
+ visualType="list"
78
+ title={t('Speaker')}
79
+ />
80
+ </>
81
+ }
82
+ menuPlacement="top"
83
+ />
84
+ </MicCaptureErrorNotification>
85
+ </Restricted>
86
+ <Restricted
87
+ requiredGrants={[OwnCapability.SEND_VIDEO]}
88
+ hasPermissionsOnly
89
+ >
90
+ <ToggleVideoPublishingButton
91
+ Menu={<CameraMenuWithBlur />}
92
+ menuPlacement="top"
93
+ />
94
+ </Restricted>
95
+ <Restricted requiredGrants={[OwnCapability.CREATE_REACTION]}>
96
+ <div className="str-video__embedded-desktop">
97
+ <ReactionsButton />
98
+ </div>
99
+ </Restricted>
100
+ <Restricted requiredGrants={[OwnCapability.SCREENSHARE]}>
101
+ <div className="str-video__embedded-desktop">
102
+ <ScreenShareButton />
103
+ </div>
104
+ </Restricted>
105
+ <RecordCallConfirmationButton />
106
+ <div className="str-video__embedded-desktop">
107
+ <CancelCallConfirmButton />
108
+ </div>
109
+ </div>
110
+ <div className="str-video__call-controls--group str-video__call-controls--sidebar">
111
+ <WithTooltip title={t('Participants')}>
112
+ <CompositeButton
113
+ active={showParticipants}
114
+ aria-label={t('Participants')}
115
+ aria-pressed={showParticipants}
116
+ onClick={onToggleParticipants}
117
+ >
118
+ <Icon icon="participants" />
119
+ </CompositeButton>
120
+ </WithTooltip>
121
+ </div>
122
+ </div>
123
+ );
124
+ };
@@ -0,0 +1,30 @@
1
+ import { useCallStateHooks } from '@stream-io/video-react-bindings';
2
+ import { useCallDuration } from '../hooks';
3
+ import { CancelCallConfirmButton, Icon } from '../../components';
4
+
5
+ /**
6
+ * Renders the call header bar with elapsed time and leave/end call button.
7
+ */
8
+ export const CallHeader = () => {
9
+ const { useCallSession } = useCallStateHooks();
10
+ const session = useCallSession();
11
+ const startedAt = session?.started_at;
12
+ const { elapsed } = useCallDuration(startedAt);
13
+
14
+ return (
15
+ <div className="str-video__embedded-call-header">
16
+ {startedAt && (
17
+ <div className="str-video__embedded-call-duration">
18
+ <Icon
19
+ icon="verified"
20
+ className="str-video__embedded-call-duration__icon"
21
+ />
22
+ <span className="str-video__embedded-call-duration__time">
23
+ {elapsed}
24
+ </span>
25
+ </div>
26
+ )}
27
+ <CancelCallConfirmButton />
28
+ </div>
29
+ );
30
+ };
@@ -0,0 +1,66 @@
1
+ import { useCallback, useState } from 'react';
2
+ import clsx from 'clsx';
3
+ import { OwnCapability } from '@stream-io/video-client';
4
+ import { Restricted } from '@stream-io/video-react-bindings';
5
+ import {
6
+ CallParticipantsList,
7
+ PermissionRequests,
8
+ SpeakingWhileMutedNotification,
9
+ } from '../../components';
10
+
11
+ import { useLayout, useWakeLock } from '../hooks';
12
+ import { ConnectionNotification } from '../shared';
13
+ import { CallControls } from './CallControls';
14
+ import { CallHeader } from './CallHeader';
15
+
16
+ /**
17
+ * CallLayout renders the active call experience with layout, controls and sidebar.
18
+ */
19
+ export const CallLayout = () => {
20
+ useWakeLock();
21
+ const [showParticipants, setShowParticipants] = useState(false);
22
+
23
+ const { Component: LayoutComponent, props: layoutProps } = useLayout();
24
+
25
+ const handleToggleParticipants = useCallback(() => {
26
+ setShowParticipants((prev) => !prev);
27
+ }, []);
28
+
29
+ return (
30
+ <div className="str-video__embedded-call">
31
+ <ConnectionNotification />
32
+ <PermissionRequests />
33
+ <div className="str-video__embedded-notifications">
34
+ <Restricted
35
+ requiredGrants={[OwnCapability.SEND_AUDIO]}
36
+ hasPermissionsOnly
37
+ >
38
+ <SpeakingWhileMutedNotification />
39
+ </Restricted>
40
+ </div>
41
+ <CallHeader />
42
+ <div className="str-video__embedded-layout">
43
+ <div className="str-video__embedded-layout__stage">
44
+ <LayoutComponent {...layoutProps} />
45
+ </div>
46
+
47
+ <div
48
+ className={clsx(
49
+ 'str-video__embedded-sidebar',
50
+ showParticipants && 'str-video__embedded-sidebar--open',
51
+ )}
52
+ >
53
+ {showParticipants && (
54
+ <div className="str-video__embedded-participants">
55
+ <CallParticipantsList onClose={handleToggleParticipants} />
56
+ </div>
57
+ )}
58
+ </div>
59
+ </div>
60
+ <CallControls
61
+ showParticipants={showParticipants}
62
+ onToggleParticipants={handleToggleParticipants}
63
+ />
64
+ </div>
65
+ );
66
+ };
@@ -0,0 +1,56 @@
1
+ import { CallingState } from '@stream-io/video-client';
2
+ import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
3
+
4
+ import { LoadingIndicator } from '../../components';
5
+ import { CallFeedback, JoinError, Lobby } from '../shared';
6
+ import { CallLayout } from './CallLayout';
7
+ import { useCallback, useState } from 'react';
8
+ import { useEmbeddedConfiguration } from '../context';
9
+
10
+ /**
11
+ * CallStateRouter is the state decider component that manages view state transitions.
12
+ */
13
+ export const CallStateRouter = () => {
14
+ const call = useCall();
15
+
16
+ const { useCallCallingState, useLocalParticipant } = useCallStateHooks();
17
+ const localParticipant = useLocalParticipant();
18
+ const callingState = useCallCallingState();
19
+ const { onError } = useEmbeddedConfiguration();
20
+
21
+ const [joinError, setJoinError] = useState(false);
22
+ const handleJoin = useCallback(async () => {
23
+ if (!call) return;
24
+
25
+ setJoinError(false);
26
+ try {
27
+ if (call.state.callingState !== CallingState.JOINED) {
28
+ await call.join();
29
+ }
30
+ } catch (e) {
31
+ onError?.(e);
32
+ setJoinError(true);
33
+ }
34
+ }, [call, onError]);
35
+
36
+ if (joinError) {
37
+ return <JoinError onJoin={handleJoin} />;
38
+ }
39
+
40
+ if (
41
+ callingState === CallingState.IDLE ||
42
+ callingState === CallingState.UNKNOWN
43
+ ) {
44
+ return <Lobby onJoin={handleJoin} />;
45
+ }
46
+
47
+ if (callingState === CallingState.JOINING && !localParticipant) {
48
+ return <LoadingIndicator className="str-video__embedded-loading" />;
49
+ }
50
+
51
+ if (callingState === CallingState.LEFT) {
52
+ return <CallFeedback onJoin={handleJoin} />;
53
+ }
54
+
55
+ return <CallLayout />;
56
+ };
@@ -0,0 +1,14 @@
1
+ import type { EmbeddedMeetingProps } from '../types';
2
+ import { EmbeddedClientProvider } from '../EmbeddedClientProvider';
3
+ import { CallStateRouter } from './CallStateRouter';
4
+
5
+ /**
6
+ * Drop-in video call component that renders a lobby, active call,
7
+ * and post-call feedback screen. Handles client and call setup internally.
8
+ */
9
+ export const EmbeddedCall = ({ children, ...props }: EmbeddedMeetingProps) => (
10
+ <EmbeddedClientProvider {...props}>
11
+ <CallStateRouter />
12
+ {children}
13
+ </EmbeddedClientProvider>
14
+ );
@@ -0,0 +1 @@
1
+ export * from './EmbeddedCall';
@@ -0,0 +1,36 @@
1
+ import { createContext, useContext, useMemo, PropsWithChildren } from 'react';
2
+ import type { LayoutOption } from '../types';
3
+
4
+ export interface EmbeddedConfiguration {
5
+ layout?: LayoutOption;
6
+ onError?: (error: any) => void;
7
+ }
8
+
9
+ const defaultConfiguration: EmbeddedConfiguration = {
10
+ layout: 'SpeakerTop',
11
+ onError: undefined,
12
+ };
13
+
14
+ const ConfigurationContext =
15
+ createContext<EmbeddedConfiguration>(defaultConfiguration);
16
+
17
+ export const ConfigurationProvider = ({
18
+ children,
19
+ layout = 'SpeakerTop',
20
+ onError,
21
+ }: PropsWithChildren<EmbeddedConfiguration>) => {
22
+ const value = useMemo(() => ({ layout, onError }), [layout, onError]);
23
+
24
+ return (
25
+ <ConfigurationContext.Provider value={value}>
26
+ {children}
27
+ </ConfigurationContext.Provider>
28
+ );
29
+ };
30
+
31
+ /**
32
+ * Hook to access embedded configuration settings.
33
+ */
34
+ export const useEmbeddedConfiguration = () => {
35
+ return useContext(ConfigurationContext);
36
+ };
@@ -0,0 +1 @@
1
+ export * from './ConfigurationContext';
@@ -0,0 +1,8 @@
1
+ export * from './useInitializeVideoClient';
2
+ export * from './useInitializeCall';
3
+ export * from './useLayout';
4
+ export * from './useNoiseCancellationLoader';
5
+ export * from './useWakeLock';
6
+ export * from './useCallDuration';
7
+ export * from './useEmbeddedClient';
8
+ export * from './useIsLivestreamPaused';
@@ -0,0 +1,40 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+
3
+ const formatElapsed = (seconds: number) => {
4
+ const h = Math.floor(seconds / 3600);
5
+ const m = Math.floor((seconds % 3600) / 60);
6
+ const s = seconds % 60;
7
+ const pad = (n: number) => String(n).padStart(2, '0');
8
+ return h > 0 ? `${pad(h)}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
9
+ };
10
+
11
+ /**
12
+ * Returns a live-updating formatted elapsed duration string
13
+ * computed from the given start date.
14
+ */
15
+ export const useCallDuration = (startedAt?: string) => {
16
+ const startedAtDate = useMemo(
17
+ () => (startedAt ? new Date(startedAt).getTime() : undefined),
18
+ [startedAt],
19
+ );
20
+
21
+ const [elapsed, setElapsed] = useState('');
22
+
23
+ useEffect(() => {
24
+ if (!startedAtDate) return;
25
+
26
+ const update = () => {
27
+ const seconds = Math.max(
28
+ 0,
29
+ Math.floor((Date.now() - startedAtDate) / 1000),
30
+ );
31
+ setElapsed(formatElapsed(seconds));
32
+ };
33
+
34
+ update();
35
+ const interval = setInterval(update, 1000);
36
+ return () => clearInterval(interval);
37
+ }, [startedAtDate]);
38
+
39
+ return { elapsed };
40
+ };
@@ -0,0 +1,64 @@
1
+ import { useEffect } from 'react';
2
+ import { useInitializeVideoClient } from './useInitializeVideoClient';
3
+ import { useInitializeCall } from './useInitializeCall';
4
+ import { useNoiseCancellationLoader } from './useNoiseCancellationLoader';
5
+ import type { EmbeddedUser } from '../types';
6
+ import { LogLevel, TokenProvider } from '@stream-io/video-client';
7
+
8
+ export interface UseEmbeddedClientProps {
9
+ apiKey: string;
10
+ user: EmbeddedUser;
11
+ callId: string;
12
+ callType: string;
13
+ token?: string;
14
+ tokenProvider?: TokenProvider;
15
+ logLevel?: LogLevel;
16
+ handleError: (error: any) => void;
17
+ }
18
+
19
+ /**
20
+ * Hook that initializes the Stream Video client and call.
21
+ * Combines useInitializeVideoClient, useInitializeCall, and useNoiseCancellationLoader.
22
+ */
23
+ export const useEmbeddedClient = ({
24
+ apiKey,
25
+ user,
26
+ callId,
27
+ callType,
28
+ token,
29
+ tokenProvider,
30
+ logLevel,
31
+ handleError,
32
+ }: UseEmbeddedClientProps) => {
33
+ const client = useInitializeVideoClient({
34
+ apiKey,
35
+ user,
36
+ token,
37
+ tokenProvider,
38
+ logLevel,
39
+ handleError,
40
+ });
41
+
42
+ const call = useInitializeCall({
43
+ client,
44
+ callType,
45
+ callId,
46
+ handleError,
47
+ });
48
+
49
+ useEffect(() => {
50
+ if (!call) return;
51
+
52
+ call.tracer.trace('embedded.initialized', { callType });
53
+ }, [call, callType]);
54
+
55
+ const { noiseCancellation, ready: noiseCancellationReady } =
56
+ useNoiseCancellationLoader(call);
57
+
58
+ return {
59
+ client,
60
+ call,
61
+ noiseCancellation,
62
+ noiseCancellationReady,
63
+ };
64
+ };