Blog-Archiv

Mittwoch, 16. September 2020

Video Title Creation with ffmpeg

UPDATE: in my next Blog there is a better variant of this script!

Working with my video cutting-plan scripts I found that I would like to completely move to ffmpeg, because OpenShot is simply too slow on my weak(?) machine (it takes minutes to move a 30 minutes clip collection 4 seconds to the right, once it even crashed). What I'm giving up then is smooth transitions between the clips, fades, and text overlays, as this would require re-encoding and takes a lot of time.

But I don't want to publish a video without title, thus I tried to create a script that produces a title that I can prepend to the cuts generated by my cutVideos.sh script. It is a simple text, optionally several lines, on a black background. This Blog contains the source code of titleForVideos.sh. Everything here refers to MP4 videos, ffmpeg for Ubuntu LINUX, and I am not an ffmpeg expert.

Prerequisites

What we need:

  1. A text file containing the title text, named title.txt by default. It can contain newlines. Locate it where the videos are, like cuts.txt. This is the best location, so why support others?

  2. A fixed file name for the resulting video: TITLE.MP4. The script cutVideos.sh can search for that naming convention and integrate the title when found.

  3. A template video that gives some basic properties to use for the title video, so that it can be prepended to the clips without re-encoding. What I took from the template is the video-encoding (only h264 is accepted), the frame-rate, the pixel format, width and height, for audio the audio-encoding and the sample-rate.

Implementation titleForVideos.sh

Following are all script fragments in the order they appear. At the end of the article you can find its complete source. I will explain every fragment in detail. Mind that this is a UNIX shell script and you need CYGWIN to run this on WINDOWS operating systems.

Script Settings

duration=4	# seconds
fontcolor=yellow	# foreground color
fontsize=100	# size of text in pixels
titleVideo=TITLE.MP4	# file name of the title video

When you want different title properties, you can set them on top of the script file (not from outside). But you shouldn't change TITLE.MP4, because this is a naming convention for cutVideos.sh.

Argument Checks

[ -z "$1" ] && {
	echo "SYNTAX: $0 videoDir/[TARGETVIDEO.MP4] [title.txt]" >&2
	echo "	Creates a $titleVideo video with same properties as videos in given directory." >&2
	echo "	The text of the title must be in a file title.txt where the videos are." >&2
	exit 1
}

if [ -d $1 ]
then
	cd $1 || exit 1
	videoTemplate=`ls -1 *.mp4 *.MP4 2>/dev/null | head -1`	# first found video
	[ -f "$videoTemplate" ] ||	{
		echo "Found no video template in $1" >&2
		exit 2
	}
	echo "Using $videoTemplate as video template" >&2
elif [ -f $1 ]
then
	cd `dirname \$1` || exit 2
	videoTemplate=`basename \$1`
else
	echo "No video template given: $1" >&2
	exit 3
fi

titleText=$2
[ -z "$titleText" ] && titleText=title.txt
[ -f $titleText ] || {
	echo "Found no file $titleText containing the title where the video is: `pwd`" >&2
	exit 4
}

If the first argument was not given (empty), the script outputs its syntax and exits.

If the first argument is a directory, the script changes to the directory and uses the first found MP4 file as properties template. When the first argument is a file, the script uses that as properties template and changes into its directory. In any other case an error is written to stderr and the script exits.

The second argument is optional and gives the name of the file holding the title text. It must be located inside the video directory. When it was not given, title.txt is assumed. If it does not exist, an error is written to stderr and the script exits.

Read Video Properties

I want to create a title video with same technical properties as the video I will use the title for, so that I can prepend it without re-encoding.

streamProperty()	{	# $1 = stream name, $2 = property name
	ffprobe -v error -of default=noprint_wrappers=1:nokey=1 -select_streams $1 -show_entries stream=$2 $videoTemplate
}

# get video properties
stream=v:0	# first found video
codec_name=`streamProperty \$stream codec_name`
[ "$codec_name" = "h264" ] ||	{
	echo "Due to a missing mapping from codec_name to ffmpeg libXXX only h264 is supported." >&2
	exit 5
}
rate=`streamProperty \$stream r_frame_rate`
pixelFormat=`streamProperty \$stream pix_fmt`
width=`streamProperty \$stream width`
height=`streamProperty \$stream height`

# get audio properties
stream=a:0	#  first found audio
audio_codec=`streamProperty \$stream codec_name`
sample_rate=`streamProperty \$stream sample_rate`

echo "rate=$rate, size=$width/$height, pix_fmt=$pixelFormat, audio_codec=$audio_codec, sample_rate=$sample_rate" 

