Adding Subtitles to a Video

Subtitling a video became a minor supporting project for another project: the Project Euler on a Microcontroller post needed a video demonstrating the binary encoded answer being returned on the LED.

The toolchain I chose on Linux wound up being ffmpeg with a script that generates a subtitle file, and using mplayer to display the time index to generate the subtitle events. Ffmpeg can use files in the Sub Station Alpha file format to render subtitles. A quick tutorial is located at https://github.com/Erkaman/ffmpeg-add-text-to-video-tutorial.

To generate accurate subtitle time indexes, I used mplayer with the options mplayer -osdlevel 3 -osd-fractions 1 /path/to/video to display timestamps.

I manually created a subtitle file and overlaid it with ffmpeg:

ffmpeg -i input.mp4 -vf subtitles=input.ass output.mp4

After a while, I was able to fiddle with the style and placement of text so that I was happy. This ended up being a bit more annoying to test than I anticipated, since ffmpeg takes a while to encode with subtitles. I was hoping to use mplayer to render the subtitles, but the formatting ended up being different between ffmpeg rendering and mplayer’s.

With that sorted, the video needed to be resized and have audio and metadata stripped, as well, so that’s additional options and an additional input filter:

ffmpeg -y -i input.mp4 -vf "scale=1280:720:flags=lanczos, subtitles=input.ass" -map_metadata -1 -an output.mp4

Then, to actually make the all of the annotations for the video I made a python script that generates all of my timings; this is likely not necessary in most cases but in my case there were predictable times for the next subtitle:

import datetime
result = 233168
time = datetime.timedelta(seconds=2, milliseconds=0)
dura = datetime.timedelta(seconds=1)
wait = datetime.timedelta(milliseconds=100)
offset = datetime.timedelta(seconds=1, milliseconds=180) # 150 too fast

def format_time(delta):
    return f"{int(delta.seconds/3600)}:{int(delta.seconds/60):02}:{delta.seconds%60:02}.{int(delta.microseconds/10000):02}"

print("""[Script Info] 
ScriptType: v4.00 
Timer: 100,0000 
 
[V4 Styles] 
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding 
Style: Default,Droid Sans Mono,36,16777215,16777215,0,0,1,2,2,3,0,0,0,0 
 
[Events] 
Format: Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text""")

for i in range(1,25):
    binary = f"{result:025b}"
    binary = f"0b{binary[-i:]}".rjust(34)
    dtime = time #+wait # next digit
    print(f"Dialogue: Marked=0,{format_time(dtime)},{format_time(dtime+dura)},Default,,0,0,0,,{binary[-i]}")
    print(f"Dialogue: Marked=0,{format_time(time)},{format_time(time+offset)},Default,,0,0,0,,{binary}")
    time += offset

offset = datetime.timedelta(seconds=5)
print(f"Dialogue: Marked=0,{format_time(time)},{format_time(time+offset)},Default,,0,0,0,,{binary} = 233168")

This resulted in the following file:

[Script Info] 
ScriptType: v4.00 
Timer: 100,0000 
 
[V4 Styles] 
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding 
Style: Default,Droid Sans Mono,36,16777215,16777215,0,0,1,2,2,3,0,0,0,0 
 
