updated readme and added layouts
This commit is contained in:
parent
2cbd8c7335
commit
a7c6c5047b
|
|
@ -1,3 +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.
|
||||
|
||||
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 { Button } from '@dailyjs/shared/components/Button';
|
||||
import { CardBody } from '@dailyjs/shared/components/Card';
|
||||
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 { 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
|
||||
|
|
@ -24,7 +36,12 @@ export const LiveStreamingModal = () => {
|
|||
|
||||
function startLiveStream() {
|
||||
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() {
|
||||
|
|
@ -37,32 +54,93 @@ export const LiveStreamingModal = () => {
|
|||
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>
|
||||
)}
|
||||
<Field label="Enter RTMP endpoint">
|
||||
<TextInput
|
||||
type="text"
|
||||
placeholder="RTMP URL"
|
||||
required
|
||||
onChange={(e) => setRtmpUrl(e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
{!isStreaming ? (
|
||||
<Button
|
||||
disabled={!rtmpUrl || pending}
|
||||
onClick={() => startLiveStream()}
|
||||
>
|
||||
{pending ? 'Starting stream...' : 'Start live streaming'}
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="warning" onClick={() => stopLiveStreaming()}>
|
||||
Stop live streaming
|
||||
</Button>
|
||||
)}
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export const LiveStreamingProvider = ({ children }) => {
|
|||
const handleStreamStarted = useCallback(() => {
|
||||
console.log('📺 Live stream started');
|
||||
setIsStreaming(true);
|
||||
setStreamError(null);
|
||||
setCustomCapsule({ variant: 'recording', label: 'Live streaming' });
|
||||
}, [setCustomCapsule]);
|
||||
|
||||
|
|
|
|||
|
|
@ -117,6 +117,12 @@ export const GlobalStyle = () => (
|
|||
padding: 2px 6px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
background: var(--gray-light);
|
||||
}
|
||||
`}</style>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue