<?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: Hammad Shams Uddin</title>
    <description>The latest articles on DEV Community by Hammad Shams Uddin (@hammad4june1999).</description>
    <link>https://dev.to/hammad4june1999</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%2F4066514%2F2ba9ed94-45a3-4e62-910f-8c42551b2b91.jpg</url>
      <title>DEV Community: Hammad Shams Uddin</title>
      <link>https://dev.to/hammad4june1999</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hammad4june1999"/>
    <language>en</language>
    <item>
      <title>ffmpeg.wasm 0.12 hung on the first frame — and the real speedup was not the upgrade</title>
      <dc:creator>Hammad Shams Uddin</dc:creator>
      <pubDate>Thu, 06 Aug 2026 22:08:19 +0000</pubDate>
      <link>https://dev.to/hammad4june1999/ffmpegwasm-012-hung-on-the-first-frame-and-the-real-speedup-was-not-the-upgrade-p45</link>
      <guid>https://dev.to/hammad4june1999/ffmpegwasm-012-hung-on-the-first-frame-and-the-real-speedup-was-not-the-upgrade-p45</guid>
      <description>&lt;p&gt;I run &lt;a href="https://utilorax.com" rel="noopener noreferrer"&gt;Utilorax&lt;/a&gt;, a set of free browser-based tools. The video tools are ffmpeg.wasm, which means every encode runs on the user's own machine and nothing is ever uploaded. That is the whole point of them — and it is also why they were slow.&lt;/p&gt;

&lt;p&gt;This is what a week of trying to fix that actually taught me. The upgrade I was sure would fix it did not, and the thing that did was much less interesting and much more effective.&lt;/p&gt;

&lt;h2&gt;
  
  
  The starting point: one core
&lt;/h2&gt;

&lt;p&gt;Every encode was pinned to a single thread:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pinThreads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;out&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;p&gt;That looks like an oversight. The core is the multi-threaded build, &lt;code&gt;ffmpeg-core.worker.js&lt;/code&gt; ships with it, and the page already pays for COOP/COEP so &lt;code&gt;SharedArrayBuffer&lt;/code&gt; is available. Letting ffmpeg use every core looks like free speed.&lt;/p&gt;

&lt;p&gt;It is not. Take the pin off on ffmpeg.wasm &lt;strong&gt;0.11&lt;/strong&gt; and the video filter dies instantly with &lt;code&gt;RuntimeError: function signature mismatch&lt;/code&gt; thrown out of &lt;code&gt;worker.js onmessage&lt;/code&gt; — a known 0.11 fault where the threaded core trips over its own dynCall table. Nothing renders. 0%.&lt;/p&gt;

&lt;p&gt;So the encoder ran on one core on machines that have eight. A 1080p60 clip filtered at about 0.09× real time. 0.12 fixed the threading fault, so 0.12 was obviously the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading to 0.12: three things nobody mentions
&lt;/h2&gt;

&lt;p&gt;The API changed shape, not just names — &lt;code&gt;run()&lt;/code&gt; became &lt;code&gt;exec()&lt;/code&gt;, the &lt;code&gt;FS()&lt;/code&gt; calls became methods, &lt;code&gt;fetchFile&lt;/code&gt; moved package, and everything returns a promise. Rather than rewrite sixty-one call sites twice (once to port, once to revert if it went wrong) I put an adapter in front that presents the 0.11 surface on top of 0.12. That part went fine.&lt;/p&gt;

&lt;p&gt;Then the interesting failures started.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The UMD bundle is not self-contained
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;@ffmpeg/ffmpeg&lt;/code&gt;'s UMD build loads its own worker as a &lt;strong&gt;separate webpack chunk&lt;/strong&gt;, &lt;code&gt;814.ffmpeg.js&lt;/code&gt;, from whatever directory &lt;code&gt;ffmpeg.js&lt;/code&gt; came from. It is not in the docs and it is not obvious — you just get a 404 and a &lt;code&gt;load()&lt;/code&gt; that never resolves. Ship it alongside &lt;code&gt;ffmpeg.js&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;classWorkerURL&lt;/code&gt; looks like the fix for that. It is a trap.
&lt;/h3&gt;

&lt;p&gt;Passing it sends the UMD build down its ESM branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;load&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;classWorkerURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file:///home/jeromewu/…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;module&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;   &lt;span class="c1"&gt;// ← ESM branch&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;u&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;814&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;          &lt;span class="c1"&gt;// ← classic&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;module&lt;/strong&gt; worker — around a chunk that is a classic script calling &lt;code&gt;importScripts()&lt;/code&gt;. It dies on load. Leaving the option out is what makes it work.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Workers need the COEP header on themselves
&lt;/h3&gt;

&lt;p&gt;A cross-origin-isolated page will refuse a worker script unless &lt;strong&gt;that script's own response&lt;/strong&gt; carries &lt;code&gt;Cross-Origin-Embedder-Policy: require-corp&lt;/code&gt;. Same-origin is not enough. Apache sends nothing on a static file, so the whole chain fails: the class worker, the core it &lt;code&gt;importScripts&lt;/code&gt;, and the pthread workers Emscripten spawns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;If&lt;/span&gt;&lt;span class="sr"&gt; "%{REQUEST_URI} =~ m#/js/vendor/ffmpeg-012/#"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Cross-Origin-Embedder-Policy "require-corp"
    &lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Cross-Origin-Resource-Policy "same-origin"
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;If&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;0.11 never needed this because it builds its worker from a &lt;strong&gt;blob URL&lt;/strong&gt;, and a blob inherits the page's policies instead of being checked for headers. That is a difference in how the worker is created, not in the API — you will not find it by reading the diff.&lt;/p&gt;

