]> granicus.if.org Git - handbrake/commitdiff
- Move frame rate code from sync to the end of render so it can account for frame...
authorvan <vanj.hb@gmail.com>
Sat, 2 May 2009 21:28:39 +0000 (21:28 +0000)
committervan <vanj.hb@gmail.com>
Sat, 2 May 2009 21:28:39 +0000 (21:28 +0000)
 - Fix a bug that would make CFR alternate between massive drops and massive dups on
some titles.
 - If we're not doing CFR add a factor-of-two fudge factor to init_delay to account f
or the awful clock resolution of some mkvs and mp4s.

git-svn-id: svn://svn.handbrake.fr/HandBrake/trunk@2368 b64f7644-9d1e-0410-96f1-a4d463321fa5

libhb/common.h
libhb/encx264.c
libhb/render.c
libhb/sync.c
libhb/work.c
test/test.c

index 1c58286fe88cf249fc2b44fdce062b7ea2ed8c98..60ba93ef6ec55a30490077b1d0a112bac061ab81 100644 (file)
@@ -183,9 +183,7 @@ struct hb_job_s
                             except with x264, to use its real QP/RF scale
          vbitrate:          output bitrate (kbps)
          vrate, vrate_base: output framerate is vrate / vrate_base
-         vfr:               boolean for variable frame rate detelecine
-         cfr:               boolean to use constant frame rates instead of
-                            passing through the source's frame durations.
+         cfr:               0 (vfr), 1 (cfr), 2 (pfr) [see render.c]
          pass:              0, 1 or 2 (or -1 for scan)
          h264_level:        vestigial boolean to decide if we're encoding for iPod
          crf:               boolean for whether to use constant rate factor with x264
index 0bc1546ffb84a2cdfdb9197bb2ab34843736685a..360dc8448a1b5fd9419b64c608e8d07dd39d1139 100644 (file)
@@ -50,6 +50,9 @@ struct hb_work_private_s
     x264_picture_t   pic_in;
     uint8_t         *x264_allocated_pic;
 
+    uint32_t       frames_in;
+    uint32_t       frames_out;
+    uint32_t       frames_split; // number of frames we had to split
     int            chap_mark;   // saved chap mark when we're propagating it
     int64_t        last_stop;   // Debugging - stop time of previous input frame
     int64_t        init_delay;
@@ -346,10 +349,14 @@ int encx264Init( hb_work_object_t * w, hb_job_t * job )
         pv->init_delay += 2;
 
         /* For VFR, libhb sees the FPS as 29.97, but the longest frames
-           will use the duration of frames running at 23.976fps instead.. */
-        if (job->vfr)
+           will use the duration of frames running at 23.976fps instead.
+           Since detelecine occasionally makes mistakes and since we have
+           to deal with some really horrible timing jitter from mkvs and
+           mp4s encoded with low resolution clocks, make the delay very
+           conservative if we're not doing CFR. */
+        if ( job->cfr != 1 )
         {
-            pv->init_delay = 7506;
+            pv->init_delay *= 2;
         }
 
         /* The delay is 1 frames for regular b-frames, 2 for b-pyramid. */
