Skip to content

luissantos/ffclj

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FFCLJ

Clojars Project


Features

  • ffmpeg! — run ffmpeg transcodes from Clojure
  • ffprobe! — probe media files, returns parsed Clojure maps
  • Filter graph DSL — build -vf, -af, and -filter_complex expressions as data
  • Progress events streamed via core.async channels
  • Works with Babashka

Planned

  • ffmpeg binary installation (via ffbinaries)
  • ClojureScript support

Installation

deps.edn

{:deps {ffclj/ffclj {:mvn/version "0.2.0"}}}

Leiningen / Boot

[ffclj/ffclj "0.2.0"]

Usage

ffprobe

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"]

ffmpeg

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))))

ffmpeg + Progress Listener

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"}

Filters

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.


Babashka

#!/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))))

Task API

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

License

Copyright © 2021-2026 Luis Santos

Distributed under the MIT License

About

Clojure ffmpeg wrapper

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors