Detect soft 2:3 pulldown (telecine) files and decode them at 23.976.
authorCarl Hetherington <cth@carlh.net>
Sun, 2 Aug 2020 20:24:05 +0000 (22:24 +0200)
committerCarl Hetherington <cth@carlh.net>
Sun, 2 Aug 2020 20:30:18 +0000 (22:30 +0200)
DVD rips from NTSC DVDs are sometimes (always?) encoded using
soft 2:3 pulldown.  The video frames are actually 23.976 but
FFmpeg detects them as 29.97.  With the current approach of the video
decoder ignoring most PTSs and assuming a constant frame rate
it is vital that the file contains the number of frames per second
that the detected frame rate predicts.

This fixes large sync errors with NTSC DVD rips (#1790).

Cherry-picked from af680761cf7c3e97660e8e55c68f42e90b026bf9
in v2.15.x.

src/lib/ffmpeg_content.cc
src/lib/ffmpeg_examiner.cc
src/lib/ffmpeg_examiner.h

index a3a1cfb0fd491da77b0b2ac23b6964fb75983309..6383fe58b6736a56c67c696c085c146383517c99 100644 (file)
@@ -324,6 +324,18 @@ FFmpegContent::examine (shared_ptr<const Film> film, shared_ptr<Job> job)
        if (examiner->has_video ()) {
                set_default_colour_conversion ();
        }
+
+       if (examiner->has_video() && examiner->pulldown() && video_frame_rate() && fabs(*video_frame_rate() - 29.97) < 0.001) {
+               /* FFmpeg has detected this file as 29.97 and the examiner thinks it is using "soft" 2:3 pulldown (telecine).
+                * This means we can treat it as a 23.976fps file.
+                */
+               set_video_frame_rate (24000.0 / 1001);
+               video->set_length (video->length() * 24.0 / 30);
+       }
+
+#ifdef DCPOMATIC_VARIANT_SWAROOP
+       _id = examiner->id ();
+#endif
 }
 
 string
index b31280cd5b3cac7b093456e2c9cb2c946a71635a..1a2c49f1cdfd66dcfd77e63f312255cbf4533be8 100644 (file)
@@ -18,6 +18,7 @@
 
 */
 
+#include "dcpomatic_log.h"
 extern "C" {
 #include <libavcodec/avcodec.h>
 #include <libavformat/avformat.h>
@@ -40,14 +41,22 @@ extern "C" {
 using std::string;
 using std::cout;
 using std::max;
+using std::vector;
 using boost::shared_ptr;
 using boost::optional;
 
+/* This is how many frames from the start of any video that we will examine to see if we
+ * can spot soft 2:3 pull-down ("telecine").
+ */
+static const int PULLDOWN_CHECK_FRAMES = 16;
+
+
 /** @param job job that the examiner is operating in, or 0 */
 FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Job> job)
        : FFmpeg (c)
        , _video_length (0)
        , _need_video_length (false)
+       , _pulldown (false)
 {
        /* Find audio and subtitle streams */
 
@@ -100,9 +109,15 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Jo
        /* Run through until we find:
         *   - the first video.
         *   - the first audio for each stream.
+        *   - the top-field-first and repeat-first-frame values ("temporal_reference") for the first PULLDOWN_CHECK_FRAMES video frames.
         */
 
        int64_t const len = _file_group.length ();
+       /* A string which we build up to describe the top-field-first and repeat-first-frame values for the first few frames.
+        * It would be nicer to use something like vector<bool> here but we want to search the array for a pattern later,
+        * and a string seems a reasonably neat way to do that.
+        */
+       string temporal_reference;
        while (true) {
                int r = av_read_frame (_format_context, &_packet);
                if (r < 0) {
@@ -120,7 +135,7 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Jo
                AVCodecContext* context = _format_context->streams[_packet.stream_index]->codec;
 
                if (_video_stream && _packet.stream_index == _video_stream.get()) {
-                       video_packet (context);
+                       video_packet (context, temporal_reference);
                }
 
                bool got_all_audio = true;
@@ -136,7 +151,7 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Jo
 
                av_packet_unref (&_packet);
 
-               if (_first_video && got_all_audio) {
+               if (_first_video && got_all_audio && temporal_reference.size() >= PULLDOWN_CHECK_FRAMES) {
                        /* All done */
                        break;
                }
@@ -165,14 +180,33 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c, shared_ptr<Jo
 
                DCPOMATIC_ASSERT (fabs (*_rotation - 90 * round (*_rotation / 90)) < 2);
        }
+
+       LOG_GENERAL("Temporal reference was %1", temporal_reference);
+       if (temporal_reference.find("T2T3B2B3T2T3B2B3") || temporal_reference.find("B2B3T2T3B2B3T2T3")) {
+               /* The magical sequence (taken from mediainfo) suggests that 2:3 pull-down is in use */
+               _pulldown = true;
+               LOG_GENERAL_NC("Suggest that this may be 2:3 pull-down (soft telecine)");
+       }
+
+#ifdef DCPOMATIC_VARIANT_SWAROOP
+       AVDictionaryEntry* e = av_dict_get (_format_context->metadata, SWAROOP_ID_TAG, 0, 0);
+       if (e) {
+               _id = e->value;
+       }
+#endif
 }
 
+
+/** @param temporal_reference A string to which we should add two characters per frame;
+ *  the first   is T or B depending on whether it's top- or bottom-field first,
+ *  ths seconds is 3 or 2 depending on whether "repeat_pict" is true or not.
+ */
 void
-FFmpegExaminer::video_packet (AVCodecContext* context)
+FFmpegExaminer::video_packet (AVCodecContext* context, string& temporal_reference)
 {
        DCPOMATIC_ASSERT (_video_stream);
 
-       if (_first_video && !_need_video_length) {
+       if (_first_video && !_need_video_length && temporal_reference.size() >= PULLDOWN_CHECK_FRAMES) {
                return;
        }
 
@@ -186,6 +220,10 @@ FFmpegExaminer::video_packet (AVCodecContext* context)
                                _format_context->streams[_video_stream.get()]
                                ).get_value_or (ContentTime ()).frames_round (video_frame_rate().get ());
                }
+               if (temporal_reference.size() < PULLDOWN_CHECK_FRAMES) {
+                       temporal_reference += (_frame->top_field_first ? "T" : "B");
+                       temporal_reference += (_frame->repeat_pict ? "3" : "2");
+               }
        }
 }
 
index 67fdcfae0baa65986a407f2adfd2b237828875da..9ff5a79e7a6d79614c2812aa99dc1e0d6aa68c5b 100644 (file)
@@ -75,8 +75,18 @@ public:
                return _rotation;
        }
 
+       bool pulldown () const {
+               return _pulldown;
+       }
+
+#ifdef DCPOMATIC_VARIANT_SWAROOP
+       boost::optional<std::string> id () const {
+               return _id;
+       }
+#endif
+
 private:
-       void video_packet (AVCodecContext *);
+       void video_packet (AVCodecContext *, std::string& temporal_reference);
        void audio_packet (AVCodecContext *, boost::shared_ptr<FFmpegAudioStream>);
 
        std::string stream_name (AVStream* s) const;
@@ -93,6 +103,7 @@ private:
        bool _need_video_length;
 
        boost::optional<double> _rotation;
+       bool _pulldown;
 
        struct SubtitleStart
        {