59

We need to split a large live WMV video feed in small chunks all of the same size. We made a script that works fine doing this, except for one thing: the video chunks don't start with a key frame, so when playing most video chunks they don't display any image until a key frame from the original video is eventually reached.

Isn't there a way to tell ffmpeg to make the output video to start with a key frame?

Here is how our command lines look right now:

ffmpeg.exe -i "C:\test.wmv" -ss 00:00:00 -t 00:00:05 -acodec copy -vcodec copy -async 1 -y  "0000.wmv"
ffmpeg.exe -i "C:\test.wmv" -ss 00:00:05 -t 00:00:05 -acodec copy -vcodec copy -async 1 -y  "0001.wmv"

and so on...

llogan
  • 121,796
  • 28
  • 232
  • 243
sboisse
  • 4,860
  • 3
  • 37
  • 48
  • 1
    Are you using a recent ffmpeg build? – llogan Dec 22 '12 at 19:45
  • 1
    Yes the latest: ffmpeg version N-47062-g26c531c. We also tried using -force_key_frames:v 00:00:00, or -force_key_frames:v 00:00:05, but it does not make a difference in the ouput. I believe using -vcodec copy actually means it is just copying groups of pictures in the new file, but won't do any kind of reencoding to add key frames. Is it the case? – sboisse Dec 22 '12 at 19:48
  • `-vcodec copy` does not re-encode: it only performs demuxing and muxing. – llogan Dec 22 '12 at 19:52
  • 1
    Is there a way for me to split my file into chunks so every chunk starts with a key frame? they don't have to be all the same duration. But they must be perfectly contiguous. If I specify -ss on the input file, it cuts at the key frame but then I see my videos overlap by re-including the same GOP in 2 consecutive chunks. – sboisse Dec 22 '12 at 19:59

6 Answers6

53

The latest builds of FFMPEG include a new option "segment" which does exactly what I think you need.

ffmpeg -i INPUT.mp4 -acodec copy -f segment -vcodec copy -reset_timestamps 1 -map 0 OUTPUT%d.mp4

This produces a series of numbered output files which are split into segments based on Key Frames. In my own testing, it's worked well, although I haven't used it on anything longer than a few minutes and only in MP4 format.

Tim Bull
  • 2,375
  • 21
  • 25
  • 1
    Seems like the examples have moved here: https://www.ffmpeg.org/ffmpeg-formats.html#Examples-9 – jox Mar 19 '18 at 10:39
  • 1
    @jox seems like they have moved again, to: https://www.ffmpeg.org/ffmpeg-formats.html#segment_002c-stream_005fsegment_002c-ssegment – TheAudiophile Jan 01 '22 at 15:41
  • @TheAudiophile you're right, thanks. The following link goes directly to the examples: https://www.ffmpeg.org/ffmpeg-formats.html#Examples-10 – jox Jan 03 '22 at 16:31
44

As stated on the official FFMPEG Docs, it has worked better for me to specify -ss timestart before -i input_file.ext, because it sets (or so I understand) the beginning of the generated video to the nearest keyframe found before your specified timestamp.

Change your example to:

ffmpeg.exe -ss 00:00:00 -i "C:\test.wmv" -t 00:00:05 -acodec copy -vcodec copy -async 1 -y  "0000.wmv"

I have tested this method working on .flv and .mp4 files.

Sopalajo de Arrierez
  • 3,543
  • 4
  • 34
  • 52
  • 3
    Though this kinda messes up the duration of the video, e.g. the video i got was displayed as 8 seconds long even though i specified that the video lasts for 5 seconds. The video indeed does end at 5 seconds, but in all media players it's shown as if its 8 seconds long. Any ideas? – IanDess Dec 06 '17 at 17:19
  • This works pretty well! Just it does guarantee a 1 second frozen frame at the beginning, but the duration is fine for me for a 30fps video. – CreativiTimothy Dec 17 '18 at 00:41
  • 11
    I think it's not correct. If you scroll down at the Seeking page, it says specifically for `codec copy` that: `Using -ss as input option together with -c:v copy might not be accurate since ffmpeg is forced to only use/split on i-frames. Though it will—if possible—adjust the start time of the stream to a negative value to compensate for that. Basically, if you specify "second 157" and there is no key frame until second 159, it will include two seconds of audio (with no video) at the start, then will start from the first key frame. So be careful when splitting and doing codec copy.` – Ondra Žižka Apr 11 '20 at 00:53
  • 1
    @Ondra: okay but how does one prevent this, to generate a video that plays correctly from start to finish? – MRule Oct 28 '21 at 17:07
  • 1
    @OndraŽižka It is good to be mindful of this. When cutting out a large portion of the beginning of a video, it's helpful to do it in two passes: one with `-ss` before the input, to inaccurately skip to just before where you want to start, and save that output as a temp file; and a second pass with `-ss` after the input, using the temp file as input, with `-ss` set more precisely. This allows you to figure out the precise value of `-ss` without repeatedly seeking to the desired cutoff point using the needlessly slow-but-accurate method. – BallpointBen Jun 29 '23 at 20:18
  • 1
    @BallpointBen - right. @MRule - see the above comment. It is tedious but correct. I hope `ffmpeg` will automate this way. – Ondra Žižka Jul 13 '23 at 20:47
22

Here is the solution that I could get to work:

