I recently had to create a video montage at a pinch—bringing hundreds of photos and videos from a media collection into a single video. Straight away FFMPEG was calling me. Surely there's a single one liner that could put this together?
As far as I could tell, that didn't seem to be the case. Straight away the concat demuxer appealed to me, but the with same codecs caveat seemed to necessitate pre-processing steps. Likewise, the examples on the wiki showed pre-processing steps even for the concat protocol.
Given this, I let go of the one-line dream, and set to work on the following process:
concat
demuxer to turn all the temporary videos into a single output.Let's start the script with some set-up. Turn on some Bash settings to catch common errors, declare some configurable settings, and clean up outputs from previous runs:
#!/usr/bin/env bash
set -euo pipefail
SECONDS_PER_IMAGE=5
TMP_DIR=".tmp"
FILES_LIST="$TMP_DIR/.files"
OUTPUT="output.mp4"
VF="scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,fps=30"
rm -r "$TMP_DIR" "$OUTPUT" || true
mkdir "$TMP_DIR"
Where $VF
is the standard format chosen: 1920×1080 @ 30FPS, with black bars placed around images not matching the video's aspect ratio.
Next, all MP4 videos were found in the media collection, converted to a video in the chosen standard format saved to $TMP_DIR
, and the new video's filename added to the final output video's $FILES_LIST
:
find . -maxdepth 1 -regex '.*\(mp4\|MP4\)' | while read i; do
ffmpeg -y -i "$i" -vf "$VF" -ar 44100 "$TMP_DIR/$i" < /dev/null
echo "file '$i'" >> "$FILES_LIST"
done
The same is then done for all images (JPGs, PNGs, and BMPs) in the media collection, with the -loop 1
flag telling FFMPEG to loop over the image until -t $SECONDS_PER_IMAGE
seconds have elapsed:
find . -maxdepth 1 -regex '.*\(jpe?g\|png\|JPG\|bmp\)' | while read i; do
ffmpeg -y -loop 1 -i "$i" -t "$SECONDS_PER_IMAGE" -vf "$VF" \
"$TMP_DIR/$i.mp4" < /dev/null
echo "file '$i.mp4'" >> "$FILES_LIST"
done
Currently, all the media would appear in the same order with videos at the start. The shuf
command line tool is then used to shuffle $FILES_LIST
in place:
shuf -o "$FILES_LIST" "$FILES_LIST"
One step left, concatenating the videos, right? I hit a bump in the road here, with the concat
demuxer emitting a video with no audio if the first video had no audio. This happened a lot given most of the videos in $FILES_LIST
were audio-less videos made from the media collection's images.
Although this could've been solved within FFMPEG (using anullsrc to add blank audio to all videos in $FILES_LIST
, or to a single frame video at the start of $OUTPUT
), I fell back to Bash. The following snippet uses ffprobe
to find the first video in $FILES_LIST
with audio and moves it to the top of the list:
first=
while read f; do
fn="$(echo "$f" | sed "s/^file '//; s/'$//")"
if ffprobe "$fn" 2>&1 | grep -q 'Audio:'; then
first="$fn"
fi
done < "$FILES_LIST"
if [ -n "$first" ]; then
{ echo "file '$first'"; cat "$FILES_LIST"; } | awk '!seen[$0]++' > files
mv files "$FILES_LIST"
fi
Now the final step can take place: concatenating the videos in $FILES_LIST
into a single $OUTPUT
video:
ffmpeg -y -f concat -safe 0 -i "$FILES_LIST" "$OUTPUT"
A copy of the full script is available in the drop-down box below. Please note, the script assumes the media collection is located in the directory where the script is run.
#!/usr/bin/env bash
set -euo pipefail
SECONDS_PER_IMAGE=5
TMP_DIR=".tmp"
FILES_LIST="$TMP_DIR/.files"
OUTPUT="output.mp4"
VF="scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,fps=30"
rm -r "$TMP_DIR" "$OUTPUT" || true
mkdir "$TMP_DIR"
find . -maxdepth 1 -regex '.*\(mp4\|MP4\)' | while read i; do
ffmpeg -y -i "$i" -vf "$VF" -ar 44100 "$TMP_DIR/$i" < /dev/null
echo "file '$i'" >> "$FILES_LIST"
done
find . -maxdepth 1 -regex '.*\(jpe?g\|png\|JPG\|bmp\)' | while read i; do
ffmpeg -y -loop 1 -i "$i" -t "$SECONDS_PER_IMAGE" -vf "$VF" \
"$TMP_DIR/$i.mp4" < /dev/null
echo "file '$i.mp4'" >> "$FILES_LIST"
done
shuf -o "$FILES_LIST" "$FILES_LIST"
first=
while read f; do
fn="$(echo "$f" | sed "s/^file '//; s/'$//")"
if ffprobe "$fn" 2>&1 | grep -q 'Audio:'; then
first="$fn"
fi
done < "$FILES_LIST"
if [ -n "$first" ]; then
{ echo "file '$first'"; cat "$FILES_LIST"; } | awk '!seen[$0]++' > files
mv files "$FILES_LIST"
fi
ffmpeg -y -f concat -safe 0 -i "$FILES_LIST" "$OUTPUT"
FFMPEG is a tremendously powerful tool, one that requires a lot of practised knowledge to use effectively. Hopefully this brain dump helps point you in the right direction for whatever video creation problem you're currently trying to solve. And hopefully the notes help me next time FFMPEG comes calling.
Let me know below if you liked this content, or found it valuable, using the buttons below.
Like this?
© Ben Talbot. All rights reserved.