<?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: Alexander Pervushen</title>
    <description>The latest articles on DEV Community by Alexander Pervushen (@alexpua).</description>
    <link>https://dev.to/alexpua</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%2F3972866%2Fc66f1fd3-22e2-41e2-b63f-43409817e2de.jpg</url>
      <title>DEV Community: Alexander Pervushen</title>
      <link>https://dev.to/alexpua</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alexpua"/>
    <language>en</language>
    <item>
      <title>Fit any video under the limit: meet DeepShrink, a one-command media compressor</title>
      <dc:creator>Alexander Pervushen</dc:creator>
      <pubDate>Tue, 21 Jul 2026 08:40:51 +0000</pubDate>
      <link>https://dev.to/deeplab/fit-any-video-under-the-limit-meet-deepshrink-a-one-command-media-compressor-2j8b</link>
      <guid>https://dev.to/deeplab/fit-any-video-under-the-limit-meet-deepshrink-a-one-command-media-compressor-2j8b</guid>
      <description>&lt;p&gt;You have a 300 MB screen recording. Discord wants it under 8 MB. Your email won't take more than 20. So you either google &lt;em&gt;"compress video to 8mb"&lt;/em&gt; and land on an ad-riddled web converter that uploads your file to someone else's server and stamps a watermark on it — or you open HandBrake and start guessing at CRF values — or you hand-write an &lt;code&gt;ffmpeg&lt;/code&gt; command, recompute the bitrate for the fifth time this month, and forget the audio track again.&lt;/p&gt;

&lt;p&gt;None of those answer the actual question: &lt;strong&gt;"make this ≤ N MB, without me thinking about it."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the whole reason &lt;a href="https://github.com/deeplabua/deepshrink" rel="noopener noreferrer"&gt;DeepShrink&lt;/a&gt; exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deepshrink gameplay.mp4 &lt;span class="nt"&gt;--for&lt;/span&gt; discord
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  gameplay.mp4   1920x1080  2m14s   312.4 MB
  target         Discord (8 MB)
  plan           H.264 · 456 kbps video · 96 kbps audio · two-pass
  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  100%   pass 2/2

  ✓ gameplay.shrink.mp4   7.6 MB   (−97.6%)   VMAF 91.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command. Local. No upload, no watermark, no guessing. And when you say &lt;code&gt;--target 8MB&lt;/code&gt;, the result is &lt;em&gt;guaranteed&lt;/em&gt; ≤ 8 MB — not "roughly."&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually is
&lt;/h2&gt;

&lt;p&gt;DeepShrink is a small, open-source CLI written in Rust. It doesn't reinvent encoding — it drives &lt;strong&gt;ffmpeg&lt;/strong&gt; for you and does the one thing ffmpeg makes annoying: hitting an exact output size.&lt;/p&gt;

&lt;p&gt;Under the hood it probes the file with &lt;code&gt;ffprobe&lt;/code&gt;, works out the bitrate budget for your target size (with a little headroom for container overhead), runs a two-pass H.264/H.265 encode for video or picks the right codec bitrate for audio (AAC / Opus / MP3), and can optionally hold a quality floor with VMAF so it never over-compresses. You don't touch any of that. You say how big, it figures out how.&lt;/p&gt;

&lt;p&gt;The design goals, in order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One obvious command solves 80% of cases.&lt;/strong&gt; &lt;code&gt;deepshrink video.mp4 --target 8MB&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presets instead of flags.&lt;/strong&gt; You shouldn't need to know what a bitrate is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never wreck the original.&lt;/strong&gt; Output is a new file (&lt;code&gt;video.shrink.mp4&lt;/code&gt;); the source is untouched unless you explicitly pass &lt;code&gt;--overwrite&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local and private.&lt;/strong&gt; Your file never leaves your machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope today is &lt;strong&gt;video and audio&lt;/strong&gt; — one engine, ffmpeg. Images, PDFs, and other formats are a deliberate future step, not a half-baked afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing with Homebrew (recommended on macOS)
&lt;/h2&gt;

