Monday, December 31, 2018

Ffmpeg and Make -- A Match Made In Heaven

Although 'make' is most often used to compile, it's use far exceeds that.  'Make's greatest strength is that of it's dependency engine.  By specifying a set of rules, 'make' executes the rules in order to satisfy the dependency.  This is precisely why 'make' is a good match for ffmpeg, the remainder of this blog will hopefully demonstrate that.

Let's start with a simple rule; we need an input video file which can be satisfied by our first make rule:



$ cat Makefile 

input.mp4 :
 ${SH} youtube-dl https://www.youtube.com/watch?v=5xUFQKxdlxE -o $@

By issuing 'make', it will attempt to satisfy resolving the 'input.mp4' target by downloading the specified file from YouTube.


$ make
youtube-dl https://www.youtube.com/watch?v=5xUFQKxdlxE -o input.mp4
[youtube] 5xUFQKxdlxE: Downloading webpage
[youtube] 5xUFQKxdlxE: Downloading video info webpage
[youtube] 5xUFQKxdlxE: Extracting video information
WARNING: unable to extract uploader nickname
[youtube] 5xUFQKxdlxE: Downloading js player vflqFr_Sb
[download] Destination: input.f133.mp4
[download] 100% of 1.13MiB in 00:03
[download] Destination: input.mp4.f140
[download] 100% of 610.47KiB in 00:00
[ffmpeg] Merging formats into "input.mp4"
Deleting original file input.f133.mp4 (pass -k to keep)
Deleting original file input.mp4.f140 (pass -k to keep)

The format of each rule has 3 primary bits: a target, a prerequisite and a command.  In our rule, the target is 'input.mp4', there is no prerequisite, and the command is the YouTube download command.  Repeated execution of make will have no affect, since the file already exists there is no need to re-execute the rule command.

While simple, this rule doesn't really demonstrate the value of 'make', mostly because of the rule's lack of a prerequisite.  Let's look at another:



$ cat Makefile

input.mp4 :
 ${SH} youtube-dl https://www.youtube.com/watch?v=5xUFQKxdlxE -o $@

1x1.mp4: input.mp4
 ${SH} ffmpeg -i $< -vf scale=640:480 -acodec copy $@

