DASH does the same job as HLS: it splits video into segments and describes them in a manifest so a player can adapt quality to the viewer's connection. The differences are that DASH is codec-agnostic, its manifest is XML instead of a playlist, and it is not natively supported on Apple platforms.
That last point decides most projects. If you deliver one format, deliver HLS. DASH is worth producing when you want VP9 or AV1 to cut bandwidth, when you need the flexibility its manifest allows, or alongside HLS for a player that prefers it.
A single-quality stream
ffmpeg -i input.mp4 \ -c:v libx264 -crf 21 -preset veryfast \ -c:a aac -b:a 128k \ -g 48 -keyint_min 48 -sc_threshold 0 \ -f dash -seg_duration 6 \ output.mpd
That produces output.mpd plus a set of segment files. Serve the whole directory and point a DASH-capable player at the .mpd.
The keyframe options are not optional decoration. -g 48 places a keyframe every 48 frames and -sc_threshold 0 stops the encoder adding extra ones at scene changes. Segments can only start at keyframes, so without this the segment lengths drift, which breaks quality switching once you have more than one level.
An adaptive ladder
Multiple quality levels are the reason to use DASH at all. This produces three:
ffmpeg -i input.mp4 \ -map 0:v -map 0:v -map 0:v -map 0:a \ -b:v:0 5000k -s:v:0 1920x1080 \ -b:v:1 2800k -s:v:1 1280x720 \ -b:v:2 1400k -s:v:2 854x480 \ -c:v libx264 -preset veryfast \ -g 48 -keyint_min 48 -sc_threshold 0 \ -c:a aac -b:a 128k \ -f dash -seg_duration 6 \ -adaptation_sets "id=0,streams=v id=1,streams=a" \ output.mpd
The -map 0:v repeated three times creates three video renditions from the same source. Each gets its own bitrate and resolution.
-adaptation_sets groups the streams: video renditions in one set that the player can switch between, audio in another. Without it FFmpeg makes its own grouping, and players sometimes cannot switch quality as a result.
Note the audio is encoded once and shared across all video levels. That is correct. There is no reason to carry three copies of identical audio.
VP9, where DASH earns its place
The reason to choose DASH over HLS is usually a codec HLS does not carry well. VP9 produces meaningfully smaller files than H.264 at the same visual quality, which matters when you are paying for bandwidth.
ffmpeg -i input.mp4 \ -c:v libvpx-vp9 -crf 30 -b:v 0 \ -c:a libopus -b:a 128k \ -g 48 -keyint_min 48 \ -f dash -seg_duration 6 \ output.mpd
-b:v 0 is required. Without it VP9 ignores the CRF value entirely and uses a default bitrate instead. The command appears to work and silently produces something other than what you asked for.
VP9 encoding is considerably slower than H.264. Budget for it, or use a faster preset via -deadline good -cpu-used 2.
Serving DASH
The manifest needs a MIME type or some players refuse it. In .htaccess:
AddType application/dash+xml .mpd AddType video/mp4 .m4s
If the player is hosted on a different domain than the media, CORS headers are required as well, or the browser blocks the segment requests while the manifest loads fine. A confusing failure that looks like a broken stream.
Segments never change once written and can be cached indefinitely. The manifest should not be cached aggressively during a live stream.
Producing both HLS and DASH
If you need both, generate them from the same source rather than transcoding twice:
ffmpeg -i input.mp4 \ -c:v libx264 -crf 21 -preset veryfast \ -c:a aac -b:a 128k \ -g 48 -keyint_min 48 -sc_threshold 0 \ -f hls -hls_time 6 -hls_playlist_type vod \ -hls_segment_filename "hls/seg_%03d.ts" hls/playlist.m3u8 \ -f dash -seg_duration 6 dash/output.mpd
One decode, two outputs. It is substantially faster than running two separate commands, since decoding the source is a large part of the work.
Be honest about whether you need both. It doubles storage, and for most sites HLS alone reaches everyone. Setting up HLS streaming walks through that path in full.
Live DASH
ffmpeg -i rtmp://source/stream \ -c:v libx264 -preset veryfast -b:v 2500k \ -g 48 -keyint_min 48 -sc_threshold 0 \ -c:a aac -b:a 128k \ -f dash -seg_duration 4 \ -window_size 6 -remove_at_exit 1 \ -streaming 1 \ live.mpd
-window_size 6 keeps six segments in the manifest and -remove_at_exit 1 cleans up on shutdown. Without a bounded window, segment files accumulate until the disk fills, which happens quietly and takes the site down with it.
When it does not play
Nothing happens, no obvious error. The browser has no native DASH support. You need a JavaScript player; DASH does not work by pointing a video element at an .mpd.
Manifest loads, segments 404. Paths in the manifest do not match the directory layout. Check where FFmpeg wrote the segments.
Plays but will not switch quality. Keyframes are not aligned, or -adaptation_sets was omitted so the renditions were not grouped.
VP9 output is far larger than expected. -b:v 0 is missing, so the CRF was ignored.
The disk filled during a live stream. No -window_size was set.
If the ladder encodes too slowly to keep up, FFmpeg performance optimization goes over preset and threading. Ahosting's FFmpeg hosting is HLS and DASH ready with FFmpeg pre-installed, from $16.79/month.
Check the manifest before blaming the player
A stream that will not play is diagnosed from the description file, which is readable text.
curl -s https://example.com/stream/manifest.mpd | head -30 curl -sI https://example.com/stream/init-0.m4s | head -1
Each representation names the files it uses. Request one of them directly and confirm it downloads, since a manifest listing segments that return errors produces a player that waits indefinitely with no message.
Relative paths inside the manifest are resolved against its own address, so a manifest served from a different directory than the segments produces exactly this failure.
The server has to answer partial requests
Adaptive playback depends on the client fetching pieces of files, and some configurations prevent it.
curl -sI https://example.com/stream/chunk-0.m4s | grep -i accept-ranges
curl -s -o /dev/null -w '%{http_code}\n' -r 0-1023 https://example.com/stream/chunk-0.m4s
The second request must answer with a partial content status. Compression is the usual reason it does not, since a compressed response cannot be served in ranges.
Excluding media types from compression costs nothing, because the files are already compressed, and it restores the behaviour the player depends on.
Serving it across origins needs permission
A player on one host fetching segments from another is refused by the browser unless the segment host permits it.
curl -sI https://cdn.example/stream/manifest.mpd | grep -i access-control
The absence of that header is the cause when a stream plays from the same host and fails from a page elsewhere, and nothing in the player log says so plainly.
This appears the moment a content delivery network is introduced, which is usually after the stream has been working for weeks. The stream did not change; the origin of the request did.