@@ -363,6 +370,12 @@ int encx264Init( hb_work_object_t * w, hb_job_t * job )
 void encx264Close( hb_work_object_t * w )
 {
     hb_work_private_t * pv = w->private_data;
+
+    if ( pv->frames_split )
+    {
+        hb_log( "encx264: %u frames had to be split (%u in, %u out)",
+                pv->frames_split, pv->frames_in, pv->frames_out );
+    }
     /*
      * Patch the x264 allocated data back in so that x264 can free it
      * we have been using our own buffers during the encode to avoid copying.
@@ -606,6 +619,7 @@ int encx264Work( hb_work_object_t * w, hb_buffer_t ** buf_in,
             hb_buffer_t *buf = nal_encode( w, &pic_out, i_nal, nal );
             if ( buf )
             {
+                ++pv->frames_out;
                 if ( last_buf == NULL )
                     *buf_out = buf;
                 else
@@ -624,6 +638,7 @@ int encx264Work( hb_work_object_t * w, hb_buffer_t ** buf_in,
     }
 
     // Not EOF - encode the packet & wrap it in a NAL
+    ++pv->frames_in;
 
     // if we're re-ordering frames, check if this frame is too large to reorder
     if ( pv->init_delay && in->stop - in->start > pv->init_delay )
@@ -639,6 +654,7 @@ int encx264Work( hb_work_object_t * w, hb_buffer_t ** buf_in,
         // error. We take advantage of the fact that x264 buffers frame
         // data internally to feed the same image into the encoder multiple
         // times, just changing its start & stop times each time.
+        ++pv->frames_split;
         int64_t orig_stop = in->stop;
         int64_t new_stop = in->start;
         hb_buffer_t *last_buf = NULL;
@@ -665,6 +681,7 @@ int encx264Work( hb_work_object_t * w, hb_buffer_t ** buf_in,
             hb_buffer_t *buf = x264_encode( w, in );
             if ( buf )
             {
+                ++pv->frames_out;
                 if ( last_buf == NULL )
                     *buf_out = buf;
                 else
@@ -676,6 +693,7 @@ int encx264Work( hb_work_object_t * w, hb_buffer_t ** buf_in,
     }
     else
     {
+        ++pv->frames_out;
         *buf_out = x264_encode( w, in );
     }
     return HB_WORK_OK;
index 5c7809dc3935287b273a6bc52a4337e0c3fc344d..fefec7792629c15df2697aa4747123266f13b3c0 100644 (file)
@@ -27,6 +27,11 @@ struct hb_work_private_s
     uint64_t             total_gained_time;
     int64_t              chapter_time;
     int                  chapter_val;
+    int                  count_frames;      // frames output so far
+    double               frame_rate;        // 90KHz ticks per frame (for CFR/PFR)
+    double               out_last_stop;     // where last frame ended (for CFR/PFR)
+    int                  drops;             // frames dropped (for CFR/PFR)
+    int                  dups;              // frames duped (for CFR/PFR)
 };
 
 int  renderInit( hb_work_object_t *, hb_job_t * );
@@ -197,6 +202,131 @@ static void ApplySub( hb_job_t * job, hb_buffer_t * buf,
     hb_buffer_close( _sub );
 }
 
+// delete the buffer 'out' from the chain of buffers whose head is 'buf_out'.
+// out & buf_out must be non-null (checks prior to anywhere this routine
+// can be called guarantee this) and out must be on buf_out's chain
+// (all places that call this get 'out' while traversing the chain).
+// 'out' is freed and its predecessor is returned.
+static hb_buffer_t *delete_buffer_from_chain( hb_buffer_t **buf_out, hb_buffer_t *out)
+{
+    hb_buffer_t *succ = out->next;
+    hb_buffer_t *pred = *buf_out;
+    if ( pred == out )
+    {
+        // we're deleting the first buffer
+        *buf_out = succ;
+    }
+    else
+    {
+        // target isn't the first buf so search for its predecessor.
+        while ( pred->next != out )
+        {
+            pred = pred->next;
+        }
+        // found 'out' - remove it from the chain
+        pred->next = succ;
+    }
+    hb_buffer_close( &out );
+    return succ;
+}
+
+// insert buffer 'succ' after buffer chain element 'pred'.
+// caller must guarantee that 'pred' and 'succ' are non-null.
+static hb_buffer_t *insert_buffer_in_chain( hb_buffer_t *pred, hb_buffer_t *succ )
+{
+    succ->next = pred->next;
+    pred->next = succ;
+    return succ;
+}
+
+// This section of the code implements video frame rate control.
+// Since filters are allowed to duplicate and drop frames (which
+// changes the timing), this has to be the last thing done in render.
+//
+// There are three options, selected by the value of job->cfr:
+//   0 - Variable Frame Rate (VFR) or 'same as source': frame times
+//       are left alone
+//   1 - Constant Frame Rate (CFR): Frame timings are adjusted so that all
+//       frames are exactly job->vrate_base ticks apart. Frames are dropped
+//       or duplicated if necessary to maintain this spacing.
+//   2 - Peak Frame Rate (PFR): job->vrate_base is treated as the peak
+//       average frame rate. I.e., the average frame rate (current frame
+//       end time divided by number of frames so far) is never allowed to be
+//       greater than job->vrate_base and frames are dropped if necessary
+//       to keep the average under this value. Other than those drops, frame
+//       times are left alone.
+//
+
+static void adjust_frame_rate( hb_work_private_t *pv, hb_buffer_t **buf_out )
+{
+    hb_buffer_t *out = *buf_out;
+
+    while ( out && out->size > 0 )
+    {
+        // this frame has to start where the last one stopped.
+        out->start = pv->out_last_stop;
+
+        // compute where this frame would stop if the frame rate were constant
+        // (this is our target stopping time for CFR and earliest possible
+        // stopping time for PFR).
+        double cfr_stop = pv->frame_rate * ( pv->count_frames + 1 );
+
+        if ( cfr_stop - (double)out->stop >= pv->frame_rate )
+        {
+            // This frame stops a frame time or more in the past - drop it
+            // but don't lose its chapter mark.
+            if ( out->new_chap )
+            {
+                pv->chapter_time = out->start;
+                pv->chapter_val = out->new_chap;
+            }
+            ++pv->drops;
+            out = delete_buffer_from_chain( buf_out, out );
+            continue;
+        }
+
+        // at this point we know that this frame doesn't push the average
+        // rate over the limit so we just pass it on for PFR. For CFR we're
+        // going to return it (with its start & stop times modified) and
+        // we may have to dup it.
+        ++pv->count_frames;
+        if ( pv->job->cfr > 1 )
+        {
+            // PFR - we're going to keep the frame but may need to
+            // adjust it's stop time to meet the average rate constraint.
+            if ( out->stop <= cfr_stop )
+            {
+                out->stop = cfr_stop;
+            }
+        }
+        else
+        {
+            // we're doing CFR so we have to either trim some time from a
+            // buffer that ends too far in the future or, if the buffer is
+            // two or more frame times long, split it into multiple pieces,
+            // each of which is a frame time long.
+            double excess_dur = (double)out->stop - cfr_stop;
+            out->stop = cfr_stop;
+            for ( ; excess_dur >= pv->frame_rate; excess_dur -= cfr_stop )
+            {
+                /* next frame too far ahead - dup current frame */
+                hb_buffer_t *dup = hb_buffer_init( out->size );
+                memcpy( dup->data, out->data, out->size );
+                hb_buffer_copy_settings( dup, out );
+                dup->new_chap = 0;
+                dup->start = cfr_stop;
+                cfr_stop += pv->frame_rate;
+                dup->stop = cfr_stop;
+                out = insert_buffer_in_chain( out, dup );
+                ++pv->dups;
+                ++pv->count_frames;
+            }
+        }
+        pv->out_last_stop = out->stop;
+        out = out->next;
+    }
+}
+
 int renderWork( hb_work_object_t * w, hb_buffer_t ** buf_in,
                 hb_buffer_t ** buf_out )
 {
@@ -217,7 +347,7 @@ int renderWork( hb_work_object_t * w, hb_buffer_t ** buf_in,
          * the correct order, and add the end of stream buffer to the
          * end.
          */     
-        while( next = hb_fifo_get( pv->delay_queue ) )
+        while( ( next = hb_fifo_get( pv->delay_queue ) ) != NULL )
         {
             
             /* We can't use the given time stamps. Previous frames
@@ -242,6 +372,10 @@ int renderWork( hb_work_object_t * w, hb_buffer_t ** buf_in,
         {
             tail->next = in;
             *buf_out = head;
+            if ( job->cfr )
+            {
+                adjust_frame_rate( pv, buf_out );
+            }
         } else {
             *buf_out = in;
         }     
@@ -270,7 +404,7 @@ int renderWork( hb_work_object_t * w, hb_buffer_t ** buf_in,
     }
 
     /* If there's a chapter mark remember it in case we delay or drop its frame */
-    if( in->new_chap && job->vfr )
+    if( in->new_chap )
     {
         pv->chapter_time = in->start;
         pv->chapter_val = in->new_chap;
@@ -321,33 +455,26 @@ int renderWork( hb_work_object_t * w, hb_buffer_t ** buf_in,
             }
             else if( result == FILTER_DROP )
             {
-                if( job->vfr )
-                {
-                    /* We need to compensate for the time lost by dropping this frame.
-                       Spread its duration out in quarters, because usually dropped frames
-                       maintain a 1-out-of-5 pattern and this spreads it out amongst the remaining ones.
-                       Store these in the lost_time array, which has 4 slots in it.
-                       Because not every frame duration divides evenly by 4, and we can't lose the
-                       remainder, we have to go through an awkward process to preserve it in the 4th array index. */
-                    uint64_t temp_duration = buf_tmp_out->stop - buf_tmp_out->start;
-                    pv->lost_time[0] += (temp_duration / 4);
-                    pv->lost_time[1] += (temp_duration / 4);
-                    pv->lost_time[2] += (temp_duration / 4);
-                    pv->lost_time[3] += ( temp_duration - (temp_duration / 4) - (temp_duration / 4) - (temp_duration / 4) );
-
-                    pv->total_lost_time += temp_duration;
-                    pv->dropped_frames++;
-
-                    /* Pop the frame's subtitle and dispose of it. */
-                    hb_buffer_t * subtitles = hb_fifo_get( pv->subtitle_queue );
-                    hb_buffer_close( &subtitles );
-                    buf_tmp_in = NULL;
-                    break;
-                }
-                else
-                {
-                    buf_tmp_in = buf_tmp_out;
-                }
+                /* We need to compensate for the time lost by dropping this frame.
+                   Spread its duration out in quarters, because usually dropped frames
+                   maintain a 1-out-of-5 pattern and this spreads it out amongst the remaining ones.
+                   Store these in the lost_time array, which has 4 slots in it.
+                   Because not every frame duration divides evenly by 4, and we can't lose the
+                   remainder, we have to go through an awkward process to preserve it in the 4th array index. */
+                uint64_t temp_duration = buf_tmp_out->stop - buf_tmp_out->start;
+                pv->lost_time[0] += (temp_duration / 4);
+                pv->lost_time[1] += (temp_duration / 4);
+                pv->lost_time[2] += (temp_duration / 4);
+                pv->lost_time[3] += ( temp_duration - (temp_duration / 4) - (temp_duration / 4) - (temp_duration / 4) );
+
+                pv->total_lost_time += temp_duration;
+                pv->dropped_frames++;
+
+                /* Pop the frame's subtitle and dispose of it. */
+                hb_buffer_t * subtitles = hb_fifo_get( pv->subtitle_queue );
+                hb_buffer_close( &subtitles );
+                buf_tmp_in = NULL;
+                break;
             }
         }
     }
@@ -429,7 +556,7 @@ int renderWork( hb_work_object_t * w, hb_buffer_t ** buf_in,
         hb_buffer_copy_settings( buf_render, buf_tmp_in );
     }
 
-    if (*buf_out && job->vfr)
+    if (*buf_out )
     {
         hb_fifo_push( pv->delay_queue, *buf_out );
         *buf_out = NULL;
@@ -440,15 +567,12 @@ int renderWork( hb_work_object_t * w, hb_buffer_t ** buf_in,
      * two always in there should we need to rewrite the durations on them.
      */
 
-    if( job->vfr )
+    if( hb_fifo_size( pv->delay_queue ) >= 4 )
     {
-        if( hb_fifo_size( pv->delay_queue ) >= 4 )
-        {
-            *buf_out = hb_fifo_get( pv->delay_queue );
-        }
+        *buf_out = hb_fifo_get( pv->delay_queue );
     }
 
-    if( *buf_out && job->vfr)
+    if( *buf_out )
     {
         /* The current frame exists. That means it hasn't been dropped by a filter.
            Make it accessible as ivtc_buffer so we can edit its duration if needed. */
@@ -527,6 +651,10 @@ int renderWork( hb_work_object_t * w, hb_buffer_t ** buf_in,
 
     }
 
+    if ( buf_out && *buf_out && job->cfr )
+    {
+        adjust_frame_rate( pv, buf_out );
+    }
     return HB_WORK_OK;
 }
 
@@ -534,6 +662,12 @@ void renderClose( hb_work_object_t * w )
 {
     hb_work_private_t * pv = w->private_data;
 
+    if ( pv->job->cfr )
+    {
+        hb_log("render: %d frames output, %d dropped and %d duped for CFR/PFR",
+               pv->count_frames, pv->drops, pv->dups );
+    }
+
     hb_log("render: lost time: %lld (%i frames)", pv->total_lost_time, pv->dropped_frames);
     hb_log("render: gained time: %lld (%i frames) (%lld not accounted for)", pv->total_gained_time, pv->extended_frames, pv->total_lost_time - pv->total_gained_time);
     if (pv->dropped_frames)
@@ -599,6 +733,8 @@ int renderInit( hb_work_object_t * w, hb_job_t * job )
     pv->chapter_time = 0;
     pv->chapter_val  = 0;
 
+    pv->frame_rate = (double)job->vrate_base * (1./300.);
+
     /* Setup filters */
     /* TODO: Move to work.c? */
     if( job->filters )
index f7b342b61676d0a79a74431c4fd70cdd4c986c72..a1a7f1ad9e430fb287dc36c5a22b30ac96274637 100644 (file)
@@ -518,85 +518,27 @@ static void SyncVideo( hb_work_object_t * w )
             }
         }
 
-        int64_t duration;
-        if ( job->mux & HB_MUX_AVI || job->cfr )
-        {
-            /*
-             * The concept of variable frame rate video was a bit too advanced
-             * for Microsoft so AVI doesn't support it. Since almost all dvd
-             * video is VFR we have to convert it to constant frame rate to
-             * put it in an AVI container. So here we duplicate, drop and
-             * otherwise trash video frames to appease the gods of Redmond.
-             */
-
-            /* mpeg durations are exact when expressed in ticks of the
-             * 27MHz System clock but not in HB's 90KHz PTS clock. To avoid
-             * a truncation bias that will eventually cause the audio to desync
-             * we compute the duration of the next frame using 27MHz ticks
-             * then truncate it to 90KHz. */
-            duration = ( (int64_t)(pv->count_frames + 1 ) * job->vrate_base ) / 300 -
-                       pv->next_start;
-
-            /* We don't want the input & output clocks to be exactly in phase
-             * otherwise small variations in the time will cause us to think
-             * we're a full frame off & there will be lots of drops and dups.
-             * We offset the input clock by half the duration so it's maximally
-             * out of phase with the output clock. */
-            if( cur->start < pv->next_start  - ( duration >> 1 ) )
-            {
-                /* current frame too old - drop it */
-                if ( cur->new_chap )
-                {
-                    pv->chap_mark = cur->new_chap;
-                }
-                hb_buffer_close( &cur );
-                pv->cur = cur = hb_fifo_get( job->fifo_raw );
-                pv->next_pts = next->start;
-                ++pv->drops;
-                continue;
-            }
-
-            if( next->start > pv->next_start + duration + ( duration >> 1 ) )
-            {
-                /* next frame too far ahead - dup current frame */
-                buf_tmp = hb_buffer_init( cur->size );
-                hb_buffer_copy_settings( buf_tmp, cur );
-                memcpy( buf_tmp->data, cur->data, cur->size );
-                buf_tmp->sequence = cur->sequence;
-                ++pv->dups;
-            }
-            else
-            {
-                /* this frame in our time window & doesn't need to be duped */
-                buf_tmp = cur;
-                pv->cur = cur = hb_fifo_get( job->fifo_raw );
-                pv->next_pts = next->start;
-            }
-        }
-        else
+        /*
+         * Adjust the pts of the current frame so that it's contiguous
+         * with the previous frame. The start time of the current frame
+         * has to be the end time of the previous frame and the stop
+         * time has to be the start of the next frame.  We don't
+         * make any adjustments to the source timestamps other than removing
+         * the clock offsets (which also removes pts discontinuities).
+         * This means we automatically encode at the source's frame rate.
+         * MP2 uses an implicit duration (frames end when the next frame
+         * starts) but more advanced containers like MP4 use an explicit
+         * duration. Since we're looking ahead one frame we set the
+         * explicit stop time from the start time of the next frame.
+         */
+        buf_tmp = cur;
+        pv->cur = cur = hb_fifo_get( job->fifo_raw );
+        pv->next_pts = cur->start;
+        int64_t duration = cur->start - buf_tmp->start;
+        if ( duration <= 0 )
         {
-            /*
-             * Adjust the pts of the current frame so that it's contiguous
-             * with the previous frame. The start time of the current frame
-             * has to be the end time of the previous frame and the stop
-             * time has to be the start of the next frame.  We don't
-             * make any adjustments to the source timestamps other than removing
-             * the clock offsets (which also removes pts discontinuities).
-             * This means we automatically encode at the source's frame rate.
-             * MP2 uses an implicit duration (frames end when the next frame
-             * starts) but more advanced containers like MP4 use an explicit
-             * duration. Since we're looking ahead one frame we set the
-             * explicit stop time from the start time of the next frame.
-             */
-            buf_tmp = cur;
-            pv->cur = cur = hb_fifo_get( job->fifo_raw );
-            pv->next_pts = cur->start;
-            duration = cur->start - buf_tmp->start;
-            if ( duration <= 0 )
-            {
-                hb_log( "sync: invalid video duration %lld, start %lld, next %lld",
-                        duration, buf_tmp->start, next->start );
-            }
+            hb_log( "sync: invalid video duration %lld, start %lld, next %lld",
+                    duration, buf_tmp->start, next->start );
         }
 
         buf_tmp->start = pv->next_start;
index 4cd9ffe6e6a46bd518a5003c4b0a2318556c16d6..8e0734ceb587483f7ee10eb67ad76696700f8e2f 100644 (file)
@@ -186,20 +186,19 @@ void hb_display_job_info( hb_job_t * job )
         hb_log( "     + bitrate %d kbps", title->video_bitrate / 1000 );
     }
     
-    if( job->vfr)
-    {
-        hb_log( "   + frame rate: %.3f fps -> variable fps",
-            (float) title->rate / (float) title->rate_base );
-    }
-    else if( !job->cfr )
+    if( !job->cfr )
     {
         hb_log( "   + frame rate: same as source (around %.3f fps)",
             (float) title->rate / (float) title->rate_base );
     }
     else
     {
-        hb_log( "   + frame rate: %.3f fps -> constant %.3f fps",
-            (float) title->rate / (float) title->rate_base, (float) job->vrate / (float) job->vrate_base );
+        static const char *frtypes[] = {
+            "", "constant", "peak rate limited to"
+        };
+        hb_log( "   + frame rate: %.3f fps -> %s %.3f fps",
+            (float) title->rate / (float) title->rate_base, frtypes[job->cfr],
+            (float) job->vrate / (float) job->vrate_base );
     }
 
     if( job->anamorphic.mode )
@@ -402,16 +401,19 @@ static void do_job( hb_job_t * job, int cpu_count )
         hb_log( "New dimensions %i * %i", job->width, job->height );
     }
 
-    if( ( job->mux & HB_MUX_AVI ) || job->cfr )
+    if( job->mux & HB_MUX_AVI )
     {
-        /* VFR detelecine is not compatible with AVI or constant frame rates. */
-        job->vfr = 0;
+        // The concept of variable frame rate video was a bit too advanced
+        // for Microsoft so AVI doesn't support it. Since almost all dvd
+        // video is VFR we have to convert it to constant frame rate to
+        // put it in an AVI container. So duplicate, drop and
+        // otherwise trash video frames to appease the gods of Redmond.
+        job->cfr = 1;
     }
 
-    if ( job->vfr )
+    if ( job->cfr == 0 )
     {
-        /* Ensure we're using "Same as source" FPS,
-           aka VFR, if we're doing VFR detelecine. */
+        /* Ensure we're using "Same as source" FPS */
         job->vrate_base = title->rate_base;
     }
 
index 36fac4e9c7145a5287dc0fb8b8564a25c5fbe1c4..2ab1594883958082a973f586f000a1f566e1a850 100644 (file)
@@ -1184,13 +1184,6 @@ static int HandleEvents( hb_handle_t * h )
             {
                 hb_filter_detelecine.settings = detelecine_opt;
                 hb_list_add( job->filters, &hb_filter_detelecine );
-                
-                if( !vrate )
-                {
-                    /* No framerate specified, so using same as source.
-                       That means VFR, so set detelecine up to drop frames. */
-                           job->vfr = 1;
-                }
             }
             if( decomb )
             {
@@ -1264,10 +1257,18 @@ static int HandleEvents( hb_handle_t * h )
             }
             if( vrate )
             {
-                job->cfr = 1;
+                job->cfr = cfr;
                 job->vrate = 27000000;
                 job->vrate_base = vrate;
             }
+            else if ( cfr )
+            {
+                // cfr or pfr flag with no rate specified implies
+                // use the title rate.
+                job->cfr = cfr;
+                job->vrate = title->rate;
+                job->vrate_base = title->rate_base;
+            }
 
             /* Grab audio tracks */
             if( atracks )
@@ -1970,6 +1971,16 @@ static void ShowHelp()
     "                            Be aware that not specifying a framerate lets\n"
     "                            HandBrake preserve a source's time stamps,\n"
     "                            potentially creating variable framerate video\n"
+    "    --vfr, --cfr, --pfr     Select variable, constant or peak-limited\n"
+    "                            frame rate control. VFR preserves the source\n"
+    "                            timing. CFR makes the output constant rate at\n"
+    "                            the rate given by the -r flag (or the source's\n"
+    "                            average rate if no -r is given). PFR doesn't\n"
+    "                            allow the rate to go over the rate specified\n"
+    "                            with the -r flag but won't change the source\n"
+    "                            timing if it's below that rate.\n"
+    "                            If none of these flags are given, the default\n"
+    "                            is --cfr when -r is given and --vfr otherwise\n"
 
     "\n"
     "### Audio Options-----------------------------------------------------------\n\n"
@@ -2201,7 +2212,9 @@ static int ParseOptions( int argc, char ** argv )
             { "previews",    required_argument, NULL,    PREVIEWS },
             { "start-at-preview", required_argument, NULL, START_AT_PREVIEW },
             { "stop-at",    required_argument, NULL,     STOP_AT },
-
+            { "vfr",         no_argument,       &cfr,    0 },
+            { "cfr",         no_argument,       &cfr,    1 },
+            { "pfr",         no_argument,       &cfr,    2 },
             { 0, 0, 0, 0 }
           };
 
@@ -2218,6 +2231,9 @@ static int ParseOptions( int argc, char ** argv )
 
         switch( c )
         {
+            case 0:
+                /* option was handled entirely in getopt_long */
+                break;
             case 'h':
                 ShowHelp();
                 exit( 0 );
@@ -2507,6 +2523,10 @@ static int ParseOptions( int argc, char ** argv )
                 {
                     fprintf( stderr, "invalid framerate %s\n", optarg );
                 }
+                else if ( cfr == 0 )
+                {
+                    cfr = 1;
+                }
                 break;
             }
             case 'R':