@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
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import { CallingState } from '@stream-io/video-client';
|
|
3
|
+
import {
|
|
4
|
+
useCall,
|
|
5
|
+
useCallStateHooks,
|
|
6
|
+
useI18n,
|
|
7
|
+
} from '@stream-io/video-react-bindings';
|
|
8
|
+
|
|
9
|
+
import { HostLayout } from './HostLayout';
|
|
10
|
+
import { LoadingIndicator } from '../../../components';
|
|
11
|
+
import { CallFeedback, JoinError, Lobby } from '../../shared';
|
|
12
|
+
import { useEmbeddedConfiguration } from '../../context';
|
|
13
|
+
|
|
14
|
+
export const HostStateRouter = () => {
|
|
15
|
+
const call = useCall();
|
|
16
|
+
const { t } = useI18n();
|
|
17
|
+
const { onError } = useEmbeddedConfiguration();
|
|
18
|
+
const {
|
|
19
|
+
useCallCallingState,
|
|
20
|
+
useIsCallLive,
|
|
21
|
+
useCallSettings,
|
|
22
|
+
useLocalParticipant,
|
|
23
|
+
} = useCallStateHooks();
|
|
24
|
+
const callingState = useCallCallingState();
|
|
25
|
+
const isLive = useIsCallLive();
|
|
26
|
+
const localParticipant = useLocalParticipant();
|
|
27
|
+
const settings = useCallSettings();
|
|
28
|
+
const isBackstageEnabled = settings?.backstage?.enabled ?? true;
|
|
29
|
+
|
|
30
|
+
const [joinError, setJoinError] = useState(false);
|
|
31
|
+
const handleJoin = useCallback(async () => {
|
|
32
|
+
if (!call) return;
|
|
33
|
+
|
|
34
|
+
setJoinError(false);
|
|
35
|
+
try {
|
|
36
|
+
if (callingState !== CallingState.JOINED) {
|
|
37
|
+
await call.join();
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
onError?.(err);
|
|
41
|
+
setJoinError(true);
|
|
42
|
+
}
|
|
43
|
+
}, [call, onError, callingState]);
|
|
44
|
+
|
|
45
|
+
const handleGoLive = useCallback(async () => {
|
|
46
|
+
if (!call) return;
|
|
47
|
+
try {
|
|
48
|
+
await call.goLive();
|
|
49
|
+
} catch (err) {
|
|
50
|
+
onError?.(err);
|
|
51
|
+
}
|
|
52
|
+
}, [call, onError]);
|
|
53
|
+
|
|
54
|
+
const handleStopLive = useCallback(async () => {
|
|
55
|
+
if (!call) return;
|
|
56
|
+
try {
|
|
57
|
+
await call.stopLive();
|
|
58
|
+
} catch (err) {
|
|
59
|
+
onError?.(err);
|
|
60
|
+
}
|
|
61
|
+
}, [call, onError]);
|
|
62
|
+
|
|
63
|
+
if (joinError) {
|
|
64
|
+
return <JoinError onJoin={handleJoin} />;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
callingState === CallingState.IDLE ||
|
|
69
|
+
callingState === CallingState.UNKNOWN
|
|
70
|
+
) {
|
|
71
|
+
return (
|
|
72
|
+
<Lobby
|
|
73
|
+
onJoin={handleJoin}
|
|
74
|
+
title={
|
|
75
|
+
isBackstageEnabled
|
|
76
|
+
? t('Prepare your livestream')
|
|
77
|
+
: t('Ready to go live')
|
|
78
|
+
}
|
|
79
|
+
joinLabel={isBackstageEnabled ? t('Enter Backstage') : t('Go Live')}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (callingState === CallingState.JOINING && !localParticipant) {
|
|
85
|
+
return <LoadingIndicator className="str-video__embedded-loading" />;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (callingState === CallingState.LEFT) {
|
|
89
|
+
return <CallFeedback onJoin={handleJoin} />;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<HostLayout
|
|
94
|
+
isLive={isLive}
|
|
95
|
+
isBackstageEnabled={isBackstageEnabled}
|
|
96
|
+
onGoLive={handleGoLive}
|
|
97
|
+
onStopLive={handleStopLive}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './EmbeddedLivestream';
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { OwnCapability } from '@stream-io/video-client';
|
|
4
|
+
import {
|
|
5
|
+
Restricted,
|
|
6
|
+
useCallStateHooks,
|
|
7
|
+
useI18n,
|
|
8
|
+
} from '@stream-io/video-react-bindings';
|
|
9
|
+
import { useCallDuration, useLayout, useWakeLock } from '../../hooks';
|
|
10
|
+
import {
|
|
11
|
+
CallParticipantsList,
|
|
12
|
+
CancelCallConfirmButton,
|
|
13
|
+
CompositeButton,
|
|
14
|
+
DeviceSelectorAudioInput,
|
|
15
|
+
DeviceSelectorAudioOutput,
|
|
16
|
+
Icon,
|
|
17
|
+
MicCaptureErrorNotification,
|
|
18
|
+
ReactionsButton,
|
|
19
|
+
ToggleAudioPublishingButton,
|
|
20
|
+
ToggleVideoPublishingButton,
|
|
21
|
+
WithTooltip,
|
|
22
|
+
} from '../../../components';
|
|
23
|
+
import {
|
|
24
|
+
CameraMenuWithBlur,
|
|
25
|
+
ConnectionNotification,
|
|
26
|
+
ViewersCount,
|
|
27
|
+
} from '../../shared';
|
|
28
|
+
|
|
29
|
+
export const ViewerLayout = () => {
|
|
30
|
+
useWakeLock();
|
|
31
|
+
const { t } = useI18n();
|
|
32
|
+
|
|
33
|
+
const { useParticipantCount, useCallSession } = useCallStateHooks();
|
|
34
|
+
const participantCount = useParticipantCount();
|
|
35
|
+
const session = useCallSession();
|
|
36
|
+
const { elapsed } = useCallDuration(session?.live_started_at);
|
|
37
|
+
const { Component: LayoutComponent, props: layoutProps } = useLayout();
|
|
38
|
+
const [showParticipants, setShowParticipants] = useState(false);
|
|
39
|
+
|
|
40
|
+
const handleCloseParticipants = useCallback(() => {
|
|
41
|
+
setShowParticipants(false);
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
const handleToggleParticipants = useCallback(() => {
|
|
45
|
+
setShowParticipants((prev) => !prev);
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="str-video__embedded-call str-video__embedded-livestream">
|
|
50
|
+
<ConnectionNotification />
|
|
51
|
+
<div className="str-video__embedded-call-header">
|
|
52
|
+
<div className="str-video__embedded-livestream-duration">
|
|
53
|
+
<span className="str-video__embedded-livestream-duration__live-badge">
|
|
54
|
+
{t('Live')}
|
|
55
|
+
</span>
|
|
56
|
+
<ViewersCount count={participantCount} />
|
|
57
|
+
{elapsed && (
|
|
58
|
+
<span className="str-video__embedded-livestream-duration__elapsed">
|
|
59
|
+
{elapsed}
|
|
60
|
+
</span>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
<CancelCallConfirmButton />
|
|
64
|
+
</div>
|
|
65
|
+
<div className="str-video__embedded-layout">
|
|
66
|
+
<div className="str-video__embedded-layout__stage">
|
|
67
|
+
<LayoutComponent {...layoutProps} />
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div
|
|
71
|
+
className={clsx(
|
|
72
|
+
'str-video__embedded-sidebar',
|
|
73
|
+
showParticipants && 'str-video__embedded-sidebar--open',
|
|
74
|
+
)}
|
|
75
|
+
>
|
|
76
|
+
{showParticipants && (
|
|
77
|
+
<div className="str-video__embedded-participants">
|
|
78
|
+
<CallParticipantsList onClose={handleCloseParticipants} />
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
<div className="str-video__embedded-call-controls str-video__call-controls">
|
|
84
|
+
<div className="str-video__call-controls--group str-video__call-controls--options">
|
|
85
|
+
<Restricted requiredGrants={[OwnCapability.CREATE_REACTION]}>
|
|
86
|
+
<div className="str-video__embedded-mobile">
|
|
87
|
+
<ReactionsButton />
|
|
88
|
+
</div>
|
|
89
|
+
</Restricted>
|
|
90
|
+
<div className="str-video__embedded-desktop">
|
|
91
|
+
<div className="str-video__embedded-livestream-duration">
|
|
92
|
+
<span className="str-video__embedded-livestream-duration__live-badge">
|
|
93
|
+
{t('Live')}
|
|
94
|
+
</span>
|
|
95
|
+
<ViewersCount count={participantCount} />
|
|
96
|
+
{elapsed && (
|
|
97
|
+
<span className="str-video__embedded-livestream-duration__elapsed">
|
|
98
|
+
{elapsed}
|
|
99
|
+
</span>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
<div className="str-video__call-controls--group str-video__call-controls--media">
|
|
105
|
+
<Restricted
|
|
106
|
+
requiredGrants={[OwnCapability.SEND_AUDIO]}
|
|
107
|
+
hasPermissionsOnly
|
|
108
|
+
>
|
|
109
|
+
<MicCaptureErrorNotification>
|
|
110
|
+
<ToggleAudioPublishingButton
|
|
111
|
+
Menu={
|
|
112
|
+
<>
|
|
113
|
+
<DeviceSelectorAudioOutput
|
|
114
|
+
visualType="list"
|
|
115
|
+
title={t('Speaker')}
|
|
116
|
+
/>
|
|
117
|
+
<DeviceSelectorAudioInput
|
|
118
|
+
visualType="list"
|
|
119
|
+
title={t('Microphone')}
|
|
120
|
+
/>
|
|
121
|
+
</>
|
|
122
|
+
}
|
|
123
|
+
menuPlacement="top"
|
|
124
|
+
/>
|
|
125
|
+
</MicCaptureErrorNotification>
|
|
126
|
+
</Restricted>
|
|
127
|
+
<Restricted
|
|
128
|
+
requiredGrants={[OwnCapability.SEND_VIDEO]}
|
|
129
|
+
hasPermissionsOnly
|
|
130
|
+
>
|
|
131
|
+
<ToggleVideoPublishingButton
|
|
132
|
+
Menu={<CameraMenuWithBlur />}
|
|
133
|
+
menuPlacement="top"
|
|
134
|
+
/>
|
|
135
|
+
</Restricted>
|
|
136
|
+
<Restricted requiredGrants={[OwnCapability.CREATE_REACTION]}>
|
|
137
|
+
<div className="str-video__embedded-desktop">
|
|
138
|
+
<ReactionsButton />
|
|
139
|
+
</div>
|
|
140
|
+
</Restricted>
|
|
141
|
+
<div className="str-video__embedded-desktop">
|
|
142
|
+
<CancelCallConfirmButton />
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
<div className="str-video__call-controls--group str-video__call-controls--sidebar">
|
|
146
|
+
<WithTooltip title={t('Participants')}>
|
|
147
|
+
<CompositeButton
|
|
148
|
+
active={showParticipants}
|
|
149
|
+
aria-label={t('Participants')}
|
|
150
|
+
aria-pressed={showParticipants}
|
|
151
|
+
onClick={handleToggleParticipants}
|
|
152
|
+
>
|
|
153
|
+
<Icon icon="participants" />
|
|
154
|
+
</CompositeButton>
|
|
155
|
+
</WithTooltip>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useCallStateHooks, useI18n } from '@stream-io/video-react-bindings';
|
|
3
|
+
import { Icon } from '../../../components';
|
|
4
|
+
import { ViewersCount } from '../../shared';
|
|
5
|
+
import { OwnCapability } from '@stream-io/video-client';
|
|
6
|
+
|
|
7
|
+
const checkCanJoinEarly = (
|
|
8
|
+
startsAt: Date | undefined,
|
|
9
|
+
joinAheadTimeSeconds: number | undefined,
|
|
10
|
+
) => {
|
|
11
|
+
if (!startsAt) return false;
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
const earliestJoin = +startsAt - (joinAheadTimeSeconds ?? 0) * 1000;
|
|
14
|
+
return now >= earliestJoin && now < +startsAt;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ViewerLobbyProps = {
|
|
18
|
+
onJoin: () => Promise<void>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const ViewerLobby = ({ onJoin }: ViewerLobbyProps) => {
|
|
22
|
+
const { t } = useI18n();
|
|
23
|
+
const {
|
|
24
|
+
useCallStartsAt,
|
|
25
|
+
useCallEndedAt,
|
|
26
|
+
useHasPermissions,
|
|
27
|
+
useParticipantCount,
|
|
28
|
+
useIsCallLive,
|
|
29
|
+
useCallSettings,
|
|
30
|
+
} = useCallStateHooks();
|
|
31
|
+
|
|
32
|
+
const startsAt = useCallStartsAt();
|
|
33
|
+
const endedAt = useCallEndedAt();
|
|
34
|
+
const canJoinEndedCall = useHasPermissions(OwnCapability.JOIN_ENDED_CALL);
|
|
35
|
+
|
|
36
|
+
const participantCount = useParticipantCount();
|
|
37
|
+
const isLive = useIsCallLive();
|
|
38
|
+
const settings = useCallSettings();
|
|
39
|
+
const joinAheadTimeSeconds = settings?.backstage.join_ahead_time_seconds;
|
|
40
|
+
|
|
41
|
+
const [autoJoin, setAutoJoin] = useState(false);
|
|
42
|
+
const [startsAtPassed, setStartsAtPassed] = useState(
|
|
43
|
+
() => !!startsAt && startsAt.getTime() < Date.now(),
|
|
44
|
+
);
|
|
45
|
+
const [canJoinEarly, setCanJoinEarly] = useState(() =>
|
|
46
|
+
checkCanJoinEarly(startsAt, joinAheadTimeSeconds),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const canJoin = (isLive || canJoinEarly) && (!endedAt || canJoinEndedCall);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (canJoin && autoJoin) {
|
|
53
|
+
onJoin();
|
|
54
|
+
}
|
|
55
|
+
}, [canJoin, autoJoin, onJoin]);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!canJoinEarly) {
|
|
59
|
+
const handle = setInterval(() => {
|
|
60
|
+
setCanJoinEarly(checkCanJoinEarly(startsAt, joinAheadTimeSeconds));
|
|
61
|
+
}, 1000);
|
|
62
|
+
|
|
63
|
+
return () => clearInterval(handle);
|
|
64
|
+
}
|
|
65
|
+
}, [canJoinEarly, startsAt, joinAheadTimeSeconds]);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!startsAt || startsAtPassed) return;
|
|
69
|
+
|
|
70
|
+
const check = () => {
|
|
71
|
+
if (startsAt.getTime() < Date.now()) {
|
|
72
|
+
setStartsAtPassed(true);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const interval = setInterval(check, 1000);
|
|
77
|
+
return () => clearInterval(interval);
|
|
78
|
+
}, [startsAt, startsAtPassed]);
|
|
79
|
+
|
|
80
|
+
const getStartsAtMessage = () => {
|
|
81
|
+
if (!startsAt) return null;
|
|
82
|
+
|
|
83
|
+
if (startsAtPassed) {
|
|
84
|
+
return t('Livestream starts soon');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return t('Livestream starts at {{ time }}', {
|
|
88
|
+
time: startsAt.toLocaleTimeString([], {
|
|
89
|
+
hour: '2-digit',
|
|
90
|
+
minute: '2-digit',
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="str-video__embedded-viewer-lobby">
|
|
97
|
+
<div className="str-video__embedded-viewer-lobby__content">
|
|
98
|
+
<div className="str-video__embedded-viewer-lobby__icon">
|
|
99
|
+
<Icon icon="streaming" />
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<h2 className="str-video__embedded-viewer-lobby__title">
|
|
103
|
+
{canJoin
|
|
104
|
+
? t('Stream is ready!')
|
|
105
|
+
: t('Waiting for the livestream to start')}
|
|
106
|
+
</h2>
|
|
107
|
+
|
|
108
|
+
{!canJoin && getStartsAtMessage() && (
|
|
109
|
+
<p className="str-video__embedded-viewer-lobby__starts-at">
|
|
110
|
+
{getStartsAtMessage()}
|
|
111
|
+
</p>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
{participantCount > 0 && <ViewersCount count={participantCount} />}
|
|
115
|
+
|
|
116
|
+
<div className="str-video__embedded-viewer-lobby__actions">
|
|
117
|
+
{canJoin ? (
|
|
118
|
+
<button className="str-video__button" onClick={onJoin}>
|
|
119
|
+
{t('Join Stream')}
|
|
120
|
+
</button>
|
|
121
|
+
) : (
|
|
122
|
+
<label className="str-video__embedded-viewer-lobby__auto-join">
|
|
123
|
+
<input
|
|
124
|
+
type="checkbox"
|
|
125
|
+
checked={autoJoin}
|
|
126
|
+
onChange={(e) => setAutoJoin(e.target.checked)}
|
|
127
|
+
/>
|
|
128
|
+
<span>{t('Join automatically when stream starts')}</span>
|
|
129
|
+
</label>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { CallingState, OwnCapability } from '@stream-io/video-client';
|
|
3
|
+
import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
|
|
4
|
+
|
|
5
|
+
import { ViewerLobby } from './ViewerLobby';
|
|
6
|
+
import { ViewerLayout } from './ViewerLayout';
|
|
7
|
+
import { LoadingIndicator } from '../../../components';
|
|
8
|
+
import { CallFeedback, JoinError } from '../../shared';
|
|
9
|
+
import { useEmbeddedConfiguration } from '../../context';
|
|
10
|
+
import { useIsLivestreamPaused } from '../../hooks';
|
|
11
|
+
|
|
12
|
+
export const ViewerStateRouter = () => {
|
|
13
|
+
const call = useCall();
|
|
14
|
+
const { onError } = useEmbeddedConfiguration();
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
useCallCallingState,
|
|
18
|
+
useCallEndedAt,
|
|
19
|
+
useHasPermissions,
|
|
20
|
+
useLocalParticipant,
|
|
21
|
+
} = useCallStateHooks();
|
|
22
|
+
const callingState = useCallCallingState();
|
|
23
|
+
const localParticipant = useLocalParticipant();
|
|
24
|
+
const endedAt = useCallEndedAt();
|
|
25
|
+
const canJoinEndedCall = useHasPermissions(OwnCapability.JOIN_ENDED_CALL);
|
|
26
|
+
|
|
27
|
+
const isLivestreamPaused = useIsLivestreamPaused();
|
|
28
|
+
const [joinError, setJoinError] = useState(false);
|
|
29
|
+
const handleJoin = useCallback(async () => {
|
|
30
|
+
if (!call) return;
|
|
31
|
+
|
|
32
|
+
setJoinError(false);
|
|
33
|
+
try {
|
|
34
|
+
if (call.state.callingState !== CallingState.JOINED) {
|
|
35
|
+
await call.join();
|
|
36
|
+
}
|
|
37
|
+
} catch (e) {
|
|
38
|
+
onError?.(e);
|
|
39
|
+
setJoinError(true);
|
|
40
|
+
}
|
|
41
|
+
}, [call, onError]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!call || callingState !== CallingState.LEFT) return;
|
|
45
|
+
|
|
46
|
+
return call.on('call.live_started', () => {
|
|
47
|
+
call.get().catch((e) => {
|
|
48
|
+
onError?.(e);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}, [call, callingState, onError]);
|
|
52
|
+
|
|
53
|
+
if (joinError) {
|
|
54
|
+
return <JoinError onJoin={handleJoin} />;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
switch (callingState) {
|
|
58
|
+
case CallingState.IDLE:
|
|
59
|
+
case CallingState.UNKNOWN:
|
|
60
|
+
return <ViewerLobby onJoin={handleJoin} />;
|
|
61
|
+
|
|
62
|
+
case CallingState.JOINING:
|
|
63
|
+
if (!localParticipant) {
|
|
64
|
+
return <LoadingIndicator className="str-video__embedded-loading" />;
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
|
|
68
|
+
case CallingState.LEFT: {
|
|
69
|
+
if (isLivestreamPaused) {
|
|
70
|
+
return <ViewerLobby onJoin={handleJoin} />;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<CallFeedback
|
|
75
|
+
onJoin={!endedAt || canJoinEndedCall ? handleJoin : undefined}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return <ViewerLayout />;
|
|
82
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { useCallStateHooks, useI18n } from '@stream-io/video-react-bindings';
|
|
4
|
+
import {
|
|
5
|
+
DeviceSelectorVideo,
|
|
6
|
+
Icon,
|
|
7
|
+
useBackgroundFilters,
|
|
8
|
+
} from '../../../components';
|
|
9
|
+
|
|
10
|
+
export const BlurToggleButton = () => {
|
|
11
|
+
const { t } = useI18n();
|
|
12
|
+
const { useCameraState } = useCallStateHooks();
|
|
13
|
+
const { isMute } = useCameraState();
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
isSupported,
|
|
17
|
+
isReady,
|
|
18
|
+
isLoading,
|
|
19
|
+
backgroundFilter,
|
|
20
|
+
applyBackgroundBlurFilter,
|
|
21
|
+
disableBackgroundFilter,
|
|
22
|
+
} = useBackgroundFilters();
|
|
23
|
+
|
|
24
|
+
const isBlurred = backgroundFilter === 'blur';
|
|
25
|
+
const isDisabled = !isReady || isLoading || isMute;
|
|
26
|
+
|
|
27
|
+
const handleClick = useCallback(() => {
|
|
28
|
+
if (isDisabled) return;
|
|
29
|
+
|
|
30
|
+
if (isBlurred) {
|
|
31
|
+
disableBackgroundFilter();
|
|
32
|
+
} else {
|
|
33
|
+
applyBackgroundBlurFilter('high');
|
|
34
|
+
}
|
|
35
|
+
}, [
|
|
36
|
+
applyBackgroundBlurFilter,
|
|
37
|
+
disableBackgroundFilter,
|
|
38
|
+
isBlurred,
|
|
39
|
+
isDisabled,
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const getLabel = () => {
|
|
43
|
+
if (isLoading) return t('Applying...');
|
|
44
|
+
return isBlurred ? t('Disable blur') : t('Blur background');
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (!isSupported) return null;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
className={clsx(
|
|
53
|
+
'str-video__embedded-blur-toggle',
|
|
54
|
+
isBlurred && 'str-video__embedded-blur-toggle--active',
|
|
55
|
+
)}
|
|
56
|
+
disabled={isDisabled}
|
|
57
|
+
aria-pressed={isBlurred}
|
|
58
|
+
onClick={handleClick}
|
|
59
|
+
>
|
|
60
|
+
<Icon icon="blur-icon" />
|
|
61
|
+
<span>{getLabel()}</span>
|
|
62
|
+
</button>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const CameraMenuWithBlur = () => {
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
<DeviceSelectorVideo visualType="list" />
|
|
70
|
+
<div className="str-video__embedded-blur-toggle-container">
|
|
71
|
+
<BlurToggleButton />
|
|
72
|
+
</div>
|
|
73
|
+
</>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useI18n } from '@stream-io/video-react-bindings';
|
|
2
|
+
import { Icon } from '../../../components';
|
|
3
|
+
|
|
4
|
+
interface CallEndedScreenProps {
|
|
5
|
+
onJoin?: () => void;
|
|
6
|
+
onFeedback: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const CallEndedScreen = ({
|
|
10
|
+
onJoin,
|
|
11
|
+
onFeedback,
|
|
12
|
+
}: CallEndedScreenProps) => {
|
|
13
|
+
const { t } = useI18n();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="str-video__embedded-call-feedback__container">
|
|
17
|
+
<h2 className="str-video__embedded-call-feedback__title">
|
|
18
|
+
{t('Call ended')}
|
|
19
|
+
</h2>
|
|
20
|
+
<div className="str-video__embedded-call-feedback__ended-actions">
|
|
21
|
+
{onJoin && (
|
|
22
|
+
<>
|
|
23
|
+
<div className="str-video__embedded-call-feedback__ended-column">
|
|
24
|
+
<p className="str-video__embedded-call-feedback__ended-label">
|
|
25
|
+
{t('Left by mistake?')}
|
|
26
|
+
</p>
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
className="str-video__embedded-call-feedback__ended-button"
|
|
30
|
+
onClick={onJoin}
|
|
31
|
+
>
|
|
32
|
+
<Icon icon="login" />
|
|
33
|
+
{t('Rejoin call')}
|
|
34
|
+
</button>
|
|
35
|
+
</div>
|
|
36
|
+
<div className="str-video__embedded-call-feedback__ended-divider" />
|
|
37
|
+
</>
|
|
38
|
+
)}
|
|
39
|
+
<div className="str-video__embedded-call-feedback__ended-column">
|
|
40
|
+
<p className="str-video__embedded-call-feedback__ended-label">
|
|
41
|
+
{t('Help us improve')}
|
|
42
|
+
</p>
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
className="str-video__embedded-call-feedback__ended-button"
|
|
46
|
+
onClick={onFeedback}
|
|
47
|
+
>
|
|
48
|
+
<Icon icon="feedback" />
|
|
49
|
+
{t('Leave feedback')}
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import { useCall } from '@stream-io/video-react-bindings';
|
|
3
|
+
import { CallEndedScreen } from './CallEndedScreen';
|
|
4
|
+
import { RatingScreen } from './RatingScreen';
|
|
5
|
+
import { ThankYouScreen } from './ThankYouScreen';
|
|
6
|
+
import { useEmbeddedConfiguration } from '../../context';
|
|
7
|
+
|
|
8
|
+
export interface CallFeedbackProps {
|
|
9
|
+
onJoin?: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type FeedbackState = 'ended' | 'rating' | 'submitted';
|
|
13
|
+
|
|
14
|
+
export const CallFeedback = ({ onJoin }: CallFeedbackProps) => {
|
|
15
|
+
const call = useCall();
|
|
16
|
+
const { onError } = useEmbeddedConfiguration();
|
|
17
|
+
const [state, setState] = useState<FeedbackState>('ended');
|
|
18
|
+
|
|
19
|
+
const onFeedback = useCallback(() => setState('rating'), []);
|
|
20
|
+
|
|
21
|
+
const handleSubmit = useCallback(
|
|
22
|
+
async (rating: number, message: string) => {
|
|
23
|
+
if (!call) return;
|
|
24
|
+
|
|
25
|
+
const clampedRating = Math.min(Math.max(1, rating), 5);
|
|
26
|
+
try {
|
|
27
|
+
await call.submitFeedback(clampedRating, {
|
|
28
|
+
reason: message,
|
|
29
|
+
custom: { message },
|
|
30
|
+
});
|
|
31
|
+
} catch (err) {
|
|
32
|
+
onError?.(err);
|
|
33
|
+
} finally {
|
|
34
|
+
setState('submitted');
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
[call, onError],
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="str-video__embedded-call-feedback">
|
|
42
|
+
{state === 'submitted' ? (
|
|
43
|
+
<ThankYouScreen />
|
|
44
|
+
) : state === 'rating' ? (
|
|
45
|
+
<RatingScreen onSubmit={handleSubmit} />
|
|
46
|
+
) : (
|
|
47
|
+
<CallEndedScreen onJoin={onJoin} onFeedback={onFeedback} />
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|