Skip to main content

Live streaming overview

New to Daily live streaming? Start here — covers prerequisites, supported RTMP providers and URL formats, and HLS streaming.
Only meeting owners or streaming admins (participants with canAdmin: ['streaming']) can start, update, or stop a live stream. See prerequisites in the overview for how to create the right meeting token.
All streaming — RTMP and HLS — goes through startLiveStreaming(). For RTMP, pass an ingest URL directly. For HLS, configure streaming_endpoints on the room first, then pass the endpoint name. The rest of this guide applies to both.

Starting a live stream

startLiveStreaming() begins streaming to one or more RTMP endpoints:
// Stream to a single RTMP URL
call.startLiveStreaming({
  rtmpUrl: 'rtmps://live.example.com/app/stream-key',
});

// Stream to multiple destinations at once
call.startLiveStreaming({
  endpoints: [
    { endpoint: 'rtmps://a.ingest.example.com/live/key-1' },
    { endpoint: 'rtmps://b.ingest.example.com/live/key-2' },
  ],
});

Options

rtmpUrl
string | string[]
One or more RTMP/RTMPS ingest URLs. Mutually exclusive with endpoints.
endpoints
DailyStreamingEndpoint[]
An array of endpoint objects { endpoint: string }. Use this instead of rtmpUrl when you need to dynamically add or remove destinations mid-stream with addLiveStreamingEndpoints() and removeLiveStreamingEndpoints().
width
number
Output width in pixels. Default: 1280. Can only be set at start.
height
number
Output height in pixels. Default: 720. Can only be set at start.
fps
number
Frames per second. Default: 30.
videoBitrate
number
Video bitrate in kbps.
audioBitrate
number
Audio bitrate in kbps.
minIdleTimeOut
number
Seconds of idle time before the stream automatically stops. Default: 60.
maxDuration
number
Maximum stream duration in seconds.
backgroundColor
string
Background color for empty areas in the composed frame. CSS hex string. Can only be set at start.
instanceId
string
Identifier for this streaming instance. Required when running multiple simultaneous streams. See multiple simultaneous streams.
layout
DailyLiveStreamingLayoutConfig
Participant tile layout for the composed output. See DailyStreamingLayoutConfig for the full type reference, or layout options below for usage examples.

Layout options

Live streaming supports the same layout presets as recording — default, single-participant, active-participant, portrait, and custom — except audio-only, which is recording-only. See DailyStreamingLayoutConfig for field-level documentation on each preset. For screenshots, see the live streaming overview.

default

call.startLiveStreaming({
  rtmpUrl: 'rtmps://live.example.com/app/key',
  layout: {
    preset: 'default',
    max_cam_streams: 6,
    participants: { sort: 'active' },
  },
});

single-participant

call.startLiveStreaming({
  rtmpUrl: 'rtmps://live.example.com/app/key',
  layout: {
    preset: 'single-participant',
    session_id: presenterSessionId,
  },
});

active-participant

call.startLiveStreaming({
  rtmpUrl: 'rtmps://live.example.com/app/key',
  layout: { preset: 'active-participant' },
});

portrait

call.startLiveStreaming({
  rtmpUrl: 'rtmps://live.example.com/app/key',
  layout: {
    preset: 'portrait',
    variant: 'inset',  // 'vertical' (default) or 'inset'
  },
});

custom

Use Daily’s VCS baseline composition to fully control the layout programmatically — modes, overlays, labels, participant ordering, and more. The startLiveStreaming() reference documents all available composition_params.
call.startLiveStreaming({
  rtmpUrl: 'rtmps://live.example.com/app/key',
  layout: {
    preset: 'custom',
    composition_id: 'daily:baseline',
    composition_params: {
      mode: 'dominant',
      'videoSettings.showParticipantLabels': true,
    },
  },
});

participants

Several presets accept a participants field to filter which participants’ video and audio are included in the stream. See DailyStreamingLayoutConfig for the full field list.
You must resend participants on every updateLiveStreaming() call — it is not persisted from the previous call. If omitted on update, all participants will be included.

Updating layout mid-stream

Call updateLiveStreaming() to switch layouts without interrupting the stream:
call.updateLiveStreaming({
  layout: { preset: 'active-participant' },
});

Adding and removing endpoints