&lt;p&gt;This is the frictionless path, and it's the one I'd reach for first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;deeplabua/tap/deepshrink
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Because the formula declares &lt;code&gt;depends_on "ffmpeg"&lt;/code&gt;, Homebrew pulls &lt;strong&gt;ffmpeg in as a dependency automatically&lt;/strong&gt; — so there are zero manual runtime steps. Install, and it works.&lt;/p&gt;

&lt;p&gt;If you'd rather tap first and install by short name (handy if you'll install other tools from the same tap later):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew tap deeplabua/tap
brew &lt;span class="nb"&gt;install &lt;/span&gt;deepshrink
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify it landed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deepshrink &lt;span class="nt"&gt;--version&lt;/span&gt;
deepshrink &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Updating later is just &lt;code&gt;brew upgrade deepshrink&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing with cargo (from crates.io)
&lt;/h2&gt;

&lt;p&gt;If you already live in the Rust toolchain, DeepShrink is on crates.io:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;deepshrink
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This compiles from source, so it's slower than the prebuilt Homebrew bottle and you need a Rust toolchain installed. Worth knowing about &lt;strong&gt;one difference from the brew path&lt;/strong&gt;: &lt;code&gt;cargo install&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; bring ffmpeg along. DeepShrink expects &lt;code&gt;ffmpeg&lt;/code&gt; and &lt;code&gt;ffprobe&lt;/code&gt; to be on your &lt;code&gt;PATH&lt;/code&gt;, and if they aren't, it exits cleanly (code &lt;code&gt;3&lt;/code&gt;) with a hint rather than crashing.&lt;/p&gt;

&lt;p&gt;So on a fresh machine, install ffmpeg alongside it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;ffmpeg

&lt;span class="c"&gt;# Debian / Ubuntu&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;ffmpeg

&lt;span class="c"&gt;# Windows (Scoop)&lt;/span&gt;
scoop &lt;span class="nb"&gt;install &lt;/span&gt;ffmpeg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the same check as before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deepshrink &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  brew vs cargo, quickly
&lt;/h3&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;&lt;code&gt;brew install deeplabua/tap/deepshrink&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;cargo install deepshrink&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;Prebuilt bottle, instant&lt;/td&gt;
&lt;td&gt;Compiles from source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rust toolchain needed&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ffmpeg&lt;/td&gt;
&lt;td&gt;Pulled in automatically&lt;/td&gt;
&lt;td&gt;You install it yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Most people, especially macOS&lt;/td&gt;
&lt;td&gt;Rust devs who already have the toolchain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prebuilt binaries for macOS, Linux, and Windows are also attached to every &lt;a href="https://github.com/deeplabua/deepshrink/releases" rel="noopener noreferrer"&gt;GitHub release&lt;/a&gt; if you'd rather not use a package manager at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;

