Merge branch 'main' of github.com:daily-demos/examples into waiting-participants-hook

This commit is contained in:
harshithpabbati 2022-04-12 12:01:26 +05:30
commit 47baaaef80
22 changed files with 271 additions and 576 deletions

View File

@ -38,9 +38,6 @@ This demo puts to work the following [shared libraries](../shared):
**[MediaDeviceProvider.js](../shared/contexts/MediaDeviceProvider.js)** **[MediaDeviceProvider.js](../shared/contexts/MediaDeviceProvider.js)**
Convenience context that provides an interface to media devices throughout app 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)** **[CallProvider.js](../shared/contexts/CallProvider.js)**
Primary call context that manages Daily call state, participant state and call object interaction Primary call context that manages Daily call state, participant state and call object interaction

View File

@ -17,7 +17,7 @@ export const App = ({ customComponentForState }) => {
...customComponentForState, ...customComponentForState,
}); });
// Memoize children to avoid unnecassary renders from HOC // Memoize children to avoid unnecessary renders from HOC
return useMemo( return useMemo(
() => ( () => (
<> <>

View File

@ -89,7 +89,7 @@ export const VideoGrid = React.memo(
return bestLayout; return bestLayout;
}, [dimensions, participants]); }, [dimensions, participants]);
// Memoize our tile list to avoid unnecassary re-renders // Memoize our tile list to avoid unnecessary re-renders
const tiles = useDeepCompareMemo( const tiles = useDeepCompareMemo(
() => () =>
participants.map((p) => ( participants.map((p) => (

View File

@ -2,6 +2,7 @@ import React, { useState, useCallback } from 'react';
import { CallProvider } from '@custom/shared/contexts/CallProvider'; import { CallProvider } from '@custom/shared/contexts/CallProvider';
import { MediaDeviceProvider } from '@custom/shared/contexts/MediaDeviceProvider'; import { MediaDeviceProvider } from '@custom/shared/contexts/MediaDeviceProvider';
import { ParticipantsProvider } from '@custom/shared/contexts/ParticipantsProvider'; import { ParticipantsProvider } from '@custom/shared/contexts/ParticipantsProvider';
import { ScreenShareProvider } from '@custom/shared/contexts/ScreenShareProvider';
import { TracksProvider } from '@custom/shared/contexts/TracksProvider'; import { TracksProvider } from '@custom/shared/contexts/TracksProvider';
import { UIStateProvider } from '@custom/shared/contexts/UIStateProvider'; import { UIStateProvider } from '@custom/shared/contexts/UIStateProvider';
import { WaitingRoomProvider } from '@custom/shared/contexts/WaitingRoomProvider'; import { WaitingRoomProvider } from '@custom/shared/contexts/WaitingRoomProvider';
@ -125,7 +126,9 @@ export default function Index({
<TracksProvider> <TracksProvider>
<MediaDeviceProvider> <MediaDeviceProvider>
<WaitingRoomProvider> <WaitingRoomProvider>
{customAppComponent || <App />} <ScreenShareProvider>
{customAppComponent || <App />}
</ScreenShareProvider>
</WaitingRoomProvider> </WaitingRoomProvider>
</MediaDeviceProvider> </MediaDeviceProvider>
</TracksProvider> </TracksProvider>

View File

@ -38,9 +38,6 @@ This demo puts to work the following [shared libraries](../shared):
**[MediaDeviceProvider.js](../shared/contexts/MediaDeviceProvider.js)** **[MediaDeviceProvider.js](../shared/contexts/MediaDeviceProvider.js)**
Convenience context that provides an interface to media devices throughout app 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)** **[CallProvider.js](../shared/contexts/CallProvider.js)**
Primary call context that manages Daily call state, participant state and call object interaction Primary call context that manages Daily call state, participant state and call object interaction

View File

@ -1,8 +1,8 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { LiveStreamingProvider } from '@custom/live-streaming/contexts/LiveStreamingProvider';
import { RecordingProvider } from '@custom/recording/contexts/RecordingProvider'; import { RecordingProvider } from '@custom/recording/contexts/RecordingProvider';
import ExpiryTimer from '@custom/shared/components/ExpiryTimer'; import ExpiryTimer from '@custom/shared/components/ExpiryTimer';
import { useCallState } from '@custom/shared/contexts/CallProvider'; import { useCallState } from '@custom/shared/contexts/CallProvider';
import { LiveStreamingProvider } from '@custom/shared/contexts/LiveStreamingProvider';
import { useCallUI } from '@custom/shared/hooks/useCallUI'; import { useCallUI } from '@custom/shared/hooks/useCallUI';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -21,7 +21,7 @@ export const App = ({ customComponentForState }) => {
...customComponentForState, ...customComponentForState,
}); });
// Memoize children to avoid unnecassary renders from HOC // Memoize children to avoid unnecessary renders from HOC
return useMemo( return useMemo(
() => ( () => (
<> <>

View File

@ -1,30 +1,23 @@
import React, { useMemo } from 'react'; import React from 'react';
import { TrayButton } from '@custom/shared/components/Tray'; import { TrayButton } from '@custom/shared/components/Tray';
import { useCallState } from '@custom/shared/contexts/CallProvider'; import { useCallState } from '@custom/shared/contexts/CallProvider';
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider'; 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'; import { ReactComponent as IconShare } from '@custom/shared/icons/share-sm.svg';
const MAX_SCREEN_SHARES = 2;
export const ScreenShareTray = () => { export const ScreenShareTray = () => {
const { callObject, enableScreenShare } = useCallState(); const { enableScreenShare } = useCallState();
const { screens, participants, localParticipant } = useParticipants(); const { localParticipant } = useParticipants();
const {
const isSharingScreen = useMemo( isSharingScreen,
() => screens.some((s) => s.isLocal), isDisabled,
[screens] startScreenShare,
); stopScreenShare
} = useScreenShare();
const screensLength = useMemo(() => screens.length, [screens]);
const toggleScreenShare = () => const toggleScreenShare = () =>
isSharingScreen ? callObject.stopScreenShare() : callObject.startScreenShare(); isSharingScreen ? stopScreenShare() : startScreenShare();
const disabled =
participants.length &&
screensLength >= MAX_SCREEN_SHARES &&
!isSharingScreen;
if (!enableScreenShare) return null; if (!enableScreenShare) return null;
if (!localParticipant.isOwner) return null; if (!localParticipant.isOwner) return null;
@ -33,7 +26,7 @@ export const ScreenShareTray = () => {
<TrayButton <TrayButton
label={isSharingScreen ? 'Stop': 'Share'} label={isSharingScreen ? 'Stop': 'Share'}
orange={isSharingScreen} orange={isSharingScreen}
disabled={disabled} disabled={isDisabled}
onClick={toggleScreenShare} onClick={toggleScreenShare}
> >
<IconShare /> <IconShare />

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { LIVE_STREAMING_MODAL } from '@custom/live-streaming/components/LiveStreamingModal'; 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 { TrayButton } from '@custom/shared/components/Tray';
import { useLiveStreaming } from '@custom/shared/contexts/LiveStreamingProvider';
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider'; import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
import { useUIState } from '@custom/shared/contexts/UIStateProvider'; import { useUIState } from '@custom/shared/contexts/UIStateProvider';
import { ReactComponent as IconStream } from '@custom/shared/icons/streaming-md.svg'; import { ReactComponent as IconStream } from '@custom/shared/icons/streaming-md.svg';

View File

@ -2,6 +2,7 @@ import React from 'react';
import { CallProvider } from '@custom/shared/contexts/CallProvider'; import { CallProvider } from '@custom/shared/contexts/CallProvider';
import { MediaDeviceProvider } from '@custom/shared/contexts/MediaDeviceProvider'; import { MediaDeviceProvider } from '@custom/shared/contexts/MediaDeviceProvider';
import { ParticipantsProvider } from '@custom/shared/contexts/ParticipantsProvider'; import { ParticipantsProvider } from '@custom/shared/contexts/ParticipantsProvider';
import { ScreenShareProvider } from '@custom/shared/contexts/ScreenShareProvider';
import { TracksProvider } from '@custom/shared/contexts/TracksProvider'; import { TracksProvider } from '@custom/shared/contexts/TracksProvider';
import { UIStateProvider } from '@custom/shared/contexts/UIStateProvider'; import { UIStateProvider } from '@custom/shared/contexts/UIStateProvider';
import { WaitingRoomProvider } from '@custom/shared/contexts/WaitingRoomProvider'; import { WaitingRoomProvider } from '@custom/shared/contexts/WaitingRoomProvider';
@ -40,7 +41,9 @@ const Room = ({
<TracksProvider> <TracksProvider>
<MediaDeviceProvider> <MediaDeviceProvider>
<WaitingRoomProvider> <WaitingRoomProvider>
{customAppComponent || <App />} <ScreenShareProvider>
{customAppComponent || <App />}
</ScreenShareProvider>
</WaitingRoomProvider> </WaitingRoomProvider>
</MediaDeviceProvider> </MediaDeviceProvider>
</TracksProvider> </TracksProvider>

View File

@ -18,7 +18,13 @@
Please note: this demo is not currently mobile optimised 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 # set both DAILY_API_KEY and DAILY_DOMAIN

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import App from '@custom/basic-call/components/App'; 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 // Extend our basic call app component with the live streaming context
export const AppWithLiveStreaming = () => ( export const AppWithLiveStreaming = () => (

View File

@ -5,10 +5,9 @@ import Field from '@custom/shared/components/Field';
import { TextInput, SelectInput } from '@custom/shared/components/Input'; import { TextInput, SelectInput } from '@custom/shared/components/Input';
import Modal from '@custom/shared/components/Modal'; import Modal from '@custom/shared/components/Modal';
import Well from '@custom/shared/components/Well'; 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 { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
import { useUIState } from '@custom/shared/contexts/UIStateProvider'; import { useUIState } from '@custom/shared/contexts/UIStateProvider';
import { useLiveStreaming } from '../contexts/LiveStreamingProvider';
export const LIVE_STREAMING_MODAL = 'live-streaming'; export const LIVE_STREAMING_MODAL = 'live-streaming';
@ -19,15 +18,19 @@ const LAYOUTS = [
]; ];
export const LiveStreamingModal = () => { export const LiveStreamingModal = () => {
const { callObject } = useCallState(); const { participants } = useParticipants();
const { allParticipants } = useParticipants();
const { currentModals, closeModal } = useUIState(); const { currentModals, closeModal } = useUIState();
const { isStreaming, streamError } = useLiveStreaming(); const {
isStreaming,
streamError,
startLiveStreaming,
stopLiveStreaming,
} = useLiveStreaming();
const [pending, setPending] = useState(false); const [pending, setPending] = useState(false);
const [rtmpUrl, setRtmpUrl] = useState(''); const [rtmpUrl, setRtmpUrl] = useState('');
const [layout, setLayout] = useState(0); const [layoutType, setLayoutType] = useState('default');
const [maxCams, setMaxCams] = useState(9); const [maxCams, setMaxCams] = useState(9);
const [participant, setParticipant] = useState(0); const [participantId, setParticipantId] = useState(0);
useEffect(() => { useEffect(() => {
// Reset pending state whenever stream state changes // Reset pending state whenever stream state changes
@ -35,20 +38,30 @@ export const LiveStreamingModal = () => {
}, [isStreaming]); }, [isStreaming]);
function startLiveStream() { function startLiveStream() {
setPending(true); const config = {
rtmpUrl,
layout: {
preset: layoutType,
},
};
const opts = if (layoutType === 'single-participant')
layout === 'single-participant' config.layout.session_id = participantId;
? { session_id: participant.id } else if (layoutType === 'default') config.layout.max_cam_streams = maxCams;
: { max_cam_streams: maxCams };
callObject.startLiveStreaming({ rtmpUrl, preset: layout, ...opts }); startLiveStreaming(config);
} }
function stopLiveStreaming() { function stopLiveStream() {
setPending(true); 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 ( return (
<Modal <Modal
title="Live stream" title="Live stream"
@ -62,7 +75,7 @@ export const LiveStreamingModal = () => {
<Button <Button
fullWidth fullWidth
disabled={!rtmpUrl || pending} disabled={!rtmpUrl || pending}
onClick={() => startLiveStream()} onClick={startLiveStream}
> >
{pending ? 'Starting stream...' : 'Start live streaming'} {pending ? 'Starting stream...' : 'Start live streaming'}
</Button> </Button>
@ -70,7 +83,7 @@ export const LiveStreamingModal = () => {
<Button <Button
fullWidth fullWidth
variant="warning" variant="warning"
onClick={() => stopLiveStreaming()} onClick={stopLiveStream}
> >
Stop live streaming Stop live streaming
</Button> </Button>
@ -85,22 +98,21 @@ export const LiveStreamingModal = () => {
<CardBody> <CardBody>
<Field label="Layout"> <Field label="Layout">
<SelectInput <SelectInput
onChange={(e) => setLayout(Number(e.target.value))} onChange={handleSelectLayoutInputChange}
value={layout} value={layoutType}
> >
{LAYOUTS.map((l, i) => ( {LAYOUTS.map((l) => (
<option value={i} key={l.value}> <option value={l.value} key={l.value}>
{l.label} {l.label}
</option> </option>
))} ))}
</SelectInput> </SelectInput>
</Field> </Field>
{layout !== {layoutType === 'default' && (
LAYOUTS.findIndex((l) => l.value === 'single-participant') && (
<Field label="Additional cameras"> <Field label="Additional cameras">
<SelectInput <SelectInput
onChange={(e) => setMaxCams(Number(e.target.value))} onChange={handleSelectMaxCamsInputChange}
value={maxCams} value={maxCams}
> >
<option value={9}>9 cameras</option> <option value={9}>9 cameras</option>
@ -116,15 +128,17 @@ export const LiveStreamingModal = () => {
</Field> </Field>
)} )}
{layout === {layoutType === 'single-participant' && (
LAYOUTS.findIndex((l) => l.value === 'single-participant') && (
<Field label="Select participant"> <Field label="Select participant">
<SelectInput <SelectInput
onChange={(e) => setParticipant(e.target.value)} onChange={handleSelectParticipantInputChange}
value={participant} value={participantId}
> >
{allParticipants.map((p) => ( <option value={0} disabled>
<option value={p.id} key={p.id}> Select
</option>
{participants.map((p) => (
<option value={p.sessionId} key={p.sessionId}>
{p.name} {p.name}
</option> </option>
))} ))}
@ -137,8 +151,14 @@ export const LiveStreamingModal = () => {
type="text" type="text"
placeholder="RTMP URL" placeholder="RTMP URL"
required 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> </Field>
</CardBody> </CardBody>
</Modal> </Modal>

View File

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { TrayButton } from '@custom/shared/components/Tray'; import { TrayButton } from '@custom/shared/components/Tray';
import { useLiveStreaming } from '@custom/shared/contexts/LiveStreamingProvider';
import { useUIState } from '@custom/shared/contexts/UIStateProvider'; import { useUIState } from '@custom/shared/contexts/UIStateProvider';
import { ReactComponent as IconStream } from '@custom/shared/icons/streaming-md.svg'; import { ReactComponent as IconStream } from '@custom/shared/icons/streaming-md.svg';
import { useLiveStreaming } from '../contexts/LiveStreamingProvider';
import { LIVE_STREAMING_MODAL } from './LiveStreamingModal'; import { LIVE_STREAMING_MODAL } from './LiveStreamingModal';
export const Tray = () => { export const Tray = () => {

View File

@ -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);

View File

@ -7,17 +7,15 @@ import MuteButton from '@custom/shared/components/MuteButton';
import Tile from '@custom/shared/components/Tile'; import Tile from '@custom/shared/components/Tile';
import { ACCESS_STATE_LOBBY } from '@custom/shared/constants'; import { ACCESS_STATE_LOBBY } from '@custom/shared/constants';
import { useCallState } from '@custom/shared/contexts/CallProvider'; 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 { import {
DEVICE_STATE_BLOCKED, DEVICE_STATE_BLOCKED,
DEVICE_STATE_NOT_FOUND, DEVICE_STATE_NOT_FOUND,
DEVICE_STATE_IN_USE, DEVICE_STATE_IN_USE,
DEVICE_STATE_PENDING, DEVICE_STATE_PENDING,
DEVICE_STATE_LOADING, useMediaDevices,
DEVICE_STATE_GRANTED, } from '@custom/shared/contexts/MediaDeviceProvider';
} from '@custom/shared/contexts/useDevices'; import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
import IconSettings from '@custom/shared/icons/settings-sm.svg'; import IconSettings from '@custom/shared/icons/settings-sm.svg';
import { useDeepCompareMemo } from 'use-deep-compare'; import { useDeepCompareMemo } from 'use-deep-compare';
@ -33,7 +31,8 @@ export const HairCheck = () => {
const { callObject } = useCallState(); const { callObject } = useCallState();
const { localParticipant } = useParticipants(); const { localParticipant } = useParticipants();
const { const {
deviceState, camState,
micState,
camError, camError,
micError, micError,
isCamMuted, 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( const tileMemo = useDeepCompareMemo(
() => ( () => (
<Tile <Tile
@ -100,21 +99,14 @@ export const HairCheck = () => {
[localParticipant] [localParticipant]
); );
const isLoading = useMemo(() => deviceState === DEVICE_STATE_LOADING, [ const isLoading = useMemo(() => camState === DEVICE_STATE_PENDING || micState === DEVICE_STATE_PENDING, [
deviceState, camState, micState,
]); ]);
const hasError = useMemo(() => { const hasError = useMemo(() => camError || micError, [camError, micError]);
return !(!deviceState ||
[
DEVICE_STATE_LOADING,
DEVICE_STATE_PENDING,
DEVICE_STATE_GRANTED,
].includes(deviceState));
}, [deviceState]);
const camErrorVerbose = useMemo(() => { const camErrorVerbose = useMemo(() => {
switch (camError) { switch (camState) {
case DEVICE_STATE_BLOCKED: case DEVICE_STATE_BLOCKED:
return 'Camera blocked by user'; return 'Camera blocked by user';
case DEVICE_STATE_NOT_FOUND: case DEVICE_STATE_NOT_FOUND:
@ -124,7 +116,20 @@ export const HairCheck = () => {
default: default:
return 'unknown'; 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(() => { const showWaitingMessage = useMemo(() => {
return ( return (
@ -206,14 +211,14 @@ export const HairCheck = () => {
<div className="overlay-message">{camErrorVerbose}</div> <div className="overlay-message">{camErrorVerbose}</div>
)} )}
{micError && ( {micError && (
<div className="overlay-message">{micError}</div> <div className="overlay-message">{micErrorVerbose}</div>
)} )}
</> </>
)} )}
</div> </div>
<div className="mute-buttons"> <div className="mute-buttons">
<MuteButton isMuted={isCamMuted} disabled={!!camError} /> <MuteButton isMuted={isCamMuted} disabled={camError} />
<MuteButton mic isMuted={isMicMuted} disabled={!!micError} /> <MuteButton mic isMuted={isMicMuted} disabled={micError} />
</div> </div>
{tileMemo} {tileMemo}
</div> </div>

View File

@ -4,7 +4,6 @@ import { PEOPLE_ASIDE } from '@custom/shared/components/Aside/PeopleAside';
import Button from '@custom/shared/components/Button'; import Button from '@custom/shared/components/Button';
import { DEVICE_MODAL } from '@custom/shared/components/DeviceSelectModal'; import { DEVICE_MODAL } from '@custom/shared/components/DeviceSelectModal';
import { useCallState } from '@custom/shared/contexts/CallProvider'; import { useCallState } from '@custom/shared/contexts/CallProvider';
import { useMediaDevices } from '@custom/shared/contexts/MediaDeviceProvider';
import { useUIState } from '@custom/shared/contexts/UIStateProvider'; import { useUIState } from '@custom/shared/contexts/UIStateProvider';
import { useResponsive } from '@custom/shared/hooks/useResponsive'; import { useResponsive } from '@custom/shared/hooks/useResponsive';
import { ReactComponent as IconCameraOff } from '@custom/shared/icons/camera-off-md.svg'; 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 IconNetwork } from '@custom/shared/icons/network-md.svg';
import { ReactComponent as IconPeople } from '@custom/shared/icons/people-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 { ReactComponent as IconSettings } from '@custom/shared/icons/settings-md.svg';
import { useMediaDevices } from '../../contexts/MediaDeviceProvider';
import { Tray, TrayButton } from './Tray'; import { Tray, TrayButton } from './Tray';
export const BasicTray = () => { export const BasicTray = () => {
@ -24,7 +24,7 @@ export const BasicTray = () => {
const [showMore, setShowMore] = useState(false); const [showMore, setShowMore] = useState(false);
const { callObject, leave } = useCallState(); const { callObject, leave } = useCallState();
const { customTrayComponent, openModal, toggleAside } = useUIState(); const { customTrayComponent, openModal, toggleAside } = useUIState();
const { isCamMuted, isMicMuted } = useMediaDevices(); const { isMicMuted, isCamMuted } = useMediaDevices();
const toggleCamera = (newState) => { const toggleCamera = (newState) => {
if (!callObject) return false; if (!callObject) return false;

View File

@ -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);

View File

@ -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 PropTypes from 'prop-types';
import { useCallState } from './CallProvider'; export const DEVICE_STATE_LOADING = 'loading';
import { useParticipants } from './ParticipantsProvider'; export const DEVICE_STATE_PENDING = 'pending';
import { useDevices } from './useDevices'; 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 MediaDeviceContext = createContext();
export const MediaDeviceProvider = ({ children }) => { export const MediaDeviceProvider = ({ children }) => {
const { callObject } = useCallState();
const { localParticipant } = useParticipants();
const { const {
camError, hasCamError,
cams, cameras,
currentCam, camState,
currentMic, setCamera,
currentSpeaker, hasMicError,
deviceState, microphones,
micError, micState,
mics, setMicrophone,
refreshDevices,
setCurrentCam,
setCurrentMic,
setCurrentSpeaker,
speakers, speakers,
} = useDevices(callObject); setSpeaker,
refreshDevices,
} = useDevices();
const selectCamera = useCallback( const daily = useDaily();
async (newCam) => { const localParticipant = daily?.participants().local;
if (!callObject || newCam.deviceId === currentCam?.deviceId) return;
const { camera } = await callObject.setInputDevicesAsync({
videoDeviceId: newCam.deviceId,
});
setCurrentCam(camera);
},
[callObject, currentCam, setCurrentCam]
);
const selectMic = useCallback( const isCamMuted = useMemo(() => {
async (newMic) => { const videoState = localParticipant?.tracks?.video?.state;
if (!callObject || newMic.deviceId === currentMic?.deviceId) return; return videoState === DEVICE_STATE_OFF || videoState === DEVICE_STATE_BLOCKED || hasCamError;
const { mic } = await callObject.setInputDevicesAsync({ }, [hasCamError, localParticipant?.tracks?.video?.state]);
audioDeviceId: newMic.deviceId,
});
setCurrentMic(mic);
},
[callObject, currentMic, setCurrentMic]
);
const selectSpeaker = useCallback( const isMicMuted = useMemo(() => {
(newSpeaker) => { const audioState = localParticipant?.tracks?.audio?.state;
if (!callObject || newSpeaker.deviceId === currentSpeaker?.deviceId) return; return audioState === DEVICE_STATE_OFF || audioState === DEVICE_STATE_BLOCKED || hasMicError;
callObject.setOutputDevice({ }, [hasMicError, localParticipant?.tracks?.audio?.state]);
outputDeviceId: newSpeaker.deviceId,
});
setCurrentSpeaker(newSpeaker);
},
[callObject, currentSpeaker, setCurrentSpeaker]
);
return ( return (
<MediaDeviceContext.Provider <MediaDeviceContext.Provider
value={{ value={{
camError, isCamMuted,
cams, isMicMuted,
currentCam, camError: hasCamError,
currentMic, cams: cameras,
currentSpeaker, camState,
deviceState, micError: hasMicError,
isCamMuted: localParticipant.isCamMuted, mics: microphones,
isMicMuted: localParticipant.isMicMuted, micState,
micError,
mics,
refreshDevices, refreshDevices,
setCurrentCam: selectCamera, setCurrentCam: setCamera,
setCurrentMic: selectMic, setCurrentMic: setMicrophone,
setCurrentSpeaker: selectSpeaker, setCurrentSpeaker: setSpeaker,
speakers, speakers,
}} }}
> >

View File

@ -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);

View File

@ -32,7 +32,7 @@ const SUBSCRIBE_OR_STAGE_ALL_VIDEO_THRESHOLD = 9;
const TracksContext = createContext(null); const TracksContext = createContext(null);
export const TracksProvider = ({ children }) => { export const TracksProvider = ({ children }) => {
const { callObject: daily, optimizeLargeCalls } = useCallState(); const { callObject: daily, optimizeLargeCalls, subscribeToTracksAutomatically } = useCallState();
const { participants } = useParticipants(); const { participants } = useParticipants();
const { viewMode } = useUIState(); const { viewMode } = useUIState();
const [state, dispatch] = useReducer(tracksReducer, initialTracksState); const [state, dispatch] = useReducer(tracksReducer, initialTracksState);
@ -327,7 +327,8 @@ export const TracksProvider = ({ children }) => {
const joinedSubscriptionQueue = useRef([]); const joinedSubscriptionQueue = useRef([]);
useEffect(() => { useEffect(() => {
if (!daily) return; if (!daily || subscribeToTracksAutomatically) return;
const joinBatchInterval = setInterval(async () => { const joinBatchInterval = setInterval(async () => {
if (!joinedSubscriptionQueue.current.length) return; if (!joinedSubscriptionQueue.current.length) return;
const ids = joinedSubscriptionQueue.current.splice(0); const ids = joinedSubscriptionQueue.current.splice(0);
@ -356,7 +357,7 @@ export const TracksProvider = ({ children }) => {
return () => { return () => {
clearInterval(joinBatchInterval); clearInterval(joinBatchInterval);
}; };
}, [daily]); }, [daily, subscribeToTracksAutomatically]);
useEffect(() => { useEffect(() => {
if (optimizeLargeCalls) { if (optimizeLargeCalls) {

View File

@ -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();
}
}

View File

@ -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;