The shell function streamProperty() works with the global variable $videoTemplate and two given parameters $1 and $2, which must be the stream and the property name. (Mind that you don't declare parameters on shell functions.) The function holds a ffprobe command with two placeholders for stream and property-name. In the moment of function-execution the command is evaluated with given $-parameters. To provide this function avoids to repeat the (complex) ffprobe command as many times as you need properties.

The function is then called in a command-substitution with different streams (video, audio) and property names. This gives the values of the properties, needed to create a compatible title video. The script exits when the video encoding is different to "h264" because I have no mapping between codecs and ffmpeg libs, thus I anticipate MP4.

Finally the properties are written to stderr, so that we can check them for correctness. They will be used in the subsequent ffmpeg command.

Title Video Creation

[ -f $titleVideo ] && rm -f $titleVideo

ffmpeg \
	-f lavfi -i color=c=black:size=$width/$height:rate=$rate -t $duration \
	-f lavfi -i anullsrc=sample_rate=$sample_rate:channel_layout=stereo -t $duration \
	-vf "drawtext=textfile=$titleText:fontcolor=$fontcolor:fontsize=$fontsize:x=(w-text_w)/2:y=(h-text_h)/2" \
	-codec:v libx264 -pix_fmt $pixelFormat -profile:v high \
	-codec:a $audio_codec \
	-shortest $titleVideo || exit 6

In case the TITLE.MP4 video already exists, it is removed.

The following ffmpeg command consists of several parts (lines). The parts are split using a backslash, which is the shell escape for newlines.

The first line creates a black color background with given dimension, frame-rate, and duration in seconds.
The second line creates an empty audio stream with given sample-rate and same duration.
The third line draws the text on center of the screen.
The 4th line defines the video output codec and sets the pixel-format.
The 5th line sets the audio codec.
The last line finally gives the output video file name.

In case the command fails, an error message is written and the script exits negatively.

Complete Source


#######################################################
# Creates a title for a video with text in file title.txt.
#######################################################

titleVideo=TITLE.MP4	# file name of the title video
duration=4	# seconds
fontcolor=yellow	# foreground color
fontsize=100	# size of text

[ -z "$1" ] && {
	echo "SYNTAX: $0 videoDir/[TARGETVIDEO.MP4] [title.txt]" >&2
	echo "	Creates a $titleVideo video with same properties as videos in given directory." >&2
	echo "	The text of the title must be in a file title.txt where the videos are." >&2
	exit 1
}

if [ -d $1 ]
then
	cd $1 || exit 1
	videoTemplate=`ls -1 *.mp4 *.MP4 2>/dev/null | head -1`	# first found video
	[ -f "$videoTemplate" ] ||	{
		echo "Found no video template in $1" >&2
		exit 2
	}
	echo "Using $videoTemplate as video template" >&2
elif [ -f $1 ]
then
	cd `dirname \$1` || exit 2
	videoTemplate=`basename \$1`
else
	echo "No video template given: $1" >&2
	exit 3
fi

titleText=$2
[ -z "$titleText" ] && titleText=title.txt
[ -f $titleText ] || {
	echo "Found no file $titleText containing the title where the video is: `pwd`" >&2
	exit 4
}

streamProperty()	{
	ffprobe -v error -of default=noprint_wrappers=1:nokey=1 -select_streams $1 -show_entries stream=$2 $videoTemplate"
}

# get video properties
stream=v:0	# first found video
codec_name=`streamProperty \$stream codec_name`
[ "$codec_name" = "h264" ] ||	{
	echo "Due to a missing mapping from codec_name to ffmpeg libXXX only h264 is supported." >&2
	exit 5
}
rate=`streamProperty \$stream r_frame_rate`
pixelFormat=`streamProperty \$stream pix_fmt`
width=`streamProperty \$stream width`
height=`streamProperty \$stream height`

# get audio properties
stream=a:0	#  first found audio
audio_codec=`streamProperty \$stream codec_name`
sample_rate=`streamProperty \$stream sample_rate`

echo "rate=$rate, size=$width/$height, pix_fmt=$pixelFormat, audio_codec=$audio_codec, sample_rate=$sample_rate" 

[ -f $titleVideo ] && rm -f $titleVideo

ffmpeg \
	-f lavfi -i color=c=black:size=$width/$height:rate=$rate -t $duration \
	-f lavfi -i anullsrc=sample_rate=$sample_rate:channel_layout=stereo -t $duration \
	-vf "drawtext=textfile=$titleText:fontcolor=$fontcolor:fontsize=$fontsize:x=(w-text_w)/2:y=(h-text_h)/2" \
	-codec:v libx264 -pix_fmt $pixelFormat -profile:v high \
	-codec:a $audio_codec \
	-shortest $titleVideo || exit 6

echo "Created $titleVideo in `pwd`"



Keine Kommentare: