I have an ffmpeg
command to create an MP4 video from a sequence of N x JPEG slides. That is, I do not know how many slides there are in the sequence, it is just a directory of JPEG-s.
I'd like to apply a common slide transition for every slide in the sequence. All the examples I've read seem to need to know how many slides there are before the filter is written. The idea is to use one (simple) transition/filter for every slide.
So far the script looks like this:
play_duration="-framerate 1/10" # 10 seconds for testing
ffmpeg \
${play_duration} \
-pattern_type glob \
-i "./slides/*.jpg" \
\
-c:v libx264 \
-filter_complex \
"pad=ceil(iw/2)*2:ceil(ih/2)*2; fade=out:120:30" \
\
./Slideshow.mp4
The first filter: "pad=...
" is necessary to deal with inconsistiencies in the JPEG input.
My limited appreciation here is that the "fade=out:120:30
" filter ought to work if I didn't need to also have the pad=
construct.
Example transition examples for I've come across so far -- there are a great many variations on the same pattern -- all look a lot like this ...
ffmpeg -loop 1 -t 3 -framerate 60 -i image1.jpg -loop 1 -t 3 \
-framerate 60 -i image2.jpg -loop 1 -t 3 -framerate 60 -i image3.jpg \
-filter_complex \
"[0]scale=1920:1280:force_original_aspect_ratio=decrease,pad=1920:1280:-1:-1[s0]; [1]scale=1920:1280:force_original_aspect_ratio=decrease,pad=1920:1280:-1:-1[s1]; [2]scale=1920:1280:force_original_aspect_ratio=decrease,pad=1920:1280:-1:-1[s2]; [s0][s1]xfade=transition=circleopen:duration=1:offset=2[f0]; [f0][s2]xfade=transition=circleopen:duration=1:offset=4" \
-c:v libx264 -pix_fmt yuv420p \
output.mp4
The requirement is to have the same filter/transition which will apply to all slide changes. It sounded so easy at first.
Don't let odd numbered WxH's anywhere near ffmpeg
.
Just pre-procces the image size and save yourself the headache.
Using -vf "pad=ceil(iw/2)*2:ceil(ih/2)*2"
to avoid "not divisible by 2" works, at first, but it's error prone and seems unreliable.
So, I made a function to prepare the images, leaving only video for ffmpeg
to handle. Default is 1080p.
#!/bin/bash
process_images() {
file_list=(*.jpg)
for jpeg_file in "${file_list[@]}"; do
source_height=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=nw=1:nk=1 "$jpeg_file")
if (( source_height % 2 != 0 )); then
target_height=$((source_height - 1))
else
target_height=$source_height
fi
source_width=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of default=nw=1:nk=1 "$jpeg_file")
if (( source_width % 2 != 0 )); then
target_width=$((source_width - 1))
else
target_width=$source_width
fi
echo "$jpeg_file" "$target_width"x"$target_height"
convert "$jpeg_file" -resize "$target_width"x"$target_height" new_"$jpeg_file"
done
}
make_videos() {
target_height=1080
file_list=(new_*.jpg)
for post_jpeg in "${file_list[@]}"; do
filename="${post_jpeg%.*}"
ffmpeg -hide_banner -framerate 1/10 -i "$post_jpeg" -pix_fmt yuv420p -c:v libx264 -r 30 -crf 12 -avoid_negative_ts auto "$filename".mkv && rm "$post_jpeg"
done
}
fade_in() {
file_list1=(in_*.mkv)
echo "Executing fade_in"
for file1 in "${file_list1[@]}"; do
ffmpeg -hide_banner -i "$file1" -vf fade=in:0:30 -crf 12 -preset fast "out_$file1"
done
mv in_*.mkv ./temp
}
fade_out() {
file_list2=(out_*.mkv)
echo "Executing fade_out"
for file2 in "${file_list2[@]}"; do
frame_count=$(ffprobe -v error -select_streams v:0 -count_packets -show_entries stream=nb_read_packets -of csv=p=0 "$file2")
frame_start=$((frame_count - 30))
ffmpeg -hide_banner -i "$file2" -vf fade=out:"$frame_start":30 -crf 12 -preset fast "to_mux_$file2"
done
mv out_in_*.mkv ./temp
}
process_images
make_videos
mkdir -p ./temp
for file in *.mkv; do
mv "$file" "in_$file"
done
fade_in
fade_out
ls --quoting-style=shell-always -1v *.mkv > tmp.txt
sed 's/^/file /' tmp.txt > list.txt && rm tmp.txt
ffmpeg -hide_banner -f concat -safe 0 -i list.txt -c:v libx264 -crf 18 -avoid_negative_ts auto -movflags +faststart "muxed_final.mkv"
mv to_mux_*.mkv ./temp
rm -rf ./temp list.txt
exit 0
Save as: sshow_fade.sh
Change mode executable: chmod +x sshow_fade.sh
Usage: ./sshow_fade.sh
Run the script inside the folder with the jpeg files.
The fade duration is in frames. fade=in:0:30
fade=out:"$frame_start":30
The 30 is 1 second, 60 for 2 seconds etc.
I already had the fade in/out script. My idea: make each image a video first.
Then use the fade effect on the video's & concatenate.
The jpeg's I used and the video. It looks good.
Update 2: The above link has a .zip
demonstrating how to add audio to each slide, with example. I figured, why not, it's the only thing missing other than captions. I'll add those also, since convert
will add text with ease.