Merge pull request #18 from daily-demos/dailyjs/livestreaming
Live streaming
This commit is contained in:
commit
6164bd86d9
|
|
@ -8,6 +8,10 @@ The basic call demo (derived from our prebuilt UI codebase) demonstrates how to
|
|||
|
||||
Send messages to other participants using sendAppMessage
|
||||
|
||||
### [📺 Live streaming](./live-streaming)
|
||||
|
||||
Broadcast call to a custom RTMP endpoint using a variety of difference layout modes
|
||||
|
||||
---
|
||||
|
||||
## Getting started
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
import React from 'react';
|
||||
import DeviceSelectModal from '@dailyjs/shared/components/DeviceSelectModal/DeviceSelectModal';
|
||||
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
|
||||
|
||||
export const Modals = () => (
|
||||
<>
|
||||
<DeviceSelectModal />
|
||||
</>
|
||||
);
|
||||
export const Modals = () => {
|
||||
const { modals } = useUIState();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeviceSelectModal />
|
||||
{modals.map((ModalComponent) => (
|
||||
<ModalComponent key={ModalComponent.name} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modals;
|
||||
|
|
|
|||
|
|
@ -23,10 +23,12 @@ export const Intro = ({
|
|||
onJoin,
|
||||
title,
|
||||
fetching = false,
|
||||
forceFetchToken = false,
|
||||
forceOwner = false,
|
||||
}) => {
|
||||
const [roomName, setRoomName] = useState();
|
||||
const [owner, setOwner] = useState(false);
|
||||
const [fetchToken, setFetchToken] = useState(false);
|
||||
const [fetchToken, setFetchToken] = useState(forceFetchToken);
|
||||
const [owner, setOwner] = useState(forceOwner);
|
||||
|
||||
useEffect(() => {
|
||||
setRoomName(room);
|
||||
|
|
@ -51,10 +53,12 @@ export const Intro = ({
|
|||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Fetch meeting token">
|
||||
<BooleanInput onChange={(e) => setFetchToken(e.target.checked)} />
|
||||
</Field>
|
||||
{fetchToken && (
|
||||
{!forceFetchToken && (
|
||||
<Field label="Fetch meeting token">
|
||||
<BooleanInput onChange={(e) => setFetchToken(e.target.checked)} />
|
||||
</Field>
|
||||
)}
|
||||
{fetchToken && !forceOwner && (
|
||||
<Field label="Join as owner">
|
||||
<BooleanInput onChange={(e) => setOwner(e.target.checked)} />
|
||||
</Field>
|
||||
|
|
@ -79,6 +83,8 @@ Intro.propTypes = {
|
|||
domain: PropTypes.string.isRequired,
|
||||
onJoin: PropTypes.func.isRequired,
|
||||
fetching: PropTypes.bool,
|
||||
forceFetchToken: PropTypes.bool,
|
||||
forceOwner: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Intro;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
|
||||
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
|
||||
|
||||
export const Header = () => {
|
||||
const { participantCount } = useParticipants();
|
||||
const { customCapsule } = useUIState();
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
|
|
@ -14,6 +16,12 @@ export const Header = () => {
|
|||
participantCount > 1 ? 'participants' : 'participant'
|
||||
}`}
|
||||
</div>
|
||||
{customCapsule && (
|
||||
<div className={`capsule ${customCapsule.variant}`}>
|
||||
{customCapsule.variant === 'recording' && <span />}
|
||||
{customCapsule.label}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style jsx>{`
|
||||
.room-header {
|
||||
|
|
@ -31,6 +39,9 @@ export const Header = () => {
|
|||
}
|
||||
|
||||
.capsule {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xxxs);
|
||||
background-color: var(--blue-dark);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: var(--spacing-xxs) var(--spacing-xs);
|
||||
|
|
@ -39,10 +50,35 @@ export const Header = () => {
|
|||
font-weight: var(--weight-medium);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.capsule.recording {
|
||||
background: var(--secondary-default);
|
||||
}
|
||||
|
||||
.capsule.recording span {
|
||||
display: block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
animation: capsulePulse 2s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes capsulePulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.25;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</header>
|
||||
),
|
||||
[participantCount]
|
||||
[participantCount, customCapsule]
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ function App({ Component, pageProps }) {
|
|||
<GlobalStyle />
|
||||
<Component
|
||||
asides={App.asides}
|
||||
modals={App.modals}
|
||||
customTrayComponent={App.customTrayComponent}
|
||||
customAppComponent={App.customAppComponent}
|
||||
{...pageProps}
|
||||
|
|
@ -33,6 +34,7 @@ App.propTypes = {
|
|||
};
|
||||
|
||||
App.asides = [];
|
||||
App.modals = [];
|
||||
App.customTrayComponent = null;
|
||||
App.customAppComponent = null;
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ export default function Index({
|
|||
domain,
|
||||
isConfigured = false,
|
||||
predefinedRoom = false,
|
||||
forceFetchToken = false,
|
||||
forceOwner = false,
|
||||
asides,
|
||||
modals,
|
||||
customTrayComponent,
|
||||
customAppComponent,
|
||||
}) {
|
||||
|
|
@ -73,6 +76,8 @@ export default function Index({
|
|||
<NotConfigured />
|
||||
) : (
|
||||
<Intro
|
||||
forceFetchToken={forceFetchToken}
|
||||
forceOwner={forceOwner}
|
||||
title={process.env.PROJECT_TITLE}
|
||||
room={roomName}
|
||||
error={tokenError}
|
||||
|
|
@ -99,7 +104,11 @@ export default function Index({
|
|||
* Main call UI
|
||||
*/
|
||||
return (
|
||||
<UIStateProvider asides={asides} customTrayComponent={customTrayComponent}>
|
||||
<UIStateProvider
|
||||
asides={asides}
|
||||
modals={modals}
|
||||
customTrayComponent={customTrayComponent}
|
||||
>
|
||||
<CallProvider domain={domain} room={roomName} token={token}>
|
||||
<ParticipantsProvider>
|
||||
<TracksProvider>
|
||||
|
|
@ -120,8 +129,11 @@ Index.propTypes = {
|
|||
predefinedRoom: PropTypes.bool,
|
||||
domain: PropTypes.string,
|
||||
asides: PropTypes.arrayOf(PropTypes.func),
|
||||
modals: PropTypes.arrayOf(PropTypes.func),
|
||||
customTrayComponent: PropTypes.node,
|
||||
customAppComponent: PropTypes.node,
|
||||
forceFetchToken: PropTypes.bool,
|
||||
forceOwner: PropTypes.bool,
|
||||
};
|
||||
|
||||
export async function getStaticProps() {
|
||||
|
|
|
|||
|
|
@ -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 { LiveStreamingProvider } from '../../contexts/LiveStreamingProvider';
|
||||
|
||||
// Extend our basic call app component with the live streaming context
|
||||
export const AppWithLiveStreaming = () => (
|
||||
<LiveStreamingProvider>
|
||||
<App />
|
||||
</LiveStreamingProvider>
|
||||
);
|
||||
|
||||
export default AppWithLiveStreaming;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { AppWithLiveStreaming as default } from './App';
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Button } from '@dailyjs/shared/components/Button';
|
||||
import { CardBody } from '@dailyjs/shared/components/Card';
|
||||
import Field from '@dailyjs/shared/components/Field';
|
||||
import { TextInput, SelectInput } from '@dailyjs/shared/components/Input';
|
||||
import Modal from '@dailyjs/shared/components/Modal';
|
||||
import { Well } from '@dailyjs/shared/components/Well';
|
||||
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
|
||||
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
|
||||
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
|
||||
import { useLiveStreaming } from '../../contexts/LiveStreamingProvider';
|
||||
|
||||
export const LIVE_STREAMING_MODAL = 'live-streaming';
|
||||
|
||||
const LAYOUTS = [
|
||||
{ label: 'Grid (default)', value: 'default' },
|
||||
{ label: 'Single participant', value: 'single-participant' },
|
||||
{ label: 'Active participant', value: 'active-participant' },
|
||||
];
|
||||
|
||||
export const LiveStreamingModal = () => {
|
||||
const { callObject } = useCallState();
|
||||
const { allParticipants } = useParticipants();
|
||||
const { currentModals, closeModal } = useUIState();
|
||||
const { isStreaming, streamError } = useLiveStreaming();
|
||||
const [pending, setPending] = useState(false);
|
||||
const [rtmpUrl, setRtmpUrl] = useState('');
|
||||
const [layout, setLayout] = useState(0);
|
||||
const [maxCams, setMaxCams] = useState(9);
|
||||
const [participant, setParticipant] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset pending state whenever stream state changes
|
||||
setPending(false);
|
||||
}, [isStreaming]);
|
||||
|
||||
function startLiveStream() {
|
||||
setPending(true);
|
||||
|
||||
const opts =
|
||||
layout === 'single-participant'
|
||||
? { session_id: participant.id }
|
||||
: { max_cam_streams: maxCams };
|
||||
callObject.startLiveStreaming({ rtmpUrl, preset: layout, ...opts });
|
||||
}
|
||||
|
||||
function stopLiveStreaming() {
|
||||
setPending(true);
|
||||
callObject.stopLiveStreaming();
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Live stream"
|
||||
isOpen={currentModals[LIVE_STREAMING_MODAL]}
|
||||
onClose={() => closeModal(LIVE_STREAMING_MODAL)}
|
||||
actions={[
|
||||
<Button fullWidth variant="outline">
|
||||
Close
|
||||
</Button>,
|
||||
!isStreaming ? (
|
||||
<Button
|
||||
fullWidth
|
||||
disabled={!rtmpUrl || pending}
|
||||
onClick={() => startLiveStream()}
|
||||
>
|
||||
{pending ? 'Starting stream...' : 'Start live streaming'}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
fullWidth
|
||||
variant="warning"
|
||||
onClick={() => stopLiveStreaming()}
|
||||
>
|
||||
Stop live streaming
|
||||
</Button>
|
||||
),
|
||||
]}
|
||||
>
|
||||
{streamError && (
|
||||
<Well variant="error">
|
||||
Unable to start stream. Error message: {streamError}
|
||||
</Well>
|
||||
)}
|
||||
<CardBody>
|
||||
<Field label="Layout">
|
||||
<SelectInput
|
||||
onChange={(e) => setLayout(Number(e.target.value))}
|
||||
value={layout}
|
||||
>
|
||||
{LAYOUTS.map((l, i) => (
|
||||
<option value={i} key={l.value}>
|
||||
{l.label}
|
||||
</option>
|
||||
))}
|
||||
</SelectInput>
|
||||
</Field>
|
||||
|
||||
{layout !==
|
||||
LAYOUTS.findIndex((l) => l.value === 'single-participant') && (
|
||||
<Field label="Additional cameras">
|
||||
<SelectInput
|
||||
onChange={(e) => setMaxCams(Number(e.target.value))}
|
||||
value={maxCams}
|
||||
>
|
||||
<option value={9}>9 cameras</option>
|
||||
<option value={8}>8 cameras</option>
|
||||
<option value={7}>7 cameras</option>
|
||||
<option value={6}>6 cameras</option>
|
||||
<option value={5}>5 cameras</option>
|
||||
<option value={4}>4 cameras</option>
|
||||
<option value={3}>3 cameras</option>
|
||||
<option value={2}>2 cameras</option>
|
||||
<option value={1}>1 camera</option>
|
||||
</SelectInput>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
{layout ===
|
||||
LAYOUTS.findIndex((l) => l.value === 'single-participant') && (
|
||||
<Field label="Select participant">
|
||||
<SelectInput
|
||||
onChange={(e) => setParticipant(e.target.value)}
|
||||
value={participant}
|
||||
>
|
||||
{allParticipants.map((p) => (
|
||||
<option value={p.id} key={p.id}>
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
</SelectInput>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field label="Enter RTMP endpoint">
|
||||
<TextInput
|
||||
type="text"
|
||||
placeholder="RTMP URL"
|
||||
required
|
||||
onChange={(e) => setRtmpUrl(e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
</CardBody>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default LiveStreamingModal;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export { LiveStreamingModal as default } from './LiveStreamingModal';
|
||||
export { LiveStreamingModal } from './LiveStreamingModal';
|
||||
export { LIVE_STREAMING_MODAL } from './LiveStreamingModal';
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
|
||||
import { TrayButton } from '@dailyjs/shared/components/Tray';
|
||||
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
|
||||
import { ReactComponent as IconStream } from '@dailyjs/shared/icons/streaming-md.svg';
|
||||
|
||||
import { useLiveStreaming } from '../../contexts/LiveStreamingProvider';
|
||||
import { LIVE_STREAMING_MODAL } from '../LiveStreamingModal';
|
||||
|
||||
export const Tray = () => {
|
||||
const { openModal } = useUIState();
|
||||
const { isStreaming } = useLiveStreaming();
|
||||
|
||||
return (
|
||||
<>
|
||||
<TrayButton
|
||||
label={isStreaming ? 'Live' : 'Stream'}
|
||||
orange={isStreaming}
|
||||
onClick={() => openModal(LIVE_STREAMING_MODAL)}
|
||||
>
|
||||
<IconStream />
|
||||
</TrayButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tray;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { Tray as default } from './Tray';
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import React, {
|
||||
useState,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
|
||||
import { useUIState } from '@dailyjs/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);
|
||||
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,24 @@
|
|||
{
|
||||
"name": "@dailyjs/live-streaming",
|
||||
"description": "Basic Call + Live Streaming",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"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 AppWithLiveStreaming from '../components/App';
|
||||
|
||||
import { LiveStreamingModal } from '../components/LiveStreamingModal';
|
||||
import Tray from '../components/Tray';
|
||||
|
||||
App.modals = [LiveStreamingModal];
|
||||
App.customAppComponent = <AppWithLiveStreaming />;
|
||||
App.customTrayComponent = <Tray />;
|
||||
|
||||
export default App;
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../basic-call/pages/api
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import Index from '@dailyjs/basic-call/pages';
|
||||
|
||||
export async function getStaticProps() {
|
||||
// Check that both domain and key env vars are set
|
||||
const isConfigured =
|
||||
!!process.env.DAILY_DOMAIN && !!process.env.DAILY_API_KEY;
|
||||
|
||||
// Pass through domain as prop
|
||||
return {
|
||||
props: {
|
||||
domain: process.env.DAILY_DOMAIN || null,
|
||||
isConfigured,
|
||||
forceFetchToken: true,
|
||||
forceOwner: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default Index;
|
||||
|
|
@ -0,0 +1 @@
|
|||
../basic-call/public
|
||||
|
|
@ -4,14 +4,16 @@ import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
|
|||
import { Button } from '../Button';
|
||||
import { DeviceSelect } from '../DeviceSelect';
|
||||
|
||||
export const DEVICE_MODAL = 'device';
|
||||
|
||||
export const DeviceSelectModal = () => {
|
||||
const { showDeviceModal, setShowDeviceModal } = useUIState();
|
||||
const { currentModals, closeModal } = useUIState();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Select your device"
|
||||
isOpen={showDeviceModal}
|
||||
onClose={() => setShowDeviceModal(false)}
|
||||
isOpen={currentModals[DEVICE_MODAL]}
|
||||
onClose={() => closeModal(DEVICE_MODAL)}
|
||||
actions={[
|
||||
<Button fullWidth variant="outline">
|
||||
Cancel
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export { DeviceSelectModal as default } from './DeviceSelectModal';
|
||||
export { DeviceSelectModal } from './DeviceSelectModal';
|
||||
export { DEVICE_MODAL } from './DeviceSelectModal';
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export const GlobalStyle = () => (
|
|||
--primary-dark: ${theme.primary.dark};
|
||||
--secondary-default: ${theme.secondary.default};
|
||||
--secondary-dark: ${theme.secondary.dark};
|
||||
--secondary-light: ${theme.secondary.light};
|
||||
--blue-light: ${theme.blue.light};
|
||||
--blue-default: ${theme.blue.default};
|
||||
--blue-dark: ${theme.blue.dark};
|
||||
|
|
@ -116,6 +117,12 @@ export const GlobalStyle = () => (
|
|||
padding: 2px 6px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
background: var(--gray-light);
|
||||
}
|
||||
`}</style>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import Button from '@dailyjs/shared/components/Button';
|
||||
import { DEVICE_MODAL } from '@dailyjs/shared/components/DeviceSelectModal/DeviceSelectModal';
|
||||
import { TextInput } from '@dailyjs/shared/components/Input';
|
||||
import Loader from '@dailyjs/shared/components/Loader';
|
||||
import { MuteButton } from '@dailyjs/shared/components/MuteButtons';
|
||||
|
|
@ -33,7 +34,7 @@ export const HairCheck = () => {
|
|||
const { localParticipant } = useParticipants();
|
||||
const { deviceState, camError, micError, isCamMuted, isMicMuted } =
|
||||
useMediaDevices();
|
||||
const { showDeviceModal, setShowDeviceModal } = useUIState();
|
||||
const { openModal } = useUIState();
|
||||
const [waiting, setWaiting] = useState(false);
|
||||
const [joining, setJoining] = useState(false);
|
||||
const [denied, setDenied] = useState();
|
||||
|
|
@ -143,7 +144,7 @@ export const HairCheck = () => {
|
|||
className="device-button"
|
||||
size="medium-square"
|
||||
variant="blur"
|
||||
onClick={() => setShowDeviceModal(!showDeviceModal)}
|
||||
onClick={() => openModal(DEVICE_MODAL)}
|
||||
>
|
||||
<IconSettings />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ export const SelectInput = ({
|
|||
SelectInput.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
value: PropTypes.number,
|
||||
value: PropTypes.any,
|
||||
variant: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { PEOPLE_ASIDE } from '@dailyjs/shared/components/Aside/PeopleAside';
|
||||
import { DEVICE_MODAL } from '@dailyjs/shared/components/DeviceSelectModal';
|
||||
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
|
||||
import { useMediaDevices } from '@dailyjs/shared/contexts/MediaDeviceProvider';
|
||||
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
|
||||
|
|
@ -14,7 +15,7 @@ import { Tray, TrayButton } from './Tray';
|
|||
|
||||
export const BasicTray = () => {
|
||||
const { callObject, leave } = useCallState();
|
||||
const { customTrayComponent, setShowDeviceModal, toggleAside } = useUIState();
|
||||
const { customTrayComponent, openModal, toggleAside } = useUIState();
|
||||
const { isCamMuted, isMicMuted } = useMediaDevices();
|
||||
|
||||
const toggleCamera = (newState) => {
|
||||
|
|
@ -43,7 +44,7 @@ export const BasicTray = () => {
|
|||
>
|
||||
{isMicMuted ? <IconMicOff /> : <IconMicOn />}
|
||||
</TrayButton>
|
||||
<TrayButton label="Settings" onClick={() => setShowDeviceModal(true)}>
|
||||
<TrayButton label="Settings" onClick={() => openModal(DEVICE_MODAL)}>
|
||||
<IconSettings />
|
||||
</TrayButton>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,34 @@
|
|||
import React, { useCallback, createContext, useContext, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDeepCompareMemo } from 'use-deep-compare';
|
||||
|
||||
export const UIStateContext = createContext();
|
||||
|
||||
export const UIStateProvider = ({ asides, customTrayComponent, children }) => {
|
||||
const [showDeviceModal, setShowDeviceModal] = useState(false);
|
||||
export const UIStateProvider = ({
|
||||
asides,
|
||||
modals,
|
||||
customTrayComponent,
|
||||
children,
|
||||
}) => {
|
||||
const [showAside, setShowAside] = useState();
|
||||
const [activeModals, setActiveModals] = useState({});
|
||||
const [customCapsule, setCustomCapsule] = useState();
|
||||
|
||||
const openModal = useCallback((modalName) => {
|
||||
setActiveModals((prevState) => ({
|
||||
...prevState,
|
||||
[modalName]: true,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const closeModal = useCallback((modalName) => {
|
||||
setActiveModals((prevState) => ({
|
||||
...prevState,
|
||||
[modalName]: false,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const currentModals = useDeepCompareMemo(() => activeModals, [activeModals]);
|
||||
|
||||
const toggleAside = useCallback((newAside) => {
|
||||
setShowAside((p) => (p === newAside ? null : newAside));
|
||||
|
|
@ -15,12 +38,16 @@ export const UIStateProvider = ({ asides, customTrayComponent, children }) => {
|
|||
<UIStateContext.Provider
|
||||
value={{
|
||||
asides,
|
||||
modals,
|
||||
customTrayComponent,
|
||||
showDeviceModal,
|
||||
setShowDeviceModal,
|
||||
openModal,
|
||||
closeModal,
|
||||
currentModals,
|
||||
toggleAside,
|
||||
showAside,
|
||||
setShowAside,
|
||||
customCapsule,
|
||||
setCustomCapsule,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
@ -31,6 +58,7 @@ export const UIStateProvider = ({ asides, customTrayComponent, children }) => {
|
|||
UIStateProvider.propTypes = {
|
||||
children: PropTypes.node,
|
||||
asides: PropTypes.arrayOf(PropTypes.func),
|
||||
modals: PropTypes.arrayOf(PropTypes.func),
|
||||
customTrayComponent: PropTypes.node,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 952 B After Width: | Height: | Size: 952 B |
|
|
@ -10,6 +10,7 @@ export const defaultTheme = {
|
|||
secondary: {
|
||||
default: '#FF9254',
|
||||
dark: '#FB651E',
|
||||
light: '#FF9254',
|
||||
},
|
||||
|
||||
blue: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue