@stream-io/video-react-sdk 1.32.4 → 1.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -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,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
|
+
};
|