Merge pull request #24 from daily-demos/dailyjs/cloud-recording
Recording
This commit is contained in:
commit
c716c455a3
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"presets": ["next/babel"],
|
||||
"plugins": ["inline-react-svg"]
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# Recording
|
||||
|
||||

|
||||
|
||||
### Live example
|
||||
|
||||
**[See it in action here ➡️](https://dailyjs-recording.vercel.app)**
|
||||
|
||||
---
|
||||
|
||||
## What does this demo do?
|
||||
|
||||
- Use [startRecording](https://docs.daily.co/reference#%EF%B8%8F-startrecording) to create a video and audio recording of your call. You can read more about Daily call recording (and the different modes and types) [here](https://docs.daily.co/reference#recordings)
|
||||
- Supports both `cloud` and `local` recording modes (specified when creating the room or managed using the Daily dashboard)
|
||||
- Coming soon: support different recording layouts / composites
|
||||
- Coming soon: use the Daily REST API to retrieve a list of cloud recordings for the currently active room
|
||||
|
||||
**To turn on recording, you need to be on the Scale plan. There is also a per minute recording fee for cloud recording.**
|
||||
|
||||
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/recording dev
|
||||
```
|
||||
|
||||
### How does this demo work?
|
||||
|
||||
This example introduces a new [RecordingProvider](./contexts/RecordingProvider.js) context that listens for the various [recording events](https://docs.daily.co/reference#recording-started), counts down to begin a recording and stops a currently active recording. We also introduce a new recording modal and tray button.
|
||||
|
||||
Remember to follow the best practises detailed in [the documentation](https://docs.daily.co/reference#recordings) to avoid lengthy or stuck recordings.
|
||||
|
||||
## 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: 281 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/recording",
|
||||
"description": "Basic Call + 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,
|
||||
useState,
|
||||
} from 'react';
|
||||
import Bowser from 'bowser';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
ACCESS_STATE_LOBBY,
|
||||
|
|
@ -31,6 +32,8 @@ export const CallProvider = ({
|
|||
}) => {
|
||||
const [videoQuality, setVideoQuality] = useState(VIDEO_QUALITY_AUTO);
|
||||
const [preJoinNonAuthorized, setPreJoinNonAuthorized] = useState(false);
|
||||
const [enableRecording, setEnableRecording] = useState(null);
|
||||
const [startCloudRecording, setStartCloudRecording] = useState(false);
|
||||
|
||||
// Daily CallMachine hook (primarily handles status of the call)
|
||||
const { daily, leave, state, setRedirectOnLeave } = useCallMachine({
|
||||
|
|
@ -40,6 +43,32 @@ export const CallProvider = ({
|
|||
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
|
||||
const addFakeParticipant = useCallback(() => {
|
||||
daily.addFakeParticipant();
|
||||
|
|
@ -72,9 +101,11 @@ export const CallProvider = ({
|
|||
preJoinNonAuthorized,
|
||||
leave,
|
||||
videoQuality,
|
||||
enableRecording,
|
||||
setVideoQuality,
|
||||
setBandwidth,
|
||||
setRedirectOnLeave,
|
||||
startCloudRecording,
|
||||
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",
|
||||
"dependencies": {
|
||||
"@daily-co/daily-js": "^0.15.0",
|
||||
"bowser": "^2.11.0",
|
||||
"classnames": "^2.3.1",
|
||||
"debounce": "^1.2.1",
|
||||
"nanoid": "^3.1.23",
|
||||
|
|
|
|||
29
yarn.lock
29
yarn.lock
|
|
@ -264,11 +264,6 @@
|
|||
"@nodelib/fs.scandir" "2.1.5"
|
||||
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":
|
||||
version "1.0.6"
|
||||
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"
|
||||
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
|
||||
|
||||
bowser@^2.8.1:
|
||||
bowser@^2.11.0, bowser@^2.8.1:
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
|
|
@ -2902,11 +2897,6 @@ react-dom@^17.0.2:
|
|||
object-assign "^4.1.1"
|
||||
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:
|
||||
version "17.0.2"
|
||||
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"
|
||||
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:
|
||||
version "0.8.3"
|
||||
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"
|
||||
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:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.1.1.tgz#e99630550fca07df9f90a06056987baa40a689c7"
|
||||
|
|
|
|||
Loading…
Reference in New Issue