33

I want a FFmpeg seeking command that fast and accurate. I found this.

The solution is that we apply -ss for both input (fast seeking) and output (accurate seeking). But: If the input seeking is not accurate, how can we be sure that the seeking position is accurate?


For example: If we wanted to seek to 00:03:00, the command is something like:

ffmpeg -ss 00:02:30 -i <INPUT> ... -ss 00:00:30 <OUTPUT>

The first -ss will seek to somewhere else, not 00:02:30, say 00:02:31. And after applying the second seek, the final result would be 00:03:01- not what we want. Is that correct?

Where does the first -ss seek to? Does it seek to the keyframe that is closest to 00:02:30?

If so, here is my thought—correct me if I'm wrong: after first seeking, we get the timestamp of the result (in this example: 00:02:31), then we apply second seeking with appropriate time, in this case 00:00:29.

Question is: How do we get time stamp of the first seek result?

slhck
  • 235,242
jackode
  • 457
  • 2
  • 6
  • 11

5 Answers5

24

To find the closest I-frame after a given timestamp, you should narrow down the output of ffprobe to only list I-frames, and only read from a particular interval. This will make the output faster than searching for the entire file. Since we can assume there to be a keyframe around every 10 seconds, we can limit the search to a few seconds after the desired timestamp.

You can get a list of I-frames from seconds 150 to 160 with:

ffprobe \
        -select_streams v \
        -read_intervals 150%+10 \
        -show_packets \
        -show_entries packet=pts_time,flags \
        -of compact=p=0 \
        -v quiet \
        input.mp4 | \
        grep flags=K

Here, the -read_intervals option specifies the interval to read from. The format is start_time%+duration, where start_time is the start time in seconds, and duration is the duration in seconds. The %+ is a separator between the start time and duration. The + is used to indicate that the duration is relative to the start time. We use grep flags=K to only show keyframes.

To see which frame is closest (comes after) a certain timestamp, you can use awk to print the first line greater than or equal to the timestamp. For example, to find the closest I-frame after 150 seconds, you can use:

ffprobe \
        -select_streams v \
        -read_intervals 150%+10 \
        -show_packets \
        -show_entries packet=pts_time,flags \
        -of compact=p=0 \
        -v quiet \
        input.mp4 | \
        awk -F'[=|]' '$4 ~ /K/ && $2 >= 150 {print $2; exit}'

For example, this would return the pts_time of the first keyframe at or after 150 seconds. We filter out possible I-frames occurring before the start time.


Note that when using -ss before -i, FFmpeg will locate the keyframe previous to the seek point, then assign negative PTS values to all following packets up until it reaches the seek point. A player should decode but not display packets with negative PTS, and the video should start accurately.

Some players do not properly respect this and will display black video or garbage. In this case, the above script can be used to find the PTS of the keyframe after your seek point, and use that to start seeking from the keyframe. This, however, will not be accurate.

Note that if you want to be super accurate while seeking—and retain compatibility with many players—you should probably convert the video to any lossless, intra-only format, where you could cut at any point, and then re-encode it. But this will not be fast.

slhck
  • 235,242
18

I understand this question is several years old, but the latest version of ffprobe has the ability to skip frames. You can pass in -skip_frame nokey to report info only on the key frames (I-frames). This can save you a lot of time! On a 2GB 1080p MP4 file it used to take 4 minutes without the skip frames. Adding the skip parameter it only takes 20 seconds.

Command:

ffprobe -select_streams v -skip_frame nokey -show_frames \
        -show_entries frame=pkt_pts_time,pict_type test.mp4

Results:

[FRAME]
pkt_pts_time=0.000000
pict_type=I
[/FRAME]
[FRAME]
pkt_pts_time=3.753750
pict_type=I
[/FRAME]
[FRAME]
pkt_pts_time=7.507500
pict_type=I
[/FRAME]
[FRAME]
pkt_pts_time=11.261250
pict_type=I
[/FRAME]
[FRAME]
pkt_pts_time=15.015000
pict_type=I
[/FRAME]

So the results will only contain info regarding the key frames.

Hind-D
  • 446
3

Building on slhck's answer, here's a bash function which will return the closest keyframe that occurs BEFORE N seconds.

This also makes use of -read_intervals to ensure that ffprobe only starts looking for your keyframe 25 seconds before N seconds. This trick and having awk exit when the timestamp is found greatly speeds things up.

function ffnearest() {
  STIME=$2; export STIME;
  ffprobe -read_intervals $[$STIME-25]% -select_streams v -show_frames -show_entries frame=pkt_pts_time,pict_type -v quiet "$1" |
  awk -F= '
    /pict_type=/ { if (index($2, "I")) { i=1; } else { i=0; } }
    /pkt_pts_time/ { if (i && ($2 <= ENVIRON["STIME"])) print $2; }
    /pkt_pts_time/ { if (i && ($2 > ENVIRON["STIME"])) exit 0; }
  ' | tail -n 1
}

example usage:

➜ ffnearest input.mkv 30
23.941000

I use this to trim video files without re-encoding them. Since you can't add new keyframes without re-encoding, I use ffnearest to seek to the keyframe before I want to cut. Here's an example:

ffmpeg  -i input.mkv -ss 00:00:$(echo "$(ffnearest input.mkv 30) - 0.5" | bc)  -c copy -y output.mkv;

Note that for that example you may need to change the format of what's passed in the -ss param if you're seeking farther than first 60 seconds.

(annoyingly, telling ffmpeg to seek to exactly to the timestamp of the keyframe seems to make ffmpeg exclude that keyframe in the output, but subtracting 0.5 seconds from the keyframe's actual timestamp does the trick. For bash you need to use bc to evaluate expressions with decimals, but in zsh -ss 00:00:$[$(ffnearest input.mkv 28)-0.5] works.)

Chris
  • 183
2

To build upon previous answers, you can use the following command on any platform to output only a list of the times at which keyframes are located. The command will return the keyframe before or exactly on the first -read_intervals value and a list of keyframes within -read_intervals values, with the second value being exlusive. You can fine-tune the interval so that the command will only return a single keyframe either before or after your target timestamp. So for example, running this command on a video with keyframes every 8 seconds :

ffprobe -read_intervals 22%25 -v error -skip_frame nokey -show_entries frame=pkt_pts_time -select_streams v -of csv=p=0 test.mp4

will yield :

16.000000
24.000000

But changing -read_intervals to 22%24 will yield :

16.000000
0

if you want to get information of the I frames, you can use

ffprobe -i input.mp4 -v quiet -select_streams v -show_entries frame=pkt_pts_time,pict_type|grep -B 1 'pict_type=I'