initial commit
This commit is contained in:
parent
56a30937c7
commit
0d27fa4b2d
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"presets": ["next/babel"],
|
||||||
|
"plugins": ["inline-react-svg"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Live Streaming
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Live example
|
||||||
|
|
||||||
|
**[See it in action here ➡️](https://dailyjs-live-streaming.vercel.app)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What does this demo do?
|
||||||
|
|
||||||
|
- Use [startLiveStreaming](https://docs.daily.co/reference#%EF%B8%8F-startlivestreaming) to send video and audio to specified RTMP endpoint
|
||||||
|
- Listen for stream started / stopped / error events
|
||||||
|
- Allows call owner to specify stream layout (grid, single participant or active speaker) and maximum cams
|
||||||
|
- Extends the basic call demo with a live streaming provider, tray button and modal
|
||||||
|
- Show a notification bubble at the top of the screen when live streaming is in progress
|
||||||
|
|
||||||
|
Please note: this demo is not currently mobile optimised
|
||||||
|
|
||||||
|
### Getting started
|
||||||
|
|
||||||
|
```
|
||||||
|
# set both DAILY_API_KEY and DAILY_DOMAIN
|
||||||
|
mv env.example .env.local
|
||||||
|
|
||||||
|
yarn
|
||||||
|
yarn workspace @dailyjs/live-streaming dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## How does this example work?
|
||||||
|
|
||||||
|
In this example we extend the [basic call demo](../basic-call) with live streaming functionality.
|
||||||
|
|
||||||
|
We pass a custom tray object, a custom app object (wrapping the original in a new `LiveStreamingProvider`) and a custom modal. We also symlink both the `public` and `pages/api` folders from the basic call.
|
||||||
|
|
||||||
|
Single live streaming is only available to call owners, you must create a token when joining the call (for simplicity, we have disabled the abiltiy to join the call as a guest.)
|
||||||
|
|
||||||
|
## Deploy your own on Vercel
|
||||||
|
|
||||||
|
[](https://vercel.com/new/daily-co/clone-flow?repository-url=https%3A%2F%2Fgithub.com%2Fdaily-demos%2Fexamples.git&env=DAILY_DOMAIN%2CDAILY_API_KEY&envDescription=Your%20Daily%20domain%20and%20API%20key%20can%20be%20found%20on%20your%20account%20dashboard&envLink=https%3A%2F%2Fdashboard.daily.co&project-name=daily-examples&repo-name=daily-examples)
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import App from '@dailyjs/basic-call/components/App';
|
||||||
|
import { RecordingProvider } from '../../contexts/RecordingProvider';
|
||||||
|
|
||||||
|
// Extend our basic call app component with the recording context
|
||||||
|
export const AppWithRecording = () => (
|
||||||
|
<RecordingProvider>
|
||||||
|
<App />
|
||||||
|
</RecordingProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default AppWithRecording;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { AppWithRecording as default } from './App';
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { Button } from '@dailyjs/shared/components/Button';
|
||||||
|
import { CardBody } from '@dailyjs/shared/components/Card';
|
||||||
|
import Modal from '@dailyjs/shared/components/Modal';
|
||||||
|
import Well from '@dailyjs/shared/components/Well';
|
||||||
|
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
|
||||||
|
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
|
||||||
|
import { enable } from 'debug';
|
||||||
|
import {
|
||||||
|
RECORDING_COUNTDOWN_1,
|
||||||
|
RECORDING_COUNTDOWN_2,
|
||||||
|
RECORDING_COUNTDOWN_3,
|
||||||
|
RECORDING_ERROR,
|
||||||
|
RECORDING_IDLE,
|
||||||
|
RECORDING_RECORDING,
|
||||||
|
RECORDING_SAVED,
|
||||||
|
RECORDING_TYPE_CLOUD,
|
||||||
|
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 fullWidth variant="outline">
|
||||||
|
Close
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
disabled={!disabled}
|
||||||
|
onClick={() => handleRecordingClick()}
|
||||||
|
>
|
||||||
|
{renderButtonLabel()}
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CardBody>
|
||||||
|
{!enableRecording ? (
|
||||||
|
<Well variant="error">
|
||||||
|
Recording is not enabled for this room (or your browser does not
|
||||||
|
support it.) Please enabled 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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{enableRecording === RECORDING_TYPE_CLOUD && (
|
||||||
|
<>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Cloud recordings can be accessed via the Daily dashboard under the
|
||||||
|
"Recordings" section.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecordingModal;
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { RecordingModal as default } from './RecordingModal';
|
||||||
|
export { RecordingModal } from './RecordingModal';
|
||||||
|
export { RECORDING_MODAL } from './RecordingModal';
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { TrayButton } from '@dailyjs/shared/components/Tray';
|
||||||
|
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
|
||||||
|
import { ReactComponent as IconRecord } from '@dailyjs/shared/icons/record-md.svg';
|
||||||
|
|
||||||
|
import {
|
||||||
|
RECORDING_ERROR,
|
||||||
|
RECORDING_RECORDING,
|
||||||
|
RECORDING_SAVED,
|
||||||
|
RECORDING_UPLOADING,
|
||||||
|
useRecording,
|
||||||
|
} from '../../contexts/RecordingProvider';
|
||||||
|
import { RECORDING_MODAL } from '../RecordingModal';
|
||||||
|
|
||||||
|
export const Tray = () => {
|
||||||
|
const { openModal } = useUIState();
|
||||||
|
const { recordingState } = useRecording();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(`⏺️ Recording state: ${recordingState}`);
|
||||||
|
|
||||||
|
if (recordingState === RECORDING_ERROR) {
|
||||||
|
// show error modal here
|
||||||
|
}
|
||||||
|
}, [recordingState]);
|
||||||
|
|
||||||
|
const isRecording = [
|
||||||
|
RECORDING_RECORDING,
|
||||||
|
RECORDING_UPLOADING,
|
||||||
|
RECORDING_SAVED,
|
||||||
|
].includes(recordingState);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TrayButton
|
||||||
|
label={isRecording ? 'Recording' : 'Record'}
|
||||||
|
orange={isRecording}
|
||||||
|
onClick={() => openModal(RECORDING_MODAL)}
|
||||||
|
>
|
||||||
|
<IconRecord />
|
||||||
|
</TrayButton>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tray;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { Tray as default } from './Tray';
|
||||||
|
|
@ -0,0 +1,304 @@
|
||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
|
||||||
|
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
|
||||||
|
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
|
||||||
|
import {
|
||||||
|
CALL_STATE_REDIRECTING,
|
||||||
|
CALL_STATE_JOINED,
|
||||||
|
} from '@dailyjs/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_LOCAL = 'local';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRecordingUploadCompleted = () => {
|
||||||
|
setRecordingState(RECORDING_SAVED);
|
||||||
|
};
|
||||||
|
|
||||||
|
callObject.on('app-message', handleAppMessage);
|
||||||
|
callObject.on('recording-upload-completed', handleRecordingUploadCompleted);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
callObject.off('app-message', handleAppMessage);
|
||||||
|
callObject.off(
|
||||||
|
'recording-upload-completed',
|
||||||
|
handleRecordingUploadCompleted
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [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) => {
|
||||||
|
if (recordingState === RECORDING_RECORDING) return;
|
||||||
|
if (event.local) {
|
||||||
|
// Recording started locally, either through UI or programmatically
|
||||||
|
setIsRecordingLocally(true);
|
||||||
|
if (!recordingStartedDate) setRecordingStartedDate(new Date());
|
||||||
|
}
|
||||||
|
setRecordingState(RECORDING_RECORDING);
|
||||||
|
},
|
||||||
|
[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 = () => {
|
||||||
|
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:
|
||||||
|
setRecordingState(RECORDING_SAVED);
|
||||||
|
setIsRecordingLocally(false);
|
||||||
|
break;
|
||||||
|
case RECORDING_TYPE_CLOUD:
|
||||||
|
setRecordingState(RECORDING_UPLOADING);
|
||||||
|
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);
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Domain excluding 'https://' and 'daily.co' e.g. 'somedomain'
|
||||||
|
DAILY_DOMAIN=
|
||||||
|
|
||||||
|
# Obtained from https://dashboard.daily.co/developers
|
||||||
|
DAILY_API_KEY=
|
||||||
|
|
||||||
|
# Daily REST API endpoint
|
||||||
|
DAILY_REST_DOMAIN=https://api.daily.co/v1
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 270 KiB |
|
|
@ -0,0 +1,13 @@
|
||||||
|
const withPlugins = require('next-compose-plugins');
|
||||||
|
const withTM = require('next-transpile-modules')([
|
||||||
|
'@dailyjs/shared',
|
||||||
|
'@dailyjs/basic-call',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const packageJson = require('./package.json');
|
||||||
|
|
||||||
|
module.exports = withPlugins([withTM], {
|
||||||
|
env: {
|
||||||
|
PROJECT_TITLE: packageJson.description,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "@dailyjs/cloud-recording",
|
||||||
|
"description": "Basic Call + Cloud Recording",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@dailyjs/shared": "*",
|
||||||
|
"@dailyjs/basic-call": "*",
|
||||||
|
"next": "^11.0.0",
|
||||||
|
"pluralize": "^8.0.0",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-plugin-module-resolver": "^4.1.0",
|
||||||
|
"next-compose-plugins": "^2.2.1",
|
||||||
|
"next-transpile-modules": "^8.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import React from 'react';
|
||||||
|
import App from '@dailyjs/basic-call/pages/_app';
|
||||||
|
import AppWithRecording from '../components/App';
|
||||||
|
|
||||||
|
import { RecordingModal } from '../components/RecordingModal';
|
||||||
|
import Tray from '../components/Tray';
|
||||||
|
|
||||||
|
App.modals = [RecordingModal];
|
||||||
|
App.customAppComponent = <AppWithRecording />;
|
||||||
|
App.customTrayComponent = <Tray />;
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
../../basic-call/pages/api
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import Index from '@dailyjs/basic-call/pages';
|
||||||
|
import getDemoProps from '@dailyjs/shared/lib/demoProps';
|
||||||
|
|
||||||
|
export async function getStaticProps() {
|
||||||
|
const defaultProps = getDemoProps();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
...defaultProps,
|
||||||
|
forceFetchToken: true,
|
||||||
|
forceOwner: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Index;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
../basic-call/public
|
||||||
|
|
@ -12,6 +12,7 @@ import React, {
|
||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import Bowser from 'bowser';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import {
|
||||||
ACCESS_STATE_LOBBY,
|
ACCESS_STATE_LOBBY,
|
||||||
|
|
@ -31,6 +32,8 @@ export const CallProvider = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [videoQuality, setVideoQuality] = useState(VIDEO_QUALITY_AUTO);
|
const [videoQuality, setVideoQuality] = useState(VIDEO_QUALITY_AUTO);
|
||||||
const [preJoinNonAuthorized, setPreJoinNonAuthorized] = useState(false);
|
const [preJoinNonAuthorized, setPreJoinNonAuthorized] = useState(false);
|
||||||
|
const [enableRecording, setEnableRecording] = useState(null);
|
||||||
|
const [startCloudRecording, setStartCloudRecording] = useState(false);
|
||||||
|
|
||||||
// Daily CallMachine hook (primarily handles status of the call)
|
// Daily CallMachine hook (primarily handles status of the call)
|
||||||
const { daily, leave, state, setRedirectOnLeave } = useCallMachine({
|
const { daily, leave, state, setRedirectOnLeave } = useCallMachine({
|
||||||
|
|
@ -40,6 +43,32 @@ export const CallProvider = ({
|
||||||
subscribeToTracksAutomatically,
|
subscribeToTracksAutomatically,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Feature detection taken from daily room object and client browser support
|
||||||
|
useEffect(() => {
|
||||||
|
if (!daily) return;
|
||||||
|
const updateRoomConfigState = async () => {
|
||||||
|
const roomConfig = await daily.room();
|
||||||
|
if (!('config' in roomConfig)) return;
|
||||||
|
|
||||||
|
const browser = Bowser.parse(window.navigator.userAgent);
|
||||||
|
const supportsRecording =
|
||||||
|
browser.platform.type === 'desktop' && browser.engine.name === 'Blink';
|
||||||
|
// recording and screen sharing is hidden in owner_only_broadcast for non-owners
|
||||||
|
if (supportsRecording) {
|
||||||
|
const recordingType =
|
||||||
|
roomConfig?.tokenConfig?.enable_recording ??
|
||||||
|
roomConfig?.config?.enable_recording;
|
||||||
|
if (['local', 'cloud'].includes(recordingType)) {
|
||||||
|
setEnableRecording(recordingType);
|
||||||
|
setStartCloudRecording(
|
||||||
|
roomConfig?.tokenConfig?.start_cloud_recording ?? false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
updateRoomConfigState();
|
||||||
|
}, [state, daily]);
|
||||||
|
|
||||||
// Convience wrapper for adding a fake participant to the call
|
// Convience wrapper for adding a fake participant to the call
|
||||||
const addFakeParticipant = useCallback(() => {
|
const addFakeParticipant = useCallback(() => {
|
||||||
daily.addFakeParticipant();
|
daily.addFakeParticipant();
|
||||||
|
|
@ -72,9 +101,11 @@ export const CallProvider = ({
|
||||||
preJoinNonAuthorized,
|
preJoinNonAuthorized,
|
||||||
leave,
|
leave,
|
||||||
videoQuality,
|
videoQuality,
|
||||||
|
enableRecording,
|
||||||
setVideoQuality,
|
setVideoQuality,
|
||||||
setBandwidth,
|
setBandwidth,
|
||||||
setRedirectOnLeave,
|
setRedirectOnLeave,
|
||||||
|
startCloudRecording,
|
||||||
subscribeToTracksAutomatically,
|
subscribeToTracksAutomatically,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 23C18.0751 23 23 18.0751 23 12C23 5.92487 18.0751 1 12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23Z" stroke="currentColor" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<circle cx="12" cy="12" r="4" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 390 B |
|
|
@ -5,6 +5,7 @@
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@daily-co/daily-js": "^0.15.0",
|
"@daily-co/daily-js": "^0.15.0",
|
||||||
|
"bowser": "^2.11.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"debounce": "^1.2.1",
|
"debounce": "^1.2.1",
|
||||||
"nanoid": "^3.1.23",
|
"nanoid": "^3.1.23",
|
||||||
|
|
|
||||||
29
yarn.lock
29
yarn.lock
|
|
@ -264,11 +264,6 @@
|
||||||
"@nodelib/fs.scandir" "2.1.5"
|
"@nodelib/fs.scandir" "2.1.5"
|
||||||
fastq "^1.6.0"
|
fastq "^1.6.0"
|
||||||
|
|
||||||
"@popperjs/core@^2.9.2":
|
|
||||||
version "2.9.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353"
|
|
||||||
integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==
|
|
||||||
|
|
||||||
"@rushstack/eslint-patch@^1.0.6":
|
"@rushstack/eslint-patch@^1.0.6":
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.0.6.tgz#023d72a5c4531b4ce204528971700a78a85a0c50"
|
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.0.6.tgz#023d72a5c4531b4ce204528971700a78a85a0c50"
|
||||||
|
|
@ -589,7 +584,7 @@ boolbase@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||||
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
|
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
|
||||||
|
|
||||||
bowser@^2.8.1:
|
bowser@^2.11.0, bowser@^2.8.1:
|
||||||
version "2.11.0"
|
version "2.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f"
|
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f"
|
||||||
integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==
|
integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==
|
||||||
|
|
@ -2209,7 +2204,7 @@ lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21:
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
|
||||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
|
loose-envify@^1.1.0, loose-envify@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||||
|
|
@ -2902,11 +2897,6 @@ react-dom@^17.0.2:
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
scheduler "^0.20.2"
|
scheduler "^0.20.2"
|
||||||
|
|
||||||
react-fast-compare@^3.0.1:
|
|
||||||
version "3.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
|
||||||
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
|
|
||||||
|
|
||||||
react-is@17.0.2:
|
react-is@17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||||
|
|
@ -2917,14 +2907,6 @@ react-is@^16.8.1:
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
|
|
||||||
react-popper@^2.2.5:
|
|
||||||
version "2.2.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.5.tgz#1214ef3cec86330a171671a4fbcbeeb65ee58e96"
|
|
||||||
integrity sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw==
|
|
||||||
dependencies:
|
|
||||||
react-fast-compare "^3.0.1"
|
|
||||||
warning "^4.0.2"
|
|
||||||
|
|
||||||
react-refresh@0.8.3:
|
react-refresh@0.8.3:
|
||||||
version "0.8.3"
|
version "0.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
||||||
|
|
@ -3634,13 +3616,6 @@ vm-browserify@1.1.2, vm-browserify@^1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
|
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
|
||||||
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
|
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
|
||||||
|
|
||||||
warning@^4.0.2:
|
|
||||||
version "4.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
|
|
||||||
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
|
|
||||||
dependencies:
|
|
||||||
loose-envify "^1.0.0"
|
|
||||||
|
|
||||||
watchpack@2.1.1:
|
watchpack@2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.1.1.tgz#e99630550fca07df9f90a06056987baa40a689c7"
|
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.1.1.tgz#e99630550fca07df9f90a06056987baa40a689c7"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue