Merge branch 'main' of github.com:daily-demos/examples into waiting-participants-hook
This commit is contained in:
commit
47baaaef80
|
|
@ -38,9 +38,6 @@ This demo puts to work the following [shared libraries](../shared):
|
|||
**[MediaDeviceProvider.js](../shared/contexts/MediaDeviceProvider.js)**
|
||||
Convenience context that provides an interface to media devices throughout app
|
||||
|
||||
**[useDevices.js](../shared/contexts/useDevices.js)**
|
||||
Hook for managing the enumeration and status of client media devices)
|
||||
|
||||
**[CallProvider.js](../shared/contexts/CallProvider.js)**
|
||||
Primary call context that manages Daily call state, participant state and call object interaction
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export const App = ({ customComponentForState }) => {
|
|||
...customComponentForState,
|
||||
});
|
||||
|
||||
// Memoize children to avoid unnecassary renders from HOC
|
||||
// Memoize children to avoid unnecessary renders from HOC
|
||||
return useMemo(
|
||||
() => (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export const VideoGrid = React.memo(
|
|||
return bestLayout;
|
||||
}, [dimensions, participants]);
|
||||
|
||||
// Memoize our tile list to avoid unnecassary re-renders
|
||||
// Memoize our tile list to avoid unnecessary re-renders
|
||||
const tiles = useDeepCompareMemo(
|
||||
() =>
|
||||
participants.map((p) => (
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useState, useCallback } from 'react';
|
|||
import { CallProvider } from '@custom/shared/contexts/CallProvider';
|
||||
import { MediaDeviceProvider } from '@custom/shared/contexts/MediaDeviceProvider';
|
||||
import { ParticipantsProvider } from '@custom/shared/contexts/ParticipantsProvider';
|
||||
import { ScreenShareProvider } from '@custom/shared/contexts/ScreenShareProvider';
|
||||
import { TracksProvider } from '@custom/shared/contexts/TracksProvider';
|
||||
import { UIStateProvider } from '@custom/shared/contexts/UIStateProvider';
|
||||
import { WaitingRoomProvider } from '@custom/shared/contexts/WaitingRoomProvider';
|
||||
|
|
@ -125,7 +126,9 @@ export default function Index({
|
|||
<TracksProvider>
|
||||
<MediaDeviceProvider>
|
||||
<WaitingRoomProvider>
|
||||
<ScreenShareProvider>
|
||||
{customAppComponent || <App />}
|
||||
</ScreenShareProvider>
|
||||
</WaitingRoomProvider>
|
||||
</MediaDeviceProvider>
|
||||
</TracksProvider>
|
||||
|
|
|
|||
|
|
@ -38,9 +38,6 @@ This demo puts to work the following [shared libraries](../shared):
|
|||
**[MediaDeviceProvider.js](../shared/contexts/MediaDeviceProvider.js)**
|
||||
Convenience context that provides an interface to media devices throughout app
|
||||
|
||||
**[useDevices.js](../shared/contexts/useDevices.js)**
|
||||
Hook for managing the enumeration and status of client media devices)
|
||||
|
||||
**[CallProvider.js](../shared/contexts/CallProvider.js)**
|
||||
Primary call context that manages Daily call state, participant state and call object interaction
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { LiveStreamingProvider } from '@custom/live-streaming/contexts/LiveStreamingProvider';
|
||||
import { RecordingProvider } from '@custom/recording/contexts/RecordingProvider';
|
||||
import ExpiryTimer from '@custom/shared/components/ExpiryTimer';
|
||||
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
||||
import { LiveStreamingProvider } from '@custom/shared/contexts/LiveStreamingProvider';
|
||||
import { useCallUI } from '@custom/shared/hooks/useCallUI';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ export const App = ({ customComponentForState }) => {
|
|||
...customComponentForState,
|
||||
});
|
||||
|
||||
// Memoize children to avoid unnecassary renders from HOC
|
||||
// Memoize children to avoid unnecessary renders from HOC
|
||||
return useMemo(
|
||||
() => (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,30 +1,23 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { TrayButton } from '@custom/shared/components/Tray';
|
||||
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
||||
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
|
||||
import { useScreenShare } from '@custom/shared/contexts/ScreenShareProvider';
|
||||
import { ReactComponent as IconShare } from '@custom/shared/icons/share-sm.svg';
|
||||
|
||||
const MAX_SCREEN_SHARES = 2;
|
||||
|
||||
export const ScreenShareTray = () => {
|
||||
const { callObject, enableScreenShare } = useCallState();
|
||||
const { screens, participants, localParticipant } = useParticipants();
|
||||
|
||||
const isSharingScreen = useMemo(
|
||||
() => screens.some((s) => s.isLocal),
|
||||
[screens]
|
||||
);
|
||||
|
||||
const screensLength = useMemo(() => screens.length, [screens]);
|
||||
const { enableScreenShare } = useCallState();
|
||||
const { localParticipant } = useParticipants();
|
||||
const {
|
||||
isSharingScreen,
|
||||
isDisabled,
|
||||
startScreenShare,
|
||||
stopScreenShare
|
||||
} = useScreenShare();
|
||||
|
||||
const toggleScreenShare = () =>
|
||||
isSharingScreen ? callObject.stopScreenShare() : callObject.startScreenShare();
|
||||
|
||||
const disabled =
|
||||
participants.length &&
|
||||
screensLength >= MAX_SCREEN_SHARES &&
|
||||
!isSharingScreen;
|
||||
isSharingScreen ? stopScreenShare() : startScreenShare();
|
||||
|
||||
if (!enableScreenShare) return null;
|
||||
if (!localParticipant.isOwner) return null;
|
||||
|
|
@ -33,7 +26,7 @@ export const ScreenShareTray = () => {
|
|||
<TrayButton
|
||||
label={isSharingScreen ? 'Stop': 'Share'}
|
||||
orange={isSharingScreen}
|
||||
disabled={disabled}
|
||||
disabled={isDisabled}
|
||||
onClick={toggleScreenShare}
|
||||
>
|
||||
<IconShare />
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
|
||||
import { LIVE_STREAMING_MODAL } from '@custom/live-streaming/components/LiveStreamingModal';
|
||||
import { useLiveStreaming } from '@custom/live-streaming/contexts/LiveStreamingProvider';
|
||||
import { TrayButton } from '@custom/shared/components/Tray';
|
||||
import { useLiveStreaming } from '@custom/shared/contexts/LiveStreamingProvider';
|
||||
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
|
||||
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
||||
import { ReactComponent as IconStream } from '@custom/shared/icons/streaming-md.svg';
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import { CallProvider } from '@custom/shared/contexts/CallProvider';
|
||||
import { MediaDeviceProvider } from '@custom/shared/contexts/MediaDeviceProvider';
|
||||
import { ParticipantsProvider } from '@custom/shared/contexts/ParticipantsProvider';
|
||||
import { ScreenShareProvider } from '@custom/shared/contexts/ScreenShareProvider';
|
||||
import { TracksProvider } from '@custom/shared/contexts/TracksProvider';
|
||||
import { UIStateProvider } from '@custom/shared/contexts/UIStateProvider';
|
||||
import { WaitingRoomProvider } from '@custom/shared/contexts/WaitingRoomProvider';
|
||||
|
|
@ -40,7 +41,9 @@ const Room = ({
|
|||
<TracksProvider>
|
||||
<MediaDeviceProvider>
|
||||
<WaitingRoomProvider>
|
||||
<ScreenShareProvider>
|
||||
{customAppComponent || <App />}
|
||||
</ScreenShareProvider>
|
||||
</WaitingRoomProvider>
|
||||
</MediaDeviceProvider>
|
||||
</TracksProvider>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,13 @@
|
|||
|
||||
Please note: this demo is not currently mobile optimised
|
||||
|
||||
### Getting started
|
||||
## Pre-requisites
|
||||
|
||||
To use this demo, you will need to create a [Daily account](https://dashboard.daily.co/signup) and a [Daily room](https://dashboard.daily.co/rooms/create).
|
||||
|
||||
You will also need to enter an RTMP URL in the demo UI to start a live stream. To learn more about where to find this value, please read Daily's [live streaming guide](https://docs.daily.co/guides/paid-features/live-streaming-with-daily). You may also find the [live streaming with AWS's IVS tutorial](https://www.daily.co/blog/live-stream-daily-calls-with-only-3-second-latency/) helpful.
|
||||
|
||||
## Getting started
|
||||
|
||||
```
|
||||
# set both DAILY_API_KEY and DAILY_DOMAIN
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import App from '@custom/basic-call/components/App';
|
||||
import { LiveStreamingProvider } from '../contexts/LiveStreamingProvider';
|
||||
import { LiveStreamingProvider } from '@custom/shared/contexts/LiveStreamingProvider';
|
||||
|
||||
// Extend our basic call app component with the live streaming context
|
||||
export const AppWithLiveStreaming = () => (
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@ import Field from '@custom/shared/components/Field';
|
|||
import { TextInput, SelectInput } from '@custom/shared/components/Input';
|
||||
import Modal from '@custom/shared/components/Modal';
|
||||
import Well from '@custom/shared/components/Well';
|
||||
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
||||
import { useLiveStreaming } from '@custom/shared/contexts/LiveStreamingProvider';
|
||||
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
|
||||
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
||||
import { useLiveStreaming } from '../contexts/LiveStreamingProvider';
|
||||
|
||||
export const LIVE_STREAMING_MODAL = 'live-streaming';
|
||||
|
||||
|
|
@ -19,15 +18,19 @@ const LAYOUTS = [
|
|||
];
|
||||
|
||||
export const LiveStreamingModal = () => {
|
||||
const { callObject } = useCallState();
|
||||
const { allParticipants } = useParticipants();
|
||||
const { participants } = useParticipants();
|
||||
const { currentModals, closeModal } = useUIState();
|
||||
const { isStreaming, streamError } = useLiveStreaming();
|
||||
const {
|
||||
isStreaming,
|
||||
streamError,
|
||||
startLiveStreaming,
|
||||
stopLiveStreaming,
|
||||
} = useLiveStreaming();
|
||||
const [pending, setPending] = useState(false);
|
||||
const [rtmpUrl, setRtmpUrl] = useState('');
|
||||
const [layout, setLayout] = useState(0);
|
||||
const [layoutType, setLayoutType] = useState('default');
|
||||
const [maxCams, setMaxCams] = useState(9);
|
||||
const [participant, setParticipant] = useState(0);
|
||||
const [participantId, setParticipantId] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset pending state whenever stream state changes
|
||||
|
|
@ -35,20 +38,30 @@ export const LiveStreamingModal = () => {
|
|||
}, [isStreaming]);
|
||||
|
||||
function startLiveStream() {
|
||||
setPending(true);
|
||||
const config = {
|
||||
rtmpUrl,
|
||||
layout: {
|
||||
preset: layoutType,
|
||||
},
|
||||
};
|
||||
|
||||
const opts =
|
||||
layout === 'single-participant'
|
||||
? { session_id: participant.id }
|
||||
: { max_cam_streams: maxCams };
|
||||
callObject.startLiveStreaming({ rtmpUrl, preset: layout, ...opts });
|
||||
if (layoutType === 'single-participant')
|
||||
config.layout.session_id = participantId;
|
||||
else if (layoutType === 'default') config.layout.max_cam_streams = maxCams;
|
||||
|
||||
startLiveStreaming(config);
|
||||
}
|
||||
|
||||
function stopLiveStreaming() {
|
||||
function stopLiveStream() {
|
||||
setPending(true);
|
||||
callObject.stopLiveStreaming();
|
||||
stopLiveStreaming();
|
||||
}
|
||||
|
||||
const handleRMTPURLChange = (e) => setRtmpUrl(e.target.value);
|
||||
const handleSelectLayoutInputChange = (e) => setLayoutType(e.target.value);
|
||||
const handleSelectParticipantInputChange = (e) => setParticipantId(e.target.value);
|
||||
const handleSelectMaxCamsInputChange = (e) => setMaxCams(e.target.valueAsNumber);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Live stream"
|
||||
|
|
@ -62,7 +75,7 @@ export const LiveStreamingModal = () => {
|
|||
<Button
|
||||
fullWidth
|
||||
disabled={!rtmpUrl || pending}
|
||||
onClick={() => startLiveStream()}
|
||||
onClick={startLiveStream}
|
||||
>
|
||||
{pending ? 'Starting stream...' : 'Start live streaming'}
|
||||
</Button>
|
||||
|
|
@ -70,7 +83,7 @@ export const LiveStreamingModal = () => {
|
|||
<Button
|
||||
fullWidth
|
||||
variant="warning"
|
||||
onClick={() => stopLiveStreaming()}
|
||||
onClick={stopLiveStream}
|
||||
>
|
||||
Stop live streaming
|
||||
</Button>
|
||||
|
|
@ -85,22 +98,21 @@ export const LiveStreamingModal = () => {
|
|||
<CardBody>
|
||||
<Field label="Layout">
|
||||
<SelectInput
|
||||
onChange={(e) => setLayout(Number(e.target.value))}
|
||||
value={layout}
|
||||
onChange={handleSelectLayoutInputChange}
|
||||
value={layoutType}
|
||||
>
|
||||
{LAYOUTS.map((l, i) => (
|
||||
<option value={i} key={l.value}>
|
||||
{LAYOUTS.map((l) => (
|
||||
<option value={l.value} key={l.value}>
|
||||
{l.label}
|
||||
</option>
|
||||
))}
|
||||
</SelectInput>
|
||||
</Field>
|
||||
|
||||
{layout !==
|
||||
LAYOUTS.findIndex((l) => l.value === 'single-participant') && (
|
||||
{layoutType === 'default' && (
|
||||
<Field label="Additional cameras">
|
||||
<SelectInput
|
||||
onChange={(e) => setMaxCams(Number(e.target.value))}
|
||||
onChange={handleSelectMaxCamsInputChange}
|
||||
value={maxCams}
|
||||
>
|
||||
<option value={9}>9 cameras</option>
|
||||
|
|
@ -116,15 +128,17 @@ export const LiveStreamingModal = () => {
|
|||
</Field>
|
||||
)}
|
||||
|
||||
{layout ===
|
||||
LAYOUTS.findIndex((l) => l.value === 'single-participant') && (
|
||||
{layoutType === 'single-participant' && (
|
||||
<Field label="Select participant">
|
||||
<SelectInput
|
||||
onChange={(e) => setParticipant(e.target.value)}
|
||||
value={participant}
|
||||
onChange={handleSelectParticipantInputChange}
|
||||
value={participantId}
|
||||
>
|
||||
{allParticipants.map((p) => (
|
||||
<option value={p.id} key={p.id}>
|
||||
<option value={0} disabled>
|
||||
Select
|
||||
</option>
|
||||
{participants.map((p) => (
|
||||
<option value={p.sessionId} key={p.sessionId}>
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
|
|
@ -137,8 +151,14 @@ export const LiveStreamingModal = () => {
|
|||
type="text"
|
||||
placeholder="RTMP URL"
|
||||
required
|
||||
onChange={(e) => setRtmpUrl(e.target.value)}
|
||||
onChange={handleRMTPURLChange}
|
||||
/>
|
||||
<a
|
||||
className="learn-more"
|
||||
href="https://docs.daily.co/guides/paid-features/live-streaming-with-daily"
|
||||
>
|
||||
Want to learn more about RTMP url?
|
||||
</a>
|
||||
</Field>
|
||||
</CardBody>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
|
||||
import { TrayButton } from '@custom/shared/components/Tray';
|
||||
import { useLiveStreaming } from '@custom/shared/contexts/LiveStreamingProvider';
|
||||
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
||||
import { ReactComponent as IconStream } from '@custom/shared/icons/streaming-md.svg';
|
||||
|
||||
import { useLiveStreaming } from '../contexts/LiveStreamingProvider';
|
||||
import { LIVE_STREAMING_MODAL } from './LiveStreamingModal';
|
||||
|
||||
export const Tray = () => {
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
import React, {
|
||||
useState,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
||||
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const LiveStreamingContext = createContext();
|
||||
|
||||
export const LiveStreamingProvider = ({ children }) => {
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
const [streamError, setStreamError] = useState();
|
||||
const { setCustomCapsule } = useUIState();
|
||||
const { callObject } = useCallState();
|
||||
|
||||
const handleStreamStarted = useCallback(() => {
|
||||
console.log('📺 Live stream started');
|
||||
setIsStreaming(true);
|
||||
setStreamError(null);
|
||||
setCustomCapsule({ variant: 'recording', label: 'Live streaming' });
|
||||
}, [setCustomCapsule]);
|
||||
|
||||
const handleStreamStopped = useCallback(() => {
|
||||
console.log('📺 Live stream stopped');
|
||||
setIsStreaming(false);
|
||||
setCustomCapsule(null);
|
||||
}, [setCustomCapsule]);
|
||||
|
||||
const handleStreamError = useCallback(
|
||||
(e) => {
|
||||
setIsStreaming(false);
|
||||
setCustomCapsule(null);
|
||||
setStreamError(e.errorMsg);
|
||||
},
|
||||
[setCustomCapsule]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!callObject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('📺 Live streaming provider listening for stream events');
|
||||
|
||||
callObject.on('live-streaming-started', handleStreamStarted);
|
||||
callObject.on('live-streaming-stopped', handleStreamStopped);
|
||||
callObject.on('live-streaming-error', handleStreamError);
|
||||
|
||||
return () => {
|
||||
callObject.off('live-streaming-started', handleStreamStarted);
|
||||
callObject.off('live-streaming-stopped', handleStreamStopped);
|
||||
callObject.on('live-streaming-error', handleStreamError);
|
||||
};
|
||||
}, [callObject, handleStreamStarted, handleStreamStopped, handleStreamError]);
|
||||
|
||||
return (
|
||||
<LiveStreamingContext.Provider value={{ isStreaming, streamError }}>
|
||||
{children}
|
||||
</LiveStreamingContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
LiveStreamingProvider.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export const useLiveStreaming = () => useContext(LiveStreamingContext);
|
||||
|
|
@ -7,17 +7,15 @@ import MuteButton from '@custom/shared/components/MuteButton';
|
|||
import Tile from '@custom/shared/components/Tile';
|
||||
import { ACCESS_STATE_LOBBY } from '@custom/shared/constants';
|
||||
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
||||
import { useMediaDevices } from '@custom/shared/contexts/MediaDeviceProvider';
|
||||
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
|
||||
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
||||
import {
|
||||
DEVICE_STATE_BLOCKED,
|
||||
DEVICE_STATE_NOT_FOUND,
|
||||
DEVICE_STATE_IN_USE,
|
||||
DEVICE_STATE_PENDING,
|
||||
DEVICE_STATE_LOADING,
|
||||
DEVICE_STATE_GRANTED,
|
||||
} from '@custom/shared/contexts/useDevices';
|
||||
useMediaDevices,
|
||||
} from '@custom/shared/contexts/MediaDeviceProvider';
|
||||
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
|
||||
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
||||
import IconSettings from '@custom/shared/icons/settings-sm.svg';
|
||||
|
||||
import { useDeepCompareMemo } from 'use-deep-compare';
|
||||
|
|
@ -33,7 +31,8 @@ export const HairCheck = () => {
|
|||
const { callObject } = useCallState();
|
||||
const { localParticipant } = useParticipants();
|
||||
const {
|
||||
deviceState,
|
||||
camState,
|
||||
micState,
|
||||
camError,
|
||||
micError,
|
||||
isCamMuted,
|
||||
|
|
@ -87,7 +86,7 @@ export const HairCheck = () => {
|
|||
}
|
||||
};
|
||||
|
||||
// Memoize the to prevent unnecassary re-renders
|
||||
// Memoize the to prevent unnecessary re-renders
|
||||
const tileMemo = useDeepCompareMemo(
|
||||
() => (
|
||||
<Tile
|
||||
|
|
@ -100,21 +99,14 @@ export const HairCheck = () => {
|
|||
[localParticipant]
|
||||
);
|
||||
|
||||
const isLoading = useMemo(() => deviceState === DEVICE_STATE_LOADING, [
|
||||
deviceState,
|
||||
const isLoading = useMemo(() => camState === DEVICE_STATE_PENDING || micState === DEVICE_STATE_PENDING, [
|
||||
camState, micState,
|
||||
]);
|
||||
|
||||
const hasError = useMemo(() => {
|
||||
return !(!deviceState ||
|
||||
[
|
||||
DEVICE_STATE_LOADING,
|
||||
DEVICE_STATE_PENDING,
|
||||
DEVICE_STATE_GRANTED,
|
||||
].includes(deviceState));
|
||||
}, [deviceState]);
|
||||
const hasError = useMemo(() => camError || micError, [camError, micError]);
|
||||
|
||||
const camErrorVerbose = useMemo(() => {
|
||||
switch (camError) {
|
||||
switch (camState) {
|
||||
case DEVICE_STATE_BLOCKED:
|
||||
return 'Camera blocked by user';
|
||||
case DEVICE_STATE_NOT_FOUND:
|
||||
|
|
@ -124,7 +116,20 @@ export const HairCheck = () => {
|
|||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}, [camError]);
|
||||
}, [camState]);
|
||||
|
||||
const micErrorVerbose = useMemo(() => {
|
||||
switch (micState) {
|
||||
case DEVICE_STATE_BLOCKED:
|
||||
return 'Microphone blocked by user';
|
||||
case DEVICE_STATE_NOT_FOUND:
|
||||
return 'Microphone not found';
|
||||
case DEVICE_STATE_IN_USE:
|
||||
return 'Microphone in use';
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}, [micState]);
|
||||
|
||||
const showWaitingMessage = useMemo(() => {
|
||||
return (
|
||||
|
|
@ -206,14 +211,14 @@ export const HairCheck = () => {
|
|||
<div className="overlay-message">{camErrorVerbose}</div>
|
||||
)}
|
||||
{micError && (
|
||||
<div className="overlay-message">{micError}</div>
|
||||
<div className="overlay-message">{micErrorVerbose}</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="mute-buttons">
|
||||
<MuteButton isMuted={isCamMuted} disabled={!!camError} />
|
||||
<MuteButton mic isMuted={isMicMuted} disabled={!!micError} />
|
||||
<MuteButton isMuted={isCamMuted} disabled={camError} />
|
||||
<MuteButton mic isMuted={isMicMuted} disabled={micError} />
|
||||
</div>
|
||||
{tileMemo}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { PEOPLE_ASIDE } from '@custom/shared/components/Aside/PeopleAside';
|
|||
import Button from '@custom/shared/components/Button';
|
||||
import { DEVICE_MODAL } from '@custom/shared/components/DeviceSelectModal';
|
||||
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
||||
import { useMediaDevices } from '@custom/shared/contexts/MediaDeviceProvider';
|
||||
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
||||
import { useResponsive } from '@custom/shared/hooks/useResponsive';
|
||||
import { ReactComponent as IconCameraOff } from '@custom/shared/icons/camera-off-md.svg';
|
||||
|
|
@ -16,6 +15,7 @@ import { ReactComponent as IconMore } from '@custom/shared/icons/more-md.svg';
|
|||
import { ReactComponent as IconNetwork } from '@custom/shared/icons/network-md.svg';
|
||||
import { ReactComponent as IconPeople } from '@custom/shared/icons/people-md.svg';
|
||||
import { ReactComponent as IconSettings } from '@custom/shared/icons/settings-md.svg';
|
||||
import { useMediaDevices } from '../../contexts/MediaDeviceProvider';
|
||||
import { Tray, TrayButton } from './Tray';
|
||||
|
||||
export const BasicTray = () => {
|
||||
|
|
@ -24,7 +24,7 @@ export const BasicTray = () => {
|
|||
const [showMore, setShowMore] = useState(false);
|
||||
const { callObject, leave } = useCallState();
|
||||
const { customTrayComponent, openModal, toggleAside } = useUIState();
|
||||
const { isCamMuted, isMicMuted } = useMediaDevices();
|
||||
const { isMicMuted, isCamMuted } = useMediaDevices();
|
||||
|
||||
const toggleCamera = (newState) => {
|
||||
if (!callObject) return false;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { useLiveStreaming as useDailyLiveStreaming } from '@daily-co/daily-react-hooks';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useUIState } from './UIStateProvider';
|
||||
|
||||
export const LiveStreamingContext = createContext();
|
||||
|
||||
export const LiveStreamingProvider = ({ children }) => {
|
||||
// setCustomCapsule allows us to set the recording capsule on the header
|
||||
// to indicate that the recording is going on.
|
||||
const { setCustomCapsule } = useUIState();
|
||||
|
||||
const handleStreamStarted = useCallback(() => {
|
||||
console.log('📺 Live stream started');
|
||||
setCustomCapsule({ variant: 'recording', label: 'Live streaming' });
|
||||
}, [setCustomCapsule]);
|
||||
|
||||
const handleStreamStopped = useCallback(() => {
|
||||
console.log('📺 Live stream stopped');
|
||||
setCustomCapsule(null);
|
||||
}, [setCustomCapsule]);
|
||||
|
||||
const handleStreamError = useCallback(
|
||||
(e) => {
|
||||
console.log('📺 Live stream error ' + e.errorMsg);
|
||||
setCustomCapsule(null);
|
||||
},
|
||||
[setCustomCapsule]
|
||||
);
|
||||
|
||||
const {
|
||||
isLiveStreaming,
|
||||
layout,
|
||||
errorMsg,
|
||||
startLiveStreaming,
|
||||
updateLiveStreaming,
|
||||
stopLiveStreaming
|
||||
} = useDailyLiveStreaming({
|
||||
onLiveStreamingStarted: handleStreamStarted,
|
||||
onLiveStreamingStopped: handleStreamStopped,
|
||||
onLiveStreamingError: handleStreamError,
|
||||
});
|
||||
|
||||
return (
|
||||
<LiveStreamingContext.Provider
|
||||
value={{
|
||||
isStreaming: isLiveStreaming,
|
||||
streamError: errorMsg,
|
||||
layout,
|
||||
startLiveStreaming,
|
||||
updateLiveStreaming,
|
||||
stopLiveStreaming,
|
||||
}}>
|
||||
{children}
|
||||
</LiveStreamingContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
LiveStreamingProvider.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export const useLiveStreaming = () => useContext(LiveStreamingContext);
|
||||
|
|
@ -1,82 +1,64 @@
|
|||
import React, { createContext, useContext, useCallback } from 'react';
|
||||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import { useDaily, useDevices } from '@daily-co/daily-react-hooks';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useCallState } from './CallProvider';
|
||||
import { useParticipants } from './ParticipantsProvider';
|
||||
import { useDevices } from './useDevices';
|
||||
export const DEVICE_STATE_LOADING = 'loading';
|
||||
export const DEVICE_STATE_PENDING = 'pending';
|
||||
export const DEVICE_STATE_ERROR = 'error';
|
||||
export const DEVICE_STATE_GRANTED = 'granted';
|
||||
export const DEVICE_STATE_NOT_FOUND = 'not-found';
|
||||
export const DEVICE_STATE_NOT_SUPPORTED = 'not-supported';
|
||||
export const DEVICE_STATE_BLOCKED = 'blocked';
|
||||
export const DEVICE_STATE_IN_USE = 'in-use';
|
||||
export const DEVICE_STATE_OFF = 'off';
|
||||
export const DEVICE_STATE_PLAYABLE = 'playable';
|
||||
export const DEVICE_STATE_SENDABLE = 'sendable';
|
||||
|
||||
export const MediaDeviceContext = createContext();
|
||||
|
||||
export const MediaDeviceProvider = ({ children }) => {
|
||||
const { callObject } = useCallState();
|
||||
const { localParticipant } = useParticipants();
|
||||
|
||||
const {
|
||||
camError,
|
||||
cams,
|
||||
currentCam,
|
||||
currentMic,
|
||||
currentSpeaker,
|
||||
deviceState,
|
||||
micError,
|
||||
mics,
|
||||
refreshDevices,
|
||||
setCurrentCam,
|
||||
setCurrentMic,
|
||||
setCurrentSpeaker,
|
||||
hasCamError,
|
||||
cameras,
|
||||
camState,
|
||||
setCamera,
|
||||
hasMicError,
|
||||
microphones,
|
||||
micState,
|
||||
setMicrophone,
|
||||
speakers,
|
||||
} = useDevices(callObject);
|
||||
setSpeaker,
|
||||
refreshDevices,
|
||||
} = useDevices();
|
||||
|
||||
const selectCamera = useCallback(
|
||||
async (newCam) => {
|
||||
if (!callObject || newCam.deviceId === currentCam?.deviceId) return;
|
||||
const { camera } = await callObject.setInputDevicesAsync({
|
||||
videoDeviceId: newCam.deviceId,
|
||||
});
|
||||
setCurrentCam(camera);
|
||||
},
|
||||
[callObject, currentCam, setCurrentCam]
|
||||
);
|
||||
const daily = useDaily();
|
||||
const localParticipant = daily?.participants().local;
|
||||
|
||||
const selectMic = useCallback(
|
||||
async (newMic) => {
|
||||
if (!callObject || newMic.deviceId === currentMic?.deviceId) return;
|
||||
const { mic } = await callObject.setInputDevicesAsync({
|
||||
audioDeviceId: newMic.deviceId,
|
||||
});
|
||||
setCurrentMic(mic);
|
||||
},
|
||||
[callObject, currentMic, setCurrentMic]
|
||||
);
|
||||
const isCamMuted = useMemo(() => {
|
||||
const videoState = localParticipant?.tracks?.video?.state;
|
||||
return videoState === DEVICE_STATE_OFF || videoState === DEVICE_STATE_BLOCKED || hasCamError;
|
||||
}, [hasCamError, localParticipant?.tracks?.video?.state]);
|
||||
|
||||
const selectSpeaker = useCallback(
|
||||
(newSpeaker) => {
|
||||
if (!callObject || newSpeaker.deviceId === currentSpeaker?.deviceId) return;
|
||||
callObject.setOutputDevice({
|
||||
outputDeviceId: newSpeaker.deviceId,
|
||||
});
|
||||
setCurrentSpeaker(newSpeaker);
|
||||
},
|
||||
[callObject, currentSpeaker, setCurrentSpeaker]
|
||||
);
|
||||
const isMicMuted = useMemo(() => {
|
||||
const audioState = localParticipant?.tracks?.audio?.state;
|
||||
return audioState === DEVICE_STATE_OFF || audioState === DEVICE_STATE_BLOCKED || hasMicError;
|
||||
}, [hasMicError, localParticipant?.tracks?.audio?.state]);
|
||||
|
||||
return (
|
||||
<MediaDeviceContext.Provider
|
||||
value={{
|
||||
camError,
|
||||
cams,
|
||||
currentCam,
|
||||
currentMic,
|
||||
currentSpeaker,
|
||||
deviceState,
|
||||
isCamMuted: localParticipant.isCamMuted,
|
||||
isMicMuted: localParticipant.isMicMuted,
|
||||
micError,
|
||||
mics,
|
||||
isCamMuted,
|
||||
isMicMuted,
|
||||
camError: hasCamError,
|
||||
cams: cameras,
|
||||
camState,
|
||||
micError: hasMicError,
|
||||
mics: microphones,
|
||||
micState,
|
||||
refreshDevices,
|
||||
setCurrentCam: selectCamera,
|
||||
setCurrentMic: selectMic,
|
||||
setCurrentSpeaker: selectSpeaker,
|
||||
setCurrentCam: setCamera,
|
||||
setCurrentMic: setMicrophone,
|
||||
setCurrentSpeaker: setSpeaker,
|
||||
speakers,
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import { useScreenShare as useDailyScreenShare } from '@daily-co/daily-react-hooks';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const MAX_SCREEN_SHARES = 2;
|
||||
|
||||
const ScreenShareContext = createContext(null);
|
||||
|
||||
export const ScreenShareProvider = ({ children }) => {
|
||||
const {
|
||||
isSharingScreen,
|
||||
screens,
|
||||
startScreenShare,
|
||||
stopScreenShare
|
||||
} = useDailyScreenShare();
|
||||
|
||||
const isDisabled = useMemo(() => screens.length >= MAX_SCREEN_SHARES && !isSharingScreen,
|
||||
[isSharingScreen, screens.length]
|
||||
);
|
||||
|
||||
return (
|
||||
<ScreenShareContext.Provider
|
||||
value={{
|
||||
isSharingScreen,
|
||||
isDisabled,
|
||||
screens,
|
||||
startScreenShare,
|
||||
stopScreenShare,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ScreenShareContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
ScreenShareProvider.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export const useScreenShare = () => useContext(ScreenShareContext);
|
||||
|
|
@ -32,7 +32,7 @@ const SUBSCRIBE_OR_STAGE_ALL_VIDEO_THRESHOLD = 9;
|
|||
const TracksContext = createContext(null);
|
||||
|
||||
export const TracksProvider = ({ children }) => {
|
||||
const { callObject: daily, optimizeLargeCalls } = useCallState();
|
||||
const { callObject: daily, optimizeLargeCalls, subscribeToTracksAutomatically } = useCallState();
|
||||
const { participants } = useParticipants();
|
||||
const { viewMode } = useUIState();
|
||||
const [state, dispatch] = useReducer(tracksReducer, initialTracksState);
|
||||
|
|
@ -327,7 +327,8 @@ export const TracksProvider = ({ children }) => {
|
|||
|
||||
const joinedSubscriptionQueue = useRef([]);
|
||||
useEffect(() => {
|
||||
if (!daily) return;
|
||||
if (!daily || subscribeToTracksAutomatically) return;
|
||||
|
||||
const joinBatchInterval = setInterval(async () => {
|
||||
if (!joinedSubscriptionQueue.current.length) return;
|
||||
const ids = joinedSubscriptionQueue.current.splice(0);
|
||||
|
|
@ -356,7 +357,7 @@ export const TracksProvider = ({ children }) => {
|
|||
return () => {
|
||||
clearInterval(joinBatchInterval);
|
||||
};
|
||||
}, [daily]);
|
||||
}, [daily, subscribeToTracksAutomatically]);
|
||||
|
||||
useEffect(() => {
|
||||
if (optimizeLargeCalls) {
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
/**
|
||||
* Call State
|
||||
* ---
|
||||
* Duck file that keeps state of call participants
|
||||
*/
|
||||
|
||||
export const ACTION_PARTICIPANT_JOINED = 'ACTION_PARTICIPANT_JOINED';
|
||||
export const ACTION_PARTICIPANT_UPDATED = 'ACTION_PARTICIPANT_UPDATED';
|
||||
export const ACTION_PARTICIPANTED_LEFT = 'ACTION_PARTICIPANT_LEFT';
|
||||
|
||||
export const initialCallState = {
|
||||
audioTracks: {},
|
||||
videoTracks: {},
|
||||
callItems: {},
|
||||
fatalError: false,
|
||||
};
|
||||
|
||||
export function isLocal(id) {
|
||||
return id === 'local';
|
||||
}
|
||||
|
||||
function getCallItems(newParticipants, prevCallItems) {
|
||||
const callItems = {};
|
||||
const entries = Object.entries(newParticipants);
|
||||
entries.forEach(([id, participant]) => {
|
||||
const prevState = prevCallItems[id];
|
||||
const hasLoaded = !prevState?.isLoading;
|
||||
const missingTracks = !(participant.audioTrack || participant.videoTrack);
|
||||
const joined = prevState?.joined || new Date().getTime() / 1000;
|
||||
const local = isLocal(id);
|
||||
|
||||
callItems[id] = {
|
||||
id,
|
||||
name: participant.user_name || 'Guest',
|
||||
audioTrack: participant.audioTrack,
|
||||
videoTrack: participant.videoTrack,
|
||||
hasNameSet: !!participant.user_name,
|
||||
isActiveSpeaker: !!prevState?.isActiveSpeaker,
|
||||
isCamMuted: !participant.video,
|
||||
isLoading: !hasLoaded && missingTracks,
|
||||
isLocal: local,
|
||||
isMicMuted: !participant.audio,
|
||||
isOwner: !!participant.owner,
|
||||
isRecording: !!participant.record,
|
||||
lastActiveDate: prevState?.lastActiveDate ?? null,
|
||||
mutedByHost: participant?.tracks?.audio?.off?.byRemoteRequest,
|
||||
isScreenshare: false,
|
||||
joined,
|
||||
};
|
||||
|
||||
if (participant.screenVideoTrack || participant.screenAudioTrack) {
|
||||
callItems[`${id}-screen`] = {
|
||||
audioTrack: participant.tracks.screenAudio.persistentTrack,
|
||||
hasNameSet: null,
|
||||
id: `${id}-screen`,
|
||||
isLoading: false,
|
||||
isLocal: local,
|
||||
isScreenshare: true,
|
||||
lastActiveDate: prevState?.lastActiveDate ?? null,
|
||||
name: participant.user_name,
|
||||
videoTrack: participant.screenVideoTrack,
|
||||
};
|
||||
}
|
||||
});
|
||||
return callItems;
|
||||
}
|
||||
|
||||
export function isScreenShare(id) {
|
||||
return id.endsWith('-screen');
|
||||
}
|
||||
|
||||
export function containsScreenShare(participants) {
|
||||
return Object.keys(participants).some((id) => isScreenShare(id));
|
||||
}
|
||||
|
||||
export function callReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case ACTION_PARTICIPANT_UPDATED:
|
||||
return {
|
||||
...state,
|
||||
callItems: getCallItems(action.participants, state.callItems),
|
||||
};
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,262 +0,0 @@
|
|||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { sortByKey } from '../lib/sortByKey';
|
||||
|
||||
export const DEVICE_STATE_LOADING = 'loading';
|
||||
export const DEVICE_STATE_PENDING = 'pending';
|
||||
export const DEVICE_STATE_ERROR = 'error';
|
||||
export const DEVICE_STATE_GRANTED = 'granted';
|
||||
export const DEVICE_STATE_NOT_FOUND = 'not-found';
|
||||
export const DEVICE_STATE_NOT_SUPPORTED = 'not-supported';
|
||||
export const DEVICE_STATE_BLOCKED = 'blocked';
|
||||
export const DEVICE_STATE_IN_USE = 'in-use';
|
||||
export const DEVICE_STATE_OFF = 'off';
|
||||
export const DEVICE_STATE_PLAYABLE = 'playable';
|
||||
export const DEVICE_STATE_SENDABLE = 'sendable';
|
||||
|
||||
export const useDevices = (callObject) => {
|
||||
const [deviceState, setDeviceState] = useState(DEVICE_STATE_LOADING);
|
||||
const [currentCam, setCurrentCam] = useState(null);
|
||||
const [currentMic, setCurrentMic] = useState(null);
|
||||
const [currentSpeaker, setCurrentSpeaker] = useState(null);
|
||||
|
||||
const [cams, setCams] = useState([]);
|
||||
const [mics, setMics] = useState([]);
|
||||
const [speakers, setSpeakers] = useState([]);
|
||||
|
||||
const [camError, setCamError] = useState(null);
|
||||
const [micError, setMicError] = useState(null);
|
||||
|
||||
const updateDeviceState = useCallback(async () => {
|
||||
if (
|
||||
typeof navigator?.mediaDevices?.getUserMedia === 'undefined' ||
|
||||
typeof navigator?.mediaDevices?.enumerateDevices === 'undefined'
|
||||
) {
|
||||
setDeviceState(DEVICE_STATE_NOT_SUPPORTED);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { devices } = await callObject.enumerateDevices();
|
||||
|
||||
const { camera, mic, speaker } = await callObject.getInputDevices();
|
||||
|
||||
setCurrentCam(camera ?? null);
|
||||
setCurrentMic(mic ?? null);
|
||||
setCurrentSpeaker(speaker ?? null);
|
||||
|
||||
const [defaultCam, ...videoDevices] = devices.filter(
|
||||
(d) => d.kind === 'videoinput' && d.deviceId !== ''
|
||||
);
|
||||
setCams(
|
||||
[
|
||||
defaultCam,
|
||||
...videoDevices.sort((a, b) => sortByKey(a, b, 'label', false)),
|
||||
].filter(Boolean)
|
||||
);
|
||||
const [defaultMic, ...micDevices] = devices.filter(
|
||||
(d) => d.kind === 'audioinput' && d.deviceId !== ''
|
||||
);
|
||||
setMics(
|
||||
[
|
||||
defaultMic,
|
||||
...micDevices.sort((a, b) => sortByKey(a, b, 'label', false)),
|
||||
].filter(Boolean)
|
||||
);
|
||||
const [defaultSpeaker, ...speakerDevices] = devices.filter(
|
||||
(d) => d.kind === 'audiooutput' && d.deviceId !== ''
|
||||
);
|
||||
setSpeakers(
|
||||
[
|
||||
defaultSpeaker,
|
||||
...speakerDevices.sort((a, b) => sortByKey(a, b, 'label', false)),
|
||||
].filter(Boolean)
|
||||
);
|
||||
|
||||
console.log(`Current cam: ${camera.label}`);
|
||||
console.log(`Current mic: ${mic.label}`);
|
||||
console.log(`Current speakers: ${speaker.label}`);
|
||||
} catch (e) {
|
||||
setDeviceState(DEVICE_STATE_NOT_SUPPORTED);
|
||||
}
|
||||
}, [callObject]);
|
||||
|
||||
const updateDeviceErrors = useCallback(() => {
|
||||
if (!callObject) return;
|
||||
const { tracks } = callObject.participants().local;
|
||||
|
||||
if (tracks.video?.blocked?.byPermissions) {
|
||||
setCamError(DEVICE_STATE_BLOCKED);
|
||||
} else if (tracks.video?.blocked?.byDeviceMissing) {
|
||||
setCamError(DEVICE_STATE_NOT_FOUND);
|
||||
} else if (tracks.video?.blocked?.byDeviceInUse) {
|
||||
setCamError(DEVICE_STATE_IN_USE);
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
DEVICE_STATE_LOADING,
|
||||
DEVICE_STATE_OFF,
|
||||
DEVICE_STATE_PLAYABLE,
|
||||
DEVICE_STATE_SENDABLE,
|
||||
].includes(tracks.video.state)
|
||||
) {
|
||||
setCamError(null);
|
||||
}
|
||||
|
||||
if (tracks.audio?.blocked?.byPermissions) {
|
||||
setMicError(DEVICE_STATE_BLOCKED);
|
||||
} else if (tracks.audio?.blocked?.byDeviceMissing) {
|
||||
setMicError(DEVICE_STATE_NOT_FOUND);
|
||||
} else if (tracks.audio?.blocked?.byDeviceInUse) {
|
||||
setMicError(DEVICE_STATE_IN_USE);
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
DEVICE_STATE_LOADING,
|
||||
DEVICE_STATE_OFF,
|
||||
DEVICE_STATE_PLAYABLE,
|
||||
DEVICE_STATE_SENDABLE,
|
||||
].includes(tracks.audio.state)
|
||||
) {
|
||||
setMicError(null);
|
||||
}
|
||||
}, [callObject]);
|
||||
|
||||
const handleParticipantUpdated = useCallback(
|
||||
({ participant }) => {
|
||||
if (!callObject || deviceState === 'not-supported' || !participant.local) return;
|
||||
|
||||
switch (participant?.tracks.video.state) {
|
||||
case DEVICE_STATE_BLOCKED:
|
||||
setDeviceState(DEVICE_STATE_ERROR);
|
||||
break;
|
||||
case DEVICE_STATE_OFF:
|
||||
case DEVICE_STATE_PLAYABLE:
|
||||
updateDeviceState();
|
||||
setDeviceState(DEVICE_STATE_GRANTED);
|
||||
break;
|
||||
}
|
||||
|
||||
updateDeviceErrors();
|
||||
},
|
||||
[callObject, deviceState, updateDeviceErrors, updateDeviceState]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!callObject) return;
|
||||
|
||||
/**
|
||||
If the user is slow to allow access, we'll update the device state
|
||||
so our app can show a prompt requesting access
|
||||
*/
|
||||
let pendingAccessTimeout;
|
||||
|
||||
const handleJoiningMeeting = () => {
|
||||
pendingAccessTimeout = setTimeout(() => {
|
||||
setDeviceState(DEVICE_STATE_PENDING);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const handleJoinedMeeting = () => {
|
||||
clearTimeout(pendingAccessTimeout);
|
||||
// Note: setOutputDevice() is not honored before join() so we must enumerate again
|
||||
updateDeviceState();
|
||||
};
|
||||
|
||||
updateDeviceState();
|
||||
callObject.on('joining-meeting', handleJoiningMeeting);
|
||||
callObject.on('joined-meeting', handleJoinedMeeting);
|
||||
callObject.on('participant-updated', handleParticipantUpdated);
|
||||
return () => {
|
||||
clearTimeout(pendingAccessTimeout);
|
||||
callObject.off('joining-meeting', handleJoiningMeeting);
|
||||
callObject.off('joined-meeting', handleJoinedMeeting);
|
||||
callObject.off('participant-updated', handleParticipantUpdated);
|
||||
};
|
||||
}, [callObject, handleParticipantUpdated, updateDeviceState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!callObject) return;
|
||||
|
||||
console.log('💻 Device provider events bound');
|
||||
|
||||
const handleCameraError = ({
|
||||
errorMsg: { errorMsg, audioOk, videoOk },
|
||||
error,
|
||||
}) => {
|
||||
switch (error?.type) {
|
||||
case 'cam-in-use':
|
||||
setDeviceState(DEVICE_STATE_ERROR);
|
||||
setCamError(DEVICE_STATE_IN_USE);
|
||||
break;
|
||||
case 'mic-in-use':
|
||||
setDeviceState(DEVICE_STATE_ERROR);
|
||||
setMicError(DEVICE_STATE_IN_USE);
|
||||
break;
|
||||
case 'cam-mic-in-use':
|
||||
setDeviceState(DEVICE_STATE_ERROR);
|
||||
setCamError(DEVICE_STATE_IN_USE);
|
||||
setMicError(DEVICE_STATE_IN_USE);
|
||||
break;
|
||||
default:
|
||||
switch (errorMsg) {
|
||||
case 'devices error':
|
||||
setDeviceState(DEVICE_STATE_ERROR);
|
||||
setCamError(videoOk ? null : DEVICE_STATE_NOT_FOUND);
|
||||
setMicError(audioOk ? null : DEVICE_STATE_NOT_FOUND);
|
||||
break;
|
||||
case 'not allowed':
|
||||
setDeviceState(DEVICE_STATE_ERROR);
|
||||
updateDeviceErrors();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = ({ errorMsg }) => {
|
||||
switch (errorMsg) {
|
||||
case 'not allowed':
|
||||
setDeviceState(DEVICE_STATE_ERROR);
|
||||
updateDeviceErrors();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartedCamera = () => {
|
||||
updateDeviceErrors();
|
||||
};
|
||||
|
||||
callObject.on('camera-error', handleCameraError);
|
||||
callObject.on('error', handleError);
|
||||
callObject.on('started-camera', handleStartedCamera);
|
||||
return () => {
|
||||
callObject.off('camera-error', handleCameraError);
|
||||
callObject.off('error', handleError);
|
||||
callObject.off('started-camera', handleStartedCamera);
|
||||
};
|
||||
}, [callObject, updateDeviceErrors]);
|
||||
|
||||
return {
|
||||
camError,
|
||||
cams,
|
||||
currentCam,
|
||||
currentMic,
|
||||
currentSpeaker,
|
||||
deviceState,
|
||||
micError,
|
||||
mics,
|
||||
refreshDevices: updateDeviceState,
|
||||
setCurrentCam,
|
||||
setCurrentMic,
|
||||
setCurrentSpeaker,
|
||||
speakers,
|
||||
};
|
||||
};
|
||||
|
||||
export default useDevices;
|
||||
Loading…
Reference in New Issue