Forcing keyframes with ffmpeg
We’ve been building an interesting 3D interactive at SHOWstudio recently where you can take a tour around the inside of a building. The interaction is a bit like Google Streetview in that you can click forwards and backwards through a virtual world. It feels a bit like you’re inside a 3D model but it’s actually just playing through a video, jumping to the appropriate “chapter” depending on which direction you navigate in.
The video is served up in JW Player, and one issue we were having was with it not always seeking accurately. Doing a jwplayer().seek(3)
for example might not actually take you to the 3 second mark, but somewhere nearby, or perhaps would even cause the video to start playing from the beginning. Not ideal.
It turns out this is simply a keyframing issue with the video file itself, in that our video didn’t have keyframes placed correctly around the points we’d actually be jumping to.
Forcing keyframes with an ffmpeg
expression
Getting it to work properly is just matter of inserting keyframes where they’re needed, and this is quite easily done on the command line with ffmpeg
. As the documentation explains, you can issue an expression to force a keyframe at the required interval. This is what I used:
ffmpeg -i in.mp4 -force_key_frames "expr:gte(t,n_forced*3)" out.mp4
Walking through that, we’re providing a gte
(greater than or equal) expression which returns 1
on the first frame we hit at each 3 second interval. In other words it’s saying “if the current time t
is greater than (the total number of keyframes so far * 3), force a keyframe” – which has the effect of forcing one keyframe every 3 seconds.
Here’s a table that shows how that expression evaluates on a per-frame basis. For simplicity this assumes a video encoded at 2 frames per second:
Frame | Time in Seconds ( t ) |
Expression, based on gte(t,n_forced*3) |
Evaluates as | Keyframes forced so far ( n_forced ) |
---|---|---|---|---|
1 | 0 | If 0 >= 0*3 | TRUE – force keyframe | 1 |
2 | 0 | If 0 >= 1*3 | FALSE | 1 |
3 | 1 | If 1 >= 1*3 | FALSE | 1 |
4 | 1 | If 1 >= 1*3 | FALSE | 1 |
5 | 2 | If 2 >= 1*3 | FALSE | 1 |
6 | 2 | If 2 >= 1*3 | FALSE | 1 |
7 | 3 | If 3 >= 1*3 | TRUE – force keyframe | 2 |
8 | 3 | If 3 >= 2*3 | FALSE | 2 |
9 | 4 | If 4 >= 2*3 | FALSE | 2 |
10 | 4 | If 4 >= 2*3 | FALSE | 2 |
11 | 5 | If 5 >= 2*3 | FALSE | 2 |
12 | 5 | If 5 > 2*3 | FALSE | 2 |
13 | 6 | If 6 >= 2*3 | TRUE – force keyframe | 3 |
14 | 6 | If 6 >= 3*3 | FALSE | 3 |
15 | 7 | If 7 >= 3*3 | FALSE | 3 |
16 | 7 | If 7 >= 3*3 | FALSE | 3 |
17 | 8 | If 8 >= 3*3 | FALSE | 3 |
18 | 8 | If 8 >= 3*3 | FALSE | 3 |
19 | 9 | If 9 >= 3*3 | TRUE – force keyframe | 4 |
ffmpeg to the rescue once again!