You can add new RTMP destinations to a running stream or remove existing ones without stopping the stream.
addLiveStreamingEndpoints() and removeLiveStreamingEndpoints() only work when the stream was started with the endpoints parameter — not rtmpUrl. If you need to add or remove destinations mid-stream, use endpoints when calling startLiveStreaming().
// Add a backup destination mid-stream
call.addLiveStreamingEndpoints({
  endpoints: [{ endpoint: 'rtmps://backup.cdn.example.com/live/key-3' }],
});

// Remove a destination (stream continues to other destinations)
call.removeLiveStreamingEndpoints({
  endpoints: [{ endpoint: 'rtmps://a.ingest.example.com/live/key-1' }],
});
Do not use removeLiveStreamingEndpoints() to remove all endpoints — use stopLiveStreaming() instead. Attempting to remove the last endpoint will fail and trigger a live-streaming-warning nonfatal error.

Stopping a live stream

stopLiveStreaming() ends all active streaming endpoints:
call.stopLiveStreaming();

Multiple simultaneous streams

Pass a unique instanceId to run parallel streams — for example, a high-quality CDN stream and a lower-bitrate social stream simultaneously:
// High-quality CDN stream
call.startLiveStreaming({
  instanceId: 'cdn',
  rtmpUrl: 'rtmps://cdn.example.com/live/key',
  width: 1920,
  height: 1080,
  videoBitrate: 6000,
  layout: { preset: 'active-participant' },
});

// Social media stream at a lower bitrate
call.startLiveStreaming({
  instanceId: 'social',
  rtmpUrl: 'rtmps://live.twitch.tv/app/stream-key',
  width: 1280,
  height: 720,
  videoBitrate: 2500,
  layout: { preset: 'active-participant' },
});

// Update only the CDN stream's layout
call.updateLiveStreaming({
  instanceId: 'cdn',
  layout: { preset: 'default', max_cam_streams: 9 },
});

// Stop each instance independently
call.stopLiveStreaming({ instanceId: 'cdn' });
call.stopLiveStreaming({ instanceId: 'social' });
For full details including billing and max_streaming_instances_per_room configuration, see the multi-instance guide.

Live streaming events

Full payload details are in the live streaming events reference.

live-streaming-started

Fired when the stream is ready and the first video frame has been delivered to the ingest server.
call.on('live-streaming-started', ({ instanceId, layout }) => {
  streamingBadge.hidden = false;
});

live-streaming-updated

Fired when the first video frame is published to the RTMP server (state: 'connected'), when the connection is interrupted (state: 'interrupted'), or when it reconnects (state: 'connected'). If streaming was started with endpoints, the payload also includes the endpoint object.
call.on('live-streaming-updated', ({ endpoint, state }) => {
  if (state === 'interrupted') {
    showReconnectingBanner();
  } else if (state === 'connected') {
    hideReconnectingBanner();
  }
});

live-streaming-stopped

call.on('live-streaming-stopped', () => {
  streamingBadge.hidden = true;
});

live-streaming-error

call.on('live-streaming-error', ({ errorMsg }) => {
  console.error('Streaming error:', errorMsg);
});

Complete example

const call = Daily.createCallObject();
await call.join({
  url: 'https://your-domain.daily.co/room',
  token: 'OWNER_OR_STREAMING_ADMIN_TOKEN',
});

const RTMP_URL = 'rtmps://live.example.com/app/your-stream-key';

document.getElementById('go-live').onclick = () => {
  call.startLiveStreaming({
    rtmpUrl: RTMP_URL,
    width: 1280,
    height: 720,
    fps: 30,
    videoBitrate: 3000,
    layout: { preset: 'active-participant' },
  });
};

document.getElementById('stop-live').onclick = () => {
  call.stopLiveStreaming();
};

call.on('live-streaming-started', () => {
  document.getElementById('go-live').disabled = true;
  document.getElementById('stop-live').disabled = false;
});

call.on('live-streaming-updated', ({ state }) => {
  const status = document.getElementById('stream-status');
  status.textContent = state === 'connected' ? '● Live' : '⚠ Reconnecting…';
});

call.on('live-streaming-stopped', () => {
  document.getElementById('go-live').disabled = false;
  document.getElementById('stop-live').disabled = true;
});

call.on('live-streaming-error', ({ errorMsg }) => {
  alert(`Stream error: ${errorMsg}`);
});