๐ŸŒENZH

Beyond Joining Segments: Relative URLs, Byte Ranges, and fMP4 in an M3U8 Converter

When we started building the M3U8-to-MP4 converter, the obvious plan was to read a playlist, download its segments, and join them. That plan missed several details. An uploaded playlist could lose its original location. Multiple segments could refer to different regions of one file. A stream could need a separate initialization section, or carry its audio in another playlist.

These notes follow the order in which the converter handles the data. The examples use fictional addresses and filenames, not downloadable sample videos. Our October 1, 2026 controlled browser tests covered relative URLs, separate audio, a server returning 200 for a Range request, and fMP4 initialization sections.

An uploaded playlist doesn't carry its original URL

An M3U8 file is a text index. Saving it preserves segment names, durations, and tags, but doesn't bundle the video or necessarily preserve its network location. Consider this file:

#EXTM3U
#EXT-X-TARGETDURATION:6
#EXTINF:6.0,
segment-01.ts
#EXTINF:6.0,
segment-02.ts
#EXT-X-ENDLIST

The playlist inspector can read it without downloading media and report two segments with a combined declared duration of 12 seconds. Conversion needs an address for segment-01.ts. A file uploaded through the browser doesn't tell us which CDN directory it came from, and resolving its segments against our own website would be wrong.

If an uploaded file or pasted playlist contains relative URIs, supply the original playlist's HTTP(S) URL. Use the address that belonged to that file, rather than a website homepage. For a playlist originally at https://media.example.com/course/hls/index.m3u8, segment-01.ts resolves within that directory.

Inspection can therefore do useful work with the text alone. Conversion needs accessible media resources. Seeing a segment in the inspector's table doesn't mean it has been downloaded or checked for availability.

Resolve URLs using their actual rules

We use URL resolution rather than trimming off the filename and joining strings. Given the base https://media.example.com/course/hls/index.m3u8, these references resolve differently:

URI in the playlistResolved address
segment.tshttps://media.example.com/course/hls/segment.ts
../audio/index.m3u8https://media.example.com/course/audio/index.m3u8
/keys/key.keyhttps://media.example.com/keys/key.key
https://cdn.example.com/segment.tsThe same absolute address

In JavaScript, new URL(resourceUri, playlistUrl) handles those cases. MDN documents the URL constructor's resolution behavior. For a playlist fetched over the network, we use the response's final URL as the base so a redirect doesn't leave us resolving resources against the old directory.

Query parameters need care too. An entry URL such as index.m3u8?token=... doesn't imply that every child request should receive that token. The source may already have signed its segment URIs or use a different authorization mechanism. Blindly copying parameters can break a signature. Our converter resolves the URIs as given instead of guessing the source's authentication rules.

When a request fails, check its actual URL in the browser's network panel. Correct a wrong base address first. If the path is correct but returns 403, check access requirements and URL expiry. Changing the filename extension won't repair either problem.

The audio may live in another playlist

A master playlist can link a video variant to an audio group whose media is listed separately. Downloading only the video branch can produce a silent output.

Our current converter chooses the variant with the highest declared bandwidth. For a separate audio group, it chooses the default rendition when one exists, otherwise the first rendition. Video and audio are then supplied to the media engine as separate inputs.

That policy doesn't preserve every audio track. Use the inspector to review variants, declared codecs, and audio groups, then check which branches were selected. Declared bandwidth is also only a playlist value; the highest number doesn't guarantee the best picture.

There is a smaller parsing trap here: a quoted CODECS value can itself contain a comma. Splitting an attribute line at every comma corrupts that value. The parser has to respect quoted strings before it can reliably interpret the variant's attributes.

A byte-range segment is only part of a file

Some playlists point several segments at different regions of the same media file:

#EXTM3U
#EXT-X-TARGETDURATION:4
#EXTINF:4.0,
#EXT-X-BYTERANGE:1000@0
media.ts
#EXTINF:4.0,
#EXT-X-BYTERANGE:1200@1000
media.ts
#EXT-X-ENDLIST

These numbers illustrate the ranges; they aren't offsets for a real playable sample. The first segment needs bytes 0โ€“999, and the second needs bytes 1000โ€“2199. Downloading the entire media.ts twice and treating each response as a segment would duplicate the content.

We request the appropriate range. For a 206 response, we check the returned length and, when the response header is readable, the reported range. Some servers ignore Range and return 200 with the whole file. In that case, we slice out the requested region and count the full response toward the download budget.

An omitted @offset doesn't always mean zero. It depends on a valid preceding range. If the required context is missing, the parser should report an error. The tag is defined in RFC 8216, Section 4.3.2.2. Byte ranges must also be part of a resource's cache identity: the URL alone doesn't identify the requested bytes.

fMP4 needs its initialization section

Fragmented MP4 media usually needs an initialization section containing track and decoding information. A playlist can reference it with EXT-X-MAP:

#EXTM3U
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:4
#EXT-X-MAP:URI="init.mp4"
#EXTINF:4.0,
part-01.m4s
#EXT-X-ENDLIST

Downloading the .m4s file while skipping init.mp4 can leave the engine unable to identify the tracks. We fetch the initialization section, write it into the engine's browser-side filesystem, and rewrite the local playlist reference.

An initialization section can have its own range and encryption information. If the active key changes later in the playlist, that later key must not overwrite the state captured when the map was declared. The encryption requirements are documented in RFC 8216, Section 4.3.2.5. Our AES-128 debugging notes describe the related fix.

Downloaded bytes still need a valid local playlist

We don't hand the original remote segment URLs to the engine and let it continue fetching them. The converter downloads the resources, assigns local filenames, and writes a playlist referencing those files.

That rewrite has to account for what was downloaded. Once a byte range has been extracted into its own local file, the original offset into the remote file no longer applies. Duration declarations, initialization references, and discontinuity markers still need to retain their meaning.

The final operation remuxes the existing audio and video without re-encoding them. It avoids another lossy encode and the cost of transcoding, but the source codecs must fit the target MP4 container. A parsed playlist, a completed download, and a playable output are separate things to verify.

The tool currently requires finished on-demand playlists containing EXT-X-ENDLIST. It doesn't continuously record a live stream. Gaps, I-frame-only playlists, and other unsupported structures stop conversion with an error. For a live playlist without an end marker, the inspector's summed duration describes the current listed window, not the length of the entire broadcast.

For a failed conversion, we'd check the base URL and network requests first, then the video and audio selection, ranges, initialization sections, and encryption. Codec and remuxing failures come after those checks. Each step gives you something specific to verify before changing how the files are combined.