[Events] 
Format: Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: Marked=0,0:00:02.00,0:00:03.00,Default,,0,0,0,,{\an5}0
Dialogue: Marked=0,0:00:02.00,0:00:03.18,Default,,0,0,0,,                               0b0
Dialogue: Marked=0,0:00:03.18,0:00:04.18,Default,,0,0,0,,{\an5}0
Dialogue: Marked=0,0:00:03.18,0:00:04.36,Default,,0,0,0,,                              0b00
Dialogue: Marked=0,0:00:04.36,0:00:05.36,Default,,0,0,0,,{\an5}0
Dialogue: Marked=0,0:00:04.36,0:00:05.54,Default,,0,0,0,,                             0b000
Dialogue: Marked=0,0:00:05.54,0:00:06.54,Default,,0,0,0,,{\an5}0
Dialogue: Marked=0,0:00:05.54,0:00:06.72,Default,,0,0,0,,                            0b0000
Dialogue: Marked=0,0:00:06.72,0:00:07.72,Default,,0,0,0,,{\an5}1
Dialogue: Marked=0,0:00:06.72,0:00:07.90,Default,,0,0,0,,                           0b10000
Dialogue: Marked=0,0:00:07.90,0:00:08.90,Default,,0,0,0,,{\an5}0
Dialogue: Marked=0,0:00:07.90,0:00:09.08,Default,,0,0,0,,                          0b010000
Dialogue: Marked=0,0:00:09.08,0:00:10.08,Default,,0,0,0,,{\an5}1
Dialogue: Marked=0,0:00:09.08,0:00:10.26,Default,,0,0,0,,                         0b1010000
Dialogue: Marked=0,0:00:10.26,0:00:11.26,Default,,0,0,0,,{\an5}1
Dialogue: Marked=0,0:00:10.26,0:00:11.44,Default,,0,0,0,,                        0b11010000
Dialogue: Marked=0,0:00:11.44,0:00:12.44,Default,,0,0,0,,{\an5}0
Dialogue: Marked=0,0:00:11.44,0:00:12.62,Default,,0,0,0,,                       0b011010000
Dialogue: Marked=0,0:00:12.62,0:00:13.62,Default,,0,0,0,,{\an5}1
Dialogue: Marked=0,0:00:12.62,0:00:13.80,Default,,0,0,0,,                      0b1011010000
Dialogue: Marked=0,0:00:13.80,0:00:14.80,Default,,0,0,0,,{\an5}1
Dialogue: Marked=0,0:00:13.80,0:00:14.98,Default,,0,0,0,,                     0b11011010000
Dialogue: Marked=0,0:00:14.98,0:00:15.98,Default,,0,0,0,,{\an5}1
Dialogue: Marked=0,0:00:14.98,0:00:16.16,Default,,0,0,0,,                    0b111011010000
Dialogue: Marked=0,0:00:16.16,0:00:17.16,Default,,0,0,0,,{\an5}0
Dialogue: Marked=0,0:00:16.16,0:00:17.34,Default,,0,0,0,,                   0b0111011010000
Dialogue: Marked=0,0:00:17.34,0:00:18.34,Default,,0,0,0,,{\an5}0
Dialogue: Marked=0,0:00:17.34,0:00:18.52,Default,,0,0,0,,                  0b00111011010000
Dialogue: Marked=0,0:00:18.52,0:00:19.52,Default,,0,0,0,,{\an5}0
Dialogue: Marked=0,0:00:18.52,0:00:19.70,Default,,0,0,0,,                 0b000111011010000
Dialogue: Marked=0,0:00:19.70,0:00:20.70,Default,,0,0,0,,{\an5}1
Dialogue: Marked=0,0:00:19.70,0:00:20.88,Default,,0,0,0,,                0b1000111011010000
Dialogue: Marked=0,0:00:20.88,0:00:21.88,Default,,0,0,0,,{\an5}1
Dialogue: Marked=0,0:00:20.88,0:00:22.06,Default,,0,0,0,,               0b11000111011010000
Dialogue: Marked=0,0:00:22.06,0:00:23.06,Default,,0,0,0,,{\an5}1
Dialogue: Marked=0,0:00:22.06,0:00:23.24,Default,,0,0,0,,              0b111000111011010000
Dialogue: Marked=0,0:00:23.24,0:00:24.24,Default,,0,0,0,,{\an5}0
Dialogue: Marked=0,0:00:23.24,0:00:24.42,Default,,0,0,0,,             0b0111000111011010000
Dialogue: Marked=0,0:00:24.42,0:00:25.42,Default,,0,0,0,,{\an5}0
Dialogue: Marked=0,0:00:24.42,0:00:25.60,Default,,0,0,0,,            0b00111000111011010000
Dialogue: Marked=0,0:00:25.60,0:00:26.60,Default,,0,0,0,,{\an5}0
Dialogue: Marked=0,0:00:25.60,0:00:26.78,Default,,0,0,0,,           0b000111000111011010000
Dialogue: Marked=0,0:00:26.78,0:00:27.78,Default,,0,0,0,,{\an5}0
Dialogue: Marked=0,0:00:26.78,0:00:27.96,Default,,0,0,0,,          0b0000111000111011010000
Dialogue: Marked=0,0:00:27.96,0:00:28.96,Default,,0,0,0,,{\an5}0
Dialogue: Marked=0,0:00:27.96,0:00:29.14,Default,,0,0,0,,         0b00000111000111011010000
Dialogue: Marked=0,0:00:29.14,0:00:30.14,Default,,0,0,0,,{\an5}0
Dialogue: Marked=0,0:00:29.14,0:00:30.32,Default,,0,0,0,,        0b000000111000111011010000
Dialogue: Marked=0,0:00:30.32,0:00:35.32,Default,,0,0,0,,        0b000000111000111011010000 = 233168

Resulting in this:

Written on July 30, 2022