<?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: Tutu</title>
    <description>The latest articles on DEV Community by Tutu (@meicut).</description>
    <link>https://dev.to/meicut</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%2F4167027%2Fe3207736-f0d1-43d4-9e19-78d31c24a6e7.png</url>
      <title>DEV Community: Tutu</title>
      <link>https://dev.to/meicut</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/meicut"/>
    <language>en</language>
    <item>
      <title>ffmpeg.wasm to WebCodecs: the 12x Gap, What Broke, and What Still Can't Move</title>
      <dc:creator>Tutu</dc:creator>
      <pubDate>Fri, 23 Oct 2026 15:03:44 +0000</pubDate>
      <link>https://dev.to/meicut/ffmpegwasm-to-webcodecs-the-12-gap-what-broke-and-what-still-cant-move-579a</link>
      <guid>https://dev.to/meicut/ffmpegwasm-to-webcodecs-the-12-gap-what-broke-and-what-still-cant-move-579a</guid>
      <description>&lt;p&gt;When we built the local-processing mode of our media toolbox (&lt;a href="https://meicut.com" rel="noopener noreferrer"&gt;meicut.com&lt;/a&gt;) — 20+ tools that trim, compress, convert, and filter video and audio entirely in the browser — the first version bet everything on &lt;strong&gt;ffmpeg.wasm&lt;/strong&gt;. It felt like the obvious choice. Within weeks, the hot paths moved to &lt;strong&gt;WebCodecs hardware encoding&lt;/strong&gt;, ffmpeg.wasm survives as a fallback plus the tools that sit at hard boundaries — audio processing, speed change, AVI/GIF output, reverse — and we have a precise map of where each technology's real boundaries are.&lt;/p&gt;

&lt;p&gt;This is the full story: why we started with WASM, the three technology-inherent problems that forced the migration, how the WebCodecs pipeline works, and the decision framework we now use to route each tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we chose ffmpeg.wasm first (and it was the right call)
&lt;/h2&gt;

&lt;p&gt;Our backend already processed everything server-side with the ffmpeg CLI. That made ffmpeg.wasm almost unfairly attractive:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Zero redesign of processing semantics.&lt;/strong&gt; Our server code was full of carefully-tuned filter graphs — &lt;code&gt;-ss&lt;/code&gt;/&lt;code&gt;-t&lt;/code&gt; input-side seeking for cuts, even-dimension rounding for crops, &lt;code&gt;maskedmerge&lt;/code&gt; + &lt;code&gt;geq&lt;/code&gt; three-plane masks for mosaic regions. With ffmpeg.wasm, the browser executor could reuse these &lt;em&gt;line by line&lt;/em&gt;. Local and cloud outputs were identical by construction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Complete capability coverage.&lt;/strong&gt; ffmpeg handles every container, codec, and filter we needed: &lt;code&gt;atempo&lt;/code&gt; for audio speed, MP3/FLAC encoding, AVI/GIF muxing, complex filter chains. WebCodecs, by contrast, is a &lt;em&gt;codec primitive&lt;/em&gt; — it encodes and decodes raw frames. It doesn't demux your MKV, it doesn't resample your audio, and it definitely doesn't do WSOLA time-stretching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. 28 tools, one UI.&lt;/strong&gt; Because the processing semantics came for free, we could share the entire frontend between cloud and local modes and ship every tool at once.&lt;/p&gt;

&lt;p&gt;We knew WebCodecs existed. We also knew its caveats: uneven browser support, no MP3/FLAC encoders, and opaque hardware encoder behavior. Betting on WASM first let us ship. That sequencing mattered — if we'd started with WebCodecs, we'd have spent weeks just on demuxing before shipping a single tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: Slow, and it sounded like a jet engine
&lt;/h2&gt;

