@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,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
+ };