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 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)
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,
};