@stream-io/video-react-sdk 1.32.3 → 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.
- package/CHANGELOG.md +20 -0
- package/dist/css/embedded.css +3630 -0
- package/dist/css/embedded.css.map +1 -0
- package/dist/css/styles.css +13 -2
- package/dist/css/styles.css.map +1 -1
- package/dist/embedded-BackgroundFilters-RdXfNf6_.es.js +353 -0
- package/dist/embedded-BackgroundFilters-RdXfNf6_.es.js.map +1 -0
- package/dist/embedded-BackgroundFilters-Zu84SkRR.cjs.js +355 -0
- package/dist/embedded-BackgroundFilters-Zu84SkRR.cjs.js.map +1 -0
- package/dist/embedded-CallStatsLatencyChart-Bj5OSYzg.es.js +57 -0
- package/dist/embedded-CallStatsLatencyChart-Bj5OSYzg.es.js.map +1 -0
- package/dist/embedded-CallStatsLatencyChart-CpL1M_s0.cjs.js +59 -0
- package/dist/embedded-CallStatsLatencyChart-CpL1M_s0.cjs.js.map +1 -0
- package/dist/embedded.cjs.js +3410 -0
- package/dist/embedded.cjs.js.map +1 -0
- package/dist/embedded.d.ts +1 -0
- package/dist/embedded.es.js +3407 -0
- package/dist/embedded.es.js.map +1 -0
- package/dist/index.cjs.js +67 -202
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +69 -204
- package/dist/index.es.js.map +1 -1
- package/dist/src/embedded/EmbeddedClientProvider.d.ts +21 -0
- package/dist/src/embedded/call/CallControls.d.ts +9 -0
- package/dist/src/embedded/call/CallHeader.d.ts +4 -0
- package/dist/src/embedded/call/CallLayout.d.ts +4 -0
- package/dist/src/embedded/call/CallStateRouter.d.ts +4 -0
- package/dist/src/embedded/call/EmbeddedCall.d.ts +6 -0
- package/dist/src/embedded/call/index.d.ts +1 -0
- package/dist/src/embedded/context/ConfigurationContext.d.ts +11 -0
- package/dist/src/embedded/context/index.d.ts +1 -0
- package/dist/src/embedded/hooks/index.d.ts +8 -0
- package/dist/src/embedded/hooks/useCallDuration.d.ts +7 -0
- package/dist/src/embedded/hooks/useEmbeddedClient.d.ts +22 -0
- package/dist/src/embedded/hooks/useInitializeCall.d.ts +11 -0
- package/dist/src/embedded/hooks/useInitializeVideoClient.d.ts +16 -0
- package/dist/src/embedded/hooks/useIsLivestreamPaused.d.ts +8 -0
- package/dist/src/embedded/hooks/useLayout.d.ts +9 -0
- package/dist/src/embedded/hooks/useNoiseCancellationLoader.d.ts +12 -0
- package/dist/src/embedded/hooks/useWakeLock.d.ts +5 -0
- package/dist/src/embedded/index.d.ts +3 -0
- package/dist/src/embedded/livestream/EmbeddedLivestream.d.ts +5 -0
- package/dist/src/embedded/livestream/LivestreamUI.d.ts +1 -0
- package/dist/src/embedded/livestream/host/HostLayout.d.ts +7 -0
- package/dist/src/embedded/livestream/host/HostStateRouter.d.ts +1 -0
- package/dist/src/embedded/livestream/index.d.ts +1 -0
- package/dist/src/embedded/livestream/viewer/ViewerLayout.d.ts +1 -0
- package/dist/src/embedded/livestream/viewer/ViewerLobby.d.ts +4 -0
- package/dist/src/embedded/livestream/viewer/ViewerStateRouter.d.ts +1 -0
- package/dist/src/embedded/shared/BlurToggleButton/BlurToggleButton.d.ts +2 -0
- package/dist/src/embedded/shared/CallFeedback/CallEndedScreen.d.ts +6 -0
- package/dist/src/embedded/shared/CallFeedback/CallFeedback.d.ts +4 -0
- package/dist/src/embedded/shared/CallFeedback/RatingScreen.d.ts +5 -0
- package/dist/src/embedded/shared/CallFeedback/StarRating.d.ts +6 -0
- package/dist/src/embedded/shared/CallFeedback/ThankYouScreen.d.ts +1 -0
- package/dist/src/embedded/shared/ConnectionNotification/ConnectionNotification.d.ts +1 -0
- package/dist/src/embedded/shared/EmbeddedParticipantViewUI/EmbeddedParticipantViewUI.d.ts +1 -0
- package/dist/src/embedded/shared/JoinError/JoinError.d.ts +5 -0
- package/dist/src/embedded/shared/Lobby/DeviceControls.d.ts +5 -0
- package/dist/src/embedded/shared/Lobby/DisabledDeviceButton.d.ts +6 -0
- package/dist/src/embedded/shared/Lobby/Lobby.d.ts +10 -0
- package/dist/src/embedded/shared/Lobby/ToggleCameraButton.d.ts +1 -0
- package/dist/src/embedded/shared/Lobby/ToggleMicButton.d.ts +1 -0
- package/dist/src/embedded/shared/Lobby/VideoPreviewFallbacks.d.ts +2 -0
- package/dist/src/embedded/shared/ViewersCount/ViewersCount.d.ts +5 -0
- package/dist/src/embedded/shared/index.d.ts +7 -0
- package/dist/src/embedded/types.d.ts +65 -0
- package/dist/src/hooks/usePersistedDevicePreferences.d.ts +3 -12
- package/dist/src/translations/index.d.ts +42 -1
- package/embedded.ts +1 -0
- package/package.json +18 -4
- package/src/core/components/CallLayout/LivestreamLayout.tsx +53 -41
- package/src/embedded/EmbeddedClientProvider.tsx +125 -0
- package/src/embedded/call/CallControls.tsx +124 -0
- package/src/embedded/call/CallHeader.tsx +30 -0
- package/src/embedded/call/CallLayout.tsx +66 -0
- package/src/embedded/call/CallStateRouter.tsx +56 -0
- package/src/embedded/call/EmbeddedCall.tsx +14 -0
- package/src/embedded/call/index.ts +1 -0
- package/src/embedded/context/ConfigurationContext.tsx +36 -0
- package/src/embedded/context/index.ts +1 -0
- package/src/embedded/hooks/index.ts +8 -0
- package/src/embedded/hooks/useCallDuration.ts +40 -0
- package/src/embedded/hooks/useEmbeddedClient.ts +64 -0
- package/src/embedded/hooks/useInitializeCall.ts +51 -0
- package/src/embedded/hooks/useInitializeVideoClient.ts +118 -0
- package/src/embedded/hooks/useIsLivestreamPaused.ts +44 -0
- package/src/embedded/hooks/useLayout.ts +100 -0
- package/src/embedded/hooks/useNoiseCancellationLoader.ts +62 -0
- package/src/embedded/hooks/useWakeLock.ts +33 -0
- package/src/embedded/index.ts +12 -0
- package/src/embedded/livestream/EmbeddedLivestream.tsx +16 -0
- package/src/embedded/livestream/LivestreamUI.tsx +17 -0
- package/src/embedded/livestream/host/HostLayout.tsx +210 -0
- package/src/embedded/livestream/host/HostStateRouter.tsx +100 -0
- package/src/embedded/livestream/index.ts +1 -0
- package/src/embedded/livestream/viewer/ViewerLayout.tsx +160 -0
- package/src/embedded/livestream/viewer/ViewerLobby.tsx +135 -0
- package/src/embedded/livestream/viewer/ViewerStateRouter.tsx +82 -0
- package/src/embedded/shared/BlurToggleButton/BlurToggleButton.tsx +75 -0
- package/src/embedded/shared/CallFeedback/CallEndedScreen.tsx +55 -0
- package/src/embedded/shared/CallFeedback/CallFeedback.tsx +51 -0
- package/src/embedded/shared/CallFeedback/RatingScreen.tsx +47 -0
- package/src/embedded/shared/CallFeedback/StarRating.tsx +46 -0
- package/src/embedded/shared/CallFeedback/ThankYouScreen.tsx +19 -0
- package/src/embedded/shared/ConnectionNotification/ConnectionNotification.tsx +59 -0
- package/src/embedded/shared/EmbeddedParticipantViewUI/EmbeddedParticipantViewUI.tsx +32 -0
- package/src/embedded/shared/JoinError/JoinError.tsx +27 -0
- package/src/embedded/shared/Lobby/DeviceControls.tsx +54 -0
- package/src/embedded/shared/Lobby/DisabledDeviceButton.tsx +21 -0
- package/src/embedded/shared/Lobby/Lobby.tsx +59 -0
- package/src/embedded/shared/Lobby/ToggleCameraButton.tsx +44 -0
- package/src/embedded/shared/Lobby/ToggleMicButton.tsx +48 -0
- package/src/embedded/shared/Lobby/VideoPreviewFallbacks.tsx +55 -0
- package/src/embedded/shared/ViewersCount/ViewersCount.tsx +18 -0
- package/src/embedded/shared/index.ts +7 -0
- package/src/embedded/types.ts +80 -0
- package/src/hooks/usePersistedDevicePreferences.ts +8 -307
- 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 {{
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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=
|
|
302
|
-
|
|
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
|
+
};
|