ffmpegslideshow

How to apply the same FFMPEG slide transition for every slide in a sequence?


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.


Solution

  • 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.