Skip to content

Instantly share code, notes, and snippets.

@alexmozaidze
Last active August 13, 2026 18:48
Show Gist options
  • Select an option

  • Save alexmozaidze/f925f09710c229b25283d5b1c5dd371a to your computer and use it in GitHub Desktop.

Select an option

Save alexmozaidze/f925f09710c229b25283d5b1c5dd371a to your computer and use it in GitHub Desktop.
A script for compressing a video to a certain length (usually to ~10MB for Discord)
{:deps {cli-matic/cli-matic {:mvn/version "0.5.2"}}}
#!/usr/bin/env bb
;; vim:ft=clojure:
(ns user
(:require [clojure.string :as string]
[cli-matic.core :refer [run-cmd]]
[babashka.process :as process]
[babashka.fs :as fs]))
(set! *warn-on-reflection* true)
(def tiny-bitrate-threshold 100000)
(defn log
[prefix & strs]
(let [output (format "%s: %s" prefix (string/join " " strs))]
(binding [*out* *err*]
(println output))))
(defn get-duration
"Gets the duration of a file."
[path]
(let [command ["ffprobe"
"-i" path
"-show_entries" "format=duration"
"-v" "quiet"
"-of" "csv=p=0"]
{duration :out} (apply process/sh command)
duration (string/trim duration)]
(if-not (empty? duration)
(try
(Float/parseFloat duration)
(catch NumberFormatException _ nil))
nil)))
(defn calculate-video-bitrate
"Calculates video bitrate to achieve the desired video file size."
[target-size-in-mb input-video-duration audio-bitrate]
(-> target-size-in-mb
(* 1024 1024 8) ; converting to bits
(/ input-video-duration)
(- audio-bitrate)
int))
(def tmpdir (delay
(let [tmpdir (fs/create-temp-dir)]
(-> (Runtime/getRuntime)
(.addShutdownHook (Thread. #(fs/delete-tree tmpdir))))
tmpdir)))
(defn build-complex-filter
[scale audio-tracks]
{:pre [(string? scale)
(coll? audio-tracks)
(every? int? audio-tracks)]}
(let [video-pipeline (format "[0:v]format=yuv420p,scale=%s[vout]" scale)
audio-mappings (->> audio-tracks
(map #(format "[0:a:%d]" %))
(apply str))
audio-pipeline (format "%samix=inputs=%d[aout]" audio-mappings (count audio-tracks))]
(str video-pipeline ";" audio-pipeline)))
(defn -main
[{:keys [input preset output fps scale]
yes? :yes
dry? :dry
target-size-in-mb :size
audio-tracks :m:a
audio-codec :c:a
audio-bitrate :b:a
allow-tiny-bitrate? :allow-tiny-bitrate}]
(let [input (fs/absolutize input)
output (fs/absolutize output)
audio-tracks (if (coll? audio-tracks) audio-tracks [audio-tracks])
duration (get-duration input)
_ (when-not (and duration (pos? duration))
(log :error (format "Could not determine the duration of %s. Is it a valid video file?" input))
(System/exit 1))
target-video-bitrate (calculate-video-bitrate target-size-in-mb duration audio-bitrate)
tiny-bitrate? (< target-video-bitrate tiny-bitrate-threshold)
excessively-tiny-bitrate? (<= target-video-bitrate 0)
args ["-i" input
"-filter_complex" (build-complex-filter scale audio-tracks)
"-c:v" "libx264"
"-c:a" audio-codec
"-b:v" target-video-bitrate
"-b:a" audio-bitrate
"-f" "mp4"
"-map" "[vout]"
"-map" "[aout]"
"-r" fps
"-preset" preset
"-movflags" "+faststart"]
first-pass-args (concat ["-y"]
args
["-pass" 1
"/dev/null"])
second-pass-args (concat (when yes? ["-y"])
args
["-pass" 2
output])
run-command (fn [args]
(let [command (into ["ffmpeg"] args)]
(if-not dry?
(apply process/shell
{:dir @tmpdir :in :inherit :out :inherit :err :inherit}
command)
(->> command
(string/join " ")
println))))
compress (fn []
(run-command first-pass-args)
(run-command second-pass-args)
(when-not (string/ends-with? output ".mp4")
(log :warning "The output does not end with `.mp4`. Keep in mind that the output is *always* an mp4 container, even if the file doesn't end with `.mp4`.")))]
(when excessively-tiny-bitrate?
(log :error "The target size is too small: the audio bitrate alone exceeds the total bitrate budget. Increase --size or lower --b:a.")
(System/exit 1))
(when tiny-bitrate?
(let [messages {:warning "The bitrate is extremely small. The target size will be achieved, however, the video will be extremely low quality."
:error "The bitrate is extremely small. In order to permit small bitrate, pass --allow-tiny-bitrate to the command. However, keep in mind that the resulting video will be extremely low quality, to the point that it may not be watchable."}
severity (cond
(and tiny-bitrate? allow-tiny-bitrate?) :warning
(and tiny-bitrate? (not allow-tiny-bitrate?)) :error)]
(log severity (messages severity))
(when-not allow-tiny-bitrate? (System/exit 1))))
(compress)))
(def cmd-spec
{:command "ffcompress"
:description "ffmpeg helper script for compressing a file to a desired file size."
:version "0.0.1"
:opts [{:option "yes"
:short "y"
:as "Whether to overwrite the output file."
:type :with-flag
:default false}
{:option "dry"
:short "d"
:as "Just print what would've been done, not actually run it. Useful for testing."
:type :with-flag
:default false}
{:option "input"
:short "i"
:as "Input video file to compress."
:type :string
:default :present}
{:option "size"
:short "s"
:as "Target size for the video in MB."
:type :float
:spec #(< 2 %)
:default 9}
{:option "preset"
:short "p"
:as "Preset to use for encoding the video."
:type :string
:spec #{"ultrafast"
"superfast"
"veryfast"
"faster"
"fast"
"medium"
"slow"
"slower"
"veryslow"
"placebo"}
:default "medium"}
{:option "m:a"
:as "Audio track to use. More than one options merge audio tracks."
:multiple true
:type :int
:spec (fn [x] (if (coll? x) (every? #(<= 0 %) x) (<= 0 x)))
:default [0]}
{:option "c:a"
:as "Audio codec to use."
:type :string
:default "libopus"}
{:option "b:a"
:as "Audio bitrate for the output."
:type :int
:spec #(< 10000 %)
:default 128000}
{:option "allow-tiny-bitrate"
:as (format "If true, the program will permit bitrate values below %d." tiny-bitrate-threshold)
:type :with-flag
:default false}
{:option "fps"
:short "r"
:as "Video framerate."
:type :int
:spec #(< 1 %)
:default 60}
{:option "scale"
:as "Video scale (resolution)."
:type :string
:spec #(->> (re-matches #"-?\d+:-?\d+" %) boolean)
:default "1280:-2"}
{:option "output"
:short "o"
:as "Compressed video file output path."
:type :string
:default :present}]
:runs -main})
(when (= *file* (System/getProperty "babashka.file"))
(run-cmd *command-line-args* cmd-spec))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment