MP4 is a great web format: the files are relatively small while still maintaining good quality, and they behave much like GIFs but at a fraction of the size. I wanted a way to easily convert demos and screen recordings into MP4 so I could share them on this blog without bloating page load times. My usual recordings are in the .mov format and are quite large. Converting these without downloading external tools usually means using a cloud service that requires uploading your files. Alternatively, you need to install a tool to do it locally. I wanted a middle ground where I could do the conversion locally, but without installing new software. For this purpose, I built the Offline MP4 Converter.

It uses the ffmpeg.wasm library to run FFmpeg commands in WebAssembly, performing the transcoding offline. Most of the base UI comes from my earlier Offline Image Converter, which meant the main challenge was getting video conversion to work reliably in the browser. This post explores those technical challenges and what I learned along the way.

Transcoding video files

When starting this project, I explored different ways to transcode video files between formats. My first thought was that there might be a Rust library for this, but none existed. The few libraries I found all relied on FFmpeg libraries and tools for the actual conversion. I also looked into different approaches that would allow transpilation to WebAssembly.

Other offline converter projects compile Rust code to WebAssembly, so my initial idea was to use video-rs. However, that didn’t work due to its dependency on FFmpeg. I also considered building rave by the same authors, but it was marked as a work in progress and had been mostly stale for a while. At that point, the Rust-based approach was abandoned.

Another idea was to follow the same approach as the Rust libraries but use the FFmpeg C libraries directly instead of relying on the FFmpeg CLI. This required a different approach, as compiling C to WebAssembly is not as straightforward as with Rust. The Emscripten toolchain provides a way to transpile C to WebAssembly, which I started looking into. That’s when I discovered the ffmpeg.wasm library, which had already done most of the hard work of compiling FFmpeg to WebAssembly. It also included examples of how to transcode video files. This became the library of choice for the project.

Using Ffmpeg.wasm

In a normal FFmpeg installation, you would run the following command:

ffmpeg -i input.mov output.mp4

This will take the input.mov file and convert it to a output.mp4 file. The ffmpeg.wasm library uses a virtual file system to run commands against, so the process looks very similar:

const ffmpegFile = await fetchFile(file);
await ffmpeg.writeFile(name, ffmpegFile);
await ffmpeg.exec(['-i', name, ...options, newName]);
const data = await ffmpeg.readFile(newName);

This produces the output file, which is then returned to the user. The options argument allow for additional parameters to be passed to ffmpeg, letting you fine-tune the conversion.

Tuning the conversion

FFmpeg supports many different arguments for customizing how the conversion is performed.
The advanced section of the project supports the following tuning settings:

  • MP4

    • Enable audio: Allows enabling or disabling audio.
    • Enable fast start: Moves the MP4 headers to the beginning so browsers can start playback sooner.
    • Preset: A collection of arguments that trade off between quality, execution time, and file size.
  • GIF

    • Frames per second: Adjusts the number of frames per second.
    • Resize (width): Resizes the GIF to a given width, with the height automatically adjusted.
    • Limit time (seconds): Cuts the GIF to end earlier.
    • Loop: Enables or disables looping.

The GIF settings in particular revolve around the tradeoff between quality and size.
Fewer frames or a smaller resolution reduce the file size, but also lower the quality.

Performance

Video transcoding is not typically a fast process, but FFmpeg uses many different techniques to speed it up. A lot of these optimizations are lost when compiling to WebAssembly. The authors of ffmpeg.wasm are aware of this, and their performance section includes a benchmark showing that the WebAssembly version is about 24 times slower. This is very noticeable in the project, as conversions take a while to finish.

The library includes a multithreaded version that provides a 2× speedup using SharedArrayBuffers to share memory between threads. Unfortunately, this requires running the website in isolation due to the security requirements of SharedArrayBuffer. Since this doesn’t work on GitHub Pages, where the project is hosted, it wasn’t possible to enable it.

During such a slow operation, it’s normal for the user to switch to other pages. Unfortunately, this exposes another downside of WebAssembly: the browser deprioritizes background tabs. The user can leave the site while the transcoding runs, but the browser may throttle processing, meaning no progress is made until they return.

Given these performance issues, an interesting future project could be to explore possible optimizations.

Converting from GIF to MP4

Conversions use the standard FFmpeg command, which accepts inputs and produces an output. This makes error handling "very simple," as it almost never throws an error. Unfortunately, that’s because the error is hidden and won’t appear until the file is played in a video player. This became a major issue when converting GIFs to MP4, as the resulting files sometimes failed to play. The solution was to:

  • Add the argument -pix_fmt yuv420p, a very common format that works with most players.
  • Add the argument -vf scale=trunc(iw/2)*2:trunc(ih/2)*2, which ensures the resolution is an even number.

I suspect this could also be a problem with other file types, but all my test files worked perfectly afterwards. Since most interactions with the library are done through command-line arguments, it’s difficult to guarantee that all input formats will be supported. The only real solution is to wait for people to report issues and then patch them.

UI

The UI is mostly the same as in the previous project, and it provides both dark and light modes:

Offline MP4 Converter light-mode Offline MP4 Converter dark-mode

Since the UI is mostly the same as in previous projects, the only new thing worth pointing out is the progress meter. The command reports progress in percentages, which are shown to the user.

Conclusion

For the past couple of years, I’ve been keeping track of basic tools I wanted to use but that weren’t installed on my PC by default. This included PDF merging, image format conversion, and video-to-MP4/GIF conversion (this project). This project therefore marks the last in the series of WebAssembly projects providing basic functionality. It’s been fun diving into the different aspects of these formats and how they work.

If you have any tools you’re missing, feel free to email me. I’m always looking for new ideas for small projects.

Go check it out at: