Skip to content

Instantly share code, notes, and snippets.

@alexmozaidze
Last active February 9, 2026 05:36
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)
#!/usr/bin/env bb
;; vim:ft=clojure:
(require '[clojure.string :as string])
(require '[cli-matic.core :refer [run-cmd]])
(require '[babashka.process :as process])
(require '[babashka.fs :as fs])
(defn warn
[& strs]
(binding [*out* *err*]
(println "Warning: " (string/join " " strs))))
(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? (string/trim duration))
(Float/parseFloat duration)
(throw (Exception. (str "Could not get duration of a file: " path))))))
(defn def-shutdown-hook [action]
(-> (Runtime/getRuntime) (.addShutdownHook (Thread. action))))
(defn calculate-video-bitrate
"Calculates video bitrate to achieve the desired video file size."
[target-size-in-mb input-video-duration audio-bitrate]
{:post [(<= 100000 %)]}
(-> target-size-in-mb
(* 1024 1024 8) ; converting to bits
(/ input-video-duration)
(- audio-bitrate)
(int)))
(defn -main
[{:keys [input preset output]
yes? :yes
dry? :dry
target-size-in-mb :size
audio-track :m:a
audio-codec :c:a
audio-bitrate :b:a}]
(let [tmpdir (fs/create-temp-dir)]
(def-shutdown-hook #(fs/delete-tree tmpdir))
(let [input (fs/absolutize input)
output (fs/absolutize output)
target-video-bitrate (calculate-video-bitrate target-size-in-mb (get-duration input) audio-bitrate)
generic-command ["ffmpeg"
"-i" input
"-c:v" "libx264"
"-c:a" audio-codec
"-b:v" target-video-bitrate
"-b:a" audio-bitrate
"-map" "0:v:0"
"-map" (str "0:a:" audio-track)
"-f" "mp4"
"-vf" "format=yuv420p"
"-preset" preset
"-movflags" "+faststart"]
first-pass-command (into generic-command
["-y"
"-pass" "1"
"/dev/null"])
second-pass-command (cond-> generic-command
yes? (conj "-y")
true (into
["-pass" "2"
output]))
commands [first-pass-command second-pass-command]]
(doseq [command commands]
(if-not dry?
(apply process/shell {:dir tmpdir :in :inherit :out :inherit :err :inherit} command)
(->> command
(string/join " ")
(println))))
(when-not (string/ends-with? output ".mp4")
(warn "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`.")))))
(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"
:type :string
:spec #{"ultrafast"
"superfast"
"veryfast"
"faster"
"fast"
"medium"
"slow"
"slower"
"veryslow"
"placebo"}
:default "medium"}
{:option "m:a"
:type :int
:spec #(<= 0 %)
: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 "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