diff --git a/dailyjs/live-streaming/README.md b/dailyjs/live-streaming/README.md index 3df1e6a..0d627a6 100644 --- a/dailyjs/live-streaming/README.md +++ b/dailyjs/live-streaming/README.md @@ -1,3 +1,41 @@ # Live Streaming ![Live Streaming](./image.png) + +### 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 + +[![Deploy with Vercel](https://vercel.com/button)](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) diff --git a/dailyjs/live-streaming/components/LiveStreamingModal/LiveStreamingModal.js b/dailyjs/live-streaming/components/LiveStreamingModal/LiveStreamingModal.js index 6280635..2cee300 100644 --- a/dailyjs/live-streaming/components/LiveStreamingModal/LiveStreamingModal.js +++ b/dailyjs/live-streaming/components/LiveStreamingModal/LiveStreamingModal.js @@ -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={[ + , + !isStreaming ? ( + + ) : ( + + ), + ]} > {streamError && ( Unable to start stream. Error message: {streamError} )} - - setRtmpUrl(e.target.value)} - /> - - {!isStreaming ? ( - - ) : ( - - )} + + + setLayout(Number(e.target.value))} + value={layout} + > + {LAYOUTS.map((l, i) => ( + + ))} + + + + {layout !== + LAYOUTS.findIndex((l) => l.value === 'single-participant') && ( + + setMaxCams(Number(e.target.value))} + value={maxCams} + > + + + + + + + + + + + + )} + + {layout === + LAYOUTS.findIndex((l) => l.value === 'single-participant') && ( + + setParticipant(e.target.value)} + value={participant} + > + {allParticipants.map((p) => ( + + ))} + + + )} + + + setRtmpUrl(e.target.value)} + /> + + ); }; diff --git a/dailyjs/live-streaming/contexts/LiveStreamingProvider.js b/dailyjs/live-streaming/contexts/LiveStreamingProvider.js index 16e1e33..f0c4a51 100644 --- a/dailyjs/live-streaming/contexts/LiveStreamingProvider.js +++ b/dailyjs/live-streaming/contexts/LiveStreamingProvider.js @@ -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]); diff --git a/dailyjs/shared/components/GlobalStyle/GlobalStyle.js b/dailyjs/shared/components/GlobalStyle/GlobalStyle.js index c54fe71..1cb3ecf 100644 --- a/dailyjs/shared/components/GlobalStyle/GlobalStyle.js +++ b/dailyjs/shared/components/GlobalStyle/GlobalStyle.js @@ -117,6 +117,12 @@ export const GlobalStyle = () => ( padding: 2px 6px; font-size: 0.875rem; } + + hr { + border: 0; + height: 1px; + background: var(--gray-light); + } `} ); diff --git a/dailyjs/shared/components/Input/Input.js b/dailyjs/shared/components/Input/Input.js index 06a8150..5ca32be 100644 --- a/dailyjs/shared/components/Input/Input.js +++ b/dailyjs/shared/components/Input/Input.js @@ -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, };