&lt;p&gt;ffmpeg.wasm's x264 is software encoding compiled to WASM — no SIMD in practice, no hardware acceleration. Compressing a high-resolution 2.5K clip made laptop fans spin up like we were mining crypto. Competitors using WebCodecs hardware encoding were silent &lt;em&gt;and&lt;/em&gt; an order of magnitude faster. Our benchmark: same 10.5 MB / 720p / 10 s file, medium quality, median of 3 runs on the same machine:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Time (median of 3)&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ffmpeg.wasm (soft x264, incl. 32 MB engine download)&lt;/td&gt;
&lt;td&gt;26.5 s&lt;/td&gt;
&lt;td&gt;1.27 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebCodecs (hardware H.264)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.3 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5.78 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(The output sizes differ because the two paths use different rate-control strategies — more on that below. Both were visually fine; the point is the 12× speed gap and the fan noise.)&lt;/p&gt;

&lt;p&gt;For a free tool whose entire pitch is "it just works in your browser", CPU-bound software encoding is a UX ceiling you can't design around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: Outputs that came out &lt;em&gt;bigger&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Pure CRF rate control targets quality, not size. Feed it an already-lean source (3.30 Mbps) and CRF 28 happily produces a &lt;em&gt;larger&lt;/em&gt; file (3.91 Mbps). Users compress a video to make it smaller; an output that grows is a bug report waiting to happen, no matter how defensible the encoding theory is.&lt;/p&gt;

&lt;p&gt;CRF-driven inflation is the flavor we first hit, and it is squarely a WASM-path failure. But "outputs that come out bigger" is not exclusive to WASM: the hardware encoder in WebCodecs uses &lt;em&gt;target-bitrate&lt;/em&gt; semantics and will happily spend the whole budget — without a per-pixel ceiling, a low-res "smaller" encode can land at 5 Mbps just because the target said so. The dual ceiling in the migration section is what stops that on both paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: The 32 MB engine bootstrap is a reliability surface
&lt;/h2&gt;