&lt;p&gt;The headline command — fit a file under an absolute size:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deepshrink big.mp4 &lt;span class="nt"&gt;--target&lt;/span&gt; 8MB
deepshrink talk.wav &lt;span class="nt"&gt;--target&lt;/span&gt; 500KB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't want to memorize platform limits? Use a &lt;strong&gt;destination preset&lt;/strong&gt; and DeepShrink applies the right cap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deepshrink clip.mp4 &lt;span class="nt"&gt;--for&lt;/span&gt; discord        &lt;span class="c"&gt;# 8 MB&lt;/span&gt;
deepshrink clip.mp4 &lt;span class="nt"&gt;--for&lt;/span&gt; email          &lt;span class="c"&gt;# 20 MB&lt;/span&gt;
deepshrink clip.mp4 &lt;span class="nt"&gt;--for&lt;/span&gt; telegram       &lt;span class="c"&gt;# only recompresses for quality&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Built-in presets today: &lt;code&gt;discord&lt;/code&gt;, &lt;code&gt;discord-nitro&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;telegram&lt;/code&gt;, &lt;code&gt;whatsapp&lt;/code&gt;, and &lt;code&gt;web&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Prefer a relative cut instead of a hard ceiling?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deepshrink video.mp4 &lt;span class="nt"&gt;--reduce&lt;/span&gt; 70%        &lt;span class="c"&gt;# 70% smaller than the source&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Audio is a first-class citizen, not an afterthought — great for podcasts, voice memos, and lectures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deepshrink lecture.wav &lt;span class="nt"&gt;--target&lt;/span&gt; 10MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  lecture.wav     stereo 48kHz   58m02s   638.1 MB (PCM)
  target          10 MB
  plan            Opus · ~22 kbps · mono (speech)
  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  100%

  ✓ lecture.shrink.opus   9.4 MB   (−98.5%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Got a whole folder? Batch it — DeepShrink walks the directory, skips files that are already small or unsupported, and prints a summary instead of dying on the first hiccup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deepshrink ./recordings &lt;span class="nt"&gt;--recursive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few flags worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--dry-run&lt;/code&gt; — show the plan (chosen bitrate, expected size) without encoding a thing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--vmaf &amp;lt;n&amp;gt;&lt;/code&gt; — hold a quality floor; it won't compress below the VMAF score you ask for.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--codec h265&lt;/code&gt; — smaller files at the same quality, when you don't need maximum compatibility.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--json&lt;/code&gt; — machine-readable output, for scripts and CI.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--overwrite&lt;/code&gt; — replace the original in place (written atomically via a temp file, so an interrupted run won't corrupt your source).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why bother, over a web converter?
&lt;/h2&gt;

&lt;p&gt;Because DeepShrink is the intersection people actually want: the &lt;strong&gt;precision and privacy of native ffmpeg&lt;/strong&gt; with the &lt;strong&gt;simplicity of a web converter&lt;/strong&gt; — and none of the downsides of either. Your file stays on your disk. You get an exact size, not an approximation. There are no watermarks, no queues, and no "upload your file to continue." It's one static binary that works offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Homebrew:&lt;/strong&gt; &lt;code&gt;brew install deeplabua/tap/deepshrink&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cargo:&lt;/strong&gt; &lt;code&gt;cargo install deepshrink&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source &amp;amp; releases:&lt;/strong&gt; &lt;a href="https://github.com/deeplabua/deepshrink" rel="noopener noreferrer"&gt;github.com/deeplabua/deepshrink&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's open source and free. If it saves you a trip to a sketchy converter even once, consider dropping a ⭐ on the repo — that's what keeps small tools like this alive.&lt;/p&gt;

&lt;p&gt;Try it on that 300 MB file you've been avoiding. &lt;code&gt;--target 8MB&lt;/code&gt;. Done.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>cli</category>
      <category>ffmpeg</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How I make ffmpeg hit an exact file size (the bitrate math nobody explains)</title>
      <dc:creator>Alexander Pervushen</dc:creator>
      <pubDate>Sat, 18 Jul 2026 18:18:30 +0000</pubDate>
      <link>https://dev.to/alexpua/how-i-make-ffmpeg-hit-an-exact-file-size-the-bitrate-math-nobody-explains-3o7o</link>
      <guid>https://dev.to/alexpua/how-i-make-ffmpeg-hit-an-exact-file-size-the-bitrate-math-nobody-explains-3o7o</guid>
      <description>&lt;p&gt;Every few weeks I hit the same wall: I have a 300 MB screen recording, and something on the other end wants it &lt;em&gt;under 8 MB&lt;/em&gt;. Discord, an email attachment, a bug tracker, a form that silently rejects anything bigger.&lt;/p&gt;

&lt;p&gt;The usual advice is "just use HandBrake" or "run ffmpeg with a lower CRF." But CRF doesn't take a target size — it takes a &lt;em&gt;quality knob&lt;/em&gt;. So you export, check the size, it's 11 MB, nudge the knob, export again, now it's 5 MB and looks like a potato, nudge back… It's a binary search you run by hand, one full encode per guess.&lt;/p&gt;

&lt;p&gt;The thing is, hitting an exact size isn't a guessing game at all. It's arithmetic you can do &lt;em&gt;before&lt;/em&gt; you encode. I ended up wrapping that arithmetic into a little Rust CLI (&lt;a href="https://github.com/deeplabua/deepshrink" rel="noopener noreferrer"&gt;DeepShrink&lt;/a&gt;), but the math is the interesting part, and almost nobody writes it down. So here it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one insight everything rests on
&lt;/h2&gt;

&lt;p&gt;File size is (roughly) &lt;strong&gt;bitrate × duration&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A bitrate is bits per second. A duration is seconds. Multiply them and the seconds cancel, leaving bits — the size of the file. That's it. That's the whole trick.&lt;/p&gt;

&lt;p&gt;Normally you treat bitrate as the input and size as whatever falls out. Flip it around: &lt;strong&gt;fix the size, measure the duration, and solve for the bitrate.&lt;/strong&gt; You know the duration (ffprobe will tell you), and you know the size you want (the platform's limit). The only unknown is the bitrate — and now it's a single division away.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bitrate = size_in_bits / duration_in_seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything below is just this equation with the real-world messiness added back in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the budget, step by step
&lt;/h2&gt;

&lt;p&gt;Say I want a 60-second clip to fit Discord's 8 MB limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Turn the target size into bits.&lt;/strong&gt; Sizes are in bytes, bitrate is in bits, so multiply by 8. (I'll use 1 MB = 1,000,000 bytes here to keep the mental math clean; if your platform means mebibytes, same method, different constant.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;target_bits = 8_000_000 bytes × 8 = 64_000_000 bits   (64 Mbit)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Reserve a little for container overhead.&lt;/strong&gt; An &lt;code&gt;.mp4&lt;/code&gt; isn't pure video and audio — there's a container, a moov atom, stream headers, muxing slack. If you budget every last bit to the media, you'll overshoot. I hold back ~2%:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;usable_bits = 64 Mbit × (1 − 0.02) = 62.72 Mbit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Pay for audio first.&lt;/strong&gt; Audio is usually a fixed, modest bitrate, so I carve it out before touching video. At 96 kbps for 60 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;audio_bits = 96_000 bps × 60 s = 5.76 Mbit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Whatever's left is the video budget.&lt;/strong&gt; Divide it by the duration to get the video bitrate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;video_bits    = 62.72 − 5.76 = 56.96 Mbit
video_bitrate = 56.96 Mbit / 60 s ≈ 949 kbps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done. Encode that 60-second clip at &lt;strong&gt;~949 kbps of video plus 96 kbps of audio&lt;/strong&gt;, and it lands right around 8 MB — no guessing, no re-exports.&lt;/p&gt;

&lt;p&gt;As a pure function it's about ten lines. This is essentially the core of DeepShrink's &lt;code&gt;budget&lt;/code&gt; module (simplified):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;video_bitrate_bps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;duration_s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audio_bps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;OVERHEAD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;target_bits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target_bytes&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;8.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;usable_bits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target_bits&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;OVERHEAD&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;audio_bits&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;audio_bps&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;duration_s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;video_bits&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usable_bits&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;audio_bits&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="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video_bits&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;duration_s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten lines of arithmetic, no I/O, trivially unit-testable. That last part matters: because it's pure, I can test the budgeting logic without ever spawning ffmpeg.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you need two-pass to actually hit it
&lt;/h2&gt;

&lt;p&gt;Knowing the target bitrate isn't enough on its own. If you hand ffmpeg a single-pass &lt;code&gt;-b:v 949k&lt;/code&gt;, the encoder doesn't know what's coming — a calm talking-head scene and a chaotic explosion get bits allocated blindly, and the &lt;em&gt;average&lt;/em&gt; drifts off your target.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two-pass&lt;/strong&gt; fixes this. The first pass analyzes the whole video and writes a stats log (it produces no output file). The second pass uses that map of "where the hard parts are" to distribute bits so the average actually lands on your number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Pass 1 — analyze, no output&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-b&lt;/span&gt;:v 949k &lt;span class="nt"&gt;-pass&lt;/span&gt; 1 &lt;span class="nt"&gt;-an&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; null /dev/null

&lt;span class="c"&gt;# Pass 2 — encode for real, using the stats from pass 1&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-b&lt;/span&gt;:v 949k &lt;span class="nt"&gt;-pass&lt;/span&gt; 2 &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-b&lt;/span&gt;:a 96k output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two passes means roughly double the encode time, but it's the difference between "about 8 MB" and "reliably under 8 MB." When the whole point is to fit under a hard limit, reliable wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  The guardrails that make it shippable
&lt;/h2&gt;

&lt;p&gt;If I stopped at the formula, it'd work maybe 80% of the time and produce garbage the other 20%. The interesting engineering is in the edges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't paint 1080p with a bitrate meant for a thumbnail.&lt;/strong&gt; A one-hour lecture squeezed into 8 MB gives you a video budget so low that full HD turns to mush — the encoder simply doesn't have the bits to describe that many pixels. So before committing, I sanity-check the bitrate against the resolution. If it's too low to look acceptable at the source resolution, I step &lt;em&gt;down&lt;/em&gt; a resolution ladder (1080p → 720p → 480p…) and pick the highest rung the budget can actually support. Fewer pixels, each with enough bits, beats many pixels all starved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify, then correct once.&lt;/strong&gt; Muxing overhead isn't perfectly predictable, so the output can miss the target by a hair. After encoding I check the real file size, and if it overshot, I re-run with a slightly corrected bitrate. One correction, not an infinite loop — the first pass already gets you within a couple percent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never make a file bigger.&lt;/strong&gt; If the source is already under the target, there's nothing to solve. Re-encoding would just waste time and &lt;em&gt;lose&lt;/em&gt; quality for no reason. In that case I skip straight to a remux (or leave it alone). "Shrink" should never accidentally inflate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fail loudly when it's impossible.&lt;/strong&gt; Sometimes the target genuinely can't be met — a two-hour movie into 2 MB isn't happening at any watchable quality. Rather than emit a slideshow and pretend it worked, DeepShrink bails with a distinct exit code (&lt;code&gt;4&lt;/code&gt;, "can't fit even at minimum reasonable quality"). A clear failure beats a technically-succeeded garbage file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audio-only is the same idea, minus a pass
&lt;/h2&gt;

&lt;p&gt;Podcasts, voice memos, and lectures hit size limits too, and they're actually &lt;em&gt;simpler&lt;/em&gt;: no resolution ladder, and no two-pass (audio codecs hit an average bitrate directly). The budget collapses to one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;audio_bitrate = target_bits / duration_s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wrinkle is that you can't pick any arbitrary number — codecs work in sensible steps, so I snap &lt;strong&gt;down&lt;/strong&gt; to the nearest one (snapping up would blow the target). And there's a floor: below a certain bitrate speech stops being intelligible, so I clamp there and, if even that won't fit, fail rather than ship noise. For speech specifically, downmixing to mono and using Opus buys a &lt;em&gt;lot&lt;/em&gt; of headroom — a mono Opus voice track at ~24 kbps is still perfectly clear and a fraction of the size.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;deepshrink lecture.wav &lt;span class="nt"&gt;--target&lt;/span&gt; 10MB

  lecture.wav     stereo 48kHz   58m02s   638.1 MB &lt;span class="o"&gt;(&lt;/span&gt;PCM&lt;span class="o"&gt;)&lt;/span&gt;
  target          10 MB
  plan            Opus · ~22 kbps · mono &lt;span class="o"&gt;(&lt;/span&gt;speech&lt;span class="o"&gt;)&lt;/span&gt;
  ✓ lecture.shrink.opus   9.4 MB   &lt;span class="o"&gt;(&lt;/span&gt;−98.5%&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;"Compress this to 8 MB" &lt;em&gt;sounds&lt;/em&gt; like it needs a fancy heuristic or an ML model. It doesn't. It's &lt;code&gt;size = bitrate × duration&lt;/code&gt; rearranged, plus a handful of guardrails so the arithmetic survives contact with real files: reserve overhead, pay audio first, drop resolution before the picture falls apart, verify and correct once, and refuse the impossible instead of faking it.&lt;/p&gt;

&lt;p&gt;The formula is ten lines. The guardrails are what make it feel like magic instead of a footgun.&lt;/p&gt;

&lt;p&gt;If you'd rather just type one command than wire this up yourself, that's exactly what I packaged DeepShrink for — it's open source (Rust, driving ffmpeg as a subprocess), MIT-licensed, and lives here: &lt;a href="https://github.com/deeplabua/deepshrink" rel="noopener noreferrer"&gt;github.com/deeplabua/deepshrink&lt;/a&gt;. But now you know what it's doing under the hood, and you could write the core of it in an afternoon.&lt;/p&gt;

&lt;p&gt;What's your go-to trick for wrangling ffmpeg? I'm always looking for edges I haven't hit yet.&lt;/p&gt;

</description>
      <category>ffmpeg</category>
      <category>rust</category>
      <category>video</category>
      <category>cli</category>
    </item>
    <item>
      <title>How I stopped fighting Oracle's "Out of host capacity" and let a script catch a free ARM server for me</title>
      <dc:creator>Alexander Pervushen</dc:creator>
      <pubDate>Sun, 07 Jun 2026 19:58:49 +0000</pubDate>
      <link>https://dev.to/alexpua/how-i-stopped-fighting-oracles-out-of-host-capacity-and-let-a-script-catch-a-free-arm-server-for-41a6</link>
      <guid>https://dev.to/alexpua/how-i-stopped-fighting-oracles-out-of-host-capacity-and-let-a-script-catch-a-free-arm-server-for-41a6</guid>
      <description>&lt;p&gt;Oracle Cloud's Always Free tier is, quietly, one of the best deals in cloud computing: &lt;strong&gt;2 Arm OCPUs and 12 GB of RAM, free forever&lt;/strong&gt;, no trial clock. That's still enough for a homelab node, a Docker stack, a VPN, or a game server - running 24/7 without paying a cent or buying hardware.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Update, June 2026: Oracle quietly halved this from 4 OCPU / 24 GB to 2 OCPU / 12 GB on 15 June 2026, with no announcement. Pay-as-you-go accounts reportedly kept the old 4/24 for free, but Oracle hasn't confirmed that publicly.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There's just one problem standing between you and that box, and if you've tried you already know its name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ServiceError&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;InternalError&lt;/span&gt;
  &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Out of host capacity.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those Arm hosts are in such heavy demand that creating an instance usually fails. US regions can be dry for hours or days. Even busy EU/APAC regions dip in and out. Capacity frees up in small, random windows — and whoever happens to be retrying at that exact second gets it.&lt;/p&gt;

&lt;p&gt;I spent the better part of a week refreshing the OCI console like it was a concert ticket drop. Then I did the obvious thing: I wrote a script to play the lottery for me, on a timer, and walked away. It worked — it caught me a 4 OCPU / 24 GB A1 instance. So I cleaned it up and open-sourced it.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://github.com/alexpua/oci-arm-catcher" rel="noopener noreferrer"&gt;github.com/alexpua/oci-arm-catcher&lt;/a&gt;&lt;/strong&gt; (MIT)&lt;/p&gt;

&lt;p&gt;[GIF: terminal showing "Out of host capacity… retrying" a few times, then "SUCCESS! Instance created"]&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea is dumb on purpose
&lt;/h2&gt;

&lt;p&gt;There's no clever exploit here. The tool just calls the &lt;strong&gt;official&lt;/strong&gt; OCI CLI command — the exact same &lt;code&gt;oci compute instance launch&lt;/code&gt; you'd run by hand — in a polite loop, and reacts to the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;loop:
  oci compute instance launch  --shape VM.Standard.A1.Flex ...
  ├─ success            → parse the instance OCID, desktop notification, exit
  ├─ "out of capacity"  → wait, (optionally rotate AD), retry
  │   InternalError / LimitExceeded / TooManyRequests / timeout
  └─ any other error    → print it, notify, STOP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only part that actually matters is the last two branches. A naive&lt;br&gt;
&lt;code&gt;while true; do launch; sleep; done&lt;/code&gt; loop is worse than useless: when your config has a typo — wrong subnet OCID, an x86 image, an auth problem — it will cheerfully retry that broken request forever and you'll never notice.&lt;/p&gt;

&lt;p&gt;So the catcher splits errors into two buckets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transient / capacity&lt;/strong&gt; (&lt;code&gt;Out of host capacity&lt;/code&gt;, &lt;code&gt;InternalError&lt;/code&gt;,
&lt;code&gt;TooManyRequests&lt;/code&gt;, timeouts) → &lt;em&gt;keep trying, this is the whole point.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything else&lt;/strong&gt; (&lt;code&gt;NotAuthorizedOrNotFound&lt;/code&gt;, &lt;code&gt;LimitExceeded&lt;/code&gt;, bad image) →
&lt;em&gt;stop immediately and tell the human.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That single distinction is the difference between "I left it running overnight and woke up to a server" and "I left it running overnight and woke up to 4,000 copies of the same error."&lt;/p&gt;
&lt;h2&gt;
  
  
  Things I added once it was more than a personal hack
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Readable error output.&lt;/strong&gt; The OCI CLI prints errors as a JSON blob to stderr.&lt;br&gt;
My first version grepped for &lt;code&gt;"message"&lt;/code&gt; and frequently printed &lt;code&gt;?: ?&lt;/code&gt; when the shape didn't match. Now a tiny Python helper parses the JSON properly and always falls back to &lt;em&gt;something&lt;/em&gt; human-readable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  -&amp;gt; InternalError: Out of host capacity.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Multi-AD rotation.&lt;/strong&gt; Regions like Ashburn, Phoenix and Frankfurt have three Availability Domains, and capacity can appear in any one of them. Give the catcher all three and it cycles through on each retry, multiplying your chances:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;AVAILABILITY_DOMAINS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Abcd:US-ASHBURN-1-AD-1,Abcd:US-ASHBURN-1-AD-2,Abcd:US-ASHBURN-1-AD-3"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A config-discovery helper.&lt;/strong&gt; The genuinely annoying part of OCI isn't the API, it's finding all the OCIDs (compartment, subnet, image, availability domain). So there's a read-only &lt;code&gt;get-config&lt;/code&gt; script that prints them, formatted to paste straight into your &lt;code&gt;.env&lt;/code&gt;. It launches nothing — just reads metadata.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-platform.&lt;/strong&gt; Bash for macOS/Linux, plus a native PowerShell port for Windows (with toast notifications). Or run the bash version under WSL2 — your call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Desktop notifications.&lt;/strong&gt; Because the whole pitch is "start it and forget it," it pings you the moment it lands — &lt;code&gt;osascript&lt;/code&gt; on macOS, &lt;code&gt;notify-send&lt;/code&gt; on Linux, a toast on Windows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tests.&lt;/strong&gt; It mocks the &lt;code&gt;oci&lt;/code&gt; CLI and asserts the important behaviors: retries on capacity errors, stops on auth errors, parses the OCID on success, never prints &lt;code&gt;?: ?&lt;/code&gt;. bats for bash, Pester for PowerShell, ShellCheck — all in CI. For a 200-line shell script that's arguably overkill, but I wanted the "stop on real errors"&lt;br&gt;
guarantee to be actually guaranteed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/alexpua/oci-arm-catcher.git
&lt;span class="nb"&gt;cd &lt;/span&gt;oci-arm-catcher

&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
./scripts/get-config.sh     &lt;span class="c"&gt;# prints your OCIDs → paste into .env&lt;/span&gt;
&lt;span class="c"&gt;# edit .env&lt;/span&gt;

&lt;span class="nb"&gt;nohup&lt;/span&gt; ./oci-arm-catcher.sh &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; catcher.log 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; catcher.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then go do something else. When capacity opens up, you get a notification and a running instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on being a good citizen
&lt;/h2&gt;

&lt;p&gt;This isn't a way to "beat" Oracle or bypass anything. It does exactly what you'd do by hand, just patiently and on an interval (default: every 5 minutes — not a hammer). You still have to stay inside your free-tier allowance. It's automation of a tedious manual task, nothing more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it / break it
&lt;/h2&gt;

&lt;p&gt;If you've been losing the capacity lottery, give it a run:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/alexpua/oci-arm-catcher" rel="noopener noreferrer"&gt;github.com/alexpua/oci-arm-catcher&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Issues and PRs welcome — I'd especially love more notification backends (Telegram, Discord, Slack), smarter backoff, and region/AD presets.&lt;/p&gt;

&lt;p&gt;And if it caught you a free server: a ⭐ on the repo helps the next person find it, and if you feel like it, you can &lt;a href="https://send.monobank.ua/jar/4Hq7auheaa" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt; ☕.&lt;br&gt;
Both totally optional — the tool's free either way.&lt;/p&gt;

</description>
      <category>oraclecloud</category>
      <category>devops</category>
      <category>opensource</category>
      <category>selfhosted</category>
    </item>
  </channel>
</rss>
