diff --git a/custom/basic-call/README.md b/custom/basic-call/README.md
index 648f221..7d4b1e6 100644
--- a/custom/basic-call/README.md
+++ b/custom/basic-call/README.md
@@ -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
diff --git a/custom/basic-call/components/App/App.js b/custom/basic-call/components/App/App.js
index 81b43d9..f648f33 100644
--- a/custom/basic-call/components/App/App.js
+++ b/custom/basic-call/components/App/App.js
@@ -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(
() => (
<>
diff --git a/custom/basic-call/components/Call/VideoGrid.js b/custom/basic-call/components/Call/VideoGrid.js
index e71ff6d..a8ba799 100644
--- a/custom/basic-call/components/Call/VideoGrid.js
+++ b/custom/basic-call/components/Call/VideoGrid.js
@@ -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) => (
diff --git a/custom/basic-call/pages/index.js b/custom/basic-call/pages/index.js
index 6ba92ec..9f64b27 100644
--- a/custom/basic-call/pages/index.js
+++ b/custom/basic-call/pages/index.js
@@ -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({
- {customAppComponent || }
+
+ {customAppComponent || }
+
diff --git a/custom/fitness-demo/README.md b/custom/fitness-demo/README.md
index 1bec2c5..833f476 100644
--- a/custom/fitness-demo/README.md
+++ b/custom/fitness-demo/README.md
@@ -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
diff --git a/custom/fitness-demo/components/App/App.js b/custom/fitness-demo/components/App/App.js
index a86b453..4f978b9 100644
--- a/custom/fitness-demo/components/App/App.js
+++ b/custom/fitness-demo/components/App/App.js
@@ -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(
() => (
<>
diff --git a/custom/fitness-demo/components/Tray/ScreenShare.js b/custom/fitness-demo/components/Tray/ScreenShare.js
index 6321f82..7079314 100644
--- a/custom/fitness-demo/components/Tray/ScreenShare.js
+++ b/custom/fitness-demo/components/Tray/ScreenShare.js
@@ -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 = () => {
diff --git a/custom/fitness-demo/components/Tray/Stream.js b/custom/fitness-demo/components/Tray/Stream.js
index a03885e..379a49e 100644
--- a/custom/fitness-demo/components/Tray/Stream.js
+++ b/custom/fitness-demo/components/Tray/Stream.js
@@ -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';
diff --git a/custom/fitness-demo/pages/[room].js b/custom/fitness-demo/pages/[room].js
index cc035bb..dfb0a8b 100644
--- a/custom/fitness-demo/pages/[room].js
+++ b/custom/fitness-demo/pages/[room].js
@@ -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 = ({
- {customAppComponent || }
+
+ {customAppComponent || }
+
diff --git a/custom/live-streaming/README.md b/custom/live-streaming/README.md
index e8da111..86031e7 100644
--- a/custom/live-streaming/README.md
+++ b/custom/live-streaming/README.md
@@ -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
diff --git a/custom/live-streaming/components/App.js b/custom/live-streaming/components/App.js
index cd47217..888fa3e 100644
--- a/custom/live-streaming/components/App.js
+++ b/custom/live-streaming/components/App.js
@@ -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 = () => (
diff --git a/custom/live-streaming/components/LiveStreamingModal.js b/custom/live-streaming/components/LiveStreamingModal.js
index 97896c8..4047f0a 100644
--- a/custom/live-streaming/components/LiveStreamingModal.js
+++ b/custom/live-streaming/components/LiveStreamingModal.js
@@ -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 (
{
@@ -70,7 +83,7 @@ export const LiveStreamingModal = () => {
@@ -85,22 +98,21 @@ export const LiveStreamingModal = () => {
setLayout(Number(e.target.value))}
- value={layout}
+ onChange={handleSelectLayoutInputChange}
+ value={layoutType}
>
- {LAYOUTS.map((l, i) => (
-
))}
- {layout !==
- LAYOUTS.findIndex((l) => l.value === 'single-participant') && (
+ {layoutType === 'default' && (
setMaxCams(Number(e.target.value))}
+ onChange={handleSelectMaxCamsInputChange}
value={maxCams}
>
@@ -116,15 +128,17 @@ export const LiveStreamingModal = () => {
)}
- {layout ===
- LAYOUTS.findIndex((l) => l.value === 'single-participant') && (
+ {layoutType === 'single-participant' && (
setParticipant(e.target.value)}
- value={participant}
+ onChange={handleSelectParticipantInputChange}
+ value={participantId}
>
- {allParticipants.map((p) => (
-
+ {participants.map((p) => (
+
))}
@@ -137,8 +151,14 @@ export const LiveStreamingModal = () => {
type="text"
placeholder="RTMP URL"
required
- onChange={(e) => setRtmpUrl(e.target.value)}
+ onChange={handleRMTPURLChange}
/>
+
+ Want to learn more about RTMP url?
+
diff --git a/custom/live-streaming/components/Tray.js b/custom/live-streaming/components/Tray.js
index baee790..2943187 100644
--- a/custom/live-streaming/components/Tray.js
+++ b/custom/live-streaming/components/Tray.js
@@ -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 = () => {
diff --git a/custom/live-streaming/contexts/LiveStreamingProvider.js b/custom/live-streaming/contexts/LiveStreamingProvider.js
deleted file mode 100644
index 6e940c8..0000000
--- a/custom/live-streaming/contexts/LiveStreamingProvider.js
+++ /dev/null
@@ -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 (
-
- {children}
-
- );
-};
-
-LiveStreamingProvider.propTypes = {
- children: PropTypes.node,
-};
-
-export const useLiveStreaming = () => useContext(LiveStreamingContext);
diff --git a/custom/shared/components/HairCheck/HairCheck.js b/custom/shared/components/HairCheck/HairCheck.js
index 4cf0659..0fd2117 100644
--- a/custom/shared/components/HairCheck/HairCheck.js
+++ b/custom/shared/components/HairCheck/HairCheck.js
@@ -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(
() => (
{
[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 = () => {
{camErrorVerbose}
)}
{micError && (
- {micError}
+ {micErrorVerbose}
)}
>
)}
-
-
+
+
{tileMemo}
diff --git a/custom/shared/components/Tray/BasicTray.js b/custom/shared/components/Tray/BasicTray.js
index 4ad4b41..fa89a20 100644
--- a/custom/shared/components/Tray/BasicTray.js
+++ b/custom/shared/components/Tray/BasicTray.js
@@ -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;
diff --git a/custom/shared/contexts/LiveStreamingProvider.js b/custom/shared/contexts/LiveStreamingProvider.js
new file mode 100644
index 0000000..b5aba2a
--- /dev/null
+++ b/custom/shared/contexts/LiveStreamingProvider.js
@@ -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 (
+
+ {children}
+
+ );
+};
+
+LiveStreamingProvider.propTypes = {
+ children: PropTypes.node,
+};
+
+export const useLiveStreaming = () => useContext(LiveStreamingContext);
diff --git a/custom/shared/contexts/MediaDeviceProvider.js b/custom/shared/contexts/MediaDeviceProvider.js
index b0b4349..6230429 100644
--- a/custom/shared/contexts/MediaDeviceProvider.js
+++ b/custom/shared/contexts/MediaDeviceProvider.js
@@ -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 (
diff --git a/custom/shared/contexts/ScreenShareProvider.js b/custom/shared/contexts/ScreenShareProvider.js
new file mode 100644
index 0000000..9b4e68c
--- /dev/null
+++ b/custom/shared/contexts/ScreenShareProvider.js
@@ -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 (
+
+ {children}
+
+ );
+};
+
+ScreenShareProvider.propTypes = {
+ children: PropTypes.node,
+};
+
+export const useScreenShare = () => useContext(ScreenShareContext);
diff --git a/custom/shared/contexts/TracksProvider.js b/custom/shared/contexts/TracksProvider.js
index d4f17e5..83b2068 100644
--- a/custom/shared/contexts/TracksProvider.js
+++ b/custom/shared/contexts/TracksProvider.js
@@ -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) {
diff --git a/custom/shared/contexts/callState.js b/custom/shared/contexts/callState.js
deleted file mode 100644
index 2ff9112..0000000
--- a/custom/shared/contexts/callState.js
+++ /dev/null
@@ -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();
- }
-}
diff --git a/custom/shared/contexts/useDevices.js b/custom/shared/contexts/useDevices.js
deleted file mode 100644
index ee4362a..0000000
--- a/custom/shared/contexts/useDevices.js
+++ /dev/null
@@ -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;