Skip to content

GuidePlayback

Play live streams

Play live streams with live-edge tracking, DVR windows, and stream-type detection.

Installation

The examples use <hlsjs-video>, so install hls.js with the framework façade:

pnpm add @videojs/html @videojs/hlsjs-video

Use the live feature bundle with a streaming media component, and give users a <media-live-button>: it shows whether playback is at the live edge and seeks back to it when activated.

<live-video-player>
  <media-container>
    <hlsjs-video src="https://your-stream.example.com/live.m3u8" autoplay muted playsinline></hlsjs-video>
    <media-live-button></media-live-button>
  </media-container>
</live-video-player>
<script type="module">
  import '@videojs/html/live-video/player';
  import '@videojs/html/media/hlsjs-video';
  import '@videojs/html/live-video/ui';
</script>

The examples use <hlsjs-video>. The smaller <hls-video> also reports live state; use it when its supported feature set fits your stream.

The liveVideoFeatures bundle starts from videoFeatures, adds the live feature, and omits playback rate, quality selection, and audio-track selection. The live presets also ship skins without a time slider, duration display, or current-time display.

Both bundles are plain arrays, so extending one is a spread:

import { createPlayer, streamTypeFeature } from '@videojs/html';
import { liveVideoFeatures } from '@videojs/html/live-video';

const { PlayerElement } = createPlayer({ features: [...liveVideoFeatures, streamTypeFeature] });

customElements.define('custom-live-player', PlayerElement);

Use <custom-live-player> in place of <live-video-player> for this configuration.

How it works

Two features describe liveness:

  • The stream type feature reports streamType: 'live', 'on-demand', or 'unknown'. Streaming media derives it from manifest metadata; plain media elements fall back to duration-based detection. liveVideoFeatures does not include it; add streamTypeFeature to the player’s features when you need streamType.
  • The live feature reports two values:
    • liveEdgeStart is where “live” begins on the timeline. When the playhead is at or past this time, the viewer is watching live; behind it, they’re watching earlier moments of the stream.
    • targetLiveWindow describes the kind of live window: 0 for a sliding window, Infinity for an event with playback history, and NaN for on-demand or unknown content. Read seekable to find the actual times viewers can rewind to.

The time feature replaces an infinite media duration with the end of the seekable range. That end advances as new segments become available; the start can also advance as old segments expire.

<media-live-button> treats playback as live when the playhead is no more than five seconds behind liveEdgeStart. If liveEdgeStart is unknown, the button uses the last ten seconds of the seekable range instead. When playback is farther behind, activating the button seeks to the seekable end. The button is disabled (data-disabled) when the media is not live or has no finite seekable end. At the edge, it is inactive (aria-disabled="true").

Availability and constraints

  • liveEdgeStart and targetLiveWindow require media that implements the live capability. Examples include hls.js video, SPF HLS video, native HLS video, and Shaka video. Native HLS video requires browser HLS support. A plain <video> can report a live streamType from its duration, but its live-window values stay NaN.
  • Live playback drifts behind the edge when the network stalls or the tab is backgrounded. Always give users a path back to the edge.
  • Both sliding live windows and growing event histories can support seeking. Check seekable for the available range. The built-in time slider starts at zero, so use it only while the seekable history starts at zero; a moving start requires a custom slider.
  • liveVideoFeatures omits playback rate control. Add it explicitly if your DVR UI needs it.

Common variations

DVR: let users scrub back

For a live event whose seekable history starts at zero, add a <media-time-slider> to your custom UI or customized live skin. Its maximum follows the seekable end. For a sliding window whose start moves forward, build a slider from the current seekable start and end; the built-in slider would include expired times.

Switch UI by stream type

Render live or on-demand controls from streamType when one player handles both kinds of content. Add streamTypeFeature to the player’s features; liveVideoFeatures does not include it:

<media-live-button> already reflects live state. To show a whole group of controls only for live streams, compose a custom player and subscribe to its store:

<stream-type-player>
  <media-container>
    <hlsjs-video src="https://your-stream.example.com/live.m3u8" autoplay muted playsinline></hlsjs-video>
    <div id="live-controls" hidden>
      <media-live-button></media-live-button>
    </div>
  </media-container>
</stream-type-player>
<script type="module">
  import { createPlayer, selectStreamType, streamTypeFeature } from '@videojs/html';
  import { liveVideoFeatures } from '@videojs/html/live-video';
  import '@videojs/html/media/hlsjs-video';
  import '@videojs/html/ui/container';
  import '@videojs/html/ui/live-button';

  const { PlayerElement } = createPlayer({
    features: [...liveVideoFeatures, streamTypeFeature],
  });
  customElements.define('stream-type-player', PlayerElement);

  const player = document.querySelector('stream-type-player');
  const store = player.store;
  const controls = document.querySelector('#live-controls');
  const update = () => {
    controls.hidden = selectStreamType(store.state)?.streamType !== 'live';
  };

  const unsubscribe = store.subscribe(update);
  update();
  // Call unsubscribe() when this player is removed.
</script>

Troubleshooting

The live badge never activates

<media-live-button> reads targetLiveWindow, currentTime, and seekable. Confirm that targetLiveWindow is 0 or Infinity and that seekable has a finite end. Use a live-capable media component such as <hlsjs-video>. The live button does not need streamTypeFeature; add it only when your own UI reads streamType.

Playback keeps falling behind the edge

Network throughput can’t sustain the stream, or the tab was backgrounded. <media-live-button> returns users to the edge; for lower drift, review the stream’s latency configuration on the encoder side.

The time slider behaves oddly on a live stream

Compare the slider’s range with seekable. The built-in slider always starts at zero. Once a sliding window moves past zero, the slider’s range includes expired history. Use a custom slider that follows both range bounds, or keep the live button without a timeline.

Components

API

Guides