0

I initially tried doing this as one command, but after struggling with it I wrote a quick script to create a bunch of intermediate files to get the job done, but that isn't quite working either.

The goal:

  • I have a bunch of video clips
  • I want to trim the start/ends of each one, then concat them all
  • I want there to be a fade between each one
  • I don't want to re-encode the other 99.9% of each video

So I wrote a quick script to generate ffmpeg commands that create trimmed clips, then a bunch of 2 second transition clips, then merge them all together. What I end up with:

ffmpeg -ss 30 -to 1010 -i part-1.mp4 -vcodec copy -an part-1-trimmed.mp4
ffmpeg -ss 32 -to 5458 -i part-2.mp4 -vcodec copy -an part-2-trimmed.mp4
ffmpeg -ss 22 -to 228 -i part-3.mp4 -vcodec copy -an part-3-trimmed.mp4
ffmpeg -ss 97 -to 3848 -i part-4.mp4 -vcodec copy -an part-4-trimmed.mp4
ffmpeg -ss 32 -to 98 -i part-5.mp4 -vcodec copy -an part-5-trimmed.mp4
ffmpeg -ss 92 -to 378 -i part-6.mp4 -vcodec copy -an part-6-trimmed.mp4
ffmpeg -ss 36 -to 5160 -i part-7.mp4 -vcodec copy -an part-7-trimmed.mp4
ffmpeg -ss 1010 -t 2 -i part-1.mp4 -ss 30 -t 2 -i part-2.mp4 -c:v libx265 -x265-params profile=main -pix_fmt yuvj420p -filter_complex "[0][1]xfade=transition=fade:duration=2[output]" -an -map "[output]" transition-1-2.mp4
ffmpeg -ss 5458 -t 2 -i part-2.mp4 -ss 20 -t 2 -i part-3.mp4 -c:v libx265 -x265-params profile=main -pix_fmt yuvj420p -filter_complex "[0][1]xfade=transition=fade:duration=2[output]" -an -map "[output]" transition-2-3.mp4
ffmpeg -ss 228 -t 2 -i part-3.mp4 -ss 95 -t 2 -i part-4.mp4 -c:v libx265 -x265-params profile=main -pix_fmt yuvj420p -filter_complex "[0][1]xfade=transition=fade:duration=2[output]" -an -map "[output]" transition-3-4.mp4
ffmpeg -ss 3848 -t 2 -i part-4.mp4 -ss 30 -t 2 -i part-5.mp4 -c:v libx265 -x265-params profile=main -pix_fmt yuvj420p -filter_complex "[0][1]xfade=transition=fade:duration=2[output]" -an -map "[output]" transition-4-5.mp4
ffmpeg -ss 98 -t 2 -i part-5.mp4 -ss 90 -t 2 -i part-6.mp4 -c:v libx265 -x265-params profile=main -pix_fmt yuvj420p -filter_complex "[0][1]xfade=transition=fade:duration=2[output]" -an -map "[output]" transition-5-6.mp4
ffmpeg -ss 378 -t 2 -i part-6.mp4 -ss 34 -t 2 -i part-7.mp4 -c:v libx265 -x265-params profile=main -pix_fmt yuvj420p -filter_complex "[0][1]xfade=transition=fade:duration=2[output]" -an -map "[output]" transition-6-7.mp4
echo -e "file 'part-1-trimmed.mp4'\nfile 'transition-1-2.mp4'\nfile 'part-2-trimmed.mp4'" > cat-files.txt
ffmpeg -f concat -i cat-files.txt -c copy final.mp4

This generates the final video, but the fade sections are broken (video player just skips over those 2 seconds), and I get loads of these errors:

Non-monotonous DTS in output stream 0:0; previous: 29398369, current: 29398268; changing to 29398370. This may result in incorrect timestamps in the output file.

The -x265-params profile=main -pix_fmt yuvj420p bit is to match the codec to the other source files I have, however they don't quite match:

source:

Stream #0:0[0x1](und): Video: hevc (Main) (hvc1 / 0x31637668), yuvj420p(pc, bt709), 3840x1920, 50052 kb/s, 29.97 fps, 29.97 tbr, 30k tbn (default)

generated transition clips:

Stream #0:0[0x1](und): Video: hevc (Main) (hev1 / 0x31766568), yuvj420p(pc, bt709, progressive), 3840x1920, 27685 kb/s, 29.97 fps, 29.97 tbr, 30k tbn (default)

I'm not sure how to get rid of progressive which is the only real difference I see.

My attempt at doing this without all the intermediate files (and only with 2 clips and trimming to 1 second clips for testing):

