updated readme and added layouts
This commit is contained in:
parent
2cbd8c7335
commit
a7c6c5047b
|
|
@ -1,3 +1,41 @@
|
||||||
# Live Streaming
|
# 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.
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,33 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button } from '@dailyjs/shared/components/Button';
|
import { Button } from '@dailyjs/shared/components/Button';
|
||||||
|
import { CardBody } from '@dailyjs/shared/components/Card';
|
||||||
import Field from '@dailyjs/shared/components/Field';
|
import Field from '@dailyjs/shared/components/Field';
|
||||||
import { TextInput } from '@dailyjs/shared/components/Input';
|
import { TextInput, SelectInput } from '@dailyjs/shared/components/Input';
|
||||||
import Modal from '@dailyjs/shared/components/Modal';
|
import Modal from '@dailyjs/shared/components/Modal';
|
||||||
import { Well } from '@dailyjs/shared/components/Well';
|
import { Well } from '@dailyjs/shared/components/Well';
|
||||||
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
|
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
|
||||||
|
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
|
||||||
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
|
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
|
||||||
import { useLiveStreaming } from '../../contexts/LiveStreamingProvider';
|
import { useLiveStreaming } from '../../contexts/LiveStreamingProvider';
|
||||||
|
|
||||||
export const LIVE_STREAMING_MODAL = 'live-streaming';
|
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 = () => {
|
export const LiveStreamingModal = () => {
|
||||||
const { callObject } = useCallState();
|
const { callObject } = useCallState();
|
||||||
|
const { allParticipants } = useParticipants();
|
||||||
const { currentModals, closeModal } = useUIState();
|
const { currentModals, closeModal } = useUIState();
|
||||||
const { isStreaming, streamError } = useLiveStreaming();
|
const { isStreaming, streamError } = useLiveStreaming();
|
||||||
const [pending, setPending] = useState(false);
|
const [pending, setPending] = useState(false);
|
||||||
const [rtmpUrl, setRtmpUrl] = useState('');
|
const [rtmpUrl, setRtmpUrl] = useState('');
|
||||||
|
const [layout, setLayout] = useState(0);
|
||||||
|
const [maxCams, setMaxCams] = useState(9);
|
||||||
|
const [participant, setParticipant] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Reset pending state whenever stream state changes
|
// Reset pending state whenever stream state changes
|
||||||
|
|
@ -24,7 +36,12 @@ export const LiveStreamingModal = () => {
|
||||||
|
|
||||||
function startLiveStream() {
|
function startLiveStream() {
|
||||||
setPending(true);
|
setPending(true);
|
||||||
callObject.startLiveStreaming({ rtmpUrl });
|
|
||||||
|
const opts =
|
||||||
|
layout === 'single-participant'
|
||||||
|
? { session_id: participant.id }
|
||||||
|
: { max_cam_streams: maxCams };
|
||||||
|
callObject.startLiveStreaming({ rtmpUrl, preset: layout, ...opts });
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopLiveStreaming() {
|
function stopLiveStreaming() {
|
||||||
|
|
@ -37,32 +54,93 @@ export const LiveStreamingModal = () => {
|
||||||
title="Live stream"
|
title="Live stream"
|
||||||
isOpen={currentModals[LIVE_STREAMING_MODAL]}
|
isOpen={currentModals[LIVE_STREAMING_MODAL]}
|
||||||
onClose={() => closeModal(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 && (
|
{streamError && (
|
||||||
<Well variant="error">
|
<Well variant="error">
|
||||||
Unable to start stream. Error message: {streamError}
|
Unable to start stream. Error message: {streamError}
|
||||||
</Well>
|
</Well>
|
||||||
)}
|
)}
|
||||||
<Field label="Enter RTMP endpoint">
|
<CardBody>
|
||||||
<TextInput
|
<Field label="Layout">
|
||||||
type="text"
|
<SelectInput
|
||||||
placeholder="RTMP URL"
|
onChange={(e) => setLayout(Number(e.target.value))}
|
||||||
required
|
value={layout}
|
||||||
onChange={(e) => setRtmpUrl(e.target.value)}
|
>
|
||||||
/>
|
{LAYOUTS.map((l, i) => (
|
||||||
</Field>
|
<option value={i} key={l.value}>
|
||||||
{!isStreaming ? (
|
{l.label}
|
||||||
<Button
|
</option>
|
||||||
disabled={!rtmpUrl || pending}
|
))}
|
||||||
onClick={() => startLiveStream()}
|
</SelectInput>
|
||||||
>
|
</Field>
|
||||||
{pending ? 'Starting stream...' : 'Start live streaming'}
|
|
||||||
</Button>
|
{layout !==
|
||||||
) : (
|
LAYOUTS.findIndex((l) => l.value === 'single-participant') && (
|
||||||
<Button variant="warning" onClick={() => stopLiveStreaming()}>
|
<Field label="Additional cameras">
|
||||||
Stop live streaming
|
<SelectInput
|
||||||
</Button>
|
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>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export const LiveStreamingProvider = ({ children }) => {
|
||||||
const handleStreamStarted = useCallback(() => {
|
const handleStreamStarted = useCallback(() => {
|
||||||
console.log('📺 Live stream started');
|
console.log('📺 Live stream started');
|
||||||
setIsStreaming(true);
|
setIsStreaming(true);
|
||||||
|
setStreamError(null);
|
||||||
setCustomCapsule({ variant: 'recording', label: 'Live streaming' });
|
setCustomCapsule({ variant: 'recording', label: 'Live streaming' });
|
||||||
}, [setCustomCapsule]);
|
}, [setCustomCapsule]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,12 @@ export const GlobalStyle = () => (
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--gray-light);
|
||||||
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -269,7 +269,7 @@ export const SelectInput = ({
|
||||||
SelectInput.propTypes = {
|
SelectInput.propTypes = {
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
value: PropTypes.number,
|
value: PropTypes.any,
|
||||||
variant: PropTypes.string,
|
variant: PropTypes.string,
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue