<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: GrabCast</title>
    <description>The latest articles on DEV Community by GrabCast (@grabcast).</description>
    <link>https://dev.to/grabcast</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4170136%2Fbac6f165-0b78-4d46-a0bb-68554d00ea18.png</url>
      <title>DEV Community: GrabCast</title>
      <link>https://dev.to/grabcast</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/grabcast"/>
    <language>en</language>
    <item>
      <title>How we built free video tools that never upload your file</title>
      <dc:creator>GrabCast</dc:creator>
      <pubDate>Thu, 08 Oct 2026 05:20:56 +0000</pubDate>
      <link>https://dev.to/grabcast/how-we-built-free-video-tools-that-never-upload-your-file-6d</link>
      <guid>https://dev.to/grabcast/how-we-built-free-video-tools-that-never-upload-your-file-6d</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I build GrabCast, the site used as the example here. This post is about the engineering, and the only link to the product is at the end.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most "free online video tools" work like this: you upload your file, a server processes it, you download the result. That is simple to build, and it means a stranger's server holds your video. For a lot of people (a family clip, a client's draft, a scan with an address on it) that is a bad trade.&lt;/p&gt;

&lt;p&gt;We wanted tools where the file never leaves the device. This is what we learned building them, including the parts that are still annoying.&lt;/p&gt;

&lt;h3&gt;
  
  
  The basic architecture
&lt;/h3&gt;

&lt;p&gt;A browser tool that never uploads has three jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read a local file the user picked, without sending it anywhere.&lt;/li&gt;
&lt;li&gt;Transform it using code that runs in the page.&lt;/li&gt;
&lt;li&gt;Hand the result back as a download.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 1 is trivial. &lt;code&gt;&amp;lt;input type="file"&amp;gt;&lt;/code&gt; or drag-and-drop gives you a &lt;code&gt;File&lt;/code&gt;, which is a handle to bytes on the user's disk. Nothing leaves the machine unless your code sends it. Step 3 is also easy: create a &lt;code&gt;Blob&lt;/code&gt;, make an object URL, trigger a download.&lt;/p&gt;

&lt;p&gt;Step 2 is where the work is, and for video it is heavy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: ffmpeg compiled to WebAssembly
&lt;/h3&gt;

&lt;p&gt;ffmpeg is the standard tool for video. It is written in C. Compiled to WebAssembly (via Emscripten), you can run it inside a web page. That gives you almost every codec and filter you already know: scale, crop, pad, concat, audio extraction, and so on.&lt;/p&gt;

&lt;p&gt;This is what our video crop and resize tool uses for export. You pick 9:16, 1:1, 4:5 or 16:9, and the page builds an ffmpeg filter graph. For "fit, no crop" it scales the video to fit, blurs a scaled copy as the background, and overlays the sharp video on top. The output is an MP4 with H.264 video and AAC audio at 128 kbps.&lt;/p&gt;

&lt;p&gt;The costs are real:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Download size.&lt;/strong&gt; The ffmpeg WebAssembly core is about 32 MB. We load it on first use and let the browser cache it, so the first export feels slow and later ones don't. We say this on the page instead of hiding it behind a spinner, because a silent 32 MB download on mobile data is rude.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed.&lt;/strong&gt; WebAssembly ffmpeg is much slower than a native binary, particularly for software H.264 encoding. A short vertical clip is fine. A long 4K video is not a good fit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory.&lt;/strong&gt; This is the one that bites. The input file is copied into the WebAssembly virtual file system, the output is written to memory, and both need to fit. The browser also caps how much memory a tab may use. On our crop tool page we tell users plainly that files over roughly 250 MB on a phone or 1.5 GB on a computer may fail, and that trimming first helps. Those are guidelines, not guarantees.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you ship something like this, put the limits where users can see them before they pick a file. Nothing erodes trust faster than a progress bar that dies at 94%.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B: WebCodecs
&lt;/h3&gt;

&lt;p&gt;WebCodecs (&lt;code&gt;VideoDecoder&lt;/code&gt;, &lt;code&gt;VideoEncoder&lt;/code&gt;, &lt;code&gt;AudioEncoder&lt;/code&gt; and related classes) lets a page use the browser's own codecs, often with hardware acceleration. For a transformation like re-encoding, this can be dramatically faster than software encoding in WebAssembly, and it does not need a 32 MB download.&lt;/p&gt;

