Shipping a Browser M3U8 Converter: Memory Use and a 25 MiB WASM File Limit
The M3U8-to-MP4 page is small compared with the media engine it loads. After working through segments, audio tracks, and AES-128 decryption, we ran into a deployment problem: the FFmpeg WASM binary exceeded our host's limit for a single file.
The converter worked locally. Its asset layout still needed to change before we could ship it. These notes use our build output and October 1, 2026 verification records. They describe the single-thread engine and static hosting setup we chose; other FFmpeg builds and hosts have different requirements.
Where the conversion runs
The tool has no server-side video conversion endpoint. The user's browser fetches playlists and media, decrypts supported segments, and remuxes the result into MP4. Our web server supplies the page and engine files rather than downloading and processing the complete video on the user's behalf.
That reduces video-processing work on our server, but puts the CPU and memory demands on the user's device. The media source still receives requests from the browser, and those requests still follow cross-origin rules. Browser-side processing doesn't make the operation offline or grant access to protected media.
We copy the existing audio and video into MP4 without re-encoding. This generally requires less computation than transcoding and preserves the original encoded quality. It also means container compatibility matters. If the source codecs or stream structure don't work with this remuxing path, the converter reports a failure rather than automatically starting a lengthy transcode.
A 256 MiB download budget isn't a RAM ceiling
Our implementation counts downloaded media bytes against a 256 MiB budget. Initialization sections and separate audio count too. If a server ignores a Range request and returns a complete file, we count what was actually downloaded, even if we only wanted a small region.
During conversion, the browser can hold response data, decrypted bytes, the engine's in-memory filesystem, and the output at the same time. Data may also be copied between stages. A download near the budget can therefore consume considerably more memory. A device with little available RAM can struggle before it reaches the download limit.
We decrypt one segment at a time instead of scheduling every segment for decryption together. That helps control concurrent work, but doesn't make this an unlimited streaming converter. The engine still needs its input and output resources. Supporting substantially larger videos would require changes to storage, input, and output handling, not just a larger number in the download limit.
For users, a short on-demand sample is a useful first compatibility check. Close other memory-heavy pages if the device is struggling. Device speed, codecs, segment count, and the source network all affect the job; we can't give a dependable conversion time per minute of video.
AES adds work without re-encoding the video
AES-128 decryption uses the browser's native crypto API. Once decrypted, the segments follow the same remuxing path as clear media. The extra work is decryption and its data buffers. Our AES-128 debugging notes cover the key, IV, and detection problems we fixed.
We haven't used a single sample to promise a particular decryption speed. A slow source can make downloading the longest stage. On a memory-constrained device, data copies and engine memory can cause more trouble. A first conversion also has to load the engine. We show separate stages so that engine loading and downloading aren't all labeled as transcoding.
Cancellation needs to cover those stages too. Our implementation aborts resource requests and terminates the engine task. Completion and cancellation release the associated object URLs and engine resources. That avoids retaining a new set of resources with each attempt, although the browser controls when memory is actually reclaimed.
The WASM binary was too large to publish as one asset
The FFmpeg core WASM file we used was 32,232,419 bytes, about 30.7 MiB. Cloudflare Pages limits an individual site asset to 25 MiB. That is a per-file limit on published output, rather than a limit on the whole site.
Our HTML, scripts, and styles could all be valid while that binary still prevented deployment in its original form. Renaming it or moving it to another directory wouldn't change its size. A successful local engine load didn't check this hosting constraint.
We changed the engine's distribution rather than the conversion code. Before building, a preparation step splits the original WASM into four files, each at most 8 MiB, and writes a manifest containing their URLs and expected lengths. Each published asset then fits under the host's limit.
Reassembling the engine in the browser
The loader fetches the manifest, downloads the parts in order, checks their lengths, and creates a WASM Blob URL from the bytes. It passes that URL to the engine and releases it after use. The engine receives the original byte sequence; splitting the file doesn't change FFmpeg's behavior or reduce the total download.
Generated filenames include a content hash to reduce the chance of mixing assets from different deployments. Manifest requests use cache revalidation. The preparation step runs during dependency installation, development startup, and production builds, so deployment doesn't depend on files someone generated manually on one machine.
We verified the split files by reassembling them and comparing the result byte for byte with the original binary in the dependency package. We also scanned the final static output for files over the hosting limit. At runtime, checking each part's expected length catches missing or truncated responses. A length check alone is not a full content-integrity check.
There are costs to this approach: more requests, some browser-side work to assemble the bytes, and memory use during loading. It solves the single-file limit in our hosting setup. It doesn't compress the engine or guarantee faster conversions.
Single-threading, HTTPS, and cross-origin access
We chose a single-thread FFmpeg WASM core for this static site. Moving to a multi-thread core that depends on shared memory would change the deployment requirements, including browser isolation requirements. The official ffmpeg.wasm overview explains the available cores and how they run. A configuration that works for our single-thread core isn't automatically suitable for a multi-thread build.
AES decryption also needs Web Crypto. Our production site uses HTTPS, and localhost supports the API during local development. Serving the page from an arbitrary insecure HTTP origin can leave the crypto interface unavailable. HTTPS doesn't solve CORS errors on the source's key or media requests; those are separate requirements.
The published asset paths matter as well. A readable manifest with one missing part still leaves the engine unable to load. An error page returned in place of binary data can also break loading. Opening the converter page is too early to declare this lazily loaded feature working.
What we checked after deployment
We checked both tool routes in all seven site languages, then the manifest and engine assets. Browser conversion tests used controlled samples for ordinary TS, separate audio, fMP4 initialization sections, ignored Range requests, and AES-128 encryption. Controlled fixtures helped us distinguish the tool's behavior from changes or failures at an external media source.
We checked the output as well: it had to be recognized as MP4, expose plausible video metadata and duration, and allow seeking close to the end. A progress bar reaching 100% or a download button appearing wouldn't establish that. These checks ran mainly in Chromium; they aren't a claim of testing every phone or browser.
Finally, we waited for the deployment to take effect and revisited the production URLs. A successful push means the repository received the commit. It doesn't mean the new pages and assets are already being served. We checked the published resources, internal links, and sitemap after the release.
The file-size problem was a deployment issue. Wrong segment paths, missing audio, and missing initialization sections belong earlier in the conversion pipeline. If those symptoms sound more like your failure, start with our notes on playlist URLs and segment layout before changing how the engine is hosted.