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/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/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/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 3a0e378..0fd2117 100644 --- a/custom/shared/components/HairCheck/HairCheck.js +++ b/custom/shared/components/HairCheck/HairCheck.js @@ -86,7 +86,7 @@ export const HairCheck = () => { } }; - // Memoize the to prevent unnecassary re-renders + // Memoize the to prevent unnecessary re-renders const tileMemo = useDeepCompareMemo( () => ( { + // 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/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(); - } -}