Last active
February 9, 2026 05:36
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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