import the livestreaming and recording contexts and hooks
This commit is contained in:
parent
8f7a7a8b70
commit
015457a913
|
|
@ -1,4 +1,6 @@
|
||||||
import React, { useMemo } from 'react';
|
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 ExpiryTimer from '@custom/shared/components/ExpiryTimer';
|
||||||
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
||||||
import { useCallUI } from '@custom/shared/hooks/useCallUI';
|
import { useCallUI } from '@custom/shared/hooks/useCallUI';
|
||||||
|
|
@ -6,8 +8,6 @@ import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { ChatProvider } from '../../contexts/ChatProvider';
|
import { ChatProvider } from '../../contexts/ChatProvider';
|
||||||
import { ClassStateProvider } from '../../contexts/ClassStateProvider';
|
import { ClassStateProvider } from '../../contexts/ClassStateProvider';
|
||||||
import { LiveStreamingProvider } from '../../contexts/LiveStreamingProvider';
|
|
||||||
import { RecordingProvider } from '../../contexts/RecordingProvider';
|
|
||||||
import Room from '../Call/Room';
|
import Room from '../Call/Room';
|
||||||
import { Asides } from './Asides';
|
import { Asides } from './Asides';
|
||||||
import { Modals } from './Modals';
|
import { Modals } from './Modals';
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,10 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { Aside } from '@custom/shared/components/Aside';
|
import { Aside } from '@custom/shared/components/Aside';
|
||||||
import Button from '@custom/shared/components/Button';
|
import Button from '@custom/shared/components/Button';
|
||||||
import { TextInput } from '@custom/shared/components/Input';
|
import { TextInput } from '@custom/shared/components/Input';
|
||||||
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
|
|
||||||
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
||||||
import { ReactComponent as IconEmoji } from '@custom/shared/icons/emoji-sm.svg';
|
import { ReactComponent as IconEmoji } from '@custom/shared/icons/emoji-sm.svg';
|
||||||
|
import { useMessageSound } from '@custom/text-chat/hooks/useMessageSound';
|
||||||
import { useChat } from '../../contexts/ChatProvider';
|
import { useChat } from '../../contexts/ChatProvider';
|
||||||
import { useMessageSound } from '../../hooks/useMessageSound';
|
|
||||||
import AsideHeader from '../App/AsideHeader';
|
import AsideHeader from '../App/AsideHeader';
|
||||||
|
|
||||||
export const CHAT_ASIDE = 'chat';
|
export const CHAT_ASIDE = 'chat';
|
||||||
|
|
@ -15,7 +14,6 @@ export const ChatAside = () => {
|
||||||
const { showAside, setShowAside } = useUIState();
|
const { showAside, setShowAside } = useUIState();
|
||||||
const { sendMessage, chatHistory, hasNewMessages, setHasNewMessages } =
|
const { sendMessage, chatHistory, hasNewMessages, setHasNewMessages } =
|
||||||
useChat();
|
useChat();
|
||||||
const { localParticipant } = useParticipants();
|
|
||||||
const [newMessage, setNewMessage] = useState('');
|
const [newMessage, setNewMessage] = useState('');
|
||||||
const playMessageSound = useMessageSound();
|
const playMessageSound = useMessageSound();
|
||||||
const [showEmojis, setShowEmojis] = useState(false);
|
const [showEmojis, setShowEmojis] = useState(false);
|
||||||
|
|
|
||||||
|
|
@ -1,157 +0,0 @@
|
||||||
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 streaming"
|
|
||||||
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>
|
|
||||||
),
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<img src="/assets/pattern-ls.svg" className="live-streaming" alt="live streaming" />
|
|
||||||
<p>Please note: live streaming requires <b>Scale Plan</b> or above</p>
|
|
||||||
{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>
|
|
||||||
<style jsx>{`
|
|
||||||
.live-streaming {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LiveStreamingModal;
|
|
||||||
|
|
@ -1,145 +0,0 @@
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
import Button from '@custom/shared/components/Button';
|
|
||||||
import { CardBody } from '@custom/shared/components/Card';
|
|
||||||
import Modal from '@custom/shared/components/Modal';
|
|
||||||
import Well from '@custom/shared/components/Well';
|
|
||||||
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
|
||||||
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
|
||||||
import {
|
|
||||||
RECORDING_COUNTDOWN_1,
|
|
||||||
RECORDING_COUNTDOWN_2,
|
|
||||||
RECORDING_COUNTDOWN_3,
|
|
||||||
RECORDING_IDLE,
|
|
||||||
RECORDING_RECORDING,
|
|
||||||
RECORDING_SAVED,
|
|
||||||
RECORDING_TYPE_CLOUD,
|
|
||||||
RECORDING_TYPE_CLOUD_BETA,
|
|
||||||
RECORDING_TYPE_RTP_TRACKS,
|
|
||||||
RECORDING_UPLOADING,
|
|
||||||
useRecording,
|
|
||||||
} from '../../contexts/RecordingProvider';
|
|
||||||
|
|
||||||
export const RECORDING_MODAL = 'recording';
|
|
||||||
|
|
||||||
export const RecordingModal = () => {
|
|
||||||
const { currentModals, closeModal } = useUIState();
|
|
||||||
const { enableRecording } = useCallState();
|
|
||||||
const {
|
|
||||||
recordingStartedDate,
|
|
||||||
recordingState,
|
|
||||||
startRecordingWithCountdown,
|
|
||||||
stopRecording,
|
|
||||||
} = useRecording();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (recordingState === RECORDING_RECORDING) {
|
|
||||||
closeModal(RECORDING_MODAL);
|
|
||||||
}
|
|
||||||
}, [recordingState, closeModal]);
|
|
||||||
|
|
||||||
const disabled =
|
|
||||||
enableRecording &&
|
|
||||||
[RECORDING_IDLE, RECORDING_RECORDING].includes(recordingState);
|
|
||||||
|
|
||||||
function renderButtonLabel() {
|
|
||||||
if (!enableRecording) {
|
|
||||||
return 'Recording disabled';
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (recordingState) {
|
|
||||||
case RECORDING_COUNTDOWN_3:
|
|
||||||
return '3...';
|
|
||||||
case RECORDING_COUNTDOWN_2:
|
|
||||||
return '2...';
|
|
||||||
case RECORDING_COUNTDOWN_1:
|
|
||||||
return '1...';
|
|
||||||
case RECORDING_RECORDING:
|
|
||||||
return 'Stop recording';
|
|
||||||
case RECORDING_UPLOADING:
|
|
||||||
case RECORDING_SAVED:
|
|
||||||
return 'Stopping recording...';
|
|
||||||
default:
|
|
||||||
return 'Start recording';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRecordingClick() {
|
|
||||||
if (recordingState === RECORDING_IDLE) {
|
|
||||||
startRecordingWithCountdown();
|
|
||||||
} else {
|
|
||||||
stopRecording();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title="Recording"
|
|
||||||
isOpen={currentModals[RECORDING_MODAL]}
|
|
||||||
onClose={() => closeModal(RECORDING_MODAL)}
|
|
||||||
actions={[
|
|
||||||
<Button key="close" fullWidth variant="outline">
|
|
||||||
Close
|
|
||||||
</Button>,
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
disabled={!disabled}
|
|
||||||
key="record"
|
|
||||||
onClick={() => handleRecordingClick()}
|
|
||||||
>
|
|
||||||
{renderButtonLabel()}
|
|
||||||
</Button>,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CardBody>
|
|
||||||
{!enableRecording ? (
|
|
||||||
<Well variant="error">
|
|
||||||
Recording is not enabled for this room (or your browser does not
|
|
||||||
support it.) Please enable recording when creating the room or via
|
|
||||||
the Daily dashboard.
|
|
||||||
</Well>
|
|
||||||
) : (
|
|
||||||
<p>
|
|
||||||
Recording type enabled: <strong>{enableRecording}</strong>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{recordingStartedDate && (
|
|
||||||
<p>Recording started: {recordingStartedDate.toString()}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{[RECORDING_TYPE_CLOUD, RECORDING_TYPE_CLOUD_BETA].includes(
|
|
||||||
enableRecording
|
|
||||||
) && (
|
|
||||||
<>
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Cloud recordings can be accessed via the Daily dashboard under the
|
|
||||||
"Recordings" section.
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{enableRecording === RECORDING_TYPE_RTP_TRACKS && (
|
|
||||||
<>
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<p>
|
|
||||||
rtp-tracks recordings can be accessed via the Daily API. See the{' '}
|
|
||||||
<a
|
|
||||||
href="https://docs.daily.co/guides/recording-calls-with-the-daily-api#retrieve-individual-tracks-from-rtp-tracks-recordings"
|
|
||||||
noreferrer
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Daily recording guide
|
|
||||||
</a>{' '}
|
|
||||||
for details.
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CardBody>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RecordingModal;
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import { TrayButton } from '@custom/shared/components/Tray';
|
import { RECORDING_MODAL } from '@custom/recording/components/RecordingModal';
|
||||||
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
|
||||||
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
|
|
||||||
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
|
||||||
import { ReactComponent as IconRecord } from '@custom/shared/icons/record-md.svg';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
RECORDING_ERROR,
|
RECORDING_ERROR,
|
||||||
RECORDING_RECORDING,
|
RECORDING_RECORDING,
|
||||||
RECORDING_SAVED,
|
RECORDING_SAVED,
|
||||||
RECORDING_UPLOADING,
|
RECORDING_UPLOADING,
|
||||||
useRecording,
|
useRecording,
|
||||||
} from '../../contexts/RecordingProvider';
|
} from '@custom/recording/contexts/RecordingProvider';
|
||||||
import { RECORDING_MODAL } from '../Modals/RecordingModal';
|
import { TrayButton } from '@custom/shared/components/Tray';
|
||||||
|
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
||||||
|
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
|
||||||
|
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
||||||
|
import { ReactComponent as IconRecord } from '@custom/shared/icons/record-md.svg';
|
||||||
|
|
||||||
|
|
||||||
export const Tray = () => {
|
export const Tray = () => {
|
||||||
const { enableRecording } = useCallState();
|
const { enableRecording } = useCallState();
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import React from 'react';
|
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 { TrayButton } from '@custom/shared/components/Tray';
|
||||||
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
|
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
|
||||||
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
||||||
import { ReactComponent as IconStream } from '@custom/shared/icons/streaming-md.svg';
|
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 = () => {
|
export const Stream = () => {
|
||||||
const { openModal } = useUIState();
|
const { openModal } = useUIState();
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
|
||||||
<LiveStreamingContext.Provider value={{ isStreaming, streamError }}>
|
|
||||||
{children}
|
|
||||||
</LiveStreamingContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
LiveStreamingProvider.propTypes = {
|
|
||||||
children: PropTypes.node,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useLiveStreaming = () => useContext(LiveStreamingContext);
|
|
||||||
|
|
@ -1,337 +0,0 @@
|
||||||
import React, {
|
|
||||||
createContext,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
|
||||||
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
|
|
||||||
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
|
||||||
import {
|
|
||||||
CALL_STATE_REDIRECTING,
|
|
||||||
CALL_STATE_JOINED,
|
|
||||||
} from '@custom/shared/contexts/useCallMachine';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useDeepCompareEffect } from 'use-deep-compare';
|
|
||||||
|
|
||||||
export const RECORDING_ERROR = 'error';
|
|
||||||
export const RECORDING_SAVED = 'saved';
|
|
||||||
export const RECORDING_RECORDING = 'recording';
|
|
||||||
export const RECORDING_UPLOADING = 'uploading';
|
|
||||||
export const RECORDING_COUNTDOWN_1 = 'starting1';
|
|
||||||
export const RECORDING_COUNTDOWN_2 = 'starting2';
|
|
||||||
export const RECORDING_COUNTDOWN_3 = 'starting3';
|
|
||||||
export const RECORDING_IDLE = 'idle';
|
|
||||||
|
|
||||||
export const RECORDING_TYPE_CLOUD = 'cloud';
|
|
||||||
export const RECORDING_TYPE_CLOUD_BETA = 'cloud-beta';
|
|
||||||
export const RECORDING_TYPE_LOCAL = 'local';
|
|
||||||
export const RECORDING_TYPE_OUTPUT_BYTE_STREAM = 'output-byte-stream';
|
|
||||||
export const RECORDING_TYPE_RTP_TRACKS = 'rtp-tracks';
|
|
||||||
|
|
||||||
const RecordingContext = createContext({
|
|
||||||
isRecordingLocally: false,
|
|
||||||
recordingStartedDate: null,
|
|
||||||
recordingState: RECORDING_IDLE,
|
|
||||||
startRecording: null,
|
|
||||||
stopRecording: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const RecordingProvider = ({ children }) => {
|
|
||||||
const {
|
|
||||||
callObject,
|
|
||||||
enableRecording,
|
|
||||||
startCloudRecording,
|
|
||||||
state,
|
|
||||||
} = useCallState();
|
|
||||||
const { participants } = useParticipants();
|
|
||||||
const [recordingStartedDate, setRecordingStartedDate] = useState(null);
|
|
||||||
const [recordingState, setRecordingState] = useState(RECORDING_IDLE);
|
|
||||||
const [isRecordingLocally, setIsRecordingLocally] = useState(false);
|
|
||||||
const [hasRecordingStarted, setHasRecordingStarted] = useState(false);
|
|
||||||
const { setCustomCapsule } = useUIState();
|
|
||||||
|
|
||||||
const handleOnUnload = useCallback(
|
|
||||||
() => 'Unsaved recording in progress. Do you really want to leave?',
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
!enableRecording ||
|
|
||||||
!isRecordingLocally ||
|
|
||||||
recordingState !== RECORDING_RECORDING ||
|
|
||||||
state === CALL_STATE_REDIRECTING
|
|
||||||
)
|
|
||||||
return false;
|
|
||||||
const prev = window.onbeforeunload;
|
|
||||||
window.onbeforeunload = handleOnUnload;
|
|
||||||
return () => {
|
|
||||||
window.onbeforeunload = prev;
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
enableRecording,
|
|
||||||
handleOnUnload,
|
|
||||||
recordingState,
|
|
||||||
isRecordingLocally,
|
|
||||||
state,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!callObject || !enableRecording) return false;
|
|
||||||
|
|
||||||
const handleAppMessage = (ev) => {
|
|
||||||
switch (ev?.data?.event) {
|
|
||||||
case 'recording-starting':
|
|
||||||
setRecordingState(RECORDING_COUNTDOWN_3);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// The 'recording-data' event is emitted when an output-byte-stream recording has started
|
|
||||||
// When the event emits, start writing data to the stream created in handleRecordingStarted()
|
|
||||||
const handleRecordingData = async (ev) => {
|
|
||||||
try {
|
|
||||||
console.log('got data', ev);
|
|
||||||
await window.writer.write(ev.data);
|
|
||||||
if (ev.finished) {
|
|
||||||
console.log('closing!');
|
|
||||||
window.writer.close();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
callObject.on('app-message', handleAppMessage);
|
|
||||||
callObject.on('recording-data', handleRecordingData);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
callObject.off('app-message', handleAppMessage);
|
|
||||||
callObject.off('recording-data', handleRecordingData);
|
|
||||||
};
|
|
||||||
}, [callObject, enableRecording]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Automatically start cloud recording, if startCloudRecording is set.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
hasRecordingStarted ||
|
|
||||||
!callObject ||
|
|
||||||
!startCloudRecording ||
|
|
||||||
enableRecording !== 'cloud' ||
|
|
||||||
state !== CALL_STATE_JOINED
|
|
||||||
)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Small timeout, in case other participants are already in-call.
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
const isSomebodyRecording = participants.some((p) => p.isRecording);
|
|
||||||
if (!isSomebodyRecording) {
|
|
||||||
callObject.startRecording();
|
|
||||||
setIsRecordingLocally(true);
|
|
||||||
setHasRecordingStarted(true);
|
|
||||||
} else {
|
|
||||||
setHasRecordingStarted(true);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
callObject,
|
|
||||||
enableRecording,
|
|
||||||
hasRecordingStarted,
|
|
||||||
participants,
|
|
||||||
startCloudRecording,
|
|
||||||
state,
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle participant updates to sync recording state.
|
|
||||||
*/
|
|
||||||
useDeepCompareEffect(() => {
|
|
||||||
if (isRecordingLocally || recordingState === RECORDING_SAVED) return;
|
|
||||||
if (participants.some(({ isRecording }) => isRecording)) {
|
|
||||||
setRecordingState(RECORDING_RECORDING);
|
|
||||||
} else {
|
|
||||||
setRecordingState(RECORDING_IDLE);
|
|
||||||
}
|
|
||||||
}, [isRecordingLocally, participants, recordingState]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle recording started.
|
|
||||||
*/
|
|
||||||
const handleRecordingStarted = useCallback(
|
|
||||||
(event) => {
|
|
||||||
console.log('RECORDING');
|
|
||||||
console.log(event);
|
|
||||||
|
|
||||||
if (recordingState === RECORDING_RECORDING) return;
|
|
||||||
setRecordingState(RECORDING_RECORDING);
|
|
||||||
if (event.local) {
|
|
||||||
// Recording started locally, either through UI or programmatically
|
|
||||||
setIsRecordingLocally(true);
|
|
||||||
if (!recordingStartedDate) setRecordingStartedDate(new Date());
|
|
||||||
// If an output-byte-stream recording has started, create a new data stream that can be piped to a third-party (in this case a file)
|
|
||||||
if (event.type === 'output-byte-stream') {
|
|
||||||
const { readable, writable } = new TransformStream({
|
|
||||||
transform: (chunk, ctrl) => {
|
|
||||||
chunk.arrayBuffer().then((b) => ctrl.enqueue(new Uint8Array(b)));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
window.writer = writable.getWriter();
|
|
||||||
readable.pipeTo(window.streamSaver.createWriteStream('test-vid.mp4'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[recordingState, recordingStartedDate]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!callObject || !enableRecording) return false;
|
|
||||||
|
|
||||||
callObject.on('recording-started', handleRecordingStarted);
|
|
||||||
return () => callObject.off('recording-started', handleRecordingStarted);
|
|
||||||
}, [callObject, enableRecording, handleRecordingStarted]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle recording stopped.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (!callObject || !enableRecording) return false;
|
|
||||||
|
|
||||||
const handleRecordingStopped = (event) => {
|
|
||||||
console.log(event);
|
|
||||||
if (isRecordingLocally) return;
|
|
||||||
setRecordingState(RECORDING_IDLE);
|
|
||||||
setRecordingStartedDate(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
callObject.on('recording-stopped', handleRecordingStopped);
|
|
||||||
return () => callObject.off('recording-stopped', handleRecordingStopped);
|
|
||||||
}, [callObject, enableRecording, isRecordingLocally]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle recording error.
|
|
||||||
*/
|
|
||||||
const handleRecordingError = useCallback(() => {
|
|
||||||
if (isRecordingLocally) setRecordingState(RECORDING_ERROR);
|
|
||||||
setIsRecordingLocally(false);
|
|
||||||
}, [isRecordingLocally]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!callObject || !enableRecording) return false;
|
|
||||||
|
|
||||||
callObject.on('recording-error', handleRecordingError);
|
|
||||||
return () => callObject.off('recording-error', handleRecordingError);
|
|
||||||
}, [callObject, enableRecording, handleRecordingError]);
|
|
||||||
|
|
||||||
const startRecording = useCallback(() => {
|
|
||||||
if (!callObject || !isRecordingLocally) return;
|
|
||||||
callObject.startRecording();
|
|
||||||
}, [callObject, isRecordingLocally]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let timeout;
|
|
||||||
switch (recordingState) {
|
|
||||||
case RECORDING_COUNTDOWN_3:
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
setRecordingState(RECORDING_COUNTDOWN_2);
|
|
||||||
}, 1000);
|
|
||||||
break;
|
|
||||||
case RECORDING_COUNTDOWN_2:
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
setRecordingState(RECORDING_COUNTDOWN_1);
|
|
||||||
}, 1000);
|
|
||||||
break;
|
|
||||||
case RECORDING_COUNTDOWN_1:
|
|
||||||
startRecording();
|
|
||||||
break;
|
|
||||||
case RECORDING_ERROR:
|
|
||||||
case RECORDING_SAVED:
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
setRecordingState(RECORDING_IDLE);
|
|
||||||
setIsRecordingLocally(false);
|
|
||||||
}, 5000);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
};
|
|
||||||
}, [recordingState, startRecording]);
|
|
||||||
|
|
||||||
// Show a custom capsule when recording in progress
|
|
||||||
useEffect(() => {
|
|
||||||
if (recordingState !== RECORDING_RECORDING) {
|
|
||||||
setCustomCapsule(null);
|
|
||||||
} else {
|
|
||||||
setCustomCapsule({ variant: 'recording', label: 'Recording' });
|
|
||||||
}
|
|
||||||
}, [recordingState, setCustomCapsule]);
|
|
||||||
|
|
||||||
const startRecordingWithCountdown = useCallback(() => {
|
|
||||||
if (!callObject || !enableRecording) return;
|
|
||||||
setIsRecordingLocally(true);
|
|
||||||
setRecordingState(RECORDING_COUNTDOWN_3);
|
|
||||||
callObject?.sendAppMessage({
|
|
||||||
event: 'recording-starting',
|
|
||||||
});
|
|
||||||
}, [callObject, enableRecording]);
|
|
||||||
|
|
||||||
const stopRecording = useCallback(() => {
|
|
||||||
if (!callObject || !enableRecording || !isRecordingLocally) return;
|
|
||||||
if (recordingState === RECORDING_RECORDING) {
|
|
||||||
switch (enableRecording) {
|
|
||||||
case RECORDING_TYPE_LOCAL:
|
|
||||||
case RECORDING_TYPE_OUTPUT_BYTE_STREAM:
|
|
||||||
setRecordingState(RECORDING_SAVED);
|
|
||||||
setIsRecordingLocally(false);
|
|
||||||
break;
|
|
||||||
case RECORDING_TYPE_CLOUD:
|
|
||||||
case RECORDING_TYPE_CLOUD_BETA:
|
|
||||||
case RECORDING_TYPE_RTP_TRACKS:
|
|
||||||
setRecordingState(RECORDING_UPLOADING);
|
|
||||||
setRecordingState(RECORDING_SAVED);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (recordingState === RECORDING_IDLE) {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
setIsRecordingLocally(false);
|
|
||||||
setRecordingState(RECORDING_IDLE);
|
|
||||||
}
|
|
||||||
setRecordingStartedDate(null);
|
|
||||||
callObject.stopRecording();
|
|
||||||
}, [callObject, enableRecording, isRecordingLocally, recordingState]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RecordingContext.Provider
|
|
||||||
value={{
|
|
||||||
isRecordingLocally,
|
|
||||||
recordingStartedDate,
|
|
||||||
recordingState,
|
|
||||||
startRecordingWithCountdown,
|
|
||||||
stopRecording,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</RecordingContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
RecordingProvider.propTypes = {
|
|
||||||
children: PropTypes.node,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useRecording = () => useContext(RecordingContext);
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { useEffect, useMemo } from 'react';
|
|
||||||
|
|
||||||
import { useSound } from '@custom/shared/hooks/useSound';
|
|
||||||
import { debounce } from 'debounce';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience hook to play `join.mp3` when participants join the call
|
|
||||||
*/
|
|
||||||
export const useMessageSound = () => {
|
|
||||||
const { load, play } = useSound('assets/message.mp3');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
load();
|
|
||||||
}, [load]);
|
|
||||||
|
|
||||||
return useMemo(() => debounce(() => play(), 5000, true), [play]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useMessageSound;
|
|
||||||
|
|
@ -2,6 +2,9 @@ const withPlugins = require('next-compose-plugins');
|
||||||
const withTM = require('next-transpile-modules')([
|
const withTM = require('next-transpile-modules')([
|
||||||
'@custom/shared',
|
'@custom/shared',
|
||||||
'@custom/basic-call',
|
'@custom/basic-call',
|
||||||
|
'@custom/text-chat',
|
||||||
|
'@custom/live-streaming',
|
||||||
|
'@custom/recording',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const packageJson = require('./package.json');
|
const packageJson = require('./package.json');
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,10 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@custom/shared": "*",
|
"@custom/shared": "*",
|
||||||
|
"@custom/basic-call": "*",
|
||||||
|
"@custom/text-chat": "*",
|
||||||
|
"@custom/live-streaming": "*",
|
||||||
|
"@custom/recording": "*",
|
||||||
"next": "^11.1.2",
|
"next": "^11.1.2",
|
||||||
"pluralize": "^8.0.0"
|
"pluralize": "^8.0.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import LiveStreamingModal from '@custom/live-streaming/components/LiveStreamingModal';
|
||||||
|
import RecordingModal from '@custom/recording/components/RecordingModal';
|
||||||
import GlobalStyle from '@custom/shared/components/GlobalStyle';
|
import GlobalStyle from '@custom/shared/components/GlobalStyle';
|
||||||
import Head from 'next/head';
|
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 Tray from '../components/Tray';
|
import Tray from '../components/Tray';
|
||||||
|
|
||||||
function App({ Component, pageProps }) {
|
function App({ Component, pageProps }) {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button } from '@custom/shared/components/Button';
|
import Button from '@custom/shared/components/Button';
|
||||||
import { CardBody } from '@custom/shared/components/Card';
|
import { CardBody } from '@custom/shared/components/Card';
|
||||||
import Field from '@custom/shared/components/Field';
|
import Field from '@custom/shared/components/Field';
|
||||||
import { TextInput, SelectInput } from '@custom/shared/components/Input';
|
import { TextInput, SelectInput } from '@custom/shared/components/Input';
|
||||||
import Modal from '@custom/shared/components/Modal';
|
import Modal from '@custom/shared/components/Modal';
|
||||||
import { Well } from '@custom/shared/components/Well';
|
import Well from '@custom/shared/components/Well';
|
||||||
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
||||||
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
|
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
|
||||||
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import Button from '@custom/shared/components/Button';
|
||||||
import { TextInput } from '@custom/shared/components/Input';
|
import { TextInput } from '@custom/shared/components/Input';
|
||||||
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
|
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
|
||||||
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
||||||
|
import { useMessageSound } from '@custom/text-chat/hooks/useMessageSound';
|
||||||
import { useChat } from '../contexts/ChatProvider';
|
import { useChat } from '../contexts/ChatProvider';
|
||||||
import { useMessageSound } from '../hooks/useMessageSound';
|
|
||||||
|
|
||||||
export const CHAT_ASIDE = 'chat';
|
export const CHAT_ASIDE = 'chat';
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue