Livestreaming

Watch livestreams and implement common features: viewer counts, waiting rooms, state handling, and more.

Best Practices

  • Handle all livestream states (backstage, live, ended) in your UI.
  • Use useIsCallLive hook to react to live state changes automatically.
  • Display viewer count and duration for better user engagement.
  • Handle connection errors gracefully with appropriate error messages.
  • Use the built-in LivestreamPlayer component for quick integration.

Watching a livestream

This guide shows how to watch a WebRTC livestream. We also support HLS and RTMP-out.

Let’s do a quick overview of these three technologies:

  • WebRTC is ideal for real-time, low-latency streaming such as video calls or live auctions.
  • HLS (HTTP Live Streaming) is great for large-scale distribution, offering broad compatibility and adaptive bitrate streaming. However, it typically has higher latency (5-30 seconds), making it less suitable for interactive use cases.
  • RTMP (Real-Time Messaging Protocol) was once the standard for low-latency streaming to platforms like YouTube or Twitch. While it’s being phased out in favor of newer protocols, it’s still commonly used for ingesting streams due to its reliability and low latency (~2-5 seconds).

We will show you how to watch the WebRTC livestream and implement some common livestreaming features.

We also offer a default component, LivestreamPlayer, that comes with a predefined UI, in case it fits your use-case.

Integrating the default livestream player is very simple, you just need the call type and id:

import { LivestreamPlayer } from "@stream-io/video-react-sdk";

// Use the component in your app
<LivestreamPlayer callType="livestream" callId="your_call_id" />;

The rest of the guide will be focused on building your own livestream player view.

Livestream states

A livestream can be in different states, which your UI needs to handle appropriately:

  • Backstage - The livestream is created but not yet started
  • Live - The livestream is active and viewers can watch
  • Ended - The livestream has finished

The React SDK provides hooks to detect these states:

import { useCallStateHooks } from "@stream-io/video-react-sdk";

const LivestreamContent = () => {
  const { useCallEndedAt, useIsCallLive } = useCallStateHooks();
  const endedAt = useCallEndedAt();
  const isLive = useIsCallLive();

  return (
    <View>
      {!isLive && <Backstage />}
      {endedAt != null && <CallEnded />}
      {endedAt == null && <CallLiveContent />}
    </View>
  );
};

State logic:

  • isLive false = backstage (only hosts with join-backstage can join, unless join_ahead_time_seconds is set)
  • endedAt not null = ended
  • Otherwise = live

Backstage mode

Show countdown or start date. useIsCallLive updates automatically when live:

import React from "react";
import { useCallStateHooks } from "@stream-io/video-react-sdk";

export const Backstage = () => {
  const { useCallSession, useCallStartsAt } = useCallStateHooks();
  const startsAt = useCallStartsAt();
  const session = useCallSession();

  // viewers who have joined the call before it went live
  const waitingCount = session?.participants_count_by_role["user"] ?? 0;

  return (
    <div className="backstage">
      {startsAt ? (
        <div className="starts-at">
          Livestream starting at {new Date(startsAt).toLocaleDateString()}
        </div>
      ) : (
        <div className="starts-at">Livestream starting soon</div>
      )}

      {waitingCount > 0 && (
        <div className="waiting-count">{waitingCount} viewers waiting</div>
      )}
    </div>
  );
};

Call Ended

Show a message and optionally display recordings:

import type { CallRecording } from "@stream-io/video-react-sdk";

export const CallEnded = () => {
  const call = useCall();
  const [recordings, setRecordings] = useState<CallRecording[]>([]);

  useEffect(() => {
    let cancel = false;

    call.listRecordings(call.state.session?.id).then(({ recordings }) => {
      if (!cancel) {
        setRecordings((prev) => [...prev, ...recordings]);
      }
    });

    const unsubscribe = call.on("call.recording_ready", (event) => {
      setRecordings((prev) => [...prev, event.call_recording]);
    });

    return () => {
      cancel = true;
      unsubscribe();
      setRecordings([]);
    };
  }, [call]);

  return (
    <div className="call-ended">
      <h2>The livestream has ended</h2>

      {recordings.length > 0 && (
        <>
          <h3>Watch recordings</h3>
          <ul>
            {recoridngs.map((recording) => {
              <a key={recording.start_time} href={recording.url}>
                {recording.start_time} - {recording.end_time}
              </a>;
            })}
          </ul>
        </>
      )}
    </div>
  );
};

Call Live View

Example active livestream component:

import React, { useEffect, useState } from "react";
import { useCallStateHooks, ParticipantView } from "@stream-io/video-react-sdk";

export const CallLiveContent = () => {
  const { useParticipants, useCallSession } = useCallStateHooks();
  const participants = useParticipants();
  const host = participant.find((p) => p.roles.includes("host"));

  const session = useCallSession();
  const [duration, setDuration] = useState(() => {
    if (!session || !session.live_started_at) {
      return 0;
    }
    const liveStartTime = new Date(session.live_started_at);
    return Math.floor((Date.now() - liveStartTime.getTime()) / 1000);
  });

  const viewerCount = session?.participants_count_by_role["user"] ?? 0;

  const formatDuration = (durationInMs: number) => {
    const durationInSeconds = Math.floor(durationInMs / 1000);
    const minutes = Math.floor(durationInSeconds / 60);
    const seconds = durationInSeconds % 60;
    return `${minutes}:${seconds.padStart(2, "0")}`;
  };

  useEffect(() => {
    const handle = setInterval(() => {
      setDuration((d) => d + 1);
    }, 1000);

    return () => {
      clearInterval(handle);
    };
  }, []);

  return (
    <div className="live">
      {host && <ParticipantView participant={host} trackType={"videoTrack"} />}
      <div className="duration">{formatDuration(duration)}</div>
      <div className="viewer-count">{viewerCount} viewers</div>
    </div>
  );
};

Rendering: Find the host by role and render with ParticipantView.

Participant count (including anonymous users):

const { useParticipants, useAnonymousParticipantCount } = useCallStateHooks();
const participants = useParticipants();
const anonymousParticipantCount = useAnonymousParticipantCount();
const totalParticipantCount = participants.length + anonymousParticipantCount;
  • Frequently, the call duration is also presented in a livestream. This information can be calculated from the call session using the useCallSession(), as you can see in the CallLiveContent snippet above.

You can also watch queried calls, as explained here. This allows you to present participant count (and other call data), even without joining a call.

Error states

Livestreaming depends on many factors, such as the network conditions on both the user publishing the stream, as well as the viewers.

A proper error handling is needed, to be transparent to the potential issues the user might be facing.

When the network drops, the SDK tries to reconnect the user to the call. However, if it fails to do that, the callingState in the CallState becomes RECONNECTING_FAILED. This gives you the chance to show an alert to the user and provide some custom handling (e.g. a message to check the network connection and try again).

Here’s an example how to do that:

const ConnectionStatus = () => {
  const { useCallCallingState } = useCallStateHooks();
  const callingState = useCallCallingState();

  let statusMessage;

  switch (callingState) {
    case CallingState.RECONNECTING:
      statusMessage = "Reconnecting, please wait";
      break;
    case CallingState.RECONNECTING_FAILED:
      statusMessage = "Cannot join livestream. Try again later";
      break;
    case CallingState.OFFLINE:
      statusMessage = "You are disconnected";
      break;
    default:
      statusMessage = "A connection error occurred";
  }

  return <>{statusMessage}</>;
};