Stream Video Downloader logo
← Back to Blog

HLS Byte-Range Segments: What #EXT-X-BYTERANGE Actually Does

July 10, 2026 · 6 min read

Most of the HLS playlists this site's tools deal with follow the pattern covered in how HLS streaming works: one segment, one URL, fetch each in order. But the HLS spec also allows a media playlist to point every segment at the same URL and instead carve each one out of a byte range inside that shared file. That mechanism is #EXT-X-BYTERANGE, it's legal HLS, real encoders and packagers produce it, and it changes what "fetch a segment" even means.

The default case, quickly recapped

Ordinarily, a media playlist looks like a flat list of segment URIs —segment0.ts, segment1.ts, and so on — each one a separate file, separate HTTP request, separate response. A parser only has to read a non-comment line and treat it as "the next segment." That's exactly what this site's own playlist parser, parseMedia() in lib/m3u8-parser.ts, does: walk the lines, and every line that isn't a #EXT-X-KEY or #EXT-X-MAP tag and doesn't start with # becomes a segment URL.

What #EXT-X-BYTERANGE actually specifies

A byte-range playlist instead looks like this, repeated for every segment:

  • #EXT-X-BYTERANGE:508000@0 then video.mp4
  • #EXT-X-BYTERANGE:494000@508000 then video.mp4
  • #EXT-X-BYTERANGE:501000@1002000 then video.mp4

The tag's value is <n>[@<o>] n bytes long, starting at offset o — and it applies to the URI line immediately following it, exactly the way #EXT-X-KEYapplies to whatever segments come after it. The offset is optional: if it's omitted, the spec says the range starts at the byte immediately following the previous sub-range of that same resource, which is how a packager can write a tight sequence of ranges without repeating the running offset on every line. Three separate playlist entries, three "segments" a player has to fetch independently — but one underlying file on the server.

It's an HTTP Range request, not a different segment format

Nothing about the segment's actual bytes changes — a byte-range segment is still an ordinary .tsor fragmented-MP4 chunk, demuxed the same way once the player has it in hand. What changes is how it's fetched: instead of a plain GET, the client sends a Range: bytes=508000-1001999 header and expects a 206 Partial Content response containing just that slice. That only works if the origin (or the CDN edge in front of it, as covered in how CDNs deliver streaming video) advertises Accept-Ranges: bytes and actually honors the header — almost every HTTP file server and CDN does, since range requests are also what lets a browser seek within a downloaded MP4 or resume an interrupted download, so the plumbing was already there long before HLS reused it.

Why a packager would do this instead of many small files

Splitting a stream into one file per segment means one object to write, list, and eventually delete per chunk — fine at a handful of segments, less fine at the scale a CDN operates at, where object count and small-file overhead are real costs independent of total bytes served. Addressing sub-ranges of one long file instead turns a few hundred small objects into one write and one growing (or already-complete) object, which is why byte-range addressing shows up most often with fragmented MP4 delivery — the same format covered in HLS vs. DASH, where CMAF packagers frequently emit one .mp4per rendition and let the playlist's #EXT-X-BYTERANGE tags do the segmenting instead of the filesystem.

It also turns up in Low-Latency HLS, where partial segments advertised via #EXT-X-PART commonly carry their own BYTERANGE attribute — a player can request a sub-range of a segment the server is still writing, instead of waiting for the whole thing to finish, which is one of the mechanisms LL-HLS uses to shrink end-to-end latency.

Why this breaks a parser that only reads URIs

A parser that treats every non-comment line as an independent segment URL — without reading the #EXT-X-BYTERANGEline above it — sees the three-entry example above and concludes there are three segments, all conveniently at the same URL. That's not wrong about the URL; it's wrong about what to do with it.

This site's converter (stageMedia() in components/Converter.tsx) is exactly that kind of parser today: it calls fetchBytes() once per SegmentInfo the way the bridge-first fetch pattern works everywhere else on the site, and writes whatever comes back to its own file in ffmpeg's virtual filesystem. Pointed at a byte-range playlist, it has no code path that reads #EXT-X-BYTERANGEat all — so instead of one ranged request per chunk, it re-fetches the entire shared file in full, once for every entry that shares that URL, and stores that many duplicate full copies rather than the small slices the playlist actually describes. The segment count and size total the UI reports would be based on that same inflated fetch, not on the real, much smaller stream. A byte-range-addressed source isn't handled correctly here yet — a genuine gap, not a subtle edge case, the same way the subtitle parser doesn't currently follow TYPE=SUBTITLES renditions.

Where Range headers intersect with CORS

Byte-range fetching adds a custom Range header on top of a plain GET, which is exactly the kind of request called out in why CORS blocks video downloads as a second point where a cross-origin fetch can be rejected: a preflight OPTIONS request has to succeed before the browser will send the ranged GETat all, on top of whatever CORS check the plain request would have needed anyway. Fetching through the extension bridge's privileged background request sidesteps that the same way it sidesteps ordinary CORS — but only once a parser is actually asking for the right range in the first place.

FAQ

How do I tell if a playlist uses byte-range segments? Look for #EXT-X-BYTERANGE lines above the segment URIs, and check whether the same URI repeats across many entries — a media-playlist page that lists one filename over and over with different byte-range tags in front of it is the signature.

Does a byte-range playlist mean the video is protected somehow? No — it's purely a delivery optimization, unrelated to AES-128 segment encryption or DRM. A stream can combine byte-range addressing with encryption, neither one, or both; they're independent tags in the same playlist.

Does every HLS player need to support #EXT-X-BYTERANGE? Any spec-compliant HLS client does, including hls.js and Safari's native player, since it's part of RFC 8216. It's specifically a tool that reads the playlist itself — rather than handing the whole URL to a full HLS player — that has to implement the tag explicitly instead of getting it for free.