&lt;p&gt;The tradeoffs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You only get the codecs the browser ships. H.264 support in &lt;code&gt;VideoEncoder&lt;/code&gt;, for example, varies by browser, platform and hardware.&lt;/li&gt;
&lt;li&gt;You have to do the muxing yourself. WebCodecs gives you encoded chunks, not a finished MP4. You need a muxer library (or your own) to put chunks into a container.&lt;/li&gt;
&lt;li&gt;Filters are your job. Scaling and cropping can be done with canvas or &lt;code&gt;VideoFrame&lt;/code&gt;, but compositing something like a blurred background means writing the drawing code.&lt;/li&gt;
&lt;li&gt;Feature detection is mandatory. Check that the API exists and that your exact config is supported before you commit to that path:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canEncodeH264&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;VideoEncoder&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;supported&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;VideoEncoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isConfigSupported&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;codec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;avc1.42001f&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// H.264 Baseline&lt;/span&gt;
    &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;bitrate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;framerate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;supported&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Choosing between them
&lt;/h3&gt;

&lt;p&gt;The rule of thumb we use: use WebCodecs when the transformation is a simple re-encode and the browser supports the exact config; use ffmpeg.wasm when you need filter graphs, odd containers, or consistent output across browsers. Both are better than uploading when the goal is privacy, and neither is free in engineering time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Proving "never uploads"
&lt;/h3&gt;

&lt;p&gt;Saying it is easy. Users should be able to verify it. Three habits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keep processing code and network code visibly separate.&lt;/strong&gt; In our tools the file is only passed to the processing function. There is no code path that attaches it to a request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tell people to open the Network tab.&lt;/strong&gt; Drop a file, run the tool, and look for a request carrying your file. If a feature does need a server (some of our AI text features do), label it and say exactly what is sent. Our AI tools that need a server send only the text or image the user provides.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be deliberate about third-party scripts on tool pages.&lt;/strong&gt; Ads and analytics are the usual suspects. Our own analytics beacon sends only the page path, the referrer and a country code, never file names or contents. The site does show ads on some content pages, so ad units are kept out of the tool area itself.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We also published the privacy model in plain language on the homepage, because "your file never leaves your device" is a claim you want to be easy to check and hard to quietly break.&lt;/p&gt;

&lt;h3&gt;
  
  
  The boring parts that matter
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Progressive enhancement.&lt;/strong&gt; Show a real error when WebAssembly or a needed API is missing, not a blank page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cancel and cleanup.&lt;/strong&gt; Free the WebAssembly file system after each job. A tab that leaks 800 MB looks like a crash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small things users notice.&lt;/strong&gt; Preserve audio, keep rotation metadata, don't re-encode if a stream copy would do. Filenames should come out sane.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No watermark.&lt;/strong&gt; This is a product decision, but it is also a trust one: if the tool is free, the output should be unmarked.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Client-side video is practical for short clips and modest sizes, and it is a real privacy win.&lt;/li&gt;
&lt;li&gt;ffmpeg.wasm gives you power and compatibility, and costs you download size, speed and memory.&lt;/li&gt;
&lt;li&gt;WebCodecs gives you speed, and costs you filters, muxing and compatibility work.&lt;/li&gt;
&lt;li&gt;Tell users the limits up front, and make "no upload" verifiable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to see the tool this article describes, it is at &lt;a href="https://grabcast.click/crop-video.html?utm_source=devto&amp;amp;utm_medium=community&amp;amp;utm_campaign=launch-2026q4" rel="noopener noreferrer"&gt;https://grabcast.click/crop-video.html?utm_source=devto&amp;amp;utm_medium=community&amp;amp;utm_campaign=launch-2026q4&lt;/a&gt; (I built it; it is free and there is no sign-up). I'd like to hear about any browser or codec combination that breaks.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was drafted with AI assistance and reviewed and edited by me for accuracy.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webassembly</category>
      <category>javascript</category>
      <category>privacy</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