As suggested by av501 and d33pika, I used ffprobe to find where the key frames are. Because ffprobe is very verbose and can take several seconds or even minutes to output all key frames and there is no way to scope the range of frames we want from a lengthy video, I proceed into 5 steps:

  1. Export a video chunk from the original file, around the double of the desired chunk size.

    ffmpeg -i source.wmv -ss 00:00:00 -t 00:00:06 -acodec copy -vcodec copy -async 1 -y  0001.wmv
    
  2. Use ffprobe to find where the keyframes are. Choose closest keyframe after desired chunk size.

    ffprobe -show_frames -select_streams v -print_format json=c=1 0001.wmv
    
  3. From the output of ffprobe get the pkt_dts_time of the frame just before that key frame.

  4. ffmpeg on the exported chunk of step 1, specifying the same input and output file, and specifying -ss 00:00:00 and -t [value found in step 3].

    ffmpeg -i 0001.wmv -ss 00:00:00 -t 00:00:03.1350000 -acodec copy -vcodec copy -async 1 -y 0001.wmv
    
  5. Restart at step 1, using -ss [cumulated sum of values found in step 3 over iterations].

Proceeding this way, I was able to have an efficient and robust way to split the video at key frames.

llogan
  • 121,796
  • 28
  • 232
  • 243
sboisse
  • 4,860
  • 3
  • 37
  • 48
  • 1
    I was able to do the same with your help, however, the initial dts/pts was shifted a little bit (500ms here). The documentation indicate that if -ss is passed BEFORE -i, then it act as a seek, and in my case, it fixed the shift. Read the man ffmpeg on the -ss section :) – tito Dec 09 '14 at 13:52
  • 1
    Why do you use the frame before the keyframe instead of the keyframe itself? Re: _"From the output of ffprobe get the pkt_dts_time of the frame just before that key frame."_ – felwithe Aug 19 '15 at 00:49
  • 1
    If you don't cut just before the key frame, that key frame will be included as the last frame of the chunk, and the next chunk will not start with a key frame. – sboisse Aug 26 '15 at 18:54
  • 1
    The statement "there is no way to scope the range of frames we want from a lengthy video" for ffprobe is not true, or at least not any longer. Adding the option -read_intervals 18.6%+4.2 for example starts reading at second 18.6 and continues for 4.2 seconds. – Sebastian Dec 02 '21 at 13:27
16

Using a newer build of ffmpeg, can achieve this by using ffprobe and the ffmpeg segment muxer.

  1. Use ffprobe and awk to identify the keyframes as close as possible to your desired chunk length.
ffprobe -show_frames -select_streams v:0 \
        -print_format csv [SOURCE_VIDEO] 2>&1 |
grep -n frame,video,1 |
awk 'BEGIN { FS="," } { print $1 " " $5 }' |
sed 's/:frame//g' |
awk 'BEGIN { previous=0; frameIdx=0; size=0; } 
{
  split($2,time,".");
  current=time[1];
  if (current-previous >= [DURATION_IN_SECONDS]){
    a[frameIdx]=$1; frameIdx++; size++; previous=current;
  }
}
END {
  str=a[0];
  for(i=1;i<size;i++) { str = str "," a[i]; } print str;
}'

Where

  • [SOURCE_VIDEO] = path to video you want to segment
  • [DURATION_IN_SECONDS] = desired segment length in seconds

The output is comma-delimited string of keyframes.

  1. Use the keyframes output above as input to ffmpeg.
ffmpeg -i [SOURCE_VIDEO] -codec copy -map 0 -f segment \
       -segment_frames [OUTPUT_OF_STEP_1] [SEGMENT_PREFIX] \
       _%03d.[SOURCE_VIDEO_EXTENSION]

Where

  • [SOURCE_VIDEO] = path to video you want to segment
  • [OUTPUT_OF_STEP_1] = comma-delimited string of keyframes
  • [SEGMENT_PREFIX] = name of segment output
  • [SOURCE_VIDEO_EXTENSION] = extension of source video (e.g., mp4, mkv)
QuickSilver
  • 3,915
  • 2
  • 13
  • 29
Steek
  • 161
  • 1
  • 3
5

Use ffprobe -show_frames -pretty <stream> to identify the key frames.

d33pika
  • 1,997
  • 14
  • 24
  • Looks like a promising solution. Will investigate. I checked in the documentation but could not easily find a way to ffprobe a small section of the video (say, only the first 10 seconds). Because the video is quite long, it generates a several MB output that takes several seconds to generate. Are there options in ffprobe to output frames of a particular stream only, and within a particular range? – sboisse Dec 23 '12 at 15:39
  • ffprobe -show_frames **-select_streams v** filtered to only video frames. Now I would only need to be able to specify a range of frame or a range of time. – sboisse Dec 23 '12 at 15:47
  • I went through the documentation and did not find anything about restraining to a particular time range of the video. So I guess I will use ffmpeg to get a chunk, then call ffprobe on it to get the key frames, and then recall ffmpeg on it to do the final split just before a key frame. – sboisse Dec 23 '12 at 15:56
  • 2
    @sboisse, `-read_intervals` now allows you to specify time or packet intervals to probe. e.g. `ffprobe -show_frames -read_intervals 10%+20,01:30%01:45 -i ` probes from 10-30 seconds, and then from 1:30-1:45. –  Dec 19 '16 at 14:29
2

If you are willing to do some scripting and want I frames at a particular interval the one way to do it is

  1. Run ffprobe and collect the locations of the I frames from the output

    ffprobe -show_streams

  2. Run a series of -ss -t commands using the same script to get the chunks you desire.

You can then have your script decide minimum number of frames [say there are two I pictures within 10 frames of each other, you really don't want to be chunking it there].

The other way to do it is to use gstreamer's multisfilesink and set the mode to key frame [however that will chunk at every key frame so that may not be ideal]

av501
  • 6,645
  • 2
  • 23
  • 34