ffmpeg!— run ffmpeg transcodes from Clojureffprobe!— probe media files, returns parsed Clojure maps- Filter graph DSL — build
-vf,-af, and-filter_complexexpressions as data - Progress events streamed via
core.asyncchannels - Works with Babashka
Planned
- ffmpeg binary installation (via ffbinaries)
- ClojureScript support
deps.edn
{:deps {ffclj/ffclj {:mvn/version "0.2.0"}}}Leiningen / Boot
[ffclj/ffclj "0.2.0"]Probe a media file and get back a Clojure map:
(require '[ffclj.core :refer [ffprobe!]])
(let [result (ffprobe! [:show_format :show_streams "video.mp4"])
s (group-by (comp keyword :codec_type) (:streams result))]
[(:codec_name (first (:video s)))
(:codec_name (first (:audio s)))])
; => ["h264" "aac"]Transcode a file:
(require '[ffclj.core :refer [ffmpeg!]]
'[ffclj.task :as task])
(let [task (ffmpeg! [:y
:i "input.mov"
:ss "00:00:00.000"
:t "5"
[:s "1280x720" :acodec "aac" :vcodec "h264" "output.mp4"]])]
(try
(task/wait-for task)
(println "Done. Exit code:" (task/exit-code task))
(finally
(task/close task))))Stream progress events to a core.async channel as the transcode runs:
(require '[ffclj.core :refer [ffmpeg!]]
'[ffclj.task :as task]
'[clojure.core.async :as async])
(let [c (async/chan)
done (async/chan)
_ (async/go
(loop []
(let [[v _] (async/alts! [c])]
(when (and v (not= "end" (:progress v)))
(clojure.pprint/pprint v)
(recur))))
(async/close! done))
task (ffmpeg! c [:y
:i "input.mov"
:ss "00:00:00.000"
:t "30"
[:s "1280x720" :acodec "aac" :vcodec "h264" "output.mp4"]])]
(try
(task/wait-for task)
(async/<!! done)
(println "Done. Exit code:" (task/exit-code task))
(finally
(task/close task))))Each progress event is a map like:
{:frame "1177"
:fps "50.98"
:bitrate "1578.5kbits/s"
:total_size "9699376"
:out_time "00:00:49.156644"
:speed "2.13x"
:progress "continue"}Build filter expressions as data and pass them directly to ffmpeg!.
DSL form — composable records:
(require '[ffclj.filter :refer [filter chain filter-complex ->str]])
;; Simple scale — passed directly as a :vf value
(ffmpeg! [:y :f "lavfi" :i "testsrc=duration=5"
:vf (filter "scale" [1280 720])
"out.mp4"])
;; filter_complex with two inputs — chain references wire pads automatically
(let [pip (chain ["[1:v]"] [(filter "scale" [320 180])])]
(ffmpeg! [:y :i "bg.mp4" :i "overlay.mp4"
:filter_complex (filter-complex
pip
(chain ["[0:v]" pip] [(filter "overlay" [10 10])]))
"out.mp4"]))String form — pass raw FFmpeg filter strings directly:
;; Simple scale as a plain string
(ffmpeg! [:y :i "input.mp4"
:vf "scale=1280:720"
"out.mp4"])
;; filter_complex as a plain string
(ffmpeg! [:y :i "bg.mp4" :i "overlay.mp4"
:filter_complex "[1:v]scale=320:180[pip];[0:v][pip]overlay=10:10"
"out.mp4"])Both forms are equivalent — use the DSL when building expressions programmatically, strings when copying directly from FFmpeg documentation.
#!/usr/bin/env bb
(require '[babashka.deps :as deps])
(deps/add-deps '{:deps {ffclj/ffclj {:mvn/version "0.2.0"}}})
(require '[ffclj.core :refer [ffmpeg! ffprobe!]]
'[ffclj.filter :refer [filter chain filter-complex]]
'[ffclj.task :as task])
(let [result (ffprobe! [:show_format :show_streams "video.mp4"])
s (group-by (comp keyword :codec_type) (:streams result))
codecs [(:codec_name (first (:video s))) (:codec_name (first (:audio s)))]]
(println codecs))
(let [pip (chain ["[1:v]"] [(filter "scale" [320 180])])
t (ffmpeg! [:y
:i "bg.mp4"
:i "overlay.mp4"
:t 30
:filter_complex (filter-complex
pip
(chain ["[0:v]" pip] [(filter "overlay" [10 10])]))
:vcodec "libx264" :pix_fmt "yuv420p"
"output.mp4"])]
(try
(task/wait-for t)
(println "Done. Exit code:" (task/exit-code t))
(finally
(task/close t))))Functions in ffclj.task:
| Function | Description |
|---|---|
(task/wait-for t) |
Block until the process finishes |
(task/wait-for t timeout-ms) |
Block with a timeout (returns true if finished in time) |
(task/exit-code t) |
Returns the process exit code |
(task/success? t) |
Returns true if exit code is 0 |
(task/done? t) |
Returns true if the process has finished |
(task/stdout t) |
Returns the process stdout stream |
(task/stderr t) |
Returns the process stderr stream |
(task/stdin t) |
Returns the process stdin stream |
(task/close t) |
Destroys the process and closes streams |
Copyright © 2021-2026 Luis Santos
Distributed under the MIT License