How media streaming works

Streaming delivers media as a sequence of small chunks rather than a monolithic file. The player first reads a manifest that describes how the content is split, then requests and reassembles those chunks during playback, potentially switching bitrates as network conditions change. For this to work, the server must support the Range HTTP request header.

What streaming enables

Producing chunks and manifests is more work than serving static files, but it opens up scenarios a plain <video> element cannot handle:

  • Adaptive streaming: The same content is encoded at multiple bitrates, and the player receives the highest-quality chunk that fits current bandwidth.
  • Live broadcast: Chunks are encoded and published in real time.
  • Media injection: Additional content such as ads can be spliced into the stream without changing the source URL.

DASH and HLS

The dominant protocols are Dynamic Adaptive Streaming over HTTP (DASH) and HTTP Live Streaming (HLS). Players supporting these protocols fetch the manifest, decide which chunks to request, and merge them into a continuous experience.

Native playback limitations

Browsers will not always play a stream directly. HLS has some native support, but DASH generally does not, so pointing a <source> element at a manifest is often insufficient:

<video controls>
  <source src="manifest.mpd" type="application/dash+xml">
</video>

This apparent drawback is actually a design advantage. Manifests typically describe multiple variants of the same media—different bitrates, audio tracks, and formats—and applications have different playback needs. Some want large buffers, others prefetch the start of the next episode, and some implement custom adaptive logic.

Media Source Extensions

The W3C's Media Source Extensions (MSE) give JavaScript a way to generate media streams. A MediaSource object is attached to a <video> element, and any data pushed into its buffers is played back as if it came from a URL:

const videoEl = document.querySelector('video');
const mediaSource = new MediaSource();

video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener(
  'sourceopen',
  () => {
    const mimeString = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
    const buffer = mediaSource.addSourceBuffer(mimeString);

    buffer.appendBuffer( /* Video data as `ArrayBuffer` object. */ )
  }
);

This example illustrates the core mechanics:

  • The <video> element sees ordinary media from a URL.
  • That URL points to a MediaSource instance.
  • The MediaSource creates one or more SourceBuffer objects.
  • Binary data is appended to the buffer, for example via fetch.

Writing a full DASH and HLS player from scratch is possible but uncommon. Most projects adopt mature libraries such as Shaka Player, JW Player, or Video.js. A demo progressive web app called Kino shows a minimal streaming site with offline playback built on the plain <video> element.

Chunk container formats

DASH and HLS historically required different chunk formats. In 2016, HLS added support for fragmented MP4 (fMP4), which DASH already used. Chunks in the fMP4 container with the H.264 codec work with both protocols and nearly every player, letting producers encode once rather than per protocol—saving both time and storage. Newer codecs like VP9 offer better quality at smaller sizes, but require encoding additional variants of the content.