Last active
January 22, 2023 09:42
-
-
Save adam-james-v/6b2ee824d0040b82f91dbdd1f7fd5330 to your computer and use it in GitHub Desktop.
Clojure/babashka script to help automate some of my video editing pipeline
This file contains 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 | |
(ns vidwiz.main | |
"This is a prototype script for automating a portion of my video editing using ffmpeg." | |
(:require [clojure.java.shell :refer [sh]] | |
[clojure.string :as st] | |
[cheshire.core :refer [parse-string]])) | |
;; util | |
(defn get-extension | |
[fname] | |
(re-find #"\.[A-Za-z\d+]+" fname)) | |
;; thanks to Burin (@burinc) for an improved impl. | |
(defn get-resolution | |
[fname] | |
(when-let [{:keys [width height]} | |
(-> (sh "ffprobe" | |
"-v" | |
"error" | |
"-select_streams" | |
"v:0" | |
"-show_entries" | |
"stream=width,height" | |
"-of" "json" | |
fname) | |
:out | |
(parse-string true) | |
:streams | |
first)] | |
[width height])) | |
(defn overlay-offsets | |
[{:keys [border base-dims overlay-dims pos gap fname]}] | |
(let [{:keys [width]} border | |
[cw ch] (map #(+ (* 2 width) %) overlay-dims) | |
{:keys [h v]} pos | |
[sw sh] base-dims] | |
[(cond (= h :l) gap | |
(= h :c) (- (/ sw 2) (/ cw 2)) | |
(= h :r) (- sw gap cw)) | |
(cond (= v :t) gap | |
(= v :c) (- (/ sh 2) (/ ch 2)) | |
(= v :b) (- sh gap ch))])) | |
(defn get-bg-color | |
[fname] | |
(let [nfname (st/replace fname (get-extension fname) ".png")] | |
(sh "ffmpeg" "-i" fname | |
"-frames:v" "1" | |
"-filter_complex" | |
(str "[0:v]crop=4:4:100:500") | |
"-y" (str nfname)) | |
(sh "convert" nfname "-colors" "1" nfname) | |
(let [col (->> (sh "identify" "-verbose" nfname) | |
:out | |
(st/split-lines) | |
(drop-while #(not (st/includes? % "Histogram"))) | |
(second) | |
(re-find #"\#......"))] | |
(sh "rm" nfname) | |
col))) | |
(defn crop-pad-screen | |
"A multi-step transformation for screen recording footage. | |
The following sequence of transforms are handled using ffmpeg's 'filter_complex': | |
- crop and pad screen recording | |
- cut screen footage into left side and right side | |
- create a 1920x1080 image with the background color as the fill | |
- stitch left and right side back together | |
- overlay stitched screen recording onto the bg image with calculated offset values" | |
[{:keys [fname left right] :as m}] | |
(let [[w h] (get-resolution fname) | |
props (merge m {:border {:width 0 :color ""} | |
:base-dims [1920 1080] | |
:overlay-dims [(+ (:width left) (:width right)) h]}) | |
[ow oh] (overlay-offsets props) | |
col (get-bg-color fname)] | |
(sh "ffmpeg" | |
"-i" fname | |
"-f" "lavfi" | |
"-i" (str "color=" col ":s=1920x1080") | |
"-filter_complex" | |
(str "[0:v]crop=" (:width left) ":" h ":" (:offset left) ":0[l];" | |
"[0:v]crop=" (:width right) ":" h ":" (- w (:width right) (:offset right)) ":0[r];" | |
"[l][r]hstack=inputs=2[scr];" | |
"[1:v][scr]overlay=" ow ":" oh ":shortest=1") | |
"-c:a" "copy" "-y" "cropped-screen.mov"))) | |
(defn clap-time | |
"Find time in seconds at which a clap is detected in the audio stream of fname. | |
The detection assumes that a clap sound exists within the first 12 seconds of a given clip." | |
[fname] | |
(->> (sh "ffmpeg" "-i" fname | |
"-ss" "00:00:00" "-t" "00:00:12" | |
"-af" "silencedetect=noise=0.5:d=0.01" | |
"-f" "null" "-") | |
:err | |
(st/split-lines) | |
(drop-while #(not (st/includes? % "silence_end:"))) | |
(first) | |
(re-find #"silence_end: .+") | |
(re-find #"\d+\.\d+") | |
(read-string))) | |
(defn overlay-camera | |
"Composes the final footage by overlaying the camera footage onto the screen footage according to given properties. | |
The composition is handled using ffmpeg's 'filter_complex', and several actions occur: | |
- overlays camera footage with border onto screen footage | |
- given screen footage, camera footage, and border width and color create combined video | |
- calculate camera delay using clap times in footage. assumes screen recording is longer than cam | |
- calculate size of border for camera | |
- create border as a solid color frame | |
- scale camera down to given overlay-dims | |
- overlay camera onto border frame | |
- overlay bordered camera onto screen footage with calculated offsets" | |
[{:keys [border overlay-dims camf scrf] :as props}] | |
(let [{:keys [width color]} border | |
[cw ch] (map #(+ (* 2 width) %) overlay-dims) | |
[ow oh] (overlay-offsets (assoc props :fname scrf | |
:base-dims (get-resolution scrf))) | |
delay (- (clap-time scrf) (clap-time camf))] | |
(sh "ffmpeg" | |
"-i" scrf | |
"-i" camf | |
"-f" "lavfi" | |
"-i" (str "color=" color ":s=" cw "x" ch) | |
"-filter_complex" | |
(str "[1:v]scale=" (apply str (interpose "x" overlay-dims)) "[scv];" | |
"[2:v][scv]overlay=" width ":" width ":shortest=1[cam];" | |
"[cam]setpts=PTS-STARTPTS+" delay "/TB[dcam];" | |
"[0:v][dcam]overlay=" ow ":" oh ":shortest=1") | |
"-c:a" "copy" "-y" "merged.mov"))) | |
(defn fix-audio | |
"Fixes issue where mono audio track plays only to the Left channel." | |
[fname] | |
(sh "ffmpeg" "-i" fname | |
"-i" fname "-af" "pan=mono|c0=FL" | |
"-c:v" "copy" "-map" "0:v:0" "-map" "1:a:0" "fixed-audio.mov")) | |
#_(spit "props.edn" | |
{:screen | |
{:fname "scr.mov" | |
:left {:width 667 :offset 0} ;; offset from left side | |
:right {:width 750 :offset 0} ;; offset from right side | |
:gap 100 | |
:pos {:h :l :v :c}} ;; :h can be [:l :c :r] :v can be [:t :c :b] | |
:camera | |
{:camf "cam.mov" | |
:scrf "cropped-screen.mov" ;; this is the hardcoded output filename from (crop-pad-screen fname) | |
:border {:width 7 :color "cyan"} | |
:overlay-dims [480 270] ;; dims of camera excluding border | |
:gap 70 | |
:pos {:h :r :v :b}}}) | |
(defn main | |
"Main runs when vidwiz is run as a script. | |
You can run this program with babashka: | |
- chmod +x vidwiz.clj | |
- ./vidwiz props.edn" | |
[] | |
(let [fname (first *command-line-args*) | |
props (when (= (get-extension fname) ".edn") | |
(read-string (slurp fname)))] | |
(when props | |
(crop-pad-screen (:screen props)) | |
(overlay-camera (:camera props)) | |
(fix-audio "merged.mov")))) | |
(main) |
I found that
get-resolution
method match something else that is not intended.
Since Babashka have support library for Json, in this case cheshire so I refactoring the code as follow:(require '[cheshire.core :refer [parse-string]]) (defn get-resolution [fname] (when-let [{:keys [width height]} (-> (sh "ffprobe" "-v" "error" "-select_streams" "v:0" "-show_entries" "stream=width,height" "-of" "json" fname) :out (parse-string true) :streams first)] [width height]))And it seems to be working better
@burinc, thanks for your contribution! I've updated the gist with your implementation. Gave it a try in my terminal and it's a nice improvement for sure :).
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I found that
get-resolution
method match something else that is not intended.Since Babashka have support library for Json, in this case cheshire so I refactoring the code as follow:
And it seems to be working better