&lt;p&gt;The WASM engine is a ~32 MB download (core JS + WASM + worker), fetched on first use. Around that download sits an entire bootstrap chain — progress reporting, caching, &lt;code&gt;ff.load()&lt;/code&gt; initialization, pthread startup for the multi-threaded core — and we learned that &lt;strong&gt;every link in that chain can fail silently&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Our progress &lt;em&gt;denominator&lt;/em&gt; was an estimate (34,135,000 bytes); the real download was 32,849,385 bytes — 96.2% of it. So the bar froze at "96%" even on healthy loads, and users reasonably read that as a hang. (Fix: exact byte counts per engine file, updated every time the core package is bumped.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;On some machines, &lt;code&gt;ff.load()&lt;/code&gt; never resolved — no error, no rejection, just silence. We added a 60 s watchdog that terminates the attempt and falls back to the single-threaded core.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We ultimately traced one production-wide outage of this bootstrap to a &lt;strong&gt;server configuration bug of our own making&lt;/strong&gt; — a missing HTTP response header that Chrome requires before it will construct a module worker. But the larger lesson stood: the engine bootstrap is &lt;em&gt;infrastructure&lt;/em&gt;, with the failure modes of infrastructure — while WebCodecs has no bootstrap at all. The browser already contains everything it needs.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That contrast is what turned "WASM is slow" into "the hot paths should not depend on a downloadable engine at all".&lt;/p&gt;

&lt;h2&gt;
  
  
  The WebCodecs migration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The pipeline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;input file
  → mediabunny (demux; we use it for muxing too — MP4 with faststart)
  → VideoDecoder (hardware) → optional per-frame canvas processing
  → VideoEncoder (hardware H.264)
  → audio: copied passthrough when container-compatible, transcoded otherwise
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First interaction runs immediately — no warmup, no download progress, no load watchdog.&lt;/p&gt;

&lt;h3&gt;
  
  
  hwOrSw: hardware first, always a way back
&lt;/h3&gt;

&lt;p&gt;Every migrated tool is wrapped in the same pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;hwOrSw&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hw&lt;/span&gt;&lt;span class="p"&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sw&lt;/span&gt;&lt;span class="p"&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&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;await&lt;/span&gt; &lt;span class="nf"&gt;hw&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="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;LocalCancelledError&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="c1"&gt;// user cancel: never "fall back" into more work&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[local] hardware path failed, falling back to wasm:&lt;/span&gt;&lt;span class="dl"&gt;'&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sw&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;Any failure in the hardware path — unsupported codec, decode error, encode error — silently lands on the proven WASM executor. A &lt;code&gt;localStorage&lt;/code&gt; flag forces all-software for production debugging.&lt;/p&gt;

&lt;h3&gt;
  
  
  The stall watchdog
&lt;/h3&gt;

&lt;p&gt;Individual files can freeze hardware decoders/encoders &lt;em&gt;silently&lt;/em&gt;. We carried over the lesson from the engine-load watchdog: &lt;strong&gt;bounded waits, never infinite hangs&lt;/strong&gt;. If progress doesn't advance for 30 seconds, we abandon the hardware attempt and fall back.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rate control: the dual ceiling that stopped inflation
&lt;/h3&gt;

&lt;p&gt;Hardware encoders use &lt;em&gt;target bitrate&lt;/em&gt; semantics — they spend the whole budget, unlike CRF which backs off on easy content. And we needed a guarantee: &lt;strong&gt;output must be smaller than input&lt;/strong&gt;. Two caps, take the minimum:&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;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;sourceVideoBitrate&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="nx"&gt;qualityRatio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// 0.8 / 0.6 / 0.4 — guarantees smaller&lt;/span&gt;
  &lt;span class="nx"&gt;outputPixels&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="nx"&gt;fps&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="nx"&gt;bpp&lt;/span&gt;                 &lt;span class="c1"&gt;// quality ceiling: 0.2 / 0.15 / 0.1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first term kills the reverse-inflation bug. The second is the subtle one: without a bits-per-pixel ceiling, a 480p "smaller" compression would cheerfully burn 5 Mbps of hardware-encoder budget because the &lt;em&gt;target&lt;/em&gt; said so.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preflight with real dimensions
&lt;/h3&gt;

&lt;p&gt;H.264 level support varies with resolution/bitrate/framerate. Asking &lt;code&gt;VideoEncoder.isConfigSupported()&lt;/code&gt; about a hypothetical config misses machines that encode 480p fine but choke on 4K. We probe with the &lt;em&gt;actual&lt;/em&gt; output dimensions before committing to the hardware path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest hard-boundary list: what stays on WASM
&lt;/h2&gt;

&lt;p&gt;A full audit of every tool produced the list WebCodecs advocates rarely publish — what it genuinely cannot do today:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool / capability&lt;/th&gt;
&lt;th&gt;Why WebCodecs can't&lt;/th&gt;
&lt;th&gt;Stays on&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Audio speed change&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;atempo&lt;/code&gt; is WSOLA time-stretching; WebCodecs has no audio &lt;em&gt;processing&lt;/em&gt; at all&lt;/td&gt;
&lt;td&gt;WASM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MP3 / FLAC export&lt;/td&gt;
&lt;td&gt;Browser &lt;code&gt;AudioEncoder&lt;/code&gt; has no MP3 or FLAC encoders; output contracts would break&lt;/td&gt;
&lt;td&gt;WASM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Volume adjust, audio convert, audio cut&lt;/td&gt;
&lt;td&gt;Same MP3/FLAC encoder gap&lt;/td&gt;
&lt;td&gt;WASM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reverse video&lt;/td&gt;
&lt;td&gt;Needs every frame in memory to play backwards; large inputs OOM the tab&lt;/td&gt;
&lt;td&gt;WASM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVI / GIF output&lt;/td&gt;
&lt;td&gt;mediabunny has no muxer for either container&lt;/td&gt;
&lt;td&gt;WASM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Most video filters&lt;/td&gt;
&lt;td&gt;ffmpeg &lt;code&gt;eq&lt;/code&gt; uses offset semantics; CSS/canvas uses multiply — visually different results. Only three presets (grayscale/sepia/invert) are pixel-equivalent&lt;/td&gt;
&lt;td&gt;WASM for the rest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crop with pan / custom background&lt;/td&gt;
&lt;td&gt;Our canvas center-crop covers the common cases; edge semantics differ&lt;/td&gt;
&lt;td&gt;WASM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-segment cuts with fades&lt;/td&gt;
&lt;td&gt;Crossfade filter graphs have no streaming equivalent&lt;/td&gt;
&lt;td&gt;WASM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Everything else — compress, mute, rotate, flip, format conversion (MP4/MOV/MKV/WebM), single-segment trim, center crop, mosaic blur, sketch effect — runs on hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision framework we ended up with
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is the capability expressible as decode → optional canvas → encode?
├─ No (audio processing, exotic containers, complex filter graphs)
│    → ffmpeg.wasm, and that's final
└─ Yes
     → Does the browser support it with THESE dimensions/codec?
          ├─ Yes → WebCodecs hardware path
          │         (30 s stall watchdog, any error → wasm)
          └─ No  → ffmpeg.wasm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three principles held up across the whole migration:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bounded waits everywhere.&lt;/strong&gt; Both paths have watchdogs (60 s engine load, 30 s stall). Silent infinite hangs are the defining failure mode of in-browser media processing; every wait must have a ceiling and a fallback.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fallbacks are architecture, not error handling.&lt;/strong&gt; &lt;code&gt;hwOrSw&lt;/code&gt; isn't a try/catch — it's the product. Users on odd hardware get slower results, never failed ones.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Match the technology to the capability, not the other way round.&lt;/strong&gt; WASM-first got us coverage and correctness; WebCodecs-second got us speed where it matters. Neither was a mistake — the mistake would have been treating either one as the whole answer.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result: the tools people actually hammer (compress, convert, trim) run at hardware speed with no engine download, the long tail still works everywhere ffmpeg works, and the 32 MB WASM engine only ships to browsers that genuinely need it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This pipeline powers the local-processing mode at &lt;/em&gt;&lt;a href="https://meicut.com/local" rel="noopener noreferrer"&gt;meicut.com&lt;/a&gt;&lt;em&gt;, where you can try both paths on your own files.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>performance</category>
      <category>webassembly</category>
    </item>
    <item>
      <title>Trimming Videos Is a Trade-off: copy-cut, re-encode, or filter-graph</title>
      <dc:creator>Tutu</dc:creator>
      <pubDate>Thu, 15 Oct 2026 15:10:13 +0000</pubDate>
      <link>https://dev.to/meicut/trimming-videos-is-a-trade-off-copy-cut-re-encode-or-filter-graph-3fom</link>
      <guid>https://dev.to/meicut/trimming-videos-is-a-trade-off-copy-cut-re-encode-or-filter-graph-3fom</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Part of the Meicut Engineering series — how we build an in-browser media toolkit.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;ffmpeg -i in.mp4 -ss 60 -c copy -t 30 out.mp4&lt;/code&gt; is everywhere. So is &lt;code&gt;-i in.mp4 -c:v libx264 ...&lt;/code&gt;. And so is &lt;code&gt;-filter_complex "...concat..."&lt;/code&gt;. Each one is presented as &lt;em&gt;the&lt;/em&gt; way to trim a video. In practice there is no single best way: three mechanisms, three different trade-offs, and the right one depends entirely on your constraints. This post lays out all three, says which we chose and why under our constraints, and is open to better answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 1: &lt;code&gt;-c copy&lt;/code&gt; — the lossless shortcut, and its price
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; it tells ffmpeg not to decode or re-encode anything. It copies compressed packets and rewrites container metadata. For MP4/MOV, that includes the timestamps and possibly an &lt;em&gt;edit list&lt;/em&gt; that tells players where to start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bit-identical output (no pixels touched)&lt;/li&gt;
&lt;li&gt;Orders of magnitude faster than any encode, since the bottleneck is I/O, not CPU&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What it costs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not frame-accurate.&lt;/strong&gt; A stream copy cannot cut between encoded frames; the cut snaps to the nearest keyframe. Your start point is accurate only to within one group-of-pictures (GOP): typically a fraction of a second to a couple of seconds, depending on &lt;code&gt;-g&lt;/code&gt;/gop_size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B-frame and IDR hazards.&lt;/strong&gt; If the cut lands mid-GOP on a source with B-frames, the first output packet may not be an independently-decodable IDR frame, and a player can glitch or show garbage at the head.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seek-position-dependent timestamp metadata.&lt;/strong&gt; With output-side seeking (&lt;code&gt;-ss&lt;/code&gt; after &lt;code&gt;-i&lt;/code&gt;), copy-cut depends on the timestamp metadata the muxer writes, whose player support is inconsistent. Input-side seeking (&lt;code&gt;-ss&lt;/code&gt; before &lt;code&gt;-i&lt;/code&gt;) is the more robust copy-cut form, but by construction it lands on a keyframe. Either way, you trade precision for speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codec/container compatibility.&lt;/strong&gt; The compressed stream has to be legal inside the target container; exotic codecs or non-standard pairings won't stream-copy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where it shines:&lt;/strong&gt; dropping an intro/outro, extracting one coarse segment, batch-trimming files with consistent parameters, when "second-precision, zero-loss, near-instant" beats "frame-perfect." It is the engine behind most "instant / lossless trim" buttons, and it is a useful trade, just one that gives up frame accuracy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 2: re-encode with input seeking — frame-accurate and portable
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; &lt;code&gt;-ss&lt;/code&gt; before &lt;code&gt;-i&lt;/code&gt; (input seeking) locates the region quickly at the demux level, then a full re-encode produces the segment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frame-accurate, but only because we re-encode.&lt;/strong&gt; Input seeking alone does not buy you frame precision. With &lt;code&gt;-c copy&lt;/code&gt;, input seeking still lands on a keyframe. The precision comes from decoding from the nearest keyframe and then starting to write exactly at the requested timestamp. That chain holds only on the re-encode path, which is the boundary from Approach 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container-portable and filter-capable.&lt;/strong&gt; You can re-target resolution, codec, bitrate, and apply effects (fades, etc.) in the same pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean output.&lt;/strong&gt; No edit-list or mid-GOP artifacts to worry about.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What it costs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slower, by definition: you encode every output frame.&lt;/li&gt;
&lt;li&gt;A second-generation quality loss, even if mild at a reasonable CRF (CRF 23 is our default preset family, as discussed in &lt;a href="https://dev.to/meicut/meicut-engineering-1-video-compression-at-product-scale-108k"&gt;Part 1: Video Compression at Product Scale&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Needs a working encode pipeline (codec, preset tuning, compatibility flags like &lt;code&gt;-pix_fmt yuv420p&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where it shines:&lt;/strong&gt; any time the cut point must be precise: a transition, a single-frame edit, trimming then merging, or any transform on top of the trim.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 3: filter-graph splicing — one pass, multiple segments
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; a single &lt;code&gt;-filter_complex&lt;/code&gt; pass trims every segment (&lt;code&gt;trim&lt;/code&gt;/&lt;code&gt;atrim&lt;/code&gt;), resets timestamps (&lt;code&gt;setpts=PTS-STARTPTS&lt;/code&gt;), and concatenates them (&lt;code&gt;concat&lt;/code&gt;), all in one ffmpeg process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One process, no intermediate files.&lt;/strong&gt; Nothing to write, clean up, or keep in sync.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frame-accurate, timestamp-continuous joins&lt;/strong&gt; at the filter level (frame-accurate, not "sample-accurate", which is an audio term). Fades compose naturally into the same graph.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What it costs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The whole input is decoded to feed the trims.&lt;/strong&gt; Because &lt;code&gt;trim&lt;/code&gt; runs in the filter layer, ffmpeg decodes the input once and streams it through every segment's trim. The decode cost is &lt;em&gt;O(file length)&lt;/em&gt; for all segments combined, not per-segment. A deep start in a long file still wastes CPU relative to a demux-level seek, but it is not multiplied by the segment count.&lt;/li&gt;
&lt;li&gt;A complex graph (multi-input, transitions) is more to get wrong and harder to debug.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One adjacent mechanism worth naming separately, because it is for &lt;em&gt;merging&lt;/em&gt; rather than trimming: the &lt;strong&gt;concat demuxer&lt;/strong&gt; (&lt;code&gt;-f concat&lt;/code&gt;) joins pre-existing files that already share stream parameters. It is fast and avoids re-encoding, but it demands parameter consistency. Resolution and framerate mismatches are the obvious breakers; the subtler killer is &lt;strong&gt;timebase&lt;/strong&gt; mismatch, which &lt;code&gt;ffprobe&lt;/code&gt; does not surface visibly and which tends to show up later as progressive audio/video drift rather than a hard failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  A comparison, side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Re-encode?&lt;/th&gt;
&lt;th&gt;Frame accuracy&lt;/th&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;th&gt;Quality&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;copy-cut&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;keyframe-level (seconds)&lt;/td&gt;
&lt;td&gt;fastest&lt;/td&gt;
&lt;td&gt;bit-identical&lt;/td&gt;
&lt;td&gt;coarse trims, intro/outro removal, consistent batches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;input-seek + re-encode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;frame-level&lt;/td&gt;
&lt;td&gt;slower (you encode)&lt;/td&gt;
&lt;td&gt;second-generation, CRF-controlled&lt;/td&gt;
&lt;td&gt;precise cuts, cross-container, add effects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;filter-graph splicing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;frame-level&lt;/td&gt;
&lt;td&gt;full decode&lt;/td&gt;
&lt;td&gt;second-generation, clean joins&lt;/td&gt;
&lt;td&gt;multi-segment, fades, one-process workflow&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Speed: filter-graph splicing depends on segment count and file length; against a single re-encode it is comparable.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Our choice, and why — not a universal answer
&lt;/h2&gt;

&lt;p&gt;Under &lt;strong&gt;our&lt;/strong&gt; constraints, frame accuracy, multi-segment joins, fades, and cross-container output are all non-negotiable product requirements, on both a browser-local path and a server path. So we currently do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single-segment trim:&lt;/strong&gt; input seeking (&lt;code&gt;-ss&lt;/code&gt; before &lt;code&gt;-i&lt;/code&gt;) + re-encode. Fast to locate, frame-accurate to cut, portable output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-segment trim:&lt;/strong&gt; one &lt;code&gt;-filter_complex&lt;/code&gt; pass (&lt;code&gt;trim&lt;/code&gt;/&lt;code&gt;atrim&lt;/code&gt; + &lt;code&gt;setpts&lt;/code&gt; + &lt;code&gt;concat&lt;/code&gt;). Single process, clean joins, fades included.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We did &lt;strong&gt;not&lt;/strong&gt; pick &lt;code&gt;-c copy&lt;/code&gt; as the default. Not because copy-cut is wrong; it is genuinely the right tool for a large class of "quick, coarse, lossless" trims. The framing is: &lt;strong&gt;in a product that promises frame-accurate cuts, a fast path that quietly snaps your cut point to a keyframe is a bug; in a coarse-trim workflow, that same snapping is a feature.&lt;/strong&gt; Our product is the former, so copy-cut cannot be our default.&lt;/p&gt;

&lt;p&gt;We also own the cost: re-encoding is slower and introduces a second generation; the quality side is usually a non-issue, because CRF 23's second-generation loss is invisible to the eye in most consumer scenarios. The speed side is the real one. On a long video, the encode step can take from seconds to minutes depending on the device and whether hardware encoding is available. That is exactly why we reserve the copy-cut fast path as a candidate for the future, rather than claiming one approach fits everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  An invitation, not a conclusion
&lt;/h2&gt;

&lt;p&gt;Trimming a video is not a single correct command. It is a series of trade-offs between precision, speed, losslessness, storage, and the transforms you need to compose. Our implementation serves a specific product: precise, multi-segment cuts. If your use case differs, say you only need second-precision and care most about zero loss and speed, copy-cut may well be &lt;em&gt;your&lt;/em&gt; right answer. I'd like to hear which approach you reach for and why; we are still evaluating where a copy-cut fast path might be worth adding alongside our re-encode default.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The trimmer behind this decision is part of the online toolkit at&lt;/em&gt; &lt;a href="https://meicut.com/tools/cut-video" rel="noopener noreferrer"&gt;meicut.com — cut video&lt;/a&gt; — &lt;em&gt;runs in the browser, no install required; files auto-delete after processing.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Meicut Engineering series:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: Video Compression at Product Scale&lt;/li&gt;
&lt;li&gt;Part 2: Trimming Videos Is a Trade-off — copy-cut, re-encode, or filter-graph&lt;/li&gt;
&lt;li&gt;Part 3: "Convert Format" Is a Misnomer — containers vs codecs&lt;/li&gt;
&lt;li&gt;Part 4: Subtitles: soft vs burned, and the font problem&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ffmpeg</category>
      <category>video</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Meicut Engineering #1: Video Compression at Product Scale</title>
      <dc:creator>Tutu</dc:creator>
      <pubDate>Tue, 06 Oct 2026 17:50:13 +0000</pubDate>
      <link>https://dev.to/meicut/meicut-engineering-1-video-compression-at-product-scale-108k</link>
      <guid>https://dev.to/meicut/meicut-engineering-1-video-compression-at-product-scale-108k</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Part of the Meicut Engineering series — how we build an in-browser media toolkit.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The encoding command — &lt;code&gt;ffmpeg -i in.mp4 -c:v libx264 -crf 23 out.mp4&lt;/code&gt; — is well documented. What is rarely discussed is the engineering that turns this single command into a service: where encoding runs, how long-running jobs behave, how concurrency is bounded, and how output is matched to the user's stated goal. This post covers those decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Where encoding runs
&lt;/h2&gt;

&lt;p&gt;The first architectural decision is the execution environment. Three options exist for a web media tool:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Execution&lt;/th&gt;
&lt;th&gt;Cost profile&lt;/th&gt;
&lt;th&gt;Constraints&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Server-side ffmpeg&lt;/td&gt;
&lt;td&gt;Encode on infrastructure&lt;/td&gt;
&lt;td&gt;Upload → encode → download&lt;/td&gt;
&lt;td&gt;Requires upload; full ffmpeg capability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ffmpeg.wasm&lt;/td&gt;
&lt;td&gt;In-browser (WebAssembly)&lt;/td&gt;
&lt;td&gt;No upload, strong privacy&lt;/td&gt;
&lt;td&gt;≈ an order of magnitude slower than native (community benchmarks); input+output held in a WASM heap capped near 2–4GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebCodecs&lt;/td&gt;
&lt;td&gt;In-browser (raw codecs)&lt;/td&gt;
&lt;td&gt;No upload&lt;/td&gt;
&lt;td&gt;Most control; most implementation work&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Browser-native execution (ffmpeg.wasm / WebCodecs) is attractive for privacy — files never leave the device. It is, however, constrained by encode throughput and memory ceiling: a 500MB input can exceed the WASM heap and crash the tab. Server-side execution reliably handles large files and the full filter set (fonts for subtitles, multi-file merges).&lt;/p&gt;

&lt;p&gt;We therefore implemented server-side encoding first, deferring browser-native processing. The ordering principle — &lt;strong&gt;support the hardest case before the common case&lt;/strong&gt; — recurs throughout the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Task pipeline
&lt;/h2&gt;

&lt;p&gt;With encoding server-side, "compress video" becomes a pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;upload → validate → queue → encode → store → downloadable link
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three constraints follow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Long-tail latency.&lt;/strong&gt; A short clip encodes in seconds; a 4K 20-minute input at a slow preset runs for minutes. The pipeline must be asynchronous — each request returns a job ID, the client polls, and progress reflects pipeline state rather than a synthetic spinner. A wall-clock cap per job, with a clear path to re-encode at a faster preset, replaces silent failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concurrency.&lt;/strong&gt; If every request spawned an ffmpeg process, a burst of 4K uploads would exhaust CPU and starve smaller jobs. We bound concurrency by job size and worker pool: small jobs take a fast lane; heavy encodes run under a concurrency limit, so no single input or burst dominates. A queue with two priority tiers sufficed for our volume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Batch as the same pipeline, reused.&lt;/strong&gt; Compression is rarely one file at a time. Many users need to &lt;strong&gt;compress multiple videos at once&lt;/strong&gt; — &lt;strong&gt;batch compression&lt;/strong&gt; is the common request behind any "video compressor" tool. Under the hood it is not a separate system: a user submits many files, each becomes a job in the same queue — sharing the concurrency bound, priority tiers, and per-file validation. Progress and results are tracked per file, and a failed item does not abort the batch; it is reported alongside the successful ones. The single-job pipeline, reused across a list of inputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input validation.&lt;/strong&gt; Users upload arbitrary inputs — truncated files, unusual containers, audio mislabeled as video. ffmpeg tolerates much until it does not; a mid-encode crash is poor UX. We probe inputs first (&lt;code&gt;ffprobe&lt;/code&gt;) and return a readable diagnostic before committing to an encode.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Encoding parameters
&lt;/h2&gt;

&lt;p&gt;A single CRF value applied to every input optimizes for no use case. We classify each job before encoding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Intended use&lt;/strong&gt; determines the parameter set: email attachment → aggressive size target with downscaling permitted; social → balanced 1080p; archive → keep resolution at high quality.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Content type&lt;/strong&gt; matters: screen recordings (static) and concert footage (motion-heavy) require different bitrate budgets under the same preset.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Output is &lt;strong&gt;CRF with a size-aware backstop&lt;/strong&gt;: quality-targeted, but if the result exceeds the size implied by the use case, it is re-encoded once at a stricter CRF. Checking the outcome against the goal, rather than the parameter, catches most poor encodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Playback compatibility
&lt;/h2&gt;

&lt;p&gt;Encoding is only half the task; guaranteeing playback is the other. Three flags are non-negotiable in a product:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-preset&lt;/span&gt; medium &lt;span class="nt"&gt;-crf&lt;/span&gt; 23 &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class="nt"&gt;-movflags&lt;/span&gt; +faststart &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-b&lt;/span&gt;:a 128k &lt;span class="nt"&gt;-ar&lt;/span&gt; 44100 out.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;-pix_fmt yuv420p&lt;/code&gt; — required for broad player and browser support; 4:4:4 or 10-bit input plays on a subset of devices.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;-movflags +faststart&lt;/code&gt; — moves the moov atom forward so playback starts before download completes, reduces perceived load time on web.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Explicit audio parameters (&lt;code&gt;aac 128k 44100&lt;/code&gt;) — avoid container defaults; inaudible or silent tracks are the most common audio support issue.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Privacy as a design constraint
&lt;/h2&gt;

&lt;p&gt;The stated promise — files are used only for the task and auto-deleted — is implemented as a pipeline property rather than a toggle. Files are written to short-TTL storage and destroyed after the job, with a scheduled cleanup as a backstop in case a process terminates abnormally. Default behavior, not user-remembered settings, is what keeps the promise reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Video compression as a product is the pipeline surrounding the encode: execution environment, long-job behavior, bounded concurrency, and output matched to the user's goal. The command itself is the smallest part; the value is in the decisions that make it reliable at scale.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The pipeline described here is implemented in the &lt;/em&gt;&lt;a href="https://meicut.com/tools/compress-video" rel="noopener noreferrer"&gt;online video compressor&lt;/a&gt;&lt;em&gt; — no install required; files auto-delete after processing.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Meicut Engineering series:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;#1: Video Compression at Product Scale&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;#2: Trimming Videos Without Re-encoding (when &lt;code&gt;-c copy&lt;/code&gt; is and isn't viable)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;#3: "Convert Format" Is a Misnomer — containers vs codecs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;#4: Subtitles: soft vs burned, and the font problem&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>systemdesign</category>
      <category>ffmpeg</category>
    </item>
  </channel>
</rss>
