FFmpeg get duration of video file without meta data

Solution 1:

For a raw bitstream, ffprobe can be used.

ffprobe -show_entries stream=r_frame_rate,nb_read_frames -select_streams v -count_frames -of compact=p=0:nk=1 -v 0 in.m2v

This produces

30/1|120

where first entry is the frame rate as a rational number, and second is the number of frames counted. Duration is 120 / (30/1) = 4.00s

Solution 2:

If you don't have metadata, then you need to build those metadata by reading and parsing the whole file (this is what ffprobe does), and get a correct answer based on the file's actual content.

There is no more accurate answer. There is, however, a faster answer. As noticed, "if you open such videos in VLC, it shows you the duration of the video immediately". In several places, VLC (and other tools too) appear to make an estimate, instead of reading in and parsing the whole file (which you might be unable or unwilling to do):

/* try to calculate movie time */
if( p_sys->p_fp->i_data_packets_count > 0 )
{
    uint64_t i_count;
    uint64_t i_size = stream_Size( p_demux->s );

    if( p_sys->i_data_end > 0 && i_size > p_sys->i_data_end )
    {
        i_size = p_sys->i_data_end;
    }

    /* real number of packets */
    i_count = ( i_size - p_sys->i_data_begin ) /
              p_sys->p_fp->i_min_data_packet_size;

    /* calculate the time duration in micro-s */
    p_sys->i_length = VLC_TICK_FROM_MSFTIME(p_sys->p_fp->i_play_duration) *
               (vlc_tick_t)i_count /
               (vlc_tick_t)p_sys->p_fp->i_data_packets_count;
    if( p_sys->i_length <= p_sys->p_fp->i_preroll )
        p_sys->i_length = 0;
    else
    {
        p_sys->i_length  -= p_sys->p_fp->i_preroll;
        p_sys->i_bitrate = 8 * i_size * CLOCK_FREQ / p_sys->i_length;
    }
}

In this case,

Stream #0:0: Video: mpeg2video (Main), yuv420p(tv), 720x576 [SAR 64:45 DAR 16:9], max. 7000 kb/s, 25 fps, 25 tbr, 1200k tbn, 50 tbc

knowing that the stream speed is 7000 kb/s and the file size, dividing the file size by 7000 kbit and multiplying by 8 immediately gives the likely duration in seconds.

For example with a file I have here (actual file duration 02:32:19, file size 733,802,496)

Stream #0:0: Video: ... 562 kb/s, 25 fps ...
Stream #0:1: Audio: ... 54 kb/s

562+54 is 616 kbits, which is 77 kbytes. 733,802,496 divided by (77*1024) is 9306, and 9306 seconds are 2 hours, 35 minutes, 6 seconds, which is not exactly correct, but pretty close.

Depending on the actual codec used, the audio/video interleaving method, any padding for the same, and whether it's CBR or VBR, the actual accuracy might vary.

Lacking reliable metadata, if you value speed over accuracy, then I fear that estimate is the way to go.

In some scenarios you might be able to do both (provide an immediate estimate based on file header and size, then start reading whenever the UI/program has nothing better to do and refine the answer. You might then even save the newly calculated metadata somewhere, to retrieve them if needed later on - in a database, a thumbnail file, an alternate data stream, or even offer to update/"repair" the video file if feasible).

Command-line

From the command line, using ffmpeg:

 #!/bin/bash

 if [ ! -r "$1" ]; then
     echo "File '$1' not found"
     exit 1
 fi
 FILESIZE=$( stat -c "%s" "$1" )
 STREAMS=$( 
      ffmpeg -i "$1" 2>&1 \
      | grep 'Stream #' \
      | tr "," "\n" \
      | grep "kb/s" \
      | tr " " "\n" \
      | grep "^[1-9][0-9]*$" )
 RATE=0
 for r in $STREAMS; do
     RATE=$[ $RATE + $r ]
 done
 # I don't think that bash has decimal support, so we use bc
 SECONDS=$( echo "$FILESIZE / $RATE" | bc )
 # To get seconds in HH:MM:SS format we use 'date'
 DURATION=$( TZ=UTC date +"%H:%M:%S" -d @$SECONDS )

This should be accurate even if you have multiple audio streams in a video file. I am not sure what happens if there is some data stream such as subtitles. The size of such a stream shouldn't be much, so it shouldn't interfere, but then again, it might.