Video pipelines?

Howdy Yall,

surely there must be someone who has their video creation pipeline so streamed its partially automated. I’m almost to the point where I’m tempted to make makefiles for rendering video and uploading. But before I do that, Any suggestions, insight or inspiration? Thank you

Are you comfortable with ffmpeg?

Here is a brief overview of my automation process for producing YouTube videos of the worship experience at my church. The majority of the work is ffmpeg embedded in a bash script. My post-production time went from 3 hours+/- to less than 10 minutes.

It is somewhat complex at the beginning of the process – there is an unknown number of files (1 to 3) that need to be concatenated and then trimmed down to the millisecond.

  • Lower thirds are automatically added.
  • Branding logo automatically superimposed.
  • Audio ripped from the video and bumpers added
  • Audio tags populated – including album art

Here is a brief description: https://markcain.com/ffmpeg.html

#!/bin/bash
##########
#
#  process video files and create the podcast
#
#  Mark Cain
#  last modified: January 19, 2020
#
#
# What I would like to do next is process the speaker's name to populate the mp3 Tags and determine
# which intro bumper to use.  I could pass the speakers name into the script as an arguement.  I would
# need to increment the argument count.
#
###############

# set the following variable to the speaker's name
speaker_name="Peter hislastname";


# process the speaker's name to generate the lower_third_reference i.e. lowercase, underscore for spaces
lower_third_reference=$(echo $speaker_name |  awk '{print tolower($0)}' | sed -e 's/ /_/');

# is the speaker "Peter hislastname" then populate bumper_reference with $lower_third_reference
if [ "$speaker_name" = "Peter hislastname" ]; then
	bumper_reference=$lower_third_reference;
else
	bumper_reference="generic";
fi

