With everything moving online this semester, a lot of presentations have turned into “please hand in a video of your talk”. The usual advice is to fire up a screen recorder, click through your slides and talk for twenty minutes straight. If you stumble over a sentence on slide 14, you either live with it or start over.
There is a nicer way: record one short audio clip per slide and let a script stitch everything together.
The idea
You need two things:
- Your slides as a single PDF (e.g. exported from LaTeX Beamer, LibreOffice, …)
- One mp3 file per slide, named so they sort in the right order (
01.mp3,02.mp3, …)
The script then
- trims the silence at the start and end of each recording (no more awkward pauses while you reach for the mouse),
- renders every PDF page into an image,
- shows each slide for exactly as long as its recording lasts,
- and puts it all into one
presentation.mp4.
Messed up slide 14? Re-record 14.mp3 and run the script again. That’s it.
Usage
Put the PDF and an audio directory into a folder:
my_talk/
├── slides.pdf
└── audio/
├── 01.mp3
├── 02.mp3
└── 03.mp3
Then just run:
cd my_talk
make_videoThe only dependencies are ffmpeg and pdftoppm (from poppler), which you most likely already have installed.
The script
#!/bin/sh
# Turn a PDF with n slides and n mp3 files (one per slide) into presentation.mp4
# Usage: run in a directory containing one PDF and audio/01.mp3, audio/02.mp3, ...
set -e
die() { echo "$*" >&2; exit 1; }
set -- ./*.pdf
[ $# -eq 1 ] && [ -f "$1" ] || die 'Exactly one PDF file required'
pdf=$1
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
# One image per slide: slide-01.png, slide-02.png, ...
pdftoppm -png -scale-to 1920 "$pdf" "$tmp/slide"
set -- "$tmp"/slide-*.png
[ $# -eq "$(ls audio/*.mp3 | wc -l)" ] || die "Need exactly one mp3 per slide ($# slides)"
# Cut silence at the start and end of each recording and
# show each slide for exactly as long as its recording lasts
trim='silenceremove=start_periods=1:start_threshold=-48dB'
for audio in audio/*.mp3; do
wav="$tmp/${1##*/}.wav"
ffmpeg -v error -i "$audio" -af "$trim,areverse,$trim,areverse" "$wav"
duration=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$wav")
printf "file '%s'\nduration %s\n" "$1" "$duration" >> "$tmp/slides.txt"
printf "file '%s'\n" "$wav" >> "$tmp/audio.txt"
last=$1; shift
done
# The concat demuxer ignores the last duration unless the last file is repeated
printf "file '%s'\n" "$last" >> "$tmp/slides.txt"
# Variable frame rate: every slide is essentially a single frame
ffmpeg -v error -y \
-f concat -safe 0 -i "$tmp/slides.txt" \
-f concat -safe 0 -i "$tmp/audio.txt" \
-vf 'scale=trunc(iw/2)*2:trunc(ih/2)*2,format=yuv420p' -fps_mode vfr \
-c:v libx264 -tune stillimage -c:a aac presentation.mp4The silence trimming uses a little trick: ffmpeg’s silenceremove filter is good at cutting silence from the start of a recording, so the script trims, reverses the audio, trims again and reverses it back.
Why is resulting video file so small?
The relevant part is the last step. A presentation is mostly a still image with someone talking over it, so there’s no point in encoding 30 identical frames per second. Instead, the script feeds ffmpeg a list like this
file '/tmp/tmp.x8Kd2/slide-01.png'
duration 12.84
file '/tmp/tmp.x8Kd2/slide-02.png'
duration 31.2
...
and encodes it with a variable frame rate. So each slide ends up as a single frame that stays on screen until the next one comes along. A 15 minute talk with full HD slides comes out at barely more than the size of the audio.
Good luck with your next remote talk!