FFmpeg timecode

March 26, 2025   

TL;DR:

Re-encoding a video with a timecode was more complicated than I expected.

Requirement

Lets assume that I have a video file, where I want to measure duration (time elapsed) of a “part of the video”.

Usually, for such things, we can simply use MPV and add timecode:

mpv --osd-fractions --osd-level=2 ${your_video_file}

However, what I needed is to overlay timecode to an existing video where I can demonstrate it to future viewers.

  • The timecode has to be in format HH:MM::SS:mmm ( millisecond resolution )
  • The timecode should appear in the selected START and END times within the video.

What I tried

Kdenlive. There used to be an easy way to use “Dynamic Text” as an effect, and use it as a timecode. However, as documented here: https://docs.kdenlive.org/en/effects_and_filters/video_effects/generate/dynamic_text.html it is not possible to define START & END times for the timecodes to be displayed: the timecode always starts with the video file.

Prerequisites

  • ffmpeg
  • your video file

Solution

STARTSEC=5
ENDSEC=20

STARTSEC2=25
ENDSEC2=70



ffmpeg -i ${your_input_video_file} -vf "drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf: \
text='%{eif\\:(t-${STARTSEC})/3600\\:d\\:2}\\:%{eif\\:mod((t-${STARTSEC})/60\\,60)\\:d\\:2}\\:%{eif\\:mod(t-${STARTSEC}\\,60)\\:d\\:2}.%{eif\\:mod((t-${STARTSEC})*1000\\,1000)\\:d\\:3}': \
x=w-tw-30: y=h-th-80: fontsize=48: fontcolor=yellow: box=1: boxcolor=black@0.5: \
enable='between(t,${STARTSEC},${ENDSEC})', \
drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf: \
text='%{eif\\:(t-${STARTSEC2})/3600\\:d\\:2}\\:%{eif\\:mod((t-${STARTSEC2})/60\\,60)\\:d\\:2}\\:%{eif\\:mod(t-${STARTSEC2}\\,60)\\:d\\:2}.%{eif\\:mod((t-${STARTSEC2})*1000\\,1000)\\:d\\:3}': \
x=w-tw-30: y=h-th-80: fontsize=48: fontcolor=yellow: box=1: boxcolor=black@0.5: \
enable='between(t,${STARTSEC2},${ENDSEC2})'
" \
-t 100 ${your_output_video_with_timecode}

Lets breakdown what does this oneliner do:


ffmpeg -i your_input ... your output

this is pretty simple, we declare the input and output files


ffmpeg -i .... -t 100 ...

I want to transcode only 100 seconds of the input video file


ffmpeg .... -vf " ...." ...

we are using a filter. in -vf " " everything here needs to be separated with a column , such as blabla:blabla:blabla

ffmpeg ... -vf "drawtext:...." ..

and the filter is “drawtext”

“drawtext” is a software only filter, so we can’t benefit from our GPU to hardware encode our video.

ffmpeg ... -vf "drawtext=fontfile=/pth/to/your/fancy/font.ttf:...

selects the font.

also

fontsize=48: fontcolor=yellow: box=1: boxcolor=black@0.5:

is related to the font.


x=w-tw-30: y=h-th-80:

the position of the text is negative 30 pixel from width and negative 80 pixel from the height ( bottom right of the video). We could write x=XXX , y=YYY here.


Now here comes the fancy part:


STARTSEC=5
ENDSEC=20

STARTSEC2=25
ENDSEC2=70

...
enable='between(t,${STARTSEC},${ENDSEC})'
enable='between(t,${STARTSEC2},${ENDSEC2})'

Lets assume I have a 2 minutes ( 120 seconds ) long video. And I want to display timecode in two portions of the video: one with starts 5 seconds after the video starts and until 20th second of the video, and a second timecode to start at 25th second of the video until 70th second of the video.

And I want these timecodes to appear in the video exactly on the times I wanted.

we use these two variable in the complicated part below:


text='%{eif\\:(t-${STARTSEC})/3600\\:d\\:2}\\:%{eif\\:mod((t-${STARTSEC})/60\\,60)\\:d\\:2}\\:%{eif\\:mod(t-${STARTSEC}\\,60)\\:d\\:2}.%{eif\\:mod((t-${STARTSEC})*1000\\,1000)\\:d\\:3}':

This statement looks pretty complicated but gives us following format:

00.00.00:000

to achieve what I needed, I had to substract the integer STARTSEC value from the current time value.

Otherwise, the value of t variable always starts with the beginning of the video, I would end up with a timecode that always starts with the beginning of the video. I didn’t wanted that. I wanted my counter to start with zero when I wanted to show my timecodes.

All those “\” are escape sequences when running these inside the bash.

And dividing t-STARTSEC/3600 gives us HOUR, mod(t-STARTSEC)/60,60 gives us minutes, mod(t-STARTSEC),60 gives us seconds, and mod(t-STARTSEC)*1000,1000 gives us millisecons.

Another critical syntax here was : How to define the number of digits?

I handled that by adding “\:d\:3” -> This prints milliseconds within 3 digits for example.

And finally, since I wanted to have two separate timecodes to be displayed, I just put a comma in between “text=…” statements.

Output

Closing words

Sadly, AI makes me more lazy these days. I used to read manuals , to earn many things much more efficiently. Now we all use AI, and we just leave such things to be figured our by the AI ( where ChatGPT, Claude, Deepseek all failed to figure out the syntax errors they suggested for this example ), which is sometimes causes more time then learning the syntax by yourself.



comments powered by Disqus