#
# Count the number of parameters passed into the script
# if the number of parameters is not within the range, halt the script and complain
#
if [[ $# -lt 4 ]] || [[ $# -gt 6 ]]
then
	printf "Looks like I didn't get the right number of parameters\n"
	printf " \n"
	printf "If you are processing a single file (trimming a video clip), I need 4 parameters:\n" 
	printf "    1) File Number \n"
	printf "    2) Starting time in minutes and seconds\n"
	printf "    3) Ending time in minutes and seconds\n"
	printf "    4) Series Title, Series Sequence Number, and Message Title\n"
	printf " \n"
	printf "    like this:\n"
	printf " \n"
	printf "        $0 1 12:14 22:45 \"Series Title - Part 2 - Message Title\"\n"
	printf " \n"
	printf " \n"
	printf "If you are processing 2 files (joining or spanning 2 files), I need 5 parameters:\n"
	printf "    1) First File Number \n"
	printf "    2) Starting time in minutes and seconds\n"
	printf "    3) Second File Number \n"
	printf "    4) Ending time in minutes and seconds\n"
	printf "    5) Series Title, Series Sequence Number, and Mesage Title\n"
	printf " \n"
	printf "    like this:\n"
	printf " \n"
	printf "        $0 1 12:14 2 22:45 \"Series Title - Part 2 - Message Title\"\n"
	printf " \n"
	printf " \n"
	printf "If you are processing 3 files (joining or spanning 3 files), I need 6 parameters:\n" 
	printf "    1) First File Number \n"
	printf "    2) Starting time in minutes and seconds\n"
	printf "    3) Second File Number \n"
	printf "    4) Third File Number \n"
	printf "    5) Ending time in minutes and seconds\n"
	printf "    6) Series Title, Series Sequence Number, and Mesage Title\n"
	printf " \n"
	printf "    like this:\n"
	printf " \n"
	printf "        $0 1 12:14 2 3 22:45 \"Series Title - Part 2 - Message Title\"\n"
	printf " \n"
	printf " \n"
	exit
fi

##########################
#
# set a time variable to use for script duration calculations
STARTTIME=$(date +%s)

# Start processing the parameters passed into the script
# if there are 4 parameters set the variables as needed
if [ $# -eq 4 ]
then
	# Assign the paraments to variables
	videos=1
	video1=$1
	video1_start=$2
	video1_end=$3

	# Calcuate the starting point as expressed in seconds
	minutes_start=$(echo $2 | cut -d: -f1)
	seconds_start=$(echo $2 | cut -d: -f2)
	start_in_seconds=$(echo "$minutes_start * 60 + $seconds_start" | bc -l)

	# Calculate the ending point as expressed in seconds
	minutes_end=$(echo $3 | cut -d: -f1)
	seconds_end=$(echo $3 | cut -d: -f2)
	ending_in_seconds=$(echo "$minutes_end * 60 + $seconds_end" | bc -l)

	# calculate the duration of the clip in seconds
	duration_in_seconds=$(echo "$ending_in_seconds - $start_in_seconds" | bc -l)

	#parse the string that was passed via agruement to get the series, part, and title

		# the way the folowing code works is that the 4th variable is piped into the cut command
		# the cut command has the option to specifiy the delimiter as a hyphen "-" hence: -d-
		# then we are look for the first field of the delimeter. hence: -f1
		# then the result is piped through sed to remove any leading spaces
		# then it is piped through sed again to remove any trailing spaces

		# store the 1st field as the series
		tag_series=$(echo $4 | cut -d- -f1 | sed -e 's/^ //' | sed -e 's/ $//')

		# store the 2nd field as the part after removing leading space
		tag_part=$(echo $4 | cut -d- -f2 | sed -e 's/^ //' | sed -e 's/ $//')

		# Grab the number element of Part and store it as a track number
		tag_track=$(echo ${tag_part: -1})

		# store the 3rd field as the title after removing leading space
		tag_title=$(echo $4 | cut -d- -f3 | sed -e 's/^ //' | sed -e 's/ $//')

		# Build the filename. Cut the beginning and trailing spaces; then 
		# remove the spaces in the filename -- use underscores instead
		filename=$(echo $4 | sed -e 's/^ //' | sed -e 's/ $//' | sed -e 's/ /_/g')

fi

# if there are 5 parameters set the variables as follows
if [ $# -eq 5 ]
then
	# Assign the paraments to variables
	videos=2

	# format the name of the video file so that it is xxxxx.MTS where leading zeros
	# proceed the actual number of the video and the extension is ".MTS"

		video1=$(echo $1 | awk '{printf "%05d.MTS", $1}')

	# format the start time of the video so that it is in HH:MM:SS.mmm
	# The current file system in the camera is exfat which limits the file size to 4.2 Gigs which is
	# roughly 26 minuts of 1080p video so there will never be a need to test for the hour segment.
	# The hour segment can always default to 00.  The following code supplies 00 for the hour
	# segment, makes the minutes segment 2 digits with a leading zero if needed, makes the
	# seconds segment 2 digits with a fragment of a second expressed in thousands
	# Thus, 6:12.15 passed into the script will be reformated to 00:06:12.150	

		video1_start=$(echo $2 | awk -F: '{printf "00:%02d:%06.3f" , $1, $2}')

	# format the name of the video file so that it is xxxxx.MTS where leading zeros
	# proceed the actual number of the video and the extension is ".MTS"

		video2=$(echo $3 | awk '{printf "%05d.MTS", $1}')

	# format the end time of the video so that it is in HH:MM:SS.mmm
	# The current file system in the camera is exfat which limits the file size to 4.2 Gigs which is
	# roughly 26 minuts of 1080p video so there will never be a need to test for the hour segment.
	# The hour segment can always default to 00.  The following code supplies 00 for the hour
	# segment, makes the minutes segment 2 digits with a leading zero if needed, makes the
	# seconds segment 2 digits with a fragment of a second expressed in thousands
	# Thus, 6:12.15 passed into the script will be reformated to 00:06:12.150	

		video2_end=$(echo $4 | awk -F: '{printf "00:%02d:%06.3f" , $1, $2}')

	#parse the string that was passed via agruement to get the series, part, and title

		# the way the folowing code works is that the 5th variable is piped into the cut command
		# the cut command has the option to specifiy the delimiter as a hyphen "-" hence: -d-
		# then we are look for the first field of the delimeter. hence: -f1
		# then the result is piped through sed to remove any leading spaces
		# then it is piped through sed again to remove any trailing spaces

	
		# store the 1st field as the series
		tag_series=$(echo $5 | cut -d- -f1 | sed -e 's/^ //' | sed -e 's/ $//')

		# store the 2nd field as the part after removing leading space
		tag_part=$(echo $5 | cut -d- -f2 | sed -e 's/^ //' | sed -e 's/ $//')  

		# Grab the number element of Part and store it as a track number
		tag_track=$(echo ${tag_part: -1})

		# store the 3rd field as the title after removing leading space
		tag_title=$(echo $5 | cut -d- -f3 | sed -e 's/^ //' | sed -e 's/ $//')

		# Build the filename. Cut the beginning and trailing spaces; then 
		# remove the spaces in the filename -- use underscores instead
		filename=$(echo $5 | sed -e 's/^ //' | sed -e 's/ $//' | sed -e 's/ /_/g')

fi

# if there are 6 parameters set the variables as follows
if [ $# -eq 6 ]
then
	# Assign the paraments to variables
	videos=3

	# format the name of the video file so that it is xxxxx.MTS where leading zeros
	# proceed the actual number of the video and the extension is ".MTS"

		video1=$(echo $1 | awk '{printf "%05d.MTS", $1}')

	# format the start time of the video so that it is in HH:MM:SS.mmm
	# The current file system in the camera is exfat which limits the file size to 4.2 Gigs which is
	# roughly 26 minuts of 1080p video so there will never be a need to test for the hour segment.
	# The hour segment can always default to 00.  The following code supplies 00 for the hour
	# segment, makes the minutes segment 2 digits with a leading zero if needed, makes the
	# seconds segment 2 digits with a fragment of a second expressed in thousands
	# Thus, 6:12.15 passed into the script will be reformated to 00:06:12.150	

		video1_start=$(echo $2 | awk -F: '{printf "00:%02d:%06.3f" , $1, $2}')

	# format the name of the video file so that it is xxxxx.MTS where leading zeros
	# proceed the actual number of the video and the extension is ".MTS"

		video2=$(echo $3 | awk '{printf "%05d.MTS", $1}')

	# format the name of the video file so that it is xxxxx.MTS where leading zeros
	# proceed the actual number of the video and the extension is ".MTS"

		video3=$(echo $4 | awk '{printf "%05d.MTS", $1}')

	# format the end time of the video so that it is in HH:MM:SS.mmm
	# The current file system in the camera is exfat which limits the file size to 4.2 Gigs which is
	# roughly 26 minuts of 1080p video so there will never be a need to test for the hour segment.
	# The hour segment can always default to 00.  The following code supplies 00 for the hour
	# segment, makes the minutes segment 2 digits with a leading zero if needed, makes the
	# seconds segment 2 digits with a fragment of a second expressed in thousands
	# Thus, 6:12.15 passed into the script will be reformated to 00:06:12.150	

		video3_end=$(echo $5 | awk -F: '{printf "00:%02d:%06.3f" , $1, $2}')

	#parse the string that was passed via agruement to get the series, part, and title

		# the way the folowing code works is that the 6th variable is piped into the cut command
		# the cut command has the option to specifiy the delimiter as a hyphen "-" hence: -d-
		# then we are look for the first field of the delimeter. hence: -f1
		# then the result is piped through sed to remove any leading spaces
		# then it is piped through sed again to remove any trailing spaces

	
		# store the 1st field as the series
		tag_series=$(echo $6 | cut -d- -f1 | sed -e 's/^ //' | sed -e 's/ $//')

		# store the 2nd field as the part after removing leading space
		tag_part=$(echo $6 | cut -d- -f2 | sed -e 's/^ //' | sed -e 's/ $//')  

		# Grab the number element of Part and store it as a track number
		tag_track=$(echo ${tag_part: -1})

		# store the 3rd field as the title after removing leading space
		tag_title=$(echo $6 | cut -d- -f3 | sed -e 's/^ //' | sed -e 's/ $//')

		# Build the filename. Cut the beginning and trailing spaces; then 
		# remove the spaces in the filename -- use underscores instead
		filename=$(echo $6 | sed -e 's/^ //' | sed -e 's/ $//' | sed -e 's/ /_/g')

fi

######################################
#
# Here are the modules that will handle the work flow
#
#

function one_video () {
	printf "We will be processing 1 video";
	printf "Using $video1 from $video1_start to $video1_end ";

	# check to see if there is an old file that needs to be deleted
	remove_this $filename.mp4

	# meaning of the options
		# -ss is the starting point either in hh:mm:ss.ttt or simply in seconds
		# -i is the input file 
		# -vcodec is the video codec and h264_nvenc exploits the NVIDIA GPU
		# the flags, pixel format and video filter is best for Youtube videos
		# -b:v is the bit rate for the video. 2M is 2 megs per second
		# -t is the duration of the output file expressed in seconds
		 
	ffmpeg -hide_banner \
		-ss 00:$video1_start \
		-i 0000$video1.MTS \
		-i ./constants/hcc_logo.svg \
		-i ./constants/lower_thirds/name_animation_$lower_third_reference.mp4 \
		-filter_complex "[1:v]scale=400:122[logo]; \
            [0:a]dynaudnorm=m=45:p=1:b=1[a2]; \
            [a2]volume=1.5[a3]; \
            [a3]asplit[a][a4]; \
		[0:v][logo]overlay=x=main_w-overlay_w-80:y=main_h-overlay_h-80[v-l]; \
		[v-l][2:v]overlay=x=0:y=0:enable='between(t,0,12.5)'[v]" \
		-vcodec h264_nvenc -preset fast \
		-flags +cgop -pix_fmt yuv420p \
		-b:v 2M -movflags +faststart \
		-map "[v]" -map "[a]" -t $duration_in_seconds $filename.mp4 \
            -map "[a4]" -t $duration_in_seconds -acodec libmp3lame bumperless_audio.mp3

 }


two_videos() {
	printf "We will be processing 2 videos\n\n";
	printf "Starting with $video1 at $video1_start and cutting $video2 at $video2_end\n\n";

	#
	# lets clean up from the last job if the files are still hanging around

		# test for the existence of the regular file trimmed1.MTS and delete if found
		remove_this trimmed1.MTS

		# test for the existence of the regular file trimmed2.MTS and delete if found
		remove_this trimmed2.MTS


    #########
    #
    # Output the first video from the start time (hh:mm:ss) to the end into a file names trimmed1.MTS
    # -hide_banner suppresses the build info and copywrite text
    # -loglevel fatal only shows fatal errors which are errors after which the prcess absolultely cannot contine
    # the "ss" moves to that position.  Using the -ss before the -i is faster than the -i before the -ss.
    # the "i" indicates the input file
	#
	printf "Trimming the first video \n";
    #
	ffmpeg -hide_banner -loglevel 32 -ss $video1_start \
	 	-i $video1 -codec copy trimmed1.MTS

    #######
    #
    # Output the second video from the beginning to the end time (hh:mm:ss) into a file names trimmed2.MTS
    # -hide_banner suppresses the build info and copywrite text
    # -loglevel fatal only shows fatal errors which are errors after which the prcess absolultely cannot contine
    # the "ss" moves to that position.  
    # the "i" indicates the input file
	#
	printf "Trimming the second video \n";
    #
	ffmpeg -hide_banner -loglevel 32 -i $video2 -ss 00:00:00.1 \
		-to $video2_end -codec copy trimmed2.MTS

    ########
    #
    # Combine the 2 Videos into one file
    # -hide_banner suppresses the build info and copywrite text
    # -loglevel fatal only shows fatal errors which are errors after which the prcess absolultely cannot contine
    #
    # the "i" indicates the input file which in this case is the concatenated files
	#
	printf "Combining the two videos \n";

	# check to see if there is an old file that needs to be deleted
	remove_this $filename.mp4


  ffmpeg -hide_banner -loglevel 32 \
            -i trimmed1.MTS \
		-i trimmed2.MTS \
            -i ./constants/hcc_logo.svg \
            -i ./constants/lower_thirds/name_animation_$lower_third_reference.mp4 \
            -filter_complex "[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1[vv][a1]; \
            [a1]dynaudnorm=m=45:p=1:b=1[a2]; \
            [a2]volume=1.5[a3]; \
            [a3]asplit[a][a4]; \
            [2:v]scale=400:122[logo]; \
            [vv][logo]overlay=x=main_w-overlay_w-80:y=main_h-overlay_h-80[v-l]; \
            [v-l][3:v]overlay=x=0:y=0:enable='between(t,0,12.5)'[v]" \
            -vcodec h264_nvenc -preset fast \
            -flags +cgop -pix_fmt yuv420p \
            -b:v 2M -movflags +faststart \
            -map "[v]" -map "[a]" $filename.mp4 \
            -map "[a4]" -acodec libmp3lame bumperless_audio.mp3


	#finished with the files trimmed1.MTS and trimmed2.MTs; so delete them
		remove_this trimmed1.MTS
		remove_this trimmed2.MTS

 } 

three_videos() {

	printf "We will be processing 3 videos";
	printf "Starting with $video1 at $video1_start, adding all of $video2 and cutting $video3 at $video3_end";

	# test for the existence of the regular file trimmed1.MTS and delete if found
		remove_this trimmed1.MTS

	# test for the existence of the regular file trimmed2.MTS and delete if found
		remove_this trimmed2.MTS

	# test for the existence of the regular file trimmed3.MTS and delete if found
		remove_this trimmed3.MTS

	
	#########
	#
	# Output the first video from the start time (hh:mm:ss) to the end into a file names trimmed1.MTS
	# -hide_banner suppresses the build info and copywrite text
	# -loglevel fatal only shows fatal errors which are errors after which the prcess absolultely cannot contine
	# the "ss" moves to that position 
	# the "i" indicates the input file
	#
	ffmpeg -hide_banner -loglevel 32 \
		-ss $video1_start -i $video1 -codec copy trimmed1.MTS

	#########
	#
	# Output the second video in it's entirety into a file names trimmed2.MTS
	# -hide_banner suppresses the build info and copywrite text
	# -loglevel fatal only shows fatal errors which are errors after which the prcess absolultely cannot contine
	# the "ss" moves to that position 
	# the "i" indicates the input file
	#
	ffmpeg -hide_banner -loglevel 32 \
		-i $video2 -codec copy trimmed2.MTS

    ########
    #
    # Output the third video from the beginning to the end time (hh:mm:ss) into a file names trimmed3.MTS
    # -hide_banner suppresses the build info and copywrite text
    # -loglevel fatal only shows fatal errors which are errors after which the prcess absolultely cannot contine
    # the "ss" moves to that position
    # the "i" indicates the input file
    #
	ffmpeg -hide_banner -loglevel 32 \
		-i $video3 -ss 00:00:00.1 -to $video3_end -codec copy trimmed3.MTS


    ########
    #
    # Combine the 3 Videos into one file
    # -hide_banner suppresses the build info and copywrite text
    # -loglevel fatal only shows fatal errors which are errors after which the prcess absolultely cannot contine
    # the "ss" moves to that position
    # the "i" indicates the input file
    #

	# check to see if there is an old file that needs to be deleted
		remove_this $filename.mp4

    ffmpeg -hide_banner -loglevel 32 \
            -i trimmed1.MTS -i trimmed2.MTS -i trimmed3.MTS \
            -i ./constants/hcc_logo.svg \
            -i ./constants/lower_thirds/name_animation_$lower_third_reference.mp4 \
            -filter_complex "[0:v][0:a][1:v][1:a][2:v][2:a]concat=n=3:v=1:a=1[vv][a1]; \
            [a1]dynaudnorm=m=45:p=1:b=1[a2]; \
            [a2]volume=1.5[a3]; \
            [a3]asplit[a][a4]; \
            [3:v]scale=400:122[logo]; \
            [vv][logo]overlay=x=main_w-overlay_w-80:y=main_h-overlay_h-80[v-l]; \
            [v-l][4:v]overlay=x=0:y=0:enable='between(t,0,12.5)'[v]" \
            -vcodec h264_nvenc -preset slow \
            -flags +cgop -pix_fmt yuv420p \
            -b:v 2M -movflags +faststart \
            -map "[v]" -map "[a]" $filename.mp4 \
            -map "[a4]" -acodec libmp3lame bumperless_audio.mp3


	#finished with the files trimmed1.MTS, trimmed2.MTS, and trimmed3.MTs; so delete them
		remove_this trimmed1.MTS
		remove_this trimmed2.MTS
		remove_this trimmed3.MTS

 }

# test for the existence of the regular file passed into the function and delete if found
remove_this() {

	if [ -e $1 ] ; then
		rm ./$1
	fi
 }


if [ $videos -eq 1 ]
then
	one_video
fi

if [ $videos -eq 2 ]
then
	two_videos
fi

if [ $videos -eq 3 ]
then
	three_videos
fi


#######
# text an alert that the file is ready
#
echo "The video for Hope City is finished and ready to be uploaded to Youtube. " | ./twilio-sms XXX-XXX-XXXX

printf "\n\n"
printf "###############\n\n"
printf "#      Step 1 of 2 - Joining the intro bumper, content, and outtro bumper and populating the ID3 tags"
printf "###############\n\n"

    ffmpeg -hide_banner -loglevel 32 \
    -i "concat:./constants/bumpers/bumper_intro_$bumper_reference.mp3|bumperless_audio.mp3|./constants/bumpers/bumper_outtro.mp3" \
            -metadata title="$tag_title" \
            -metadata album="$tag_series" \
            -metadata Year="2020" \
            -metadata author="$speaker_name" \
            -metadata artist="$speaker_name" \
            -metadata genre="Speech" \
            -metadata track="$tag_track" \
            -c copy wo_art.mp3

	# finished with the file bumperless_audio.mp3; so delete it
		remove_this bumperless_audio.mp3


printf "\n\n"
printf "###############\n\n"
printf "#      Step 2 of 2 -  Add the front cover artwork to the $filename.mp3 \n\n"
printf "###############\n\n"

    ffmpeg -i wo_art.mp3 -i ./constants/album_covers/cover.png \
    -c copy -map 0 -map 1 -metadata:s:v title="Album cover" -metadata:s:v comment="Cover (front)" \
    $filename.mp3

	# finished with the file wo_art.mp3; so delete it
		remove_this wo_art.mp3

printf "\n\n"
printf "###############\n\n"
ENDTIME=$(date +%s)
ELAPSED_TIME=$(($ENDTIME - $STARTTIME))

echo "Finished! Execution time was $((ELAPSED_TIME/60)) minutes and $((ELAPSED_TIME%60)) seconds."
printf "\n\n"

echo "The podcast for Hope City Church is finished and ready to upload to libsyn.com.  Execution time was $((ELAPSED_TIME/60)) minutes and $((ELAPSED_TIME%60)) seconds." | ./twilio-sms XXX-XXX-XXXX

I currently use blender for my video editing, So im not too scared of FFMPEG,

using ffmpeg to perform some basic audio adjustments like compression is actually a great idea. as well as injecting the logo.

Ill see if I can do something similar.

I looked at your website a couple weeks ago and came to this thread to say “ask Mark!” Cool stuff with FFMPEG by the way, I use it to make forward and reverse stop motion videos. It’s the best!

1 Like

Is there an automated way to upload? I presume you’re talking about YouTube. It would be great if there was a git push equivalent for YT

In that case, python scripts for blender – maybe? I haven’t used them, so I’m not sure if that’s the solution.

At the time I did the research on auto uploading to YT, it was not possible. That’s why, in my code, I use twilio to send a sms text to myself when the process is finished. BTW, thanks for checking out my site.