ffmpeg -i part-1.mp4 -i part-2.mp4 \
-filter_complex "[0:v]trim=start=227:duration=1[0_middle];\
[0:v]trim=start=228:duration=1[0_end];\
[1:v]trim=start=30:duration=1[1_start];\
[1:v]trim=start=31:duration=1[1_middle];\
[0_end][1_start]xfade=transition=fade:duration=1[fade_0_1];\
[0_middle][fade_0_1][1_middle]concat=n=3[output]" \
-map "[output]" \
output.mp4

However this a) reencodes everything (If I add -vcodec copy it tells me filtering and a streamcopy can't be used together, but I can't find another way of specifying this) and b) just hangs and eventually says:

More than 1000 frames duplicated

I'd prefer to do this without all the intermediate files, but help with either method is appreciated!

2
  • You want to merge part-1-trimmed.mp4 through part-7-trimmed.mp4, each with two second fade in/out of each one?
    – JayCravens
    Commented Jul 4 at 1:25
  • Correct. Without reencoding the majority of the video. It's 5 hours of 8k footage, I don't want to reencode it to add 12 seconds of transitions ;)
    – user1844116
    Commented Jul 4 at 9:52

1 Answer 1

0

Here's a bash script to splice the fades into the video's
It then merges into a single file.

#!/bin/bash

mkdir -p temp source

# change source container to mkv with ts to zero
for file in *.mp4; do
    filename="${file%.*}"
    ffmpeg -hide_banner -i "$file" -c copy -an -avoid_negative_ts make_zero "$filename".mkv && mv "$file" ./source
done

prepare_source() {
  for filename in *.mkv; do
    file_num=1
    ffmpeg -hide_banner -i "$filename" -to 10 -c copy -an -avoid_negative_ts make_zero "$file_num-seg1-$filename"
    
        ffprobe -report -i "$filename"
        duration=$(cat ffprobe*.log | grep DURATION | cut -d':' -f2-4 | tr -d ' ')
        rm ffprobe*.log

        # strip leading zeros
        hours=$(echo "$duration" | awk -F: '{print $1}' | sed 's/^0*//')
        minutes=$(echo "$duration" | awk -F: '{print $2}' | sed 's/^0*//')
        seconds=$(echo "$duration" | awk -F: '{print $3}' | cut -d '.' -f 1 | sed 's/^0*//')

        # if fields are 00 then set to 0
        hours=${hours:-0}
        minutes=${minutes:-0}
        seconds=${seconds:-0}

        runtime_seconds=$((hours * 3600 + minutes * 60 + seconds))
        runtime_minus=$((runtime_seconds - 10))

        echo -e "$hours $minutes $seconds\n$runtime_seconds\n$runtime_minus"

    ffmpeg -hide_banner -i "$filename" -ss 10 -to "$runtime_minus" -c copy -an -avoid_negative_ts make_zero "to_mux_$file_num-seg2-$filename"
    ffmpeg -hide_banner -i "$filename" -ss "$runtime_minus" -c copy -an -avoid_negative_ts make_zero "$file_num-seg3-$filename"
    file_num=$((file_num + 1))

  done
}

fade_in() {
  file_list1=(*seg1*.mkv)
  for file1 in "${file_list1[@]}"; do
    ffmpeg -hide_banner -y -i "$file1" -vf fade=in:0:60 -an -avoid_negative_ts auto -preset ultrafast "to_mux_$file1"
    mv "$file1" ./temp
  done
}

fade_out() {
  file_list2=(*seg3*.mkv)
  for file2 in "${file_list2[@]}"; do
    frame_count=$(ffmpeg -i "$file2" -map 0:v:0 -c copy -f null -y /dev/null 2>&1 | grep -Eo 'frame= *[0-9]+ *' | grep -Eo '[0-9]+' | tail -1)
    frame_start=$((frame_count - 60))
    ffmpeg -hide_banner -y -i "$file2" -vf fade=out:"$frame_start":60 -an -avoid_negative_ts auto -preset ultrafast "to_mux_$file2"
    mv "$file2" ./temp
  done
}

prepare_source
fade_in
fade_out

ls --quoting-style=shell-always -1v to_mux_*.mkv > tmp.txt
sed 's/^/file /' tmp.txt > list.txt && rm tmp.txt

ffmpeg -f concat -safe 0 -i list.txt -c copy -avoid_negative_ts auto -movflags +faststart faded_muxed_final.mkv

rm to_mux_* list.txt
rm -rf ./temp

exit 0

Save as: fade_splice.sh
Make executable: chmod +x fade_splice.sh
Usage: ./fade_splice.sh

Run from the directory containing only the trimmed files.

0

You must log in to answer this question.