Why the Native Video Element Falls Short on Poor Connections
The HTML5 <video> element works well for straightforward playback, but its default delivery mechanism — progressive download — carries significant performance trade-offs. The browser fetches a single MP4 file from the server using HTTP range requests (206 Partial Content), pulling specific byte ranges on demand. This avoids downloading the entire file upfront, but there is no adaptation. If the network degrades or the device is underpowered, playback stalls because the player cannot switch to a lower-quality version of the same content.
In cases where range requests are unsupported, the browser downloads the complete file before playback can begin, making high-resolution video on slow connections impractical. More importantly, even with range requests, a single encoded file offers no flexibility: the same bitrate is served regardless of available bandwidth or device capabilities.
How Adaptive Bitrate Streaming Works
Adaptive bitrate (ABR) streaming takes a different approach. Rather than serving one monolithic file, video is segmented into short chunks, each encoded at multiple resolutions and bitrates. During playback, an ABR algorithm selects the highest-quality segment that can be downloaded in time for smooth playback, continuously reevaluating as network conditions change.
Two browser APIs enable this behavior:
- Media Source Extensions (MSE): Allows passing a
MediaSourceobject to the video element’ssrcattribute, feeding it multipleSourceBufferobjects that represent individual video segments. - Media Capabilities API: Supplies information about the device’s video decoding and encoding capabilities so ABR can make informed quality decisions.
Together, these APIs let the player serve optimized video chunks in real time, avoiding the pitfalls of progressive downloading.
Protocol Choice: MPEG-DASH vs. HLS
ABR streaming relies on a protocol to orchestrate segment selection. The two dominant protocols are MPEG-DASH and HTTP Live Streaming (HLS). Both operate over standard HTTP and are compatible with ordinary web servers. This article focuses on MPEG-DASH, which is widely supported across browsers and platforms, though not on Apple devices — those require HLS.
MPEG-DASH operates via two components:
- Media Presentation Description (MPD): An XML manifest describing how to select and manage streams and adapt to network changes.
- Segmented media files: Video and audio divided into segments at various resolutions and durations, encoded with DASH-compliant codecs.
The DASH player reads the MPD, monitors bandwidth, and requests the appropriate segment for current conditions, repeating this loop throughout playback.
Building the Streaming Pipeline
Setting up adaptive streaming involves four steps: encoding the source video into multiple renditions, generating the MPD manifest, hosting the output files, and building the player. Here is the complete workflow.
Step 1: Encode the Source Video
Start by installing FFmpeg. On macOS, install it via Homebrew:
brew install ffmpeg
First, generate the audio-only rendition in WebM format:
ffmpeg -i "input_video.mp4" -vn -acodec libvorbis -ab 128k "audio.webm"
Key flags: -vn disables video, -acodec libvorbis selects the Vorbis audio codec, and -ab 128k sets a 128 kbps bitrate.
Next, produce three video renditions. The resolutions should scale proportionally from the source. For a vertical 576×1024 input at 30 fps, the following script creates three quality levels:
ffmpeg -i "input_video.mp4" -c:v libvpx-vp9 -keyint_min 150 -g 150 \
-tile-columns 4 -frame-parallel 1 -f webm \
-an -vf scale=576:1024 -b:v 1500k "input_video_576x1024_1500k.webm" \
-an -vf scale=480:854 -b:v 1000k "input_video_480x854_1000k.webm" \
-an -vf scale=360:640 -b:v 750k "input_video_360x640_750k.webm"
Notable parameters: -c:v libvpx-vp9 uses the VP9 encoder, -keyint_min 150 and -g 150 set a keyframe every five seconds (enabling bitrate switches at those intervals), and -tile-columns 4 plus -frame-parallel 1 improve encoding throughput. Each rendition sets its own resolution via -vf scale=... and bitrate via -b:v, with -an excluding audio.
WebM is the chosen container because VP9 files are compact, widely supported in browsers, and optimized for adaptive streaming.
Step 2: Generate the MPD Manifest
Combine the renditions and audio track into a single DASH manifest with this script:
ffmpeg \
-f webm_dash_manifest -i "input_video_576x1024_1500k.webm" \
-f webm_dash_manifest -i "input_video_480x854_1000k.webm" \
-f webm_dash_manifest -i "input_video_360x640_750k.webm" \
-f webm_dash_manifest -i "audio.webm" \
-c copy \
-map 0 -map 1 -map 2 -map 3 \
-f webm_dash_manifest \
-adaptation_sets "id=0,streams=0,1,2 id=1,streams=3" \
"input_video_manifest.mpd"
The -f webm_dash_manifest flag marks each input for DASH use. The -map directives include all three video streams plus audio. The -adaptation_sets option then groups streams: video streams 0, 1, and 2 form one set, while audio stream 3 becomes a separate set.
The output is input_video_manifest.mpd, which describes the available streams and enables dynamic bitrate switching.
At this point, the working directory holds three video renditions — 576×1024, 480×854, and 360×640 — one audio track, the MPD manifest, and the original MP4 file:
input_video.mp4
audio.webm
input_video_576x1024_1500k.webm
input_video_480x854_1000k.webm
input_video_360x640_750k.webm
input_video_manifest.mpd
Keep the original MP4 as a fallback source in case a browser does not support MPEG-DASH.
Step 3: Host the Files
The generated files can be served from local storage, but for production playback it is recommended to use object storage such as AWS S3 or Cloudflare R2 behind a CDN. Both providers support the HTTP range requests that DASH segments rely on by default.
Integrating Dash.js into React
With the streaming assets ready, build the player interface. Install the Dash.js npm package:
npm i dashjs
Create a <DashVideoPlayer /> component that initializes a Dash MediaPlayer instance on mount, pointing it at the MPD file. Use a ref to bind event listeners after the component mounts. For resilience, include the original MP4 URL inside a <source> element as a non-DASH fallback.
If you are using the Next.js app router, add the 'use client' directive so the component hydrates client-side, because media players cannot initialize on the server.
Full component implementation:
import dashjs from 'dashjs'
import { useCallback, useRef } from 'react'
export const DashVideoPlayer = () => {
const playerRef = useRef()
const callbackRef = useCallback((node) => {
if (node !== null) {
playerRef.current = dashjs.MediaPlayer().create()
playerRef.current.initialize(node, "https://example.com/uri/to/input_video_manifest.mpd", false)
playerRef.current.on('canPlay', () => {
// upon video is playable
})
playerRef.current.on('error', (e) => {
// handle error
})
playerRef.current.on('playbackStarted', () => {
// handle playback started
})
playerRef.current.on('playbackPaused', () => {
// handle playback paused
})
playerRef.current.on('playbackWaiting', () => {
// handle playback buffering
})
}
},[])
return (
<video ref={callbackRef} width={310} height={548} controls>
<source src="https://example.com/uri/to/input_video.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
)
}
When testing with Chrome DevTools by shifting the network profile from Fast 4G to 3G, the player adapts in real time — switching from a higher resolution (such as 480p) down to 360p — demonstrating how adaptive streaming adjusts quality to match available bandwidth.
Why ABR Improves Playback Performance
The core advantage of ABR streaming is performance. Video is delivered as small chunks rather than as one large file, so playback can begin earlier, even on slow connections. Because the player serves multiple quality variants from the same content, it continuously chooses the best format for the current network and device conditions, preventing the buffering and stalling that plague progressive downloads on constrained links.



