initial commit
This commit is contained in:
parent
29eeda01fe
commit
c889c43b50
|
|
@ -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://custom-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 @custom/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 '@custom/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,148 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Button } from '@custom/shared/components/Button';
|
||||
import { CardBody } from '@custom/shared/components/Card';
|
||||
import Field from '@custom/shared/components/Field';
|
||||
import { TextInput, SelectInput } from '@custom/shared/components/Input';
|
||||
import Modal from '@custom/shared/components/Modal';
|
||||
import { Well } from '@custom/shared/components/Well';
|
||||
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
||||
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
|
||||
import { useUIState } from '@custom/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 key="close" 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,25 @@
|
|||
import React from 'react';
|
||||
|
||||
import { TrayButton } from '@custom/shared/components/Tray';
|
||||
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
||||
import { ReactComponent as IconStream } from '@custom/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,71 @@
|
|||
import React, {
|
||||
useState,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
||||
import { useUIState } from '@custom/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);
|
||||
|
|
@ -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 @@
|
|||
// Note: I am here because next-transpile-modules requires a mainfile
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
const withPlugins = require('next-compose-plugins');
|
||||
const withTM = require('next-transpile-modules')([
|
||||
'@custom/shared',
|
||||
'@custom/basic-call',
|
||||
]);
|
||||
|
||||
const packageJson = require('./package.json');
|
||||
|
||||
module.exports = withPlugins([withTM], {
|
||||
env: {
|
||||
PROJECT_TITLE: packageJson.description,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "@custom/live-streaming",
|
||||
"description": "Basic Call + Live Streaming",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@custom/shared": "*",
|
||||
"@custom/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 '@custom/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,16 @@
|
|||
import Index from '@custom/basic-call/pages';
|
||||
import getDemoProps from '@custom/shared/lib/demoProps';
|
||||
|
||||
export async function getStaticProps() {
|
||||
const defaultProps = getDemoProps();
|
||||
|
||||
return {
|
||||
props: {
|
||||
...defaultProps,
|
||||
forceFetchToken: true,
|
||||
forceOwner: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default Index;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<svg width="80" height="32" viewBox="0 0 80 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.5886 27.0149C23.8871 27.0149 25.7976 25.6716 26.6035 24.0896V26.6866H30.9021V4H26.6035V13.403C25.7379 11.8209 24.1856 10.7164 21.6782 10.7164C17.7677 10.7164 14.8125 13.7313 14.8125 18.8657V19.1045C14.8125 24.2985 17.7976 27.0149 21.5886 27.0149ZM22.8722 23.6418C20.7229 23.6418 19.2304 22.1194 19.2304 19.0149V18.7761C19.2304 15.6716 20.5737 14.0299 22.9916 14.0299C25.3498 14.0299 26.7229 15.6119 26.7229 18.7164V18.9552C26.7229 22.1194 25.1409 23.6418 22.8722 23.6418Z" fill="#121A24"/>
|
||||
<path d="M37.9534 27.0149C40.4011 27.0149 41.7743 26.0597 42.6698 24.806V26.6866H46.8787V16.5075C46.8787 12.2687 44.1623 10.7164 40.3414 10.7164C36.5205 10.7164 33.5951 12.3582 33.3265 16.0597H37.416C37.5951 14.7164 38.3713 13.8507 40.0728 13.8507C42.0429 13.8507 42.6101 14.8657 42.6101 16.7164V17.3433H40.8489C36.0728 17.3433 32.7295 18.7164 32.7295 22.3582C32.7295 25.6418 35.1175 27.0149 37.9534 27.0149ZM39.2369 24C37.6548 24 36.9683 23.2537 36.9683 22.1194C36.9683 20.4478 38.431 19.9104 40.9384 19.9104H42.6101V21.2239C42.6101 22.9552 41.1474 24 39.2369 24Z" fill="#121A24"/>
|
||||
<path d="M49.647 26.6866H53.9456V11.0746H49.647V26.6866ZM51.7665 8.95522C53.1694 8.95522 54.2441 7.9403 54.2441 6.59702C54.2441 5.25373 53.1694 4.23881 51.7665 4.23881C50.3933 4.23881 49.3187 5.25373 49.3187 6.59702C49.3187 7.9403 50.3933 8.95522 51.7665 8.95522Z" fill="#121A24"/>
|
||||
<path d="M56.9765 26.6866H61.275V4H56.9765V26.6866Z" fill="#121A24"/>
|
||||
<path d="M70.5634 32L78.9917 11.0746H74.8711L71.3499 20.4478L67.5589 11.0746H62.9021L69.1523 25.1953L66.3779 32H70.5634Z" fill="#121A24"/>
|
||||
<path d="M0 32H4.1875L17.0766 0H12.9916L0 32Z" fill="url(#paint0_linear)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="8.88727" y1="-0.222885" x2="8.88727" y2="30" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1BEBB9"/>
|
||||
<stop offset="1" stop-color="#FF9254"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -0,0 +1,14 @@
|
|||
<svg width="80" height="32" viewBox="0 0 80 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.5886 27.0149C23.8871 27.0149 25.7976 25.6716 26.6035 24.0896V26.6866H30.9021V4H26.6035V13.403C25.7379 11.8209 24.1856 10.7164 21.6782 10.7164C17.7677 10.7164 14.8125 13.7313 14.8125 18.8657V19.1045C14.8125 24.2985 17.7976 27.0149 21.5886 27.0149ZM22.8722 23.6418C20.7229 23.6418 19.2304 22.1194 19.2304 19.0149V18.7761C19.2304 15.6716 20.5737 14.0299 22.9916 14.0299C25.3498 14.0299 26.7229 15.6119 26.7229 18.7164V18.9552C26.7229 22.1194 25.1409 23.6418 22.8722 23.6418Z" fill="white"/>
|
||||
<path d="M37.9534 27.0149C40.4011 27.0149 41.7743 26.0597 42.6698 24.806V26.6866H46.8787V16.5075C46.8787 12.2687 44.1623 10.7164 40.3414 10.7164C36.5205 10.7164 33.5951 12.3582 33.3265 16.0597H37.416C37.5951 14.7164 38.3713 13.8507 40.0728 13.8507C42.0429 13.8507 42.6101 14.8657 42.6101 16.7164V17.3433H40.8489C36.0728 17.3433 32.7295 18.7164 32.7295 22.3582C32.7295 25.6418 35.1175 27.0149 37.9534 27.0149ZM39.2369 24C37.6548 24 36.9683 23.2537 36.9683 22.1194C36.9683 20.4478 38.431 19.9104 40.9384 19.9104H42.6101V21.2239C42.6101 22.9552 41.1474 24 39.2369 24Z" fill="white"/>
|
||||
<path d="M49.647 26.6866H53.9456V11.0746H49.647V26.6866ZM51.7665 8.95522C53.1694 8.95522 54.2441 7.9403 54.2441 6.59702C54.2441 5.25373 53.1694 4.23881 51.7665 4.23881C50.3933 4.23881 49.3187 5.25373 49.3187 6.59702C49.3187 7.9403 50.3933 8.95522 51.7665 8.95522Z" fill="white"/>
|
||||
<path d="M56.9765 26.6866H61.275V4H56.9765V26.6866Z" fill="white"/>
|
||||
<path d="M70.5634 32L78.9917 11.0746H74.8711L71.3499 20.4478L67.5589 11.0746H62.9021L69.1523 25.1953L66.3779 32H70.5634Z" fill="white"/>
|
||||
<path d="M0 32H4.1875L17.0766 0H12.9916L0 32Z" fill="url(#paint0_linear)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="8.88727" y1="-0.222885" x2="8.88727" y2="30" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1BEBB9"/>
|
||||
<stop offset="1" stop-color="#FF9254"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
Loading…
Reference in New Issue