DEV Community

H. Lin /h-lin/
H. Lin /h-lin/

Posted on

How I Built a Browser-Only Video Upscaler with WebGPU and ONNX Runtime

You can try the finished tool here: SquishyFile Video Upscaler

I did not build this because browser-based AI is trendy.

I built it because GPU servers are expensive, especially when the product is a video tool and every uploaded file can keep a GPU busy for minutes.

Video upscaling is a particularly unpleasant workload for a small service. Every frame needs to be decoded, processed, and encoded again. A short clip is manageable. A long clip at 30 or 60 frames per second becomes thousands of inference jobs. Add unpredictable traffic, large uploads, storage, bandwidth, queues, and retries, and a simple “upload a video” feature starts looking like a recurring infrastructure bill.

So I moved the work to the device that already has the file.

The browser reads the local video, uses the user's GPU when it can, processes the frames, encodes a new MP4, and gives the result back as a local download. There is no video upload and no GPU endpoint behind the button.

That does not make the work free. It changes who pays for it. The user pays with processing time, heat, memory, and battery instead of me paying for GPU minutes.

For this product, that is a good trade.

What the tool actually does

The interface is intentionally simple: choose a video, select 2x or 4x, wait for processing, compare the result, and download the new file.

Underneath the interface, the browser has to do a full media pipeline:

  • Read the input container and find the main video track.
  • Decode the video frame by frame.
  • Pick an upscaling method based on the source resolution.
  • Process every frame without freezing the page.
  • Keep the audio track.
  • Encode the result as an MP4.
  • Return a playable file to the user.

The Svelte UI does not perform that work directly. It starts a Web Worker and sends the local file to it. The worker owns decoding, upscaling, encoding, progress, and cancellation. The UI only displays what is happening.

That separation is not a theoretical architecture preference. Without it, the browser tab becomes unresponsive while the first few frames are being processed.

Why there are two upscaling paths

My first idea was to run the AI model on every video. That is easy to explain and a bad idea for large inputs.

A two-minute video at 30 FPS contains 3,600 frames. An AI model that feels acceptable on one small frame can become painfully slow when it has to process every frame, especially when the source is already 1080p or larger.

The tool therefore uses a simple resolution rule:

  • Sources up to 720 pixels tall use FSRCNN.
  • Larger sources use a shader-based FSR path.

This is a performance decision, not a claim that one algorithm is universally superior. Smaller video has fewer pixels and is a reasonable place to spend more compute on learned reconstruction. Larger video already contains more spatial information, so a faster spatial upscaler gives a better browser experience.

The user chooses the scale. The tool chooses the engine.

The AI path uses FSRCNN

The AI path uses two model files: one native FSRCNN model for 2x and another for 4x. The 4x mode does not run the 2x model twice.

There is an important implementation detail here: this FSRCNN model works on luminance rather than directly producing a complete RGB frame. The worker converts the source pixels to a Y channel, runs the model on that channel, and combines the enhanced Y with chroma taken from the original frame.

On a WebGPU-capable browser, custom compute shaders handle much of the pixel packing and color recombination. This avoids pushing every intermediate pixel through a slow JavaScript loop.

The browser tries to run the model through ONNX Runtime Web's WebGPU execution provider. If that is not available, it falls back to WebAssembly. There is also a runtime fallback for cases where WebGPU appears to work during initialization but fails when a specific model operation is actually executed.

WASM is not a magic performance solution. On an older machine it can be very slow. It exists so that the tool produces a result instead of becoming unusable just because the browser cannot run the GPU path.

The FSRCNN models are cached after the first download, so the initial engine load is not repeated for every new video.

The larger-video path uses FSR

For sources above 720p, the tool uses FidelityFX Super Resolution 1.0 implemented as WebGPU compute shaders.

The path has two stages. EASU performs edge-adaptive spatial upsampling, and RCAS applies sharpening afterward. It is much lighter than running a neural network over a large frame, which matters when the input contains millions of pixels and the output has even more.

This path is deliberately less ambitious than the AI path. It is not pretending to recover learned detail. It is a practical way to make larger videos look cleaner and sharper without making the user wait for a server job or paying for a server GPU.

If WebGPU is unavailable, the tool falls back again, this time to a high-quality Canvas 2D resize. The result is not equivalent to FSR, but a lower-quality result is more useful than a hard failure.

Encoding was half the problem

Upscaling individual frames is only half of a video tool. The frames still need to become a valid video, and the audio should not disappear.

I use Mediabunny to connect the input, frame processing, and output stages. The processed frames are encoded as AVC video, the audio is encoded as AAC, and the final result is exposed as a local MP4 Blob.

That last part has a limitation: the output is held in browser memory. Large videos can put real pressure on RAM because decoded frames, intermediate canvases, GPU buffers, and the encoded result all exist during the job. This is one of the reasons the tool is aimed at short, practical clips rather than replacing a professional desktop editor.

The bugs that only appear with real videos

The happy-path demo is easy. The difficult files are the ones people actually upload.

Phone videos often store portrait orientation in metadata. The worker uses the video's display dimensions and bakes the orientation into the processed pixels. It also disables the original rotation metadata in the output. Otherwise a portrait video can be rotated twice when played back.

WebGPU buffer layout is another source of corrupted frames. Texture-to-buffer copies require row alignment, so a staging buffer can contain padding at the end of every row. That padding has to be removed before the bytes become an image. The shader can be completely correct while the displayed video is broken because the readback code ignored this rule.

Cancellation is also necessary. A user can start a 4x job, see how long it will take, and decide to stop. The worker cancels the media conversion, and the UI locks the file selection while a job is active so the result cannot silently belong to a different file.

Progress without lying

An upscale job contains several different kinds of work: probing the video, downloading a model, loading the runtime, compiling shaders, processing frames, and encoding the output.

They do not share one honest percentage.

The tool reports stages instead. Download progress is based on real bytes when the server provides a content length. Model and shader compilation are shown as initialization because there is no useful byte counter for that work. Encoding reports frame progress from the media pipeline.

This is less flashy than a smooth fake progress bar, but it is more useful when the browser spends a long time compiling or processing a frame.

What the user pays instead of me

Browser processing has real costs:

  • A capable GPU can make the job practical.
  • A WASM fallback can be painfully slow.
  • A 4x job uses more memory and takes longer.
  • A laptop can get hot during a long conversion.
  • A phone may run out of battery or memory.
  • The browser tab needs to remain open.
  • Results vary across browsers, GPUs, and drivers.

That is the honest trade. I am not claiming that local processing is faster for everyone. I am saying it is cheaper for me to operate, avoids handling large private video uploads, and works well enough for the short clips this tool is designed for.

If the product needed long 4K videos, batch processing, predictable throughput, or professional codec controls, I would use a backend or a desktop application instead.

What upscaling cannot do

The output may have 1080p or 4K dimensions, but that does not make it native 1080p or native 4K footage. The model reconstructs plausible detail. It cannot recover information that the original camera never captured.

Very blurry, noisy, or heavily compressed footage can still produce a disappointing result. That is why the tool includes a before-and-after comparison instead of asking users to trust a success message and a download button.

Try it

The live tool is here: squishyfile.com/video-upscaler

Drop in a short video, try 2x first, and compare the result before downloading. The file stays in your browser throughout the process.

I did not eliminate the cost of video upscaling. I chose who pays it.

Top comments (0)