Note the 2nd rule has a prerequisite.  A simple way to read the 2nd rule is "When I need the 1x1.mp4 file, I first need the 'input.mp4' file and once I have it use it in the rule command".  Suppose neither the 'input.mp4' nor the '1x1.mp4' file exists and you issue 'make 1x1.mp4';

  • Make determines in order to create '1x1.mp4' it needs 'input.mp4' (it's prerequisite)
  • Make then finds the 'input.mp4' rule and executes the rule command, downloading the file from YouTube and generates the target file (e.g. 'input.mp4')
  • Now that the pre-requisite is resolved, make returns to the '1x1.mp4' rule and executes the rule command, scaling the video file and generating the target
This chaining of dependencies, when done correctly, can execute a complex series of commands in satisfying the final target.  Better yet, with make's intrinsic parallelism it can do so quicker than a sequential script.  That, my friend, is b-e-a-utiful.

Let's look at a complete makefile;


$ cat Makefile 
all: output.mp4

input.mp4 :
 ${SH} youtube-dl https://www.youtube.com/watch?v=5xUFQKxdlxE -o $@

1x1.mp4: input.mp4
 ${SH} ffmpeg -i $< -vf scale=640:480 -acodec copy $@

2x2.mp4: input.mp4
 ${SH} ffmpeg -i $< -i $< -i $< -i $< \
 -filter_complex " \
 nullsrc=size=640x480 [base]; \
 [0:v] setpts=PTS-STARTPTS, scale=320x240 [upperleft]; \
 [1:v] setpts=PTS-STARTPTS, scale=320x240 [upperright]; \
 [2:v] setpts=PTS-STARTPTS, scale=320x240 [lowerleft]; \
 [3:v] setpts=PTS-STARTPTS, scale=320x240 [lowerright]; \
 [base][upperleft] overlay=shortest=1 [tmp1]; \
 [tmp1][upperright] overlay=shortest=1:x=320 [tmp2]; \
 [tmp2][lowerleft] overlay=shortest=1:y=240 [tmp3]; \
 [tmp3][lowerright] overlay=shortest=1:x=320:y=240 \
 " -c:v libx264 -acodec copy $@

4x4.mp4: 2x2.mp4
 ${SH} ffmpeg -i $< -i $< -i $< -i $< \
 -filter_complex " \
 nullsrc=size=640x480 [base]; \
 [0:v] setpts=PTS-STARTPTS, scale=320x240 [upperleft]; \
 [1:v] setpts=PTS-STARTPTS, scale=320x240 [upperright]; \
 [2:v] setpts=PTS-STARTPTS, scale=320x240 [lowerleft]; \
 [3:v] setpts=PTS-STARTPTS, scale=320x240 [lowerright]; \
 [base][upperleft] overlay=shortest=1 [tmp1]; \
 [tmp1][upperright] overlay=shortest=1:x=320 [tmp2]; \
 [tmp2][lowerleft] overlay=shortest=1:y=240 [tmp3]; \
 [tmp3][lowerright] overlay=shortest=1:x=320:y=240 \
 " -c:v libx264 -acodec copy $@
8x8.mp4: 4x4.mp4
 ${SH} ffmpeg -i $< -i $< -i $< -i $< \
 -filter_complex " \
 nullsrc=size=640x480 [base]; \
 [0:v] setpts=PTS-STARTPTS, scale=320x240 [upperleft]; \
 [1:v] setpts=PTS-STARTPTS, scale=320x240 [upperright]; \
 [2:v] setpts=PTS-STARTPTS, scale=320x240 [lowerleft]; \
 [3:v] setpts=PTS-STARTPTS, scale=320x240 [lowerright]; \
 [base][upperleft] overlay=shortest=1 [tmp1]; \
 [tmp1][upperright] overlay=shortest=1:x=320 [tmp2]; \
 [tmp2][lowerleft] overlay=shortest=1:y=240 [tmp3]; \
 [tmp3][lowerright] overlay=shortest=1:x=320:y=240 \
 " -c:v libx264 -acodec copy $@

16x16.mp4: 8x8.mp4
 ${SH} ffmpeg -i $< -i $< -i $< -i $< \
 -filter_complex " \
 nullsrc=size=640x480 [base]; \
 [0:v] setpts=PTS-STARTPTS, scale=320x240 [upperleft]; \
 [1:v] setpts=PTS-STARTPTS, scale=320x240 [upperright]; \
 [2:v] setpts=PTS-STARTPTS, scale=320x240 [lowerleft]; \
 [3:v] setpts=PTS-STARTPTS, scale=320x240 [lowerright]; \
 [base][upperleft] overlay=shortest=1 [tmp1]; \
 [tmp1][upperright] overlay=shortest=1:x=320 [tmp2]; \
 [tmp2][lowerleft] overlay=shortest=1:y=240 [tmp3]; \
 [tmp3][lowerright] overlay=shortest=1:x=320:y=240 \
 " -c:v libx264 -acodec copy $@

clip01.mp4: 1x1.mp4
 ${SH} ffmpeg -i $< -ss 0 -t 5 -acodec copy $@

clip02.mp4: 2x2.mp4
 ${SH} ffmpeg -i $< -ss 5 -t 5 -acodec copy $@

clip03.mp4: 4x4.mp4
 ${SH} ffmpeg -i $< -ss 10 -t 5 -acodec copy $@

clip04.mp4: 8x8.mp4
 ${SH} ffmpeg -i $< -ss 15 -t 5 -acodec copy $@

clip05.mp4: 16x16.mp4
 ${SH} ffmpeg -i $< -ss 20 -t 5 -acodec copy $@

clip06.mp4: 4x4.mp4
 ${SH} ffmpeg -i $< -ss 25 -acodec copy $@

output.mp4: clip01.mp4 clip02.mp4 clip03.mp4 clip04.mp4 clip05.mp4 clip06.mp4
 ${RM} ./files.txt
 ${SH} for f in `echo $^`; do echo "file '$$f'" >> ./files.txt; done
 ${SH} ffmpeg -y -f concat -i ./files.txt -c copy $@
 ${RM} ./files.txt

clean:
 ${RM} *.mp4

The final target 'output.mp4' is defined as a pre-requisite of the first make rule (e.g. all).  Make attempts to execute the final rule, finds a series of pre-requisites (e.g. clip01.mp4...) attempting to satisfy each pre-requisite each of which has pre-requisites of their own.  Following the chain of dependencies you'll come to the YouTube download rule, with no pre-requisites.  Make then executes that rule, generating the input file and works its way back the dependency chain 'til it is capable of generating the final target.

The final video will start as a 1x1 frame, grow to a 2x2 mosaic, proceed to a 4x4 mosaic.....all the way to a 16x16 mosaic.


I've now used make and ffmpeg in a number of video projects and the more I use them, the more I love using them together.  The dependency engine prevents unnecessarily issuing ffmpeg commands when the target file already exists and easily allows incrementally building the series of files necessary for creating the final video.

Happy Encoding!

No comments:

Post a Comment