dan's internet pad

I have a friend who is my (power)lifting coach. Coaches and trainers are very useful for exercise selection, bouncing new ideas off of and to help guide you along. Improving one’s lifting numbers is non-linear and I love having someone I can always discuss approaches with. Results boil down to the usual suspects (eg. consistency, intensity, recovery, etc.) but i’ve found it very beneficial.

We use a spreadsheet to communicate exercises, and numbers for the each training week. At the end of each week, I send an email with all of my lifts, notes and videos of specific lifts. The videos reside on Google Photos and I generate a shareable link, embed it in the email and share it with him. This has worked fine, but didn’t feel great. Why not make a single video containing all weekly lifts instead of having him click on links to short clips? I did not want to do this with a video editor, and thus began my dive into ffmpeg.

ffmpeg

FFMPEG is a CLI tool for manipulating audio and video files. It feels like a swiss-army knife. Good at the core use-cases and many others you aren’t even aware of. I knew of it because I know VLC used it, or its internal libraries for most of its functionality.

All I needed ffmpeg for was to:

Like any good programmer in 2025, I had an LLM do the first pass. I used Anthropic Claude 3.7 Sonnet to get started.

ffmpeg primer

Adding text to a video using ffmpeg was very easy. The first key call to ffmpeg added the overlay and did a few other transformations to speed up the process.

ffmpeg -nostdin -i "$filename" \
    -vf "drawtext=text='$file_stem':\
fontcolor=white:\
fontsize=72:\
box=1:\
boxcolor=black@0.5:\
boxborderw=5:\
x=(w-text_w)/2:\
y=h-th-10,\
scale=iw/3:-2" \
	-c:v libx265 \
	-crf 30 \
	-preset veryfast \
	-c:a copy \
	"$output_temp" > /dev/null 2> /dev/null

The number of options may look like a lot but it isn’t.

For the text positioning, the upper left hand corner of a video frame is (0,0) with the x axis increasing as usual, and y axis increasing downward. That is why the y coordinate is so large despite it being at the bottom. The positioning syntax gives you access to the video width w, height h, the text width text_w and text height th. This is cool and lets you customize the positioning easily.

The second ffmpeg command was to concatenate videos. It was relatively simple compared to the above.

ffmpeg -f concat -safe 0 -i "file_list.txt" -c:v libx265 -crf 23 -preset medium -c:a copy "../output_concatenated.mp4"

No-free lunch

Despite the script looking good, it was failing to do what I wanted. It was not processing all of the files I passed to it. It looked like it kept skipping some of them.

# files to process
dang3r@wintermute:~/dev/forge/workout-videos-create$ find B13W1/ -type f | sort
B13W1/B13W1D1_InclineDBPress_75x8.mp4
B13W1/B13W1D1_PausedBenchPress_245x6.mp4
B13W1/B13W1D2_Pullups_15x7.mp4
B13W1/B13W1D3_GobletSquat_70x10.mp4
B13W1/B13W1D3_RomanianDeadlift_215x7.mp4
B13W1/B13W1D4_InclineSmith_225x6.mp4

# It only processed two?
dang3r@wintermute:~/dev/forge/workout-videos-create$ find B13W1/ -type f | sort | ./create_compilation.sh
Processing: 'B13W1/B13W1D1_InclineDBPress_75x8.mp4'
Processing: '3W1/B13W1D3_GobletSquat_70x10.mp4'

After adding the ffmpeg logging back, I got a clue.

dang3r@wintermute:~/dev/forge/workout-videos-create$ find B13W1/ -type f | sort | ./create_compilation.sh
...
...
1/B13W1D3_GobletSquat_70x10.mp4: No such file or directory

I then tried only processing D1 videos.

dang3r@wintermute:~/dev/forge/workout-videos-create$ find B13W1/ -type f | grep D1 | sort
B13W1/B13W1D1_InclineDBPress_75x8.mp4
B13W1/B13W1D1_PausedBenchPress_245x6.mp4

dang3r@wintermute:~/dev/forge/workout-videos-create$ find B13W1/ -type f | grep D1 | sort | ./create_compilation.sh
...
Done! Final video saved as output_concatenated.mp4

# only shows the incline db press video!
dang3r@wintermute:~/dev/forge/workout-videos-create$ open output_concatenated.mp4 

After some frustration, Claude came back with the answer.

By default, ffmpeg reads bytes from stdin. Even when I pass it the filepath via -i. The filepaths were being passed as stdin to the parent script and consumed by the file-loop. And then, ffmpeg would also reads bytes from stdin, causing the outer loop to fail. Something like:

The solution? Add -nostdin to the inner ffmpegcalls. This prevents the stdin over-reading and everything generated correctly!

final-thoughts

I was able to generate the compilation video just fine. It is here. For the code, see github.

Despite the single issue, I love how easy it is to use ffmpeg. Google photos does now have the ability to edit photos and aggregate clips (I suspect you can add text on top too). However, I like this CLI-based approach I can use again with no effort.

#Blog #Project