diff --git a/custom/fitness-demo/components/App/App.js b/custom/fitness-demo/components/App/App.js index a86b453..4c3170d 100644 --- a/custom/fitness-demo/components/App/App.js +++ b/custom/fitness-demo/components/App/App.js @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import { LiveStreamingProvider } from '@custom/live-streaming/contexts/LiveStreamingProvider'; +import { LiveStreamingProvider } from '@custom/shared/contexts/LiveStreamingProvider'; import { RecordingProvider } from '@custom/recording/contexts/RecordingProvider'; import ExpiryTimer from '@custom/shared/components/ExpiryTimer'; import { useCallState } from '@custom/shared/contexts/CallProvider'; 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/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..f4cdee1 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,18 +38,23 @@ 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(); } return ( @@ -62,7 +70,7 @@ export const LiveStreamingModal = () => { @@ -70,7 +78,7 @@ export const LiveStreamingModal = () => { @@ -85,19 +93,18 @@ export const LiveStreamingModal = () => { setLayout(Number(e.target.value))} - value={layout} + onChange={(e) => setLayoutType(e.target.value)} + value={layoutType} > - {LAYOUTS.map((l, i) => ( - ))} - {layout !== - LAYOUTS.findIndex((l) => l.value === 'single-participant') && ( + {layoutType === 'default' && ( setMaxCams(Number(e.target.value))} @@ -116,15 +123,17 @@ export const LiveStreamingModal = () => { )} - {layout === - LAYOUTS.findIndex((l) => l.value === 'single-participant') && ( + {layoutType === 'single-participant' && ( setParticipant(e.target.value)} - value={participant} + onChange={(e) => setParticipantId(e.target.value)} + value={participantId} > - {allParticipants.map((p) => ( - + {participants.map((p) => ( + ))} 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/contexts/LiveStreamingProvider.js b/custom/shared/contexts/LiveStreamingProvider.js new file mode 100644 index 0000000..8e877a9 --- /dev/null +++ b/custom/shared/contexts/LiveStreamingProvider.js @@ -0,0 +1,65 @@ +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 }) => { + 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(); - } -}