@stream-io/video-react-sdk 0.3.47 → 0.4.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 +15 -0
- package/README.md +1 -1
- package/dist/index.cjs.js +324 -882
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.es.js +325 -867
- package/dist/index.es.js.map +1 -1
- package/dist/src/components/Notification/SpeakingWhileMutedNotification.d.ts +3 -0
- package/dist/src/components/{Video → VideoPreview}/VideoPreview.d.ts +1 -9
- package/dist/src/components/index.d.ts +1 -1
- package/dist/src/core/components/ParticipantView/ParticipantView.d.ts +3 -9
- package/dist/src/core/components/ParticipantView/ParticipantViewContext.d.ts +9 -0
- package/dist/src/core/components/ParticipantView/index.d.ts +1 -0
- package/dist/src/core/components/StreamCall/StreamCall.d.ts +2 -11
- package/dist/src/core/hooks/index.d.ts +0 -2
- package/dist/src/core/hooks/useDevices.d.ts +0 -99
- package/dist/src/core/index.d.ts +0 -1
- package/dist/src/hooks/index.d.ts +1 -3
- package/dist/src/hooks/usePersistedDevicePreferences.d.ts +13 -0
- package/dist/src/translations/index.d.ts +2 -0
- package/index.ts +2 -2
- package/package.json +3 -3
- package/src/components/CallControls/CallControls.tsx +6 -8
- package/src/components/CallControls/ScreenShareButton.tsx +14 -10
- package/src/components/CallControls/ToggleAudioButton.tsx +21 -24
- package/src/components/CallControls/ToggleAudioOutputButton.tsx +1 -1
- package/src/components/CallControls/ToggleVideoButton.tsx +21 -22
- package/src/components/CallParticipantsList/CallParticipantsList.tsx +1 -1
- package/src/components/DeviceSettings/DeviceSelectorAudio.tsx +20 -26
- package/src/components/DeviceSettings/DeviceSelectorVideo.tsx +9 -8
- package/src/components/Icon/Icon.tsx +1 -1
- package/src/components/Notification/SpeakingWhileMutedNotification.tsx +5 -49
- package/src/components/VideoPreview/VideoPreview.tsx +67 -0
- package/src/components/index.ts +1 -1
- package/src/core/components/CallLayout/PaginatedGridLayout.tsx +2 -5
- package/src/core/components/ParticipantView/DefaultParticipantViewUI.tsx +7 -6
- package/src/core/components/ParticipantView/ParticipantView.tsx +2 -19
- package/src/core/components/ParticipantView/ParticipantViewContext.tsx +17 -0
- package/src/core/components/ParticipantView/index.ts +1 -0
- package/src/core/components/StreamCall/StreamCall.tsx +2 -28
- package/src/core/hooks/index.ts +0 -2
- package/src/core/hooks/useDevices.ts +0 -195
- package/src/core/index.ts +0 -1
- package/src/hooks/index.ts +1 -3
- package/src/hooks/usePersistedDevicePreferences.ts +118 -0
- package/src/translations/en.json +3 -0
- package/dist/src/core/contexts/MediaDevicesContext.d.ts +0 -180
- package/dist/src/core/contexts/index.d.ts +0 -1
- package/dist/src/core/hooks/useAudioPublisher.d.ts +0 -12
- package/dist/src/core/hooks/useVideoPublisher.d.ts +0 -12
- package/dist/src/hooks/useToggleAudioMuteState.d.ts +0 -4
- package/dist/src/hooks/useToggleScreenShare.d.ts +0 -5
- package/dist/src/hooks/useToggleVideoMuteState.d.ts +0 -4
- package/src/components/Video/VideoPreview.tsx +0 -152
- package/src/core/contexts/MediaDevicesContext.tsx +0 -416
- package/src/core/contexts/index.ts +0 -1
- package/src/core/hooks/useAudioPublisher.ts +0 -146
- package/src/core/hooks/useVideoPublisher.ts +0 -177
- package/src/hooks/useToggleAudioMuteState.ts +0 -34
- package/src/hooks/useToggleScreenShare.ts +0 -43
- package/src/hooks/useToggleVideoMuteState.ts +0 -34
- /package/dist/src/components/{Video → VideoPreview}/index.d.ts +0 -0
- /package/src/components/{Video → VideoPreview}/index.ts +0 -0
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import { PropsWithChildren
|
|
2
|
-
import { createSoundDetector, SfuModels } from '@stream-io/video-client';
|
|
1
|
+
import { PropsWithChildren } from 'react';
|
|
3
2
|
import { useCallStateHooks, useI18n } from '@stream-io/video-react-bindings';
|
|
4
|
-
|
|
5
|
-
import { useMediaDevices } from '../../core';
|
|
6
3
|
import { Notification } from './Notification';
|
|
7
4
|
|
|
8
5
|
export type SpeakingWhileMutedNotificationProps = {
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Text message displayed by the notification.
|
|
11
8
|
*/
|
|
12
9
|
text?: string;
|
|
13
10
|
};
|
|
@@ -16,52 +13,11 @@ export const SpeakingWhileMutedNotification = ({
|
|
|
16
13
|
children,
|
|
17
14
|
text,
|
|
18
15
|
}: PropsWithChildren<SpeakingWhileMutedNotificationProps>) => {
|
|
19
|
-
const {
|
|
20
|
-
const
|
|
21
|
-
const { getAudioStream } = useMediaDevices();
|
|
16
|
+
const { useMicrophoneState } = useCallStateHooks();
|
|
17
|
+
const { isSpeakingWhileMuted } = useMicrophoneState();
|
|
22
18
|
const { t } = useI18n();
|
|
23
19
|
|
|
24
20
|
const message = text ?? t('You are muted. Unmute to speak.');
|
|
25
|
-
const isAudioMute = !localParticipant?.publishedTracks.includes(
|
|
26
|
-
SfuModels.TrackType.AUDIO,
|
|
27
|
-
);
|
|
28
|
-
const audioDeviceId = localParticipant?.audioDeviceId;
|
|
29
|
-
const [isSpeakingWhileMuted, setIsSpeakingWhileMuted] = useState(false);
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
// do nothing when not muted
|
|
32
|
-
if (!isAudioMute) return;
|
|
33
|
-
const disposeSoundDetector = getAudioStream({
|
|
34
|
-
deviceId: audioDeviceId,
|
|
35
|
-
}).then((audioStream) =>
|
|
36
|
-
createSoundDetector(audioStream, ({ isSoundDetected }) => {
|
|
37
|
-
setIsSpeakingWhileMuted((isNotified) =>
|
|
38
|
-
isNotified ? isNotified : isSoundDetected,
|
|
39
|
-
);
|
|
40
|
-
}),
|
|
41
|
-
);
|
|
42
|
-
disposeSoundDetector.catch((err) => {
|
|
43
|
-
console.error('Error while creating sound detector', err);
|
|
44
|
-
});
|
|
45
|
-
return () => {
|
|
46
|
-
disposeSoundDetector
|
|
47
|
-
.then((dispose) => dispose())
|
|
48
|
-
.catch((err) => {
|
|
49
|
-
console.error('Error while disposing sound detector', err);
|
|
50
|
-
});
|
|
51
|
-
setIsSpeakingWhileMuted(false);
|
|
52
|
-
};
|
|
53
|
-
}, [audioDeviceId, getAudioStream, isAudioMute]);
|
|
54
|
-
|
|
55
|
-
useEffect(() => {
|
|
56
|
-
if (!isSpeakingWhileMuted) return;
|
|
57
|
-
const timeout = setTimeout(() => {
|
|
58
|
-
setIsSpeakingWhileMuted(false);
|
|
59
|
-
}, 3500);
|
|
60
|
-
return () => {
|
|
61
|
-
clearTimeout(timeout);
|
|
62
|
-
setIsSpeakingWhileMuted(false);
|
|
63
|
-
};
|
|
64
|
-
}, [isSpeakingWhileMuted]);
|
|
65
21
|
return (
|
|
66
22
|
<Notification message={message} isVisible={isSpeakingWhileMuted}>
|
|
67
23
|
{children}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ComponentType } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { useCallStateHooks } from '@stream-io/video-react-bindings';
|
|
4
|
+
import { BaseVideo } from '../../core/components/Video';
|
|
5
|
+
import { LoadingIndicator } from '../LoadingIndicator';
|
|
6
|
+
|
|
7
|
+
const DefaultDisabledVideoPreview = () => {
|
|
8
|
+
return <div>Video is disabled</div>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const DefaultNoCameraPreview = () => {
|
|
12
|
+
return <div>No camera found</div>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type VideoPreviewProps = {
|
|
16
|
+
/**
|
|
17
|
+
* Component rendered when user turns off the video.
|
|
18
|
+
*/
|
|
19
|
+
DisabledVideoPreview?: ComponentType;
|
|
20
|
+
/**
|
|
21
|
+
* Enforces mirroring of the video on the X axis. Defaults to true.
|
|
22
|
+
*/
|
|
23
|
+
mirror?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Component rendered when no camera devices are available.
|
|
26
|
+
*/
|
|
27
|
+
NoCameraPreview?: ComponentType;
|
|
28
|
+
/**
|
|
29
|
+
* Component rendered above the BaseVideo until the video is ready (meaning until the play event is emitted).
|
|
30
|
+
*/
|
|
31
|
+
StartingCameraPreview?: ComponentType;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const VideoPreview = ({
|
|
35
|
+
mirror = true,
|
|
36
|
+
DisabledVideoPreview = DefaultDisabledVideoPreview,
|
|
37
|
+
NoCameraPreview = DefaultNoCameraPreview,
|
|
38
|
+
StartingCameraPreview = LoadingIndicator,
|
|
39
|
+
}: VideoPreviewProps) => {
|
|
40
|
+
const { useCameraState } = useCallStateHooks();
|
|
41
|
+
const { devices, status, isMute, mediaStream } = useCameraState();
|
|
42
|
+
|
|
43
|
+
let contents;
|
|
44
|
+
if (isMute && devices?.length === 0) {
|
|
45
|
+
contents = <NoCameraPreview />;
|
|
46
|
+
} else if (status === 'enabled') {
|
|
47
|
+
const loading = !mediaStream;
|
|
48
|
+
contents = (
|
|
49
|
+
<>
|
|
50
|
+
{mediaStream && (
|
|
51
|
+
<BaseVideo
|
|
52
|
+
stream={mediaStream}
|
|
53
|
+
className={clsx('str-video__video-preview', {
|
|
54
|
+
'str-video__video-preview--mirror': mirror,
|
|
55
|
+
'str-video__video-preview--loading': loading,
|
|
56
|
+
})}
|
|
57
|
+
/>
|
|
58
|
+
)}
|
|
59
|
+
{loading && <StartingCameraPreview />}
|
|
60
|
+
</>
|
|
61
|
+
);
|
|
62
|
+
} else {
|
|
63
|
+
contents = <DisabledVideoPreview />;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return <div className="str-video__video-preview-container">{contents}</div>;
|
|
67
|
+
};
|
package/src/components/index.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
|
|
3
|
-
import {
|
|
4
|
-
StreamVideoLocalParticipant,
|
|
5
|
-
StreamVideoParticipant,
|
|
6
|
-
} from '@stream-io/video-client';
|
|
3
|
+
import { StreamVideoParticipant } from '@stream-io/video-client';
|
|
7
4
|
import clsx from 'clsx';
|
|
8
5
|
|
|
9
6
|
import {
|
|
@@ -22,7 +19,7 @@ type PaginatedGridLayoutGroupProps = {
|
|
|
22
19
|
/**
|
|
23
20
|
* The group of participants to render.
|
|
24
21
|
*/
|
|
25
|
-
group: Array<StreamVideoParticipant
|
|
22
|
+
group: Array<StreamVideoParticipant>;
|
|
26
23
|
} & Pick<ParticipantViewProps, 'VideoPlaceholder'> &
|
|
27
24
|
Required<Pick<ParticipantViewProps, 'ParticipantViewUI'>>;
|
|
28
25
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { forwardRef } from 'react';
|
|
2
2
|
import { Placement } from '@floating-ui/react';
|
|
3
3
|
import { SfuModels } from '@stream-io/video-client';
|
|
4
|
-
import { useCall } from '@stream-io/video-react-bindings';
|
|
5
|
-
import
|
|
4
|
+
import { useCall, useI18n } from '@stream-io/video-react-bindings';
|
|
5
|
+
import clsx from 'clsx';
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
Icon,
|
|
@@ -17,7 +17,7 @@ import { Reaction } from '../../../components/Reaction';
|
|
|
17
17
|
import { DebugParticipantPublishQuality } from '../../../components/Debug/DebugParticipantPublishQuality';
|
|
18
18
|
import { DebugStatsView } from '../../../components/Debug/DebugStatsView';
|
|
19
19
|
import { useIsDebugMode } from '../../../components/Debug/useIsDebugMode';
|
|
20
|
-
import { useParticipantViewContext } from './
|
|
20
|
+
import { useParticipantViewContext } from './ParticipantViewContext';
|
|
21
21
|
|
|
22
22
|
export type DefaultParticipantViewUIProps = {
|
|
23
23
|
/**
|
|
@@ -42,22 +42,23 @@ const ToggleButton = forwardRef<HTMLButtonElement, ToggleMenuButtonProps>(
|
|
|
42
42
|
|
|
43
43
|
export const DefaultScreenShareOverlay = () => {
|
|
44
44
|
const call = useCall();
|
|
45
|
+
const { t } = useI18n();
|
|
45
46
|
|
|
46
47
|
const stopScreenShare = () => {
|
|
47
|
-
call?.
|
|
48
|
+
call?.screenShare.disable();
|
|
48
49
|
};
|
|
49
50
|
|
|
50
51
|
return (
|
|
51
52
|
<div className="str-video__screen-share-overlay">
|
|
52
53
|
<Icon icon="screen-share-off" />
|
|
53
54
|
<span className="str-video__screen-share-overlay__title">
|
|
54
|
-
You are presenting your screen
|
|
55
|
+
{t('You are presenting your screen')}
|
|
55
56
|
</span>
|
|
56
57
|
<button
|
|
57
58
|
onClick={stopScreenShare}
|
|
58
59
|
className="str-video__screen-share-overlay__button"
|
|
59
60
|
>
|
|
60
|
-
<Icon icon="close" /> Stop Screen Sharing
|
|
61
|
+
<Icon icon="close" /> {t('Stop Screen Sharing')}
|
|
61
62
|
</button>
|
|
62
63
|
</div>
|
|
63
64
|
);
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ComponentType,
|
|
3
|
-
createContext,
|
|
4
3
|
forwardRef,
|
|
5
4
|
ReactElement,
|
|
6
|
-
useContext,
|
|
7
5
|
useMemo,
|
|
8
6
|
useState,
|
|
9
7
|
} from 'react';
|
|
10
8
|
import clsx from 'clsx';
|
|
11
9
|
import {
|
|
12
10
|
SfuModels,
|
|
13
|
-
StreamVideoLocalParticipant,
|
|
14
11
|
StreamVideoParticipant,
|
|
15
12
|
VideoTrackType,
|
|
16
13
|
} from '@stream-io/video-client';
|
|
@@ -20,27 +17,13 @@ import { Video, VideoProps } from '../Video';
|
|
|
20
17
|
import { useTrackElementVisibility } from '../../hooks';
|
|
21
18
|
import { DefaultParticipantViewUI } from './DefaultParticipantViewUI';
|
|
22
19
|
import { applyElementToRef, isComponentType } from '../../../utilities';
|
|
23
|
-
|
|
24
|
-
export type ParticipantViewContextValue = Required<
|
|
25
|
-
Pick<ParticipantViewProps, 'participant' | 'trackType'>
|
|
26
|
-
> & {
|
|
27
|
-
participantViewElement: HTMLDivElement | null;
|
|
28
|
-
videoElement: HTMLVideoElement | null;
|
|
29
|
-
videoPlaceholderElement: HTMLDivElement | null;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const ParticipantViewContext = createContext<
|
|
33
|
-
ParticipantViewContextValue | undefined
|
|
34
|
-
>(undefined);
|
|
35
|
-
|
|
36
|
-
export const useParticipantViewContext = () =>
|
|
37
|
-
useContext(ParticipantViewContext) as ParticipantViewContextValue;
|
|
20
|
+
import { ParticipantViewContext } from './ParticipantViewContext';
|
|
38
21
|
|
|
39
22
|
export type ParticipantViewProps = {
|
|
40
23
|
/**
|
|
41
24
|
* The participant whose video/audio stream we want to play.
|
|
42
25
|
*/
|
|
43
|
-
participant: StreamVideoParticipant
|
|
26
|
+
participant: StreamVideoParticipant;
|
|
44
27
|
|
|
45
28
|
/**
|
|
46
29
|
* Override the default UI for rendering participant information/actions.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import { ParticipantViewProps } from './ParticipantView';
|
|
3
|
+
|
|
4
|
+
export type ParticipantViewContextValue = Required<
|
|
5
|
+
Pick<ParticipantViewProps, 'participant' | 'trackType'>
|
|
6
|
+
> & {
|
|
7
|
+
participantViewElement: HTMLDivElement | null;
|
|
8
|
+
videoElement: HTMLVideoElement | null;
|
|
9
|
+
videoPlaceholderElement: HTMLDivElement | null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const ParticipantViewContext = createContext<
|
|
13
|
+
ParticipantViewContextValue | undefined
|
|
14
|
+
>(undefined);
|
|
15
|
+
|
|
16
|
+
export const useParticipantViewContext = () =>
|
|
17
|
+
useContext(ParticipantViewContext) as ParticipantViewContextValue;
|
|
@@ -1,30 +1,4 @@
|
|
|
1
|
-
import { PropsWithChildren } from 'react';
|
|
2
|
-
import { Call } from '@stream-io/video-client';
|
|
3
1
|
import { StreamCallProvider } from '@stream-io/video-react-bindings';
|
|
4
|
-
import {
|
|
5
|
-
MediaDevicesProvider,
|
|
6
|
-
MediaDevicesProviderProps,
|
|
7
|
-
} from '../../contexts';
|
|
8
2
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* An optional props to pass to the `MediaDevicesProvider`.
|
|
14
|
-
*/
|
|
15
|
-
mediaDevicesProviderProps?: MediaDevicesProviderProps;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export const StreamCall = ({
|
|
19
|
-
children,
|
|
20
|
-
call,
|
|
21
|
-
mediaDevicesProviderProps,
|
|
22
|
-
}: PropsWithChildren<StreamCallProps>) => {
|
|
23
|
-
return (
|
|
24
|
-
<StreamCallProvider call={call}>
|
|
25
|
-
<MediaDevicesProvider {...mediaDevicesProviderProps}>
|
|
26
|
-
{children}
|
|
27
|
-
</MediaDevicesProvider>
|
|
28
|
-
</StreamCallProvider>
|
|
29
|
-
);
|
|
30
|
-
};
|
|
3
|
+
// re-exporting the StreamCallProvider as StreamCall
|
|
4
|
+
export const StreamCall = StreamCallProvider;
|
package/src/core/hooks/index.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
import { ChangeEvent, useEffect, useState } from 'react';
|
|
2
|
-
import { Observable, pairwise } from 'rxjs';
|
|
3
|
-
import {
|
|
4
|
-
getAudioDevices,
|
|
5
|
-
getAudioOutputDevices,
|
|
6
|
-
getVideoDevices,
|
|
7
|
-
} from '@stream-io/video-client';
|
|
8
2
|
|
|
9
3
|
export const useHasBrowserPermissions = (permissionName: PermissionName) => {
|
|
10
4
|
const [canSubscribe, enableSubscription] = useState(false);
|
|
@@ -37,192 +31,3 @@ export const useHasBrowserPermissions = (permissionName: PermissionName) => {
|
|
|
37
31
|
|
|
38
32
|
return canSubscribe;
|
|
39
33
|
};
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Observes changes in connected devices and maintains an up-to-date array of connected MediaDeviceInfo objects.
|
|
43
|
-
* @param observeDevices
|
|
44
|
-
* @category Device Management
|
|
45
|
-
*/
|
|
46
|
-
export const useDevices = (
|
|
47
|
-
observeDevices: () => Observable<MediaDeviceInfo[]>,
|
|
48
|
-
) => {
|
|
49
|
-
const [devices, setDevices] = useState<MediaDeviceInfo[]>([]);
|
|
50
|
-
|
|
51
|
-
useEffect(() => {
|
|
52
|
-
const subscription = observeDevices().subscribe(setDevices);
|
|
53
|
-
|
|
54
|
-
return () => {
|
|
55
|
-
subscription.unsubscribe();
|
|
56
|
-
};
|
|
57
|
-
}, [observeDevices]);
|
|
58
|
-
|
|
59
|
-
return devices;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Observes changes and maintains an array of connected video input devices
|
|
64
|
-
* @category Device Management
|
|
65
|
-
*/
|
|
66
|
-
export const useVideoDevices = () => useDevices(getVideoDevices);
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Observes changes and maintains an array of connected audio input devices
|
|
70
|
-
* @category Device Management
|
|
71
|
-
*/
|
|
72
|
-
export const useAudioInputDevices = () => useDevices(getAudioDevices);
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Observes changes and maintains an array of connected audio output devices
|
|
76
|
-
* @category Device Management
|
|
77
|
-
*/
|
|
78
|
-
export const useAudioOutputDevices = () => useDevices(getAudioOutputDevices);
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Verifies that newly selected device id exists among the registered devices.
|
|
82
|
-
* If the selected device id is not found among existing devices, switches to the default device.
|
|
83
|
-
* The media devices are observed only if a given permission ('camera' resp. 'microphone') is granted in browser.
|
|
84
|
-
* Regardless of current permissions settings, an intent to observe devices will take place in Firefox.
|
|
85
|
-
* This is due to the fact that Firefox does not allow to query for 'camera' and 'microphone' permissions.
|
|
86
|
-
* @param canObserve
|
|
87
|
-
* @param devices$
|
|
88
|
-
* @param switchToDefaultDevice
|
|
89
|
-
* @param selectedDeviceId
|
|
90
|
-
* @category Device Management
|
|
91
|
-
*/
|
|
92
|
-
export const useDeviceFallback = (
|
|
93
|
-
canObserve: boolean,
|
|
94
|
-
devices$: Observable<MediaDeviceInfo[]>,
|
|
95
|
-
switchToDefaultDevice: () => void,
|
|
96
|
-
selectedDeviceId?: string,
|
|
97
|
-
) => {
|
|
98
|
-
useEffect(() => {
|
|
99
|
-
if (!canObserve) return;
|
|
100
|
-
const validateDeviceId = devices$.pipe().subscribe((devices) => {
|
|
101
|
-
const deviceFound = devices.find(
|
|
102
|
-
(device) => device.deviceId === selectedDeviceId,
|
|
103
|
-
);
|
|
104
|
-
if (!deviceFound) switchToDefaultDevice();
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
return () => {
|
|
108
|
-
validateDeviceId.unsubscribe();
|
|
109
|
-
};
|
|
110
|
-
}, [canObserve, devices$, selectedDeviceId, switchToDefaultDevice]);
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Verifies that newly selected video device id exists among the registered devices.
|
|
115
|
-
* If the selected device id is not found among existing devices, switches to the default video device.
|
|
116
|
-
* The media devices are observed only if 'camera' permission is granted in browser.
|
|
117
|
-
* It is integrators responsibility to instruct users how to enable required permissions.
|
|
118
|
-
* Regardless of current permissions settings, an intent to observe devices will take place in Firefox.
|
|
119
|
-
* This is due to the fact that Firefox does not allow to query for 'camera' and 'microphone' permissions.
|
|
120
|
-
* @param switchToDefaultDevice
|
|
121
|
-
* @param canObserve
|
|
122
|
-
* @param selectedDeviceId
|
|
123
|
-
* @category Device Management
|
|
124
|
-
*/
|
|
125
|
-
export const useVideoDeviceFallback = (
|
|
126
|
-
switchToDefaultDevice: () => void,
|
|
127
|
-
canObserve: boolean,
|
|
128
|
-
selectedDeviceId?: string,
|
|
129
|
-
) =>
|
|
130
|
-
useDeviceFallback(
|
|
131
|
-
canObserve,
|
|
132
|
-
getVideoDevices(),
|
|
133
|
-
switchToDefaultDevice,
|
|
134
|
-
selectedDeviceId,
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Verifies that newly selected audio input device id exists among the registered devices.
|
|
139
|
-
* If the selected device id is not found among existing devices, switches to the default audio input device.
|
|
140
|
-
* The media devices are observed only if 'microphone' permission is granted in browser.
|
|
141
|
-
* It is integrators responsibility to instruct users how to enable required permissions.
|
|
142
|
-
* Regardless of current permissions settings, an intent to observe devices will take place in Firefox.
|
|
143
|
-
* This is due to the fact that Firefox does not allow to query for 'camera' and 'microphone' permissions.
|
|
144
|
-
* @param switchToDefaultDevice
|
|
145
|
-
* @param canObserve
|
|
146
|
-
* @param selectedDeviceId
|
|
147
|
-
* @category Device Management
|
|
148
|
-
*/
|
|
149
|
-
export const useAudioInputDeviceFallback = (
|
|
150
|
-
switchToDefaultDevice: () => void,
|
|
151
|
-
canObserve: boolean,
|
|
152
|
-
selectedDeviceId?: string,
|
|
153
|
-
) =>
|
|
154
|
-
useDeviceFallback(
|
|
155
|
-
canObserve,
|
|
156
|
-
getAudioDevices(),
|
|
157
|
-
switchToDefaultDevice,
|
|
158
|
-
selectedDeviceId,
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Verifies that newly selected audio output device id exists among the registered devices.
|
|
163
|
-
* If the selected device id is not found among existing devices, switches to the default audio output device.
|
|
164
|
-
* The media devices are observed only if 'microphone' permission is granted in browser.
|
|
165
|
-
* It is integrators responsibility to instruct users how to enable required permissions.
|
|
166
|
-
* Regardless of current permissions settings, an intent to observe devices will take place in Firefox.
|
|
167
|
-
* This is due to the fact that Firefox does not allow to query for 'camera' and 'microphone' permissions.
|
|
168
|
-
* @param switchToDefaultDevice
|
|
169
|
-
* @param canObserve
|
|
170
|
-
* @param selectedDeviceId
|
|
171
|
-
* @category Device Management
|
|
172
|
-
*/
|
|
173
|
-
export const useAudioOutputDeviceFallback = (
|
|
174
|
-
switchToDefaultDevice: () => void,
|
|
175
|
-
canObserve: boolean,
|
|
176
|
-
selectedDeviceId?: string,
|
|
177
|
-
) =>
|
|
178
|
-
useDeviceFallback(
|
|
179
|
-
canObserve,
|
|
180
|
-
getAudioOutputDevices(),
|
|
181
|
-
switchToDefaultDevice,
|
|
182
|
-
selectedDeviceId,
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Observes devices of certain kind are made unavailable and executes onDisconnect callback.
|
|
187
|
-
* @param observeDevices
|
|
188
|
-
* @param onDisconnect
|
|
189
|
-
* @category Device Management
|
|
190
|
-
*/
|
|
191
|
-
export const useOnUnavailableDevices = (
|
|
192
|
-
observeDevices: Observable<MediaDeviceInfo[]>,
|
|
193
|
-
onDisconnect: () => void,
|
|
194
|
-
) => {
|
|
195
|
-
useEffect(() => {
|
|
196
|
-
const subscription = observeDevices
|
|
197
|
-
.pipe(pairwise())
|
|
198
|
-
.subscribe(([prev, current]) => {
|
|
199
|
-
if (prev.length > 0 && current.length === 0) onDisconnect();
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
return () => subscription.unsubscribe();
|
|
203
|
-
}, [observeDevices, onDisconnect]);
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Observes disconnect of all video devices and executes onDisconnect callback.
|
|
208
|
-
* @param onDisconnect
|
|
209
|
-
* @category Device Management
|
|
210
|
-
*/
|
|
211
|
-
export const useOnUnavailableVideoDevices = (onDisconnect: () => void) =>
|
|
212
|
-
useOnUnavailableDevices(getVideoDevices(), onDisconnect);
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Observes disconnect of all audio input devices and executes onDisconnect callback.
|
|
216
|
-
* @param onDisconnect
|
|
217
|
-
* @category Device Management
|
|
218
|
-
*/
|
|
219
|
-
export const useOnUnavailableAudioInputDevices = (onDisconnect: () => void) =>
|
|
220
|
-
useOnUnavailableDevices(getAudioDevices(), onDisconnect);
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Observes disconnect of all audio output devices and executes onDisconnect callback.
|
|
224
|
-
* @param onDisconnect
|
|
225
|
-
* @category Device Management
|
|
226
|
-
*/
|
|
227
|
-
export const useOnUnavailableAudioOutputDevices = (onDisconnect: () => void) =>
|
|
228
|
-
useOnUnavailableDevices(getAudioOutputDevices(), onDisconnect);
|
package/src/core/index.ts
CHANGED
package/src/hooks/index.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
export * from './useFloatingUIPreset';
|
|
2
|
+
export * from './usePersistedDevicePreferences';
|
|
2
3
|
export * from './useScrollPosition';
|
|
3
|
-
export * from './useToggleAudioMuteState';
|
|
4
|
-
export * from './useToggleVideoMuteState';
|
|
5
|
-
export * from './useToggleScreenShare';
|
|
6
4
|
export * from './useToggleCallRecording';
|
|
7
5
|
export * from './useRequestPermission';
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
|
|
3
|
+
|
|
4
|
+
export type LocalDevicePreference = {
|
|
5
|
+
selectedDeviceId: string;
|
|
6
|
+
muted: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type LocalDevicePreferences = {
|
|
10
|
+
[type in 'mic' | 'camera' | 'speaker']: LocalDevicePreference;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* This hook will persist the device settings to local storage.
|
|
15
|
+
*
|
|
16
|
+
* @param key the key to use for local storage.
|
|
17
|
+
*/
|
|
18
|
+
const usePersistDevicePreferences = (key: string) => {
|
|
19
|
+
const { useMicrophoneState, useCameraState, useSpeakerState } =
|
|
20
|
+
useCallStateHooks();
|
|
21
|
+
const mic = useMicrophoneState();
|
|
22
|
+
const camera = useCameraState();
|
|
23
|
+
const speaker = useSpeakerState();
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
try {
|
|
26
|
+
const defaultDevice = 'default';
|
|
27
|
+
const preferences: LocalDevicePreferences = {
|
|
28
|
+
mic: {
|
|
29
|
+
selectedDeviceId: mic.selectedDevice || defaultDevice,
|
|
30
|
+
muted: mic.isMute,
|
|
31
|
+
},
|
|
32
|
+
camera: {
|
|
33
|
+
selectedDeviceId: camera.selectedDevice || defaultDevice,
|
|
34
|
+
muted: camera.isMute,
|
|
35
|
+
},
|
|
36
|
+
speaker: {
|
|
37
|
+
selectedDeviceId: speaker.selectedDevice || defaultDevice,
|
|
38
|
+
muted: false,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
window.localStorage.setItem(key, JSON.stringify(preferences));
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.warn('Failed to save device preferences', err);
|
|
44
|
+
}
|
|
45
|
+
}, [
|
|
46
|
+
camera.isMute,
|
|
47
|
+
camera.selectedDevice,
|
|
48
|
+
key,
|
|
49
|
+
mic.isMute,
|
|
50
|
+
mic.selectedDevice,
|
|
51
|
+
speaker.selectedDevice,
|
|
52
|
+
]);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* This hook will apply the device settings from local storage.
|
|
57
|
+
*
|
|
58
|
+
* @param key the key to use for local storage.
|
|
59
|
+
*/
|
|
60
|
+
const useApplyDevicePreferences = (key: string) => {
|
|
61
|
+
const call = useCall();
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!call) return;
|
|
64
|
+
|
|
65
|
+
const apply = async () => {
|
|
66
|
+
const initMic = async (setting: LocalDevicePreference) => {
|
|
67
|
+
await call.microphone.select(setting.selectedDeviceId);
|
|
68
|
+
if (setting.muted) {
|
|
69
|
+
await call.microphone.disable();
|
|
70
|
+
} else {
|
|
71
|
+
await call.microphone.enable();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const initCamera = async (setting: LocalDevicePreference) => {
|
|
76
|
+
await call.camera.select(setting.selectedDeviceId);
|
|
77
|
+
if (setting.muted) {
|
|
78
|
+
await call.camera.disable();
|
|
79
|
+
} else {
|
|
80
|
+
await call.camera.enable();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const initSpeaker = (setting: LocalDevicePreference) => {
|
|
85
|
+
call.speaker.select(setting.selectedDeviceId);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const preferences = JSON.parse(
|
|
90
|
+
window.localStorage.getItem(key)!,
|
|
91
|
+
) as LocalDevicePreferences;
|
|
92
|
+
if (preferences) {
|
|
93
|
+
await initMic(preferences.mic);
|
|
94
|
+
await initCamera(preferences.camera);
|
|
95
|
+
initSpeaker(preferences.speaker);
|
|
96
|
+
}
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.warn('Failed to load device preferences', err);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
apply().catch((err) => {
|
|
103
|
+
console.warn('Failed to apply device preferences', err);
|
|
104
|
+
});
|
|
105
|
+
}, [call, key]);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* This hook will apply and persist the device preferences from local storage.
|
|
110
|
+
*
|
|
111
|
+
* @param key the key to use for local storage.
|
|
112
|
+
*/
|
|
113
|
+
export const usePersistedDevicePreferences = (
|
|
114
|
+
key: string = '@stream-io/device-preferences',
|
|
115
|
+
) => {
|
|
116
|
+
useApplyDevicePreferences(key);
|
|
117
|
+
usePersistDevicePreferences(key);
|
|
118
|
+
};
|
package/src/translations/en.json
CHANGED
|
@@ -38,6 +38,9 @@
|
|
|
38
38
|
"Unknown": "Unknown",
|
|
39
39
|
"Toggle device menu": "Toggle device menu",
|
|
40
40
|
|
|
41
|
+
"You are presenting your screen": "You are presenting your screen",
|
|
42
|
+
"Stop Screen Sharing": "Stop Screen Sharing",
|
|
43
|
+
|
|
41
44
|
"Allow": "Allow",
|
|
42
45
|
"Revoke": "Revoke",
|
|
43
46
|
"Dismiss": "Dismiss",
|