&lt;p&gt;One more: keep your service worker away from those files. It caches whatever headers an entry had when it was written, so one fetch made before the header rule existed gets served back forever, and it surfaces as a COEP error pointing at the worker rather than at the cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then it hung on the first frame
&lt;/h2&gt;

&lt;p&gt;Headers fixed, pin removed, and the filter printed exactly one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;frame=    1 fps=0.0 q=0.0 size= 0kB time=00:00:00.14 bitrate= 2.6kbits/s speed=2.04x
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then nothing. No error. 0% forever.&lt;/p&gt;

&lt;p&gt;The core pre-allocates &lt;strong&gt;exactly 32 pthread workers&lt;/strong&gt; and cannot grow past them here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;getNewWorker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;function &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="nx"&gt;PThread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unusedWorkers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;PThread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allocateUnusedWorker&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;PThread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadWasmModuleToWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PThread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unusedWorkers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;  &lt;span class="c1"&gt;// ← async&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;…&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Growing the pool means loading the module into a fresh worker and &lt;strong&gt;waiting for it to answer&lt;/strong&gt; — but ffmpeg runs synchronously inside the very worker that would have to process that answer. The thread it is blocked on can never start. Ask for a 33rd and everything stops, silently.&lt;/p&gt;

&lt;p&gt;Left to itself ffmpeg asks for far more than 32. It spends threads in three places at once: one set per input decoder, one for the filter graph, and x264 takes 1.5 per core on top. On anything with nine or more logical cores you are already over the line — which is why this only appears once the pin comes off.&lt;/p&gt;

&lt;p&gt;So: give each stage an explicit budget instead of the machine's core count.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;THREADS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hardwareConcurrency&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// worst case ≈ (inputs + 2) × THREADS — a 5-clip merge at 4 is already 28 of the 32&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-filter_threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-filter_complex_threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-filter_threads&lt;/code&gt; matters more than it looks. It is a global option and defaults to one thread per core on its own, which was most of the overspend.&lt;/p&gt;

&lt;h2&gt;
  
  
  The error that ate its own error message
&lt;/h2&gt;

&lt;p&gt;Two ops aborted instead of hanging, and every one of them reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TypeError: Cannot read properties of undefined (reading 'startsWith')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not your bug. It is the core's own error handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_ffmpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;stringsToPtr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Aborted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;e&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;p&gt;&lt;code&gt;e&lt;/code&gt; is not always an &lt;code&gt;Error&lt;/code&gt;. When it isn't, &lt;code&gt;e.message&lt;/code&gt; is &lt;code&gt;undefined&lt;/code&gt;, and the handler throws while inspecting the crash — so whatever ffmpeg actually said is lost. If you see that string, the real failure is upstream and you cannot see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually shipped
&lt;/h2&gt;

&lt;p&gt;I put the tools back on 0.11.&lt;/p&gt;

&lt;p&gt;Not because 0.12 is bad, but because the premise was wrong. 0.12 is worth a few times one core. A 6-minute 1080p60 clip is about 21,000 frames, and software x264 in wasm will not get through that quickly at any thread count. I was optimising the wrong axis.&lt;/p&gt;

&lt;p&gt;The real win was &lt;strong&gt;not running the encoder at all&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Container conversion&lt;/strong&gt; — MKV→MP4 and friends are the same H.264 in a different box. &lt;code&gt;-c copy&lt;/code&gt; instead of decode-and-re-encode: seconds, and no second round of lossy compression. Judge the &lt;em&gt;output&lt;/em&gt;, not the promise — ffmpeg.wasm resolves even when ffmpeg gave up, so try the copy and fall back if the file is missing or tiny. Trying the copy &lt;strong&gt;is&lt;/strong&gt; the probe, and it is cheaper than the transcode it replaces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate&lt;/strong&gt; — a quarter turn is a flag on the video track, not a change to the pixels. Stream copy plus &lt;code&gt;-metadata:s:v:0 rotate=90&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trim&lt;/strong&gt; — copy the streams instead of rebuilding them. The cut then lands on a keyframe, so it is a real trade rather than a free win: make it a checkbox.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rotate has a catch worth knowing. The risk is not that the copy fails, it is that it &lt;strong&gt;silently does nothing&lt;/strong&gt;: phone footage often already carries a rotation, and when the streams are copied that existing flag can survive and override the one you set. The file comes out perfectly valid, and unturned. So measure the output rather than trusting it — a &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; applies a container's rotation when it reports &lt;code&gt;videoWidth&lt;/code&gt;, so a turn that worked has the sides swapped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;displayedSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mime&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;video&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&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;var&lt;/span&gt; &lt;span class="nx"&gt;settle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revokeObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// an answer that never comes is the freeze you were avoiding&lt;/span&gt;
    &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onloadedmetadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;videoWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;videoHeight&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&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;p&gt;No swap, no proof, re-encode.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell myself a week ago
&lt;/h2&gt;

&lt;p&gt;Threads were the interesting problem. Not encoding was the useful one. The ops that got fast — &lt;a href="https://utilorax.com/documents/video-convert" rel="noopener noreferrer"&gt;convert&lt;/a&gt;, &lt;a href="https://utilorax.com/documents/video-rotate" rel="noopener noreferrer"&gt;rotate&lt;/a&gt;, trim, mute — got fast because they stopped doing work, not because they did it on more cores.&lt;/p&gt;

&lt;p&gt;And when the number of threads is the thing you are tuning, check what the runtime can actually give you before you ask for more. A pool of 32 that cannot grow is a hard ceiling, and the failure mode is a silent hang rather than an error — which is a bad way to find the edge.&lt;/p&gt;

&lt;p&gt;For the ops that genuinely must decode every frame — filters, resize, compress — wasm is still wasm, and the next honest step is WebCodecs and the hardware encoder. That is a different post.&lt;/p&gt;

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