Search code examples
videoffmpegmp4seekhevc

ffmpeg inaccurate seeking keyframes with H265 vcopy


I've got an H265 4K MP4 29.97fps video with a GOP-size of exactly 30 frames. When I try to cut from the start using :

ffmpeg -ss 1 -i INPUT.MP4 -vcodec copy OUTPUT_1SEC.MP4
ffmpeg -ss 2 -i INPUT.MP4 -vcodec copy OUTPUT_2SEC.MP4
ffmpeg -ss 3 -i INPUT.MP4 -vcodec copy OUTPUT_3SEC.MP4
ffmpeg -ss 4 -i INPUT.MP4 -vcodec copy OUTPUT_4SEC.MP4
ffmpeg -ss 5 -i INPUT.MP4 -vcodec copy OUTPUT_5SEC.MP4
ffmpeg -ss 6 -i INPUT.MP4 -vcodec copy OUTPUT_6SEC.MP4
ffmpeg -ss 7 -i INPUT.MP4 -vcodec copy OUTPUT_7SEC.MP4
ffmpeg -ss 8 -i INPUT.MP4 -vcodec copy OUTPUT_8SEC.MP4
ffmpeg -ss 9 -i INPUT.MP4 -vcodec copy OUTPUT_9SEC.MP4

The output videos starts at either 0 (-ss 1~4), 4 (-ss 5~8) or 8 sec (-ss 9) as shown below:

Premiere Pro Timeline

So it seems ffmpeg somehow detect a GOP of 4 seconds instead of 1 seconds. Is it normal ?

Also, how can I burn the correct timecode in the output video ? For example, I tried many combinations such as:

ffmpeg -ss 5 -i INPUT.MP4 -vcodec copy -timecode 00:00:05:00 OUTPUT_5SEC.MP4
ffmpeg -ss 5 -i INPUT.MP4 -vcodec copy -copyts OUTPUT_5SEC.MP4
ffmpeg -start_at_zero -ss 5 -i INPUT.MP4 -vcodec copy -copyts OUTPUT_5SEC.MP4

But it either give me the exact timecode I put (first line) or starts at zero (two last lines)

Originally, I was thinking about seeking at the exact second (or a few frame after) so I knew I would get a keyframe so I could guess the exact timecode the output would start, but it seems ffmpeg -ss is not exactly based on keyframes ? Maybe I'm missing something here ? Thanks for the help.

Additionnal infos

I'd like to script the cut process, that's why I want to know where this 4-sec "keyframe interval" come from.

Here is the ffprobe output of my input:

Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'INPUT.MP4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2mp41
    encoder         : Lavf57.72.101
    comment         : DE=None, Mode=M, DSW=0001
    location-{    : +XX.4914-0XX.5164+XX.000000/
    location        : +XX.4914-0XX.5164+XX.000000/
  Duration: 00:01:45.31, start: 0.000000, bitrate: 100065 kb/s
    Stream #0:0(eng): Video: hevc (Main) (hev1 / 0x31766568), yuv420p(tv, bt709), 4096x2160 [SAR 1:1 DAR 256:135], 100062 kb/s, 29.97 fps, 29.97 tbr, 30k tbn, 29.97 tbc (default)
    Metadata:
      handler_name    : VideoHandler
    Stream #0:1(eng): Subtitle: mov_text (tx3g / 0x67337874), 2 kb/s (default)
    Metadata:
      handler_name    : SubtitleHandler

Here is the command I used to check gop-size ('I' type at 1,31,61,... and 'P' in between):

ffprobe -i INPUT.MP4 -select_streams v -show_frames -show_entries frame=pict_type -of csv > OUTPUT.CSV

ffmpeg version N-86330-gbd1179e and ffmpeg version N-86330-gbd1179e

Edit : Sample video here


Solution

  • Although there is a keyframe each second, in the MOOV box, only three frames are set as sync samples

    /moov/trak/mdia/minf/stbl/stss                              @ 0x77e8515
      Box size: 0x1c    version: 0x0    flags: 0x0
      entry_count:              0x3
        sample_number:
        0x1    0x79    0xf1
    

    (those are the 1st, 121st and 241st frames.)

    FFmpeg relies on this info when seeking.


    Workaround is to mux to TS and then remux to MP4.

    ffmpeg -i input.mp4 -c copy input.ts
    

    and then

    ffmpeg -i input.ts -c copy newinput.mp4
    

    Or in one command

    ffmpeg -i input.mp4 -c copy -f mpegts - | ffmpeg -f mpegts -i - -c copy newinput.mp4
    

    MPEG-TS files don't have an index so if you want to use that file for extraction, specify a seek point before the keyframe you wish to cut from.

    As to why the sync table is that way, don't know. That's upto the original writing application and the settings/arguments used there.