diff --git a/custom/fitness-demo/components/App/App.js b/custom/fitness-demo/components/App/App.js index 15ba19d..d24562e 100644 --- a/custom/fitness-demo/components/App/App.js +++ b/custom/fitness-demo/components/App/App.js @@ -5,6 +5,7 @@ import { useCallUI } from '@custom/shared/hooks/useCallUI'; import PropTypes from 'prop-types'; import { ChatProvider } from '../../contexts/ChatProvider'; +import { LiveStreamingProvider } from '../../contexts/LiveStreamingProvider'; import { RecordingProvider } from '../../contexts/RecordingProvider'; import Room from '../Call/Room'; import { Asides } from './Asides'; @@ -25,23 +26,25 @@ export const App = ({ customComponentForState }) => { <> - {roomExp && } -
- {componentForState()} - - - -
+ + {roomExp && } +
+ {componentForState()} + + + +
+
diff --git a/custom/fitness-demo/components/Modals/LiveStreamingModal.js b/custom/fitness-demo/components/Modals/LiveStreamingModal.js new file mode 100644 index 0000000..7169e45 --- /dev/null +++ b/custom/fitness-demo/components/Modals/LiveStreamingModal.js @@ -0,0 +1,148 @@ +import React, { useEffect, useState } from 'react'; +import Button from '@custom/shared/components/Button'; +import { CardBody } from '@custom/shared/components/Card'; +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 { 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'; + +const LAYOUTS = [ + { label: 'Grid (default)', value: 'default' }, + { label: 'Single participant', value: 'single-participant' }, + { label: 'Active participant', value: 'active-participant' }, +]; + +export const LiveStreamingModal = () => { + const { callObject } = useCallState(); + const { allParticipants } = useParticipants(); + const { currentModals, closeModal } = useUIState(); + const { isStreaming, streamError } = useLiveStreaming(); + const [pending, setPending] = useState(false); + const [rtmpUrl, setRtmpUrl] = useState(''); + const [layout, setLayout] = useState(0); + const [maxCams, setMaxCams] = useState(9); + const [participant, setParticipant] = useState(0); + + useEffect(() => { + // Reset pending state whenever stream state changes + setPending(false); + }, [isStreaming]); + + function startLiveStream() { + setPending(true); + + const opts = + layout === 'single-participant' + ? { session_id: participant.id } + : { max_cam_streams: maxCams }; + callObject.startLiveStreaming({ rtmpUrl, preset: layout, ...opts }); + } + + function stopLiveStreaming() { + setPending(true); + callObject.stopLiveStreaming(); + } + + return ( + closeModal(LIVE_STREAMING_MODAL)} + actions={[ + , + !isStreaming ? ( + + ) : ( + + ), + ]} + > + {streamError && ( + + Unable to start stream. Error message: {streamError} + + )} + + + setLayout(Number(e.target.value))} + value={layout} + > + {LAYOUTS.map((l, i) => ( + + ))} + + + + {layout !== + LAYOUTS.findIndex((l) => l.value === 'single-participant') && ( + + setMaxCams(Number(e.target.value))} + value={maxCams} + > + + + + + + + + + + + + )} + + {layout === + LAYOUTS.findIndex((l) => l.value === 'single-participant') && ( + + setParticipant(e.target.value)} + value={participant} + > + {allParticipants.map((p) => ( + + ))} + + + )} + + + setRtmpUrl(e.target.value)} + /> + + + + ); +}; + +export default LiveStreamingModal; \ No newline at end of file diff --git a/custom/fitness-demo/components/Tray/Stream.js b/custom/fitness-demo/components/Tray/Stream.js new file mode 100644 index 0000000..e998a38 --- /dev/null +++ b/custom/fitness-demo/components/Tray/Stream.js @@ -0,0 +1,25 @@ +import React from 'react'; + +import { TrayButton } from '@custom/shared/components/Tray'; +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 '../Modals/LiveStreamingModal'; + +export const Stream = () => { + const { openModal } = useUIState(); + const { isStreaming } = useLiveStreaming(); + + return ( + openModal(LIVE_STREAMING_MODAL)} + > + + + ); +}; + +export default Stream; \ No newline at end of file diff --git a/custom/fitness-demo/components/Tray/index.js b/custom/fitness-demo/components/Tray/index.js index 40271c8..ea94d25 100644 --- a/custom/fitness-demo/components/Tray/index.js +++ b/custom/fitness-demo/components/Tray/index.js @@ -2,6 +2,7 @@ import React from 'react'; import ChatTray from './Chat'; import RecordTray from './Record'; import ScreenShareTray from './ScreenShare'; +import StreamTray from './Stream'; export const Tray = () => { return ( @@ -9,6 +10,7 @@ export const Tray = () => { + ); }; diff --git a/custom/fitness-demo/contexts/LiveStreamingProvider.js b/custom/fitness-demo/contexts/LiveStreamingProvider.js new file mode 100644 index 0000000..d6bfb4d --- /dev/null +++ b/custom/fitness-demo/contexts/LiveStreamingProvider.js @@ -0,0 +1,71 @@ +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); \ No newline at end of file diff --git a/custom/fitness-demo/pages/_app.js b/custom/fitness-demo/pages/_app.js index df5330e..4eba667 100644 --- a/custom/fitness-demo/pages/_app.js +++ b/custom/fitness-demo/pages/_app.js @@ -4,6 +4,7 @@ import Head from 'next/head'; import PropTypes from 'prop-types'; import { App as CustomApp } from '../components/App/App'; import ChatAside from '../components/Call/ChatAside'; +import LiveStreamingModal from '../components/Modals/LiveStreamingModal'; import RecordingModal from '../components/Modals/RecordingModal'; import Tray from '../components/Tray'; @@ -36,7 +37,7 @@ App.propTypes = { }; App.asides = [ChatAside]; -App.modals = [RecordingModal]; +App.modals = [RecordingModal, LiveStreamingModal]; App.customTrayComponent = ; App.customAppComponent = ;