@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.
Files changed (119) hide show
  1. package/CHANGELOG.md +20 -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
@@ -0,0 +1,51 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Call, CallingState, StreamVideoClient } from '@stream-io/video-client';
3
+
4
+ export interface UseInitializeCallProps {
5
+ client?: StreamVideoClient;
6
+ callType: string;
7
+ callId: string;
8
+ handleError: (error: any) => void;
9
+ }
10
+
11
+ /**
12
+ * Hook to initialize and manage a Call instance.
13
+ */
14
+ export const useInitializeCall = ({
15
+ client,
16
+ callType,
17
+ callId,
18
+ handleError,
19
+ }: UseInitializeCallProps): Call | undefined => {
20
+ const [call, setCall] = useState<Call>();
21
+
22
+ useEffect(() => {
23
+ if (!client || !callId) return;
24
+
25
+ let cancelled = false;
26
+ const _call = client.call(callType, callId);
27
+
28
+ _call
29
+ .get()
30
+ .then(() => {
31
+ if (!cancelled) setCall(_call);
32
+ })
33
+ .catch((err) => {
34
+ if (cancelled) return;
35
+
36
+ handleError(err);
37
+ });
38
+
39
+ return () => {
40
+ cancelled = true;
41
+ setCall(undefined);
42
+ if (_call.state.callingState !== CallingState.LEFT) {
43
+ _call
44
+ .leave()
45
+ .catch((err) => console.error('Failed to leave call:', err));
46
+ }
47
+ };
48
+ }, [client, callType, callId, handleError]);
49
+
50
+ return call;
51
+ };
@@ -0,0 +1,118 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import {
3
+ LogLevel,
4
+ StreamVideoClient,
5
+ TokenProvider,
6
+ User,
7
+ } from '@stream-io/video-client';
8
+ import type { EmbeddedUser } from '../types';
9
+
10
+ export interface UseInitializeVideoClientProps {
11
+ apiKey: string;
12
+ user: EmbeddedUser;
13
+ token?: string;
14
+ tokenProvider?: TokenProvider;
15
+ logLevel?: LogLevel;
16
+ handleError: (error: any) => void;
17
+ }
18
+
19
+ /**
20
+ * Hook that creates a StreamVideoClient and connects the user.
21
+ * Disconnects and cleans up on unmount or when props change.
22
+ *
23
+ */
24
+ export const useInitializeVideoClient = ({
25
+ apiKey,
26
+ user,
27
+ token,
28
+ tokenProvider,
29
+ logLevel,
30
+ handleError,
31
+ }: UseInitializeVideoClientProps): StreamVideoClient | undefined => {
32
+ const [client, setClient] = useState<StreamVideoClient | undefined>();
33
+
34
+ const tokenProviderRef = useRef(tokenProvider);
35
+ tokenProviderRef.current = tokenProvider;
36
+
37
+ useEffect(() => {
38
+ if (!apiKey) return;
39
+
40
+ const options = logLevel ? { logLevel } : undefined;
41
+ let _client: StreamVideoClient | undefined;
42
+ try {
43
+ if (user?.type === 'guest') {
44
+ const streamUser: User = {
45
+ id: user.id,
46
+ type: 'guest',
47
+ name: user.name,
48
+ image: user.image,
49
+ };
50
+
51
+ _client = new StreamVideoClient({
52
+ apiKey,
53
+ user: streamUser,
54
+ options,
55
+ });
56
+ } else if (user?.type === 'anonymous') {
57
+ const streamUser: User = {
58
+ id: '!anon',
59
+ type: 'anonymous',
60
+ name: user.name,
61
+ image: user.image,
62
+ };
63
+
64
+ _client = new StreamVideoClient({
65
+ apiKey,
66
+ user: streamUser,
67
+ token,
68
+ tokenProvider: tokenProviderRef.current,
69
+ options,
70
+ });
71
+ } else {
72
+ const streamUser: User = {
73
+ id: user.id,
74
+ name: user.name,
75
+ image: user.image,
76
+ };
77
+
78
+ const currentTokenProvider = tokenProviderRef.current;
79
+ _client = new StreamVideoClient({
80
+ apiKey,
81
+ user: streamUser,
82
+ ...(token
83
+ ? {
84
+ token,
85
+ ...(currentTokenProvider
86
+ ? { tokenProvider: currentTokenProvider }
87
+ : {}),
88
+ }
89
+ : { tokenProvider: currentTokenProvider! }),
90
+ options,
91
+ });
92
+ }
93
+
94
+ setClient(_client);
95
+ } catch (err) {
96
+ handleError(err);
97
+ }
98
+
99
+ return () => {
100
+ _client
101
+ ?.disconnectUser()
102
+ .catch((err) => console.error('Failed to disconnect user:', err));
103
+
104
+ setClient(undefined);
105
+ };
106
+ }, [
107
+ apiKey,
108
+ user.id,
109
+ user.type,
110
+ user.name,
111
+ user.image,
112
+ token,
113
+ logLevel,
114
+ handleError,
115
+ ]);
116
+
117
+ return client;
118
+ };
@@ -0,0 +1,44 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { CallingState, SfuModels } from '@stream-io/video-client';
3
+ import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
4
+
5
+ /**
6
+ * Distinguishes a temporary live pause (host went backstage) from a real
7
+ * call end. Returns `true` when the viewer was kicked because the host
8
+ * paused the livestream — not because the call was fully terminated.
9
+ *
10
+ * Resets to `false` on rejoin or when `call.ended` (real end) arrives.
11
+ */
12
+ export const useIsLivestreamPaused = () => {
13
+ const call = useCall();
14
+ const { useCallCallingState } = useCallStateHooks();
15
+ const callingState = useCallCallingState();
16
+ const [isPaused, setIsPaused] = useState(false);
17
+
18
+ useEffect(() => {
19
+ if (!call) return;
20
+
21
+ const unsubSfu = call.on('callEnded', (e) => {
22
+ if (e.reason === SfuModels.CallEndedReason.LIVE_ENDED) {
23
+ setIsPaused(true);
24
+ }
25
+ });
26
+
27
+ const unsubEnded = call.on('call.ended', () => {
28
+ setIsPaused(false);
29
+ });
30
+
31
+ return () => {
32
+ unsubSfu();
33
+ unsubEnded();
34
+ };
35
+ }, [call]);
36
+
37
+ useEffect(() => {
38
+ if (callingState === CallingState.JOINED) {
39
+ setIsPaused(false);
40
+ }
41
+ }, [callingState]);
42
+
43
+ return isPaused;
44
+ };
@@ -0,0 +1,100 @@
1
+ import type { ComponentType } from 'react';
2
+ import { useEffect, useState } from 'react';
3
+ import { useCallStateHooks } from '@stream-io/video-react-bindings';
4
+ import {
5
+ LivestreamLayout,
6
+ PaginatedGridLayout,
7
+ SpeakerLayout,
8
+ } from '../../core';
9
+ import type { LayoutOption } from '../types';
10
+ import { useEmbeddedConfiguration } from '../context';
11
+ import { EmbeddedParticipantViewUI } from '../shared';
12
+
13
+ interface LayoutConfig {
14
+ Component: ComponentType<Record<string, unknown>>;
15
+ props?: Record<string, unknown>;
16
+ }
17
+
18
+ const Layouts: Record<LayoutOption, LayoutConfig> = {
19
+ Livestream: {
20
+ Component: LivestreamLayout,
21
+ props: {
22
+ showLiveBadge: false,
23
+ showParticipantCount: false,
24
+ showDuration: false,
25
+ },
26
+ },
27
+ PaginatedGrid: {
28
+ Component: PaginatedGridLayout,
29
+ props: { groupSize: 16, ParticipantViewUI: EmbeddedParticipantViewUI },
30
+ },
31
+ SpeakerLeft: {
32
+ Component: SpeakerLayout,
33
+ props: {
34
+ participantsBarPosition: 'left',
35
+ ParticipantViewUISpotlight: EmbeddedParticipantViewUI,
36
+ ParticipantViewUIBar: EmbeddedParticipantViewUI,
37
+ },
38
+ },
39
+ SpeakerRight: {
40
+ Component: SpeakerLayout,
41
+ props: {
42
+ participantsBarPosition: 'right',
43
+ ParticipantViewUISpotlight: EmbeddedParticipantViewUI,
44
+ ParticipantViewUIBar: EmbeddedParticipantViewUI,
45
+ },
46
+ },
47
+ SpeakerTop: {
48
+ Component: SpeakerLayout,
49
+ props: {
50
+ participantsBarPosition: 'top',
51
+ ParticipantViewUISpotlight: EmbeddedParticipantViewUI,
52
+ ParticipantViewUIBar: EmbeddedParticipantViewUI,
53
+ },
54
+ },
55
+ SpeakerBottom: {
56
+ Component: SpeakerLayout,
57
+ props: {
58
+ participantsBarPosition: 'bottom',
59
+ ParticipantViewUISpotlight: EmbeddedParticipantViewUI,
60
+ ParticipantViewUIBar: EmbeddedParticipantViewUI,
61
+ },
62
+ },
63
+ };
64
+
65
+ const EMPTY_PROPS: Record<string, unknown> = {};
66
+ const VALID_LAYOUTS = Object.keys(Layouts) as LayoutOption[];
67
+
68
+ const isValidLayout = (layout: string): layout is LayoutOption =>
69
+ VALID_LAYOUTS.includes(layout as LayoutOption);
70
+
71
+ /**
72
+ * Hook to manage layout selection.
73
+ * Returns the layout Component and its props.
74
+ */
75
+ export const useLayout = () => {
76
+ const { layout: configuredLayout } = useEmbeddedConfiguration();
77
+ const { useHasOngoingScreenShare } = useCallStateHooks();
78
+ const hasScreenShare = useHasOngoingScreenShare();
79
+
80
+ const defaultLayout = isValidLayout(configuredLayout ?? '')
81
+ ? configuredLayout!
82
+ : 'SpeakerTop';
83
+
84
+ const [layout, setLayout] = useState<LayoutOption>(defaultLayout);
85
+
86
+ useEffect(() => {
87
+ if (hasScreenShare) {
88
+ setLayout((currentLayout) => {
89
+ if (currentLayout.startsWith('Speaker')) return currentLayout;
90
+ return 'SpeakerRight';
91
+ });
92
+ } else {
93
+ setLayout(defaultLayout);
94
+ }
95
+ }, [hasScreenShare, defaultLayout]);
96
+
97
+ const { Component, props = EMPTY_PROPS } = Layouts[layout];
98
+
99
+ return { Component, props };
100
+ };
@@ -0,0 +1,62 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import {
3
+ NoiseCancellationSettingsModeEnum,
4
+ type Call,
5
+ } from '@stream-io/video-client';
6
+
7
+ type INoiseCancellation =
8
+ import('@stream-io/audio-filters-web').INoiseCancellation;
9
+
10
+ /**
11
+ * Hook that lazily loads the noise cancellation module from @stream-io/audio-filters-web.
12
+ * Skips loading if the server-side noise cancellation setting is disabled.
13
+ * Returns the NoiseCancellation instance when loaded, or undefined if unavailable.
14
+ * The `ready` flag becomes `true` once loading completes (even on failure),
15
+ * or immediately if noise cancellation is disabled by server settings.
16
+ */
17
+ export const useNoiseCancellationLoader = (call?: Call) => {
18
+ const [noiseCancellation, setNoiseCancellation] =
19
+ useState<INoiseCancellation>();
20
+ const [ready, setReady] = useState(false);
21
+ const ncLoader = useRef<Promise<void> | undefined>(undefined);
22
+
23
+ const ncSettings = call?.state.settings?.audio?.noise_cancellation;
24
+ const isNoiseCancellationEnabled = !!(
25
+ ncSettings && ncSettings.mode !== NoiseCancellationSettingsModeEnum.DISABLED
26
+ );
27
+
28
+ useEffect(() => {
29
+ if (!call) return;
30
+
31
+ if (!isNoiseCancellationEnabled) {
32
+ setReady(true);
33
+ return;
34
+ }
35
+
36
+ const load = (ncLoader.current || Promise.resolve())
37
+ .then(() => import('@stream-io/audio-filters-web'))
38
+ .then(({ NoiseCancellation }) => {
39
+ const nc = new NoiseCancellation({});
40
+ setNoiseCancellation(nc);
41
+ })
42
+ .catch((err) => {
43
+ console.warn(
44
+ '[EmbeddedStreamClient] Failed to load noise cancellation. ' +
45
+ 'Make sure @stream-io/audio-filters-web is installed.',
46
+ err,
47
+ );
48
+ })
49
+ .finally(() => {
50
+ setReady(true);
51
+ });
52
+
53
+ return () => {
54
+ ncLoader.current = load.then(() => {
55
+ setNoiseCancellation(undefined);
56
+ setReady(false);
57
+ });
58
+ };
59
+ }, [call, isNoiseCancellationEnabled]);
60
+
61
+ return { noiseCancellation, ready };
62
+ };
@@ -0,0 +1,33 @@
1
+ import { useEffect } from 'react';
2
+ import { CallingState } from '@stream-io/video-client';
3
+ import { useCallStateHooks } from '@stream-io/video-react-bindings';
4
+
5
+ /**
6
+ * Hook to prevent screen from going to sleep during active calls.
7
+ * Uses the Screen Wake Lock API when available.
8
+ */
9
+ export const useWakeLock = () => {
10
+ const { useCallCallingState } = useCallStateHooks();
11
+ const callState = useCallCallingState();
12
+
13
+ useEffect(() => {
14
+ if (callState !== CallingState.JOINED || !('wakeLock' in navigator)) return;
15
+
16
+ let interrupted = false;
17
+ let wakeLockSentinel: null | WakeLockSentinel = null;
18
+ navigator.wakeLock
19
+ .request('screen')
20
+ .then((wls: WakeLockSentinel) => {
21
+ if (interrupted) return wls.release();
22
+ wakeLockSentinel = wls;
23
+ })
24
+ .catch((error: unknown) =>
25
+ console.debug(`Couldn't setup WakeLock due to: ${error}`),
26
+ );
27
+
28
+ return () => {
29
+ interrupted = true;
30
+ wakeLockSentinel?.release();
31
+ };
32
+ }, [callState]);
33
+ };
@@ -0,0 +1,12 @@
1
+ export { EmbeddedCall } from './call';
2
+ export { EmbeddedLivestream } from './livestream';
3
+
4
+ export type {
5
+ EmbeddedMeetingProps,
6
+ EmbeddedLivestreamProps,
7
+ EmbeddedUser,
8
+ EmbeddedAuthenticatedUser,
9
+ EmbeddedGuestUser,
10
+ EmbeddedAnonymousUser,
11
+ LayoutOption,
12
+ } from './types';
@@ -0,0 +1,16 @@
1
+ import type { EmbeddedLivestreamProps } from '../types';
2
+ import { EmbeddedClientProvider } from '../EmbeddedClientProvider';
3
+ import { LivestreamUI } from './LivestreamUI';
4
+
5
+ /**
6
+ * Drop-in livestream component. Renders host or viewer UI based on permissions.
7
+ */
8
+ export const EmbeddedLivestream = ({
9
+ children,
10
+ ...props
11
+ }: EmbeddedLivestreamProps) => (
12
+ <EmbeddedClientProvider {...props}>
13
+ <LivestreamUI />
14
+ {children}
15
+ </EmbeddedClientProvider>
16
+ );
@@ -0,0 +1,17 @@
1
+ import { OwnCapability } from '@stream-io/video-client';
2
+ import { useCallStateHooks } from '@stream-io/video-react-bindings';
3
+
4
+ import { HostStateRouter } from './host/HostStateRouter';
5
+ import { ViewerStateRouter } from './viewer/ViewerStateRouter';
6
+
7
+ export const LivestreamUI = () => {
8
+ const { useHasPermissions } = useCallStateHooks();
9
+
10
+ const isHost = useHasPermissions(OwnCapability.JOIN_BACKSTAGE);
11
+
12
+ if (isHost) {
13
+ return <HostStateRouter />;
14
+ }
15
+
16
+ return <ViewerStateRouter />;
17
+ };
@@ -0,0 +1,210 @@
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 { useLayout } from '../../hooks';
10
+ import {
11
+ CallParticipantsList,
12
+ CancelCallConfirmButton,
13
+ CompositeButton,
14
+ DeviceSelectorAudioInput,
15
+ DeviceSelectorAudioOutput,
16
+ Icon,
17
+ MicCaptureErrorNotification,
18
+ PermissionRequests,
19
+ ReactionsButton,
20
+ RecordCallConfirmationButton,
21
+ ScreenShareButton,
22
+ ToggleAudioPublishingButton,
23
+ ToggleVideoPublishingButton,
24
+ WithTooltip,
25
+ } from '../../../components';
26
+ import { useCallDuration } from '../../hooks';
27
+ import {
28
+ CameraMenuWithBlur,
29
+ ConnectionNotification,
30
+ ViewersCount,
31
+ } from '../../shared';
32
+
33
+ export type HostViewProps = {
34
+ isLive: boolean;
35
+ isBackstageEnabled: boolean;
36
+ onGoLive: () => void;
37
+ onStopLive: () => void;
38
+ };
39
+
40
+ export const HostLayout = ({
41
+ isLive,
42
+ isBackstageEnabled,
43
+ onGoLive,
44
+ onStopLive,
45
+ }: HostViewProps) => {
46
+ const { t } = useI18n();
47
+ const { useParticipantCount, useCallSession } = useCallStateHooks();
48
+ const participantCount = useParticipantCount();
49
+ const session = useCallSession();
50
+ const { elapsed } = useCallDuration(session?.live_started_at);
51
+ const { Component: LayoutComponent, props: layoutProps } = useLayout();
52
+ const [showParticipants, setShowParticipants] = useState(false);
53
+
54
+ const handleCloseParticipants = useCallback(() => {
55
+ setShowParticipants(false);
56
+ }, []);
57
+
58
+ const handleToggleParticipants = useCallback(() => {
59
+ setShowParticipants((prev) => !prev);
60
+ }, []);
61
+
62
+ const livestreamStatus = (
63
+ <div
64
+ className="str-video__embedded-livestream-duration"
65
+ aria-live="polite"
66
+ aria-atomic="true"
67
+ >
68
+ <span
69
+ className={
70
+ isLive
71
+ ? 'str-video__embedded-livestream-duration__live-badge'
72
+ : 'str-video__embedded-livestream-duration__backstage-badge'
73
+ }
74
+ >
75
+ {isLive ? t('Live') : t('Backstage')}
76
+ </span>
77
+ <ViewersCount count={participantCount} />
78
+ {isLive && elapsed && (
79
+ <span className="str-video__embedded-livestream-duration__elapsed">
80
+ {elapsed}
81
+ </span>
82
+ )}
83
+ </div>
84
+ );
85
+
86
+ return (
87
+ <div className="str-video__embedded-call str-video__embedded-livestream">
88
+ <ConnectionNotification />
89
+ <PermissionRequests />
90
+ <div className="str-video__embedded-call-header">
91
+ {livestreamStatus}
92
+ <CancelCallConfirmButton />
93
+ </div>
94
+ <div className="str-video__embedded-layout">
95
+ <div className="str-video__embedded-layout__stage">
96
+ <LayoutComponent {...layoutProps} />
97
+ </div>
98
+
99
+ <div
100
+ className={clsx(
101
+ 'str-video__embedded-sidebar',
102
+ showParticipants && 'str-video__embedded-sidebar--open',
103
+ )}
104
+ >
105
+ {showParticipants && (
106
+ <div className="str-video__embedded-participants">
107
+ <CallParticipantsList onClose={handleCloseParticipants} />
108
+ </div>
109
+ )}
110
+ </div>
111
+ </div>
112
+ <div className="str-video__embedded-call-controls str-video__call-controls">
113
+ <div className="str-video__call-controls--group str-video__call-controls--options">
114
+ <Restricted requiredGrants={[OwnCapability.CREATE_REACTION]}>
115
+ <div className="str-video__embedded-mobile">
116
+ <ReactionsButton />
117
+ </div>
118
+ </Restricted>
119
+ <div className="str-video__embedded-desktop">{livestreamStatus}</div>
120
+ </div>
121
+ <div className="str-video__call-controls--group str-video__call-controls--media">
122
+ <Restricted
123
+ requiredGrants={[OwnCapability.SEND_AUDIO]}
124
+ hasPermissionsOnly
125
+ >
126
+ <MicCaptureErrorNotification>
127
+ <ToggleAudioPublishingButton
128
+ Menu={
129
+ <>
130
+ <DeviceSelectorAudioOutput
131
+ visualType="list"
132
+ title={t('Speaker')}
133
+ />
134
+ <DeviceSelectorAudioInput
135
+ visualType="list"
136
+ title={t('Microphone')}
137
+ />
138
+ </>
139
+ }
140
+ menuPlacement="top"
141
+ />
142
+ </MicCaptureErrorNotification>
143
+ </Restricted>
144
+ <Restricted
145
+ requiredGrants={[OwnCapability.SEND_VIDEO]}
146
+ hasPermissionsOnly
147
+ >
148
+ <ToggleVideoPublishingButton
149
+ Menu={<CameraMenuWithBlur />}
150
+ menuPlacement="top"
151
+ />
152
+ </Restricted>
153
+ <Restricted requiredGrants={[OwnCapability.CREATE_REACTION]}>
154
+ <div className="str-video__embedded-desktop">
155
+ <ReactionsButton />
156
+ </div>
157
+ </Restricted>
158
+ <Restricted requiredGrants={[OwnCapability.SCREENSHARE]}>
159
+ <div className="str-video__embedded-desktop">
160
+ <ScreenShareButton />
161
+ </div>
162
+ </Restricted>
163
+ <RecordCallConfirmationButton />
164
+ {isBackstageEnabled && (
165
+ <Restricted requiredGrants={[OwnCapability.UPDATE_CALL]}>
166
+ {isLive ? (
167
+ <WithTooltip title={t('End Stream')}>
168
+ <button
169
+ type="button"
170
+ className="str-video__embedded-end-stream-button"
171
+ onClick={onStopLive}
172
+ >
173
+ <Icon icon="call-end" />
174
+ <span>{t('Stop Live')}</span>
175
+ </button>
176
+ </WithTooltip>
177
+ ) : (
178
+ <WithTooltip title={t('Start Stream')}>
179
+ <button
180
+ type="button"
181
+ className="str-video__embedded-go-live-button"
182
+ onClick={onGoLive}
183
+ >
184
+ <Icon icon="streaming" />
185
+ <span>{t('Go Live')}</span>
186
+ </button>
187
+ </WithTooltip>
188
+ )}
189
+ </Restricted>
190
+ )}
191
+ <div className="str-video__embedded-desktop">
192
+ <CancelCallConfirmButton />
193
+ </div>
194
+ </div>
195
+ <div className="str-video__call-controls--group str-video__call-controls--sidebar">
196
+ <WithTooltip title={t('Participants')}>
197
+ <CompositeButton
198
+ active={showParticipants}
199
+ aria-label={t('Participants')}
200
+ aria-pressed={showParticipants}
201
+ onClick={handleToggleParticipants}
202
+ >
203
+ <Icon icon="participants" />
204
+ </CompositeButton>
205
+ </WithTooltip>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ );
210
+ };