Add live streaming option to the demo
This commit is contained in:
parent
99ea4f6504
commit
1358fd6142
|
|
@ -5,6 +5,7 @@ import { useCallUI } from '@custom/shared/hooks/useCallUI';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { ChatProvider } from '../../contexts/ChatProvider';
|
import { ChatProvider } from '../../contexts/ChatProvider';
|
||||||
|
import { LiveStreamingProvider } from '../../contexts/LiveStreamingProvider';
|
||||||
import { RecordingProvider } from '../../contexts/RecordingProvider';
|
import { RecordingProvider } from '../../contexts/RecordingProvider';
|
||||||
import Room from '../Call/Room';
|
import Room from '../Call/Room';
|
||||||
import { Asides } from './Asides';
|
import { Asides } from './Asides';
|
||||||
|
|
@ -25,23 +26,25 @@ export const App = ({ customComponentForState }) => {
|
||||||
<>
|
<>
|
||||||
<ChatProvider>
|
<ChatProvider>
|
||||||
<RecordingProvider>
|
<RecordingProvider>
|
||||||
{roomExp && <ExpiryTimer expiry={roomExp} />}
|
<LiveStreamingProvider>
|
||||||
<div className="app">
|
{roomExp && <ExpiryTimer expiry={roomExp} />}
|
||||||
{componentForState()}
|
<div className="app">
|
||||||
<Modals />
|
{componentForState()}
|
||||||
<Asides />
|
<Modals />
|
||||||
<style jsx>{`
|
<Asides />
|
||||||
color: white;
|
<style jsx>{`
|
||||||
height: 100vh;
|
color: white;
|
||||||
display: flex;
|
height: 100vh;
|
||||||
align-items: center;
|
display: flex;
|
||||||
justify-content: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
.loader {
|
.loader {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
|
</LiveStreamingProvider>
|
||||||
</RecordingProvider>
|
</RecordingProvider>
|
||||||
</ChatProvider>
|
</ChatProvider>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
<Modal
|
||||||
|
title="Live stream"
|
||||||
|
isOpen={currentModals[LIVE_STREAMING_MODAL]}
|
||||||
|
onClose={() => closeModal(LIVE_STREAMING_MODAL)}
|
||||||
|
actions={[
|
||||||
|
<Button key="close" fullWidth variant="outline">
|
||||||
|
Close
|
||||||
|
</Button>,
|
||||||
|
!isStreaming ? (
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
disabled={!rtmpUrl || pending}
|
||||||
|
onClick={() => startLiveStream()}
|
||||||
|
>
|
||||||
|
{pending ? 'Starting stream...' : 'Start live streaming'}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="warning"
|
||||||
|
onClick={() => stopLiveStreaming()}
|
||||||
|
>
|
||||||
|
Stop live streaming
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{streamError && (
|
||||||
|
<Well variant="error">
|
||||||
|
Unable to start stream. Error message: {streamError}
|
||||||
|
</Well>
|
||||||
|
)}
|
||||||
|
<CardBody>
|
||||||
|
<Field label="Layout">
|
||||||
|
<SelectInput
|
||||||
|
onChange={(e) => setLayout(Number(e.target.value))}
|
||||||
|
value={layout}
|
||||||
|
>
|
||||||
|
{LAYOUTS.map((l, i) => (
|
||||||
|
<option value={i} key={l.value}>
|
||||||
|
{l.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</SelectInput>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
{layout !==
|
||||||
|
LAYOUTS.findIndex((l) => l.value === 'single-participant') && (
|
||||||
|
<Field label="Additional cameras">
|
||||||
|
<SelectInput
|
||||||
|
onChange={(e) => setMaxCams(Number(e.target.value))}
|
||||||
|
value={maxCams}
|
||||||
|
>
|
||||||
|
<option value={9}>9 cameras</option>
|
||||||
|
<option value={8}>8 cameras</option>
|
||||||
|
<option value={7}>7 cameras</option>
|
||||||
|
<option value={6}>6 cameras</option>
|
||||||
|
<option value={5}>5 cameras</option>
|
||||||
|
<option value={4}>4 cameras</option>
|
||||||
|
<option value={3}>3 cameras</option>
|
||||||
|
<option value={2}>2 cameras</option>
|
||||||
|
<option value={1}>1 camera</option>
|
||||||
|
</SelectInput>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{layout ===
|
||||||
|
LAYOUTS.findIndex((l) => l.value === 'single-participant') && (
|
||||||
|
<Field label="Select participant">
|
||||||
|
<SelectInput
|
||||||
|
onChange={(e) => setParticipant(e.target.value)}
|
||||||
|
value={participant}
|
||||||
|
>
|
||||||
|
{allParticipants.map((p) => (
|
||||||
|
<option value={p.id} key={p.id}>
|
||||||
|
{p.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</SelectInput>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Field label="Enter RTMP endpoint">
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
placeholder="RTMP URL"
|
||||||
|
required
|
||||||
|
onChange={(e) => setRtmpUrl(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</CardBody>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LiveStreamingModal;
|
||||||
|
|
@ -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 (
|
||||||
|
<TrayButton
|
||||||
|
label={isStreaming ? 'Live' : 'Stream'}
|
||||||
|
orange={isStreaming}
|
||||||
|
onClick={() => openModal(LIVE_STREAMING_MODAL)}
|
||||||
|
>
|
||||||
|
<IconStream />
|
||||||
|
</TrayButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Stream;
|
||||||
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||||
import ChatTray from './Chat';
|
import ChatTray from './Chat';
|
||||||
import RecordTray from './Record';
|
import RecordTray from './Record';
|
||||||
import ScreenShareTray from './ScreenShare';
|
import ScreenShareTray from './ScreenShare';
|
||||||
|
import StreamTray from './Stream';
|
||||||
|
|
||||||
export const Tray = () => {
|
export const Tray = () => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -9,6 +10,7 @@ export const Tray = () => {
|
||||||
<ChatTray />
|
<ChatTray />
|
||||||
<ScreenShareTray />
|
<ScreenShareTray />
|
||||||
<RecordTray />
|
<RecordTray />
|
||||||
|
<StreamTray />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
<LiveStreamingContext.Provider value={{ isStreaming, streamError }}>
|
||||||
|
{children}
|
||||||
|
</LiveStreamingContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
LiveStreamingProvider.propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLiveStreaming = () => useContext(LiveStreamingContext);
|
||||||
|
|
@ -4,6 +4,7 @@ import Head from 'next/head';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { App as CustomApp } from '../components/App/App';
|
import { App as CustomApp } from '../components/App/App';
|
||||||
import ChatAside from '../components/Call/ChatAside';
|
import ChatAside from '../components/Call/ChatAside';
|
||||||
|
import LiveStreamingModal from '../components/Modals/LiveStreamingModal';
|
||||||
import RecordingModal from '../components/Modals/RecordingModal';
|
import RecordingModal from '../components/Modals/RecordingModal';
|
||||||
import Tray from '../components/Tray';
|
import Tray from '../components/Tray';
|
||||||
|
|
||||||
|
|
@ -36,7 +37,7 @@ App.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
App.asides = [ChatAside];
|
App.asides = [ChatAside];
|
||||||
App.modals = [RecordingModal];
|
App.modals = [RecordingModal, LiveStreamingModal];
|
||||||
App.customTrayComponent = <Tray />;
|
App.customTrayComponent = <Tray />;
|
||||||
App.customAppComponent = <CustomApp />;
|
App.customAppComponent = <CustomApp />;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue