]> git.ipfire.org Git - thirdparty/tvheadend.git/commitdiff
transcode: add advanced options for deinterlacing
authorJames Hutchinson <jahutchinson99@googlemail.com>
Wed, 19 Mar 2025 13:12:37 +0000 (13:12 +0000)
committerFlole <Flole998@users.noreply.github.com>
Mon, 21 Jul 2025 15:07:29 +0000 (17:07 +0200)
This patch exposes additional configuration options for the
deinterlace_vaapi (hardware) and yadif (software) deinterlace filters:
* Deinterlace rate type (rate): frame or field
* Deinterlace fields only (auto): only deinterlace interlaced fields
* VAAPI Deinterlace mode (mode): Bob, Weave, MADI, MCDI (for VAAPI only)

These options allow the transcode deinterlace configuration to be
fine-tuned. Most notably, the deinterlace filters can now be configured
with field-rate deinterlacing, which causes (for example) 25fps
interlaced input at a 90kHz timebase to produce 50fps output with a
180kHz timebase.

To maintain MPEG-TS compliance, the output timebase is fixed at 90kHz,
and both the adjusted output frame rate (e.g. 50fps) and frame
timestamps are rescaled accordingly before encoding. For accuracy, this
rescaling is performed dynamically using libav functions such as
av_rescale_q(), based on the timebase of the final filter in the
AVFilterContext chain and the timebase of the output AVCodecContext.
This approach supports fractional frame rates and remains robust against
future changes to the filter configuration, including various
combinations of deinterlace options.

When field-rate deinterlacing is selected, this produces frames with
(for example) correct timing of 50fps playback in a 90kHz container,
ensuring that the transcoded output stream preserves the intended
cadence and temporal fidelity of the original interlaced source.

src/transcoding/codec/codecs/libs/vaapi.c
src/transcoding/codec/internals.h
src/transcoding/codec/profile_video_class.c
src/transcoding/transcode/hwaccels/hwaccels.c
src/transcoding/transcode/hwaccels/vaapi.c
src/transcoding/transcode/video.c
src/webui/static/app/codec.js

index 1f436aee8a7dfa41307d9dc2395dbc6345098eec..8452d1379570f442826c3c860bc8b33f15b7e127 100644 (file)
 #define VAAPI_ENC_B_REFERENCE_I_P       1
 #define VAAPI_ENC_B_REFERENCE_I_P_B     2
 
+#define VAAPI_DEINT_MODE_DEFAULT        0
+#define VAAPI_DEINT_MODE_BOB            1
+#define VAAPI_DEINT_MODE_WEAVE          2
+#define VAAPI_DEINT_MODE_MADI           3
+#define VAAPI_DEINT_MODE_MCDI           4
+
 #define UI_CODEC_AVAILABLE_OFFSET       0
 #define UI_MAX_B_FRAMES_OFFSET          1
 #define UI_MAX_QUALITY_OFFSET           4
@@ -134,6 +140,19 @@ rc_mode_get_list( void *o, const char *lang )
     return strtab2htsmsg(tab, 1, lang);
 }
 
+static htsmsg_t *
+deinterlace_vaapi_mode_get_list( void *o, const char *lang )
+{
+    static const struct strtab tab[] = {
+        { N_("Default"),                                 VAAPI_DEINT_MODE_DEFAULT },
+        { N_("Bob Deinterlacing"),                       VAAPI_DEINT_MODE_BOB },
+        { N_("Weave Deinterlacing"),                     VAAPI_DEINT_MODE_WEAVE },
+        { N_("Motion Adaptive Deinterlacing (MADI)"),    VAAPI_DEINT_MODE_MADI },
+        { N_("Motion Compensated Deinterlacing (MCDI)"), VAAPI_DEINT_MODE_MCDI },
+    };
+    return strtab2htsmsg(tab, 1, lang);
+}
+
 // h264
 
 static htsmsg_t *
@@ -201,82 +220,6 @@ hevc_level_get_list( void *o, const char *lang )
 
 /* vaapi ==================================================================== */
 
-typedef struct {
-    TVHVideoCodecProfile;
-    int qp;
-    int quality;
-    int global_quality;
-    int async_depth;
-/**
- * VAAPI Encoder availablity.
- * @note
- * return: 
- * bit0 - will show if normal encoder is available (VAEntrypointEncSlice)
- */
-    int ui;
-/**
- * VAAPI Encoder Low power availablity.
- * @note
- * return: 
- * bit0 - will show if low power encoder is available (VAEntrypointEncSliceLP)
- */
-    int uilp;
-/**
- * VAAPI Frame used as reference for B-frame [b_depth]
- * https://www.ffmpeg.org/ffmpeg-codecs.html#toc-VAAPI-encoders
- * @note
- * int: 
- * 0 - skip
- * 1 - all B-frames will refer only to P- or I-frames
- * 2 - multiple layers of B-frames will be present
- */
-    int b_reference;
-/**
- * VAAPI Maximum consecutive B-frame [bf]
- * https://www.ffmpeg.org/ffmpeg-codecs.html#toc-VAAPI-encoders
- * @note
- * int: 
- * 0 - no B-Frames allowed
- * >0 - number of consecutive B-frames (valid with b_reference = 1 --> "use P- or I-frames")
- */
-    int desired_b_depth;
-/**
- * VAAPI Maximum bitrate [maxrate]
- * https://www.ffmpeg.org/ffmpeg-codecs.html#toc-VAAPI-encoders
- * @note
- * int: 
- * VALUE - max bitrate in bps
- */
-    double max_bit_rate;
-/**
- * VAAPI Maximum bitrate [maxrate]
- * https://www.ffmpeg.org/ffmpeg-codecs.html#toc-VAAPI-encoders
- * @note
- * double: 
- * VALUE - max bitrate in bps
- */
-    double bit_rate_scale_factor;
-/**
- * VAAPI Platform hardware [not ffmpeg parameter]
- * https://www.ffmpeg.org/ffmpeg-codecs.html#toc-VAAPI-encoders
- * @note
- * int: 
- * 0 - Unconstrained (usefull for debug)
- * 1 - Intel
- * 2 - AMD
- */
-    int platform;
-    int loop_filter_level;
-    int loop_filter_sharpness;
-    double buff_factor;
-    int rc_mode;
-    int tier;
-    int level;
-    int qmin;
-    int qmax;
-    int super_frame;
-} tvh_codec_profile_vaapi_t;
-
 #if defined(__linux__)
 #include <linux/types.h>
 #include <asm/ioctl.h>
@@ -545,6 +488,19 @@ static const codec_profile_class_t codec_profile_vaapi_class = {
                 .off      = offsetof(tvh_codec_profile_vaapi_t, bit_rate_scale_factor),
                 .def.d    = 0,
             },
+            {
+                .type     = PT_INT,
+                .id       = "deinterlace_vaapi_mode",
+                .name     = N_("VAAPI Deinterlace mode"),
+                .desc     = N_("Mode to use for VAAPI Deinterlacing. "
+                               "'Default' selects the most advanced deinterlacer, i.e. the mode appearing last in this list. "
+                               "Tip: MADI and MCDI usually yield the smoothest results, especially when used with field rate output."),
+                .group    = 2,
+                .opts     = PO_ADVANCED,
+                .off      = offsetof(tvh_codec_profile_vaapi_t, deinterlace_vaapi_mode),
+                .list     = deinterlace_vaapi_mode_get_list,
+                .def.i    = VAAPI_DEINT_MODE_DEFAULT,
+            },
             {
                 .type     = PT_INT,
                 .id       = "hw_denoise",     // Don't change
index e74e15b91f11badd976a1ed73d2481fdef23ed86..408c6fedb6dc4460262cb6507df512cff6a74834 100644 (file)
 #define HWACCEL_PRIORITIZE_MMAL  3
 #endif
 
+#define DEINT_RATE_FRAME         0
+#define DEINT_RATE_FIELD         1
+
+#define DEINT_AUTO_OFF           0
+#define DEINT_AUTO_ON            1
+
 /* codec_profile_class ====================================================== */
 
 uint32_t
@@ -301,6 +307,25 @@ typedef struct tvh_codec_profile_video {
      * - 1 - enabled
      */
     int deinterlace;
+
+    /**
+     * SW or HW deinterlace enable field rate (applies to deinterlace filters)
+     * @note
+     * int:
+     * - 0 - Output at frame rate (one frame of output for each field-pair)
+     * - 1 - Output at field rate (one frame of output for each field)
+     */
+    int deinterlace_field_rate;
+
+    /**
+     * SW or HW deinterlace 'auto' mode (applies to deinterlace filters)
+     * @note
+     * int:
+     * - 0 - Disabled (deinterlace all content, including progressive frames)
+     * - 1 - Enabled (only deinterlace interlaced fields; progressive frames are passed through unchanged)
+     */
+    int deinterlace_enable_auto;
+
     int height;
     /**
      * SW or HW scaling mode  (applies for decoding)
@@ -346,6 +371,94 @@ typedef struct tvh_codec_profile_video {
     AVRational size;
 } TVHVideoCodecProfile;
 
+typedef struct {
+    TVHVideoCodecProfile;
+    int qp;
+    int quality;
+    int global_quality;
+    int async_depth;
+/**
+ * VAAPI Encoder availablity.
+ * @note
+ * return:
+ * bit0 - will show if normal encoder is available (VAEntrypointEncSlice)
+ */
+    int ui;
+/**
+ * VAAPI Encoder Low power availablity.
+ * @note
+ * return:
+ * bit0 - will show if low power encoder is available (VAEntrypointEncSliceLP)
+ */
+    int uilp;
+/**
+ * VAAPI Frame used as reference for B-frame [b_depth]
+ * https://www.ffmpeg.org/ffmpeg-codecs.html#toc-VAAPI-encoders
+ * @note
+ * int:
+ * 0 - skip
+ * 1 - all B-frames will refer only to P- or I-frames
+ * 2 - multiple layers of B-frames will be present
+ */
+    int b_reference;
+/**
+ * VAAPI Maximum consecutive B-frame [bf]
+ * https://www.ffmpeg.org/ffmpeg-codecs.html#toc-VAAPI-encoders
+ * @note
+ * int:
+ * 0 - no B-Frames allowed
+ * >0 - number of consecutive B-frames (valid with b_reference = 1 --> "use P- or I-frames")
+ */
+    int desired_b_depth;
+/**
+ * VAAPI Maximum bitrate [maxrate]
+ * https://www.ffmpeg.org/ffmpeg-codecs.html#toc-VAAPI-encoders
+ * @note
+ * int:
+ * VALUE - max bitrate in bps
+ */
+    double max_bit_rate;
+/**
+ * VAAPI Maximum bitrate [maxrate]
+ * https://www.ffmpeg.org/ffmpeg-codecs.html#toc-VAAPI-encoders
+ * @note
+ * double:
+ * VALUE - max bitrate in bps
+ */
+    double bit_rate_scale_factor;
+/**
+ * VAAPI Platform hardware [not ffmpeg parameter]
+ * https://www.ffmpeg.org/ffmpeg-codecs.html#toc-VAAPI-encoders
+ * @note
+ * int:
+ * 0 - Unconstrained (usefull for debug)
+ * 1 - Intel
+ * 2 - AMD
+ */
+    int platform;
+/**
+ * VAAPI Deinterlace mode [deinterlace_vaapi mode parameter]
+ * https://ffmpeg.org/doxygen/6.1/vf__deinterlace__vaapi_8c.html
+ * @note
+ * int:
+ * 0 - Default: Use the highest-numbered (and therefore most advanced) deinterlacing algorithm
+ * 1 - Use the bob deinterlacing algorithm
+ * 2 - Use the weave deinterlacing algorithm
+ * 3 - Use the motion adaptive deinterlacing algorithm
+ * 4 - Use the motion compensated deinterlacing algorithm
+ */
+    int deinterlace_vaapi_mode;
+
+    int loop_filter_level;
+    int loop_filter_sharpness;
+    double buff_factor;
+    int rc_mode;
+    int tier;
+    int level;
+    int qmin;
+    int qmax;
+    int super_frame;
+} tvh_codec_profile_vaapi_t;
 
 /* audio */
 
index 07a392fa86626bd351c1be746678fefef976bdd8..bb500c54a861e880e7dcc05a16666967f8b022b7 100644 (file)
@@ -51,6 +51,26 @@ hwaccel_get_list( void *o, const char *lang )
     return strtab2htsmsg(tab, 1, lang);
 }
 
+static htsmsg_t *
+deinterlace_field_rate_get_list( void *o, const char *lang )
+{
+    static const struct strtab tab[] = {
+        { N_("Frame Rate"),                         DEINT_RATE_FRAME },
+        { N_("Field Rate"),                         DEINT_RATE_FIELD },
+    };
+    return strtab2htsmsg(tab, 1, lang);
+}
+
+static htsmsg_t *
+deinterlace_enable_auto_get_list( void *o, const char *lang )
+{
+    static const struct strtab tab[] = {
+        { N_("Disable"),                            DEINT_AUTO_OFF },
+        { N_("Enable"),                             DEINT_AUTO_ON },
+    };
+    return strtab2htsmsg(tab, 1, lang);
+}
+
 
 /* TVHCodec ================================================================= */
 
@@ -135,7 +155,6 @@ tvh_codec_profile_video_is_copy(TVHVideoCodecProfile *self, tvh_ssc_t *ssc)
 static int
 tvh_codec_profile_video_open(TVHVideoCodecProfile *self, AVDictionary **opts)
 {
-    AV_DICT_SET_INT(opts, "tvh_filter_deint", self->deinterlace, 0);
     // video_size
     AV_DICT_SET_INT(opts, "width", self->size.num, 0);
     AV_DICT_SET_INT(opts, "height", self->size.den, 0);
@@ -194,16 +213,6 @@ const codec_profile_class_t codec_profile_video_class = {
         .ic_class      = "codec_profile_video",
         .ic_caption    = N_("video"),
         .ic_properties = (const property_t[]) {
-            {
-                .type     = PT_BOOL,
-                .id       = "deinterlace",
-                .name     = N_("Deinterlace"),
-                .desc     = N_("Deinterlace."),
-                .group    = 2,
-                .off      = offsetof(TVHVideoCodecProfile, deinterlace),
-                .set      = codec_profile_video_class_deinterlace_set,
-                .def.i    = 1,
-            },
             {
                 .type     = PT_INT,
                 .id       = "height",
@@ -244,6 +253,45 @@ const codec_profile_class_t codec_profile_video_class = {
                 .list     = hwaccel_get_list,
                 .def.i    = 0,
             },
+            {
+                .type     = PT_BOOL,
+                .id       = "deinterlace",
+                .name     = N_("Deinterlace"),
+                .desc     = N_("Deinterlace."),
+                .group    = 2,
+                .off      = offsetof(TVHVideoCodecProfile, deinterlace),
+                .set      = codec_profile_video_class_deinterlace_set,
+                .def.i    = 1,
+            },
+            {
+                .type     = PT_INT,
+                .id       = "deinterlace_field_rate",
+                .name     = N_("Deinterlace rate type"),
+                .desc     = N_("Frame rate combines the two interlaced fields to create a single frame. "
+                               "Field rate processes each field independently, outputting as individual frames, "
+                               "which enables higher temporal resolution by producing one frame per field. "
+                               "Note: with field rate deinterlacing the resulting stream will have double "
+                               "frame-rate (for example 25i becomes 50p), which can result in smoother video "
+                               "since the original temporal properties of the interlaced video are retained."),
+                .group    = 2,
+                .opts     = PO_ADVANCED,
+                .off      = offsetof(TVHVideoCodecProfile, deinterlace_field_rate),
+                .list     = deinterlace_field_rate_get_list,
+                .def.i    = DEINT_RATE_FRAME,
+            },
+            {
+                .type     = PT_INT,
+                .id       = "deinterlace_enable_auto",
+                .name     = N_("Deinterlace fields only"),
+                .desc     = N_("Enable this option to only deinterlace fields, passing progressive frames "
+                               "unchanged. This is useful for mixed content, allowing progressive frames "
+                               "to bypass deinterlacing for improved efficiency and quality."),
+                .group    = 2,
+                .opts     = PO_EXPERT,
+                .off      = offsetof(TVHVideoCodecProfile, deinterlace_enable_auto),
+                .list     = deinterlace_enable_auto_get_list,
+                .def.i    = DEINT_AUTO_OFF,
+            },
             {
                 .type     = PT_INT,
                 .id       = "pix_fmt",
index 57524c44e249fd3316b5e936f6bb4ea32b796eda..55b772ccb6d9663956c9901f6dc6d8fb9c61ee35 100644 (file)
@@ -157,7 +157,7 @@ hwaccels_get_scale_filter(AVCodecContext *iavctx, AVCodecContext *oavctx,
 int
 hwaccels_get_deint_filter(AVCodecContext *avctx, char *filter, size_t filter_len)
 {
-    TVHContext *ctx = avctx->opaque;
+    const TVHContext *ctx = avctx->opaque;
 
     if (ctx->hw_accel_ictx) {
         switch (avctx->pix_fmt) {
@@ -169,7 +169,7 @@ hwaccels_get_deint_filter(AVCodecContext *avctx, char *filter, size_t filter_len
                 break;
         }
     }
-    
+
     return -1;
 }
 
index f9f9aa08e0621ba5e62db27d477eaeba008ee9cb..c15ddfe85d7b4dbdc1e1ab034a94169494104cde 100644 (file)
@@ -695,7 +695,18 @@ vaapi_get_scale_filter(AVCodecContext *iavctx, AVCodecContext *oavctx,
 int
 vaapi_get_deint_filter(AVCodecContext *avctx, char *filter, size_t filter_len)
 {
-    snprintf(filter, filter_len, "deinterlace_vaapi");
+    const TVHContext *ctx = avctx->opaque;
+
+    // Map user selected rate (0=frame,1=field) to VAAPI rate (1=frame,2=field)
+    int rate = (((TVHVideoCodecProfile *)ctx->profile)->deinterlace_field_rate == 1) ? 2 : 1;
+    int enable_auto = ((TVHVideoCodecProfile *)ctx->profile)->deinterlace_enable_auto;
+    int mode = ((tvh_codec_profile_vaapi_t *)ctx->profile)->deinterlace_vaapi_mode;
+
+    if (str_snprintf(filter, filter_len, "deinterlace_vaapi=mode=%d:rate=%d:auto=%d",
+                                         mode, rate, enable_auto)) {
+        return -1;
+    }
+
     return 0;
 }
 
index ae221d9a2d53c2a6b8d599c37ba88c9153d78906..ad3033db14d91dae8fcc028eb70472890347aafb 100644 (file)
@@ -37,12 +37,22 @@ _video_filters_hw_pix_fmt(enum AVPixelFormat pix_fmt)
     return 0;
 }
 
+static int
+_video_get_sw_deint_filter(TVHContext *self, char *deint, size_t deint_len)
+{
+    if (str_snprintf(deint, deint_len, "yadif=mode=%d:deint=%d",
+                     ((TVHVideoCodecProfile *)self->profile)->deinterlace_field_rate,
+                     ((TVHVideoCodecProfile *)self->profile)->deinterlace_enable_auto )) {
+        return -1;
+    }
+    return 0;
+}
 
 static int
 _video_filters_get_filters(TVHContext *self, AVDictionary **opts, char **filters)
 {
     char download[48];
-    char deint[8];
+    char deint[64];
     char hw_deint[64];
     char scale[24];
     char hw_scale[64];
@@ -52,18 +62,11 @@ _video_filters_get_filters(TVHContext *self, AVDictionary **opts, char **filters
     int ihw = _video_filters_hw_pix_fmt(self->iavctx->pix_fmt);
     int ohw = _video_filters_hw_pix_fmt(self->oavctx->pix_fmt);
     int filter_scale = (self->iavctx->height != self->oavctx->height);
-    int filter_deint = 0, filter_download = 0, filter_upload = 0;
+    int filter_deint = ((TVHVideoCodecProfile *)self->profile)->deinterlace;
+    int filter_download = 0, filter_upload = 0;
 #if ENABLE_HWACCELS
-    int filter_denoise = 0;
-    int filter_sharpness = 0;
-#endif
-
-    if (tvh_context_get_int_opt(opts, "tvh_filter_deint", &filter_deint)) {
-        return -1;
-    }
-#if ENABLE_HWACCELS
-    filter_denoise = ((TVHVideoCodecProfile *)self->profile)->filter_hw_denoise;
-    filter_sharpness = ((TVHVideoCodecProfile *)self->profile)->filter_hw_sharpness;
+    int filter_denoise = ((TVHVideoCodecProfile *)self->profile)->filter_hw_denoise;
+    int filter_sharpness = ((TVHVideoCodecProfile *)self->profile)->filter_hw_sharpness;
 #endif
     //  in --> out  |  download   |   upload 
     // -------------|-------------|------------
@@ -81,18 +84,20 @@ _video_filters_get_filters(TVHContext *self, AVDictionary **opts, char **filters
         // when hwaccel is enabled we have two options:
         if (ihw) {
             // hw deint
-            hwaccels_get_deint_filter(self->iavctx, hw_deint, sizeof(hw_deint));
+            if (hwaccels_get_deint_filter(self->iavctx, hw_deint, sizeof(hw_deint))) {
+                return -1;
+            }
         }
         else {
             // sw deint
-            if (str_snprintf(deint, sizeof(deint), "yadif")) {
+            if (_video_get_sw_deint_filter(self, deint, sizeof(deint))) {
                 return -1;
             }
         }
     }
 #else
     if (filter_deint) {
-        if (str_snprintf(deint, sizeof(deint), "yadif")) {
+        if (_video_get_sw_deint_filter(self, deint, sizeof(deint))) {
             return -1;
         }
     }
@@ -217,6 +222,8 @@ static int
 tvh_video_context_open_encoder(TVHContext *self, AVDictionary **opts)
 {
     AVRational ticks_per_frame;
+    int deinterlace  = ((TVHVideoCodecProfile *)self->profile)->deinterlace;
+    int field_rate = (deinterlace && ((TVHVideoCodecProfile *)self->profile)->deinterlace_field_rate) ? 2 : 1;
 
     if (tvh_context_get_int_opt(opts, "pix_fmt", &self->oavctx->pix_fmt) ||
         tvh_context_get_int_opt(opts, "width", &self->oavctx->width) ||
@@ -261,8 +268,8 @@ tvh_video_context_open_encoder(TVHContext *self, AVDictionary **opts)
     if (!self->iavctx->framerate.num) {
         self->iavctx->framerate = av_make_q(30, 1);
     }
-    self->oavctx->framerate = self->iavctx->framerate;
-    self->oavctx->ticks_per_frame = (90000 * self->iavctx->framerate.den) / self->iavctx->framerate.num; // We assume 90kHz as timebase which is mandatory for MPEG-TS
+    self->oavctx->framerate = av_mul_q(self->iavctx->framerate, (AVRational) { field_rate, 1 }); //take into account double rate i.e. field-based deinterlacers
+    self->oavctx->ticks_per_frame = (90000 * self->oavctx->framerate.den) / self->oavctx->framerate.num; // We assume 90kHz as timebase which is mandatory for MPEG-TS
     ticks_per_frame = av_make_q(self->oavctx->ticks_per_frame, 1);
     self->oavctx->time_base = av_inv_q(av_mul_q(
         self->oavctx->framerate, ticks_per_frame));
@@ -271,6 +278,21 @@ tvh_video_context_open_encoder(TVHContext *self, AVDictionary **opts)
     self->oavctx->gop_size *= 3;
 
     self->oavctx->sample_aspect_ratio = self->iavctx->sample_aspect_ratio;
+
+    tvh_context_log(self, LOG_DEBUG,
+        "Encoder configuration:\n"
+        "  framerate:              %d/%d (%.3f fps)\n"
+        "  time_base:              %.0fHz\n"
+        "  frame duration:         %" PRId64 " ticks (%.6f sec)\n"
+        "  gop_size:               %d\n"
+        "  sample_aspect_ratio:    %d/%d",
+        self->oavctx->framerate.num, self->oavctx->framerate.den, av_q2d(self->oavctx->framerate),
+        av_q2d(av_inv_q(self->oavctx->time_base)),
+        av_rescale_q(1, av_inv_q(self->oavctx->framerate), self->oavctx->time_base),
+        av_q2d(av_inv_q(self->oavctx->framerate)),
+        self->oavctx->gop_size,
+        self->oavctx->sample_aspect_ratio.num, self->oavctx->sample_aspect_ratio.den);
+
     return 0;
 }
 
@@ -344,7 +366,57 @@ tvh_video_context_open(TVHContext *self, TVHOpenPhase phase, AVDictionary **opts
 static int
 tvh_video_context_encode(TVHContext *self, AVFrame *avframe)
 {
-    avframe->pts = avframe->best_effort_timestamp;
+    if (!self || !self->oavctx) {
+        return -1;
+    }
+
+    AVFilterLink *outlink = NULL;
+#if LIBAVUTIL_VERSION_MAJOR >= 58
+    int64_t *frame_duration = &avframe->duration;
+#else
+    int64_t *frame_duration = &avframe->pkt_duration;
+#endif
+
+    tvh_context_log(self, LOG_TRACE,
+        "Decoded frame: pts=%" PRId64 ", dts=%" PRId64 ", duration=%" PRId64,
+        avframe->pts, avframe->pkt_dts, *frame_duration);
+
+    if (self->oavfltctx && self->oavfltctx->nb_inputs > 0) {
+        outlink = self->oavfltctx->inputs[0];
+    }
+
+    // filters exist and their time base differs from the encoder (e.g field-rate deinterlacer)
+    if (outlink && outlink->time_base.num > 0 && outlink->time_base.den > 0 &&
+        av_cmp_q(outlink->time_base, self->oavctx->time_base) != 0) {
+
+        // Rescale PTS from filter graph time_base to encoder time_base
+        if (avframe->pts != AV_NOPTS_VALUE) {
+            avframe->pts = av_rescale_q(avframe->pts,
+                                        outlink->time_base,
+                                        self->oavctx->time_base);
+            // Deinterlace filters don't update DTS, so align DTS with PTS
+            // This prevents duplicate or incorrect DTS values reaching the encoder
+            avframe->pkt_dts = avframe->pts;
+        }
+
+        if (*frame_duration > 0) {
+            // Rescale current frame duration from filter output time base -> encoder time base
+            *frame_duration = av_rescale_q(*frame_duration,
+                                           outlink->time_base,
+                                           self->oavctx->time_base);
+        } else if (self->oavctx->framerate.num > 0 && self->oavctx->framerate.den > 0) {
+            // If duration is blank then fallback to expected duration based on encoder frame rate
+            *frame_duration = av_rescale_q(1, av_inv_q(self->oavctx->framerate),
+                                           self->oavctx->time_base);
+        }
+
+        tvh_context_log(self, LOG_TRACE,
+            "Rescaled frame {%d/%d}->{%d/%d}: pts=%" PRId64 ", dts=%" PRId64 ", duration=%" PRId64,
+            outlink->time_base.num, outlink->time_base.den,
+            self->oavctx->time_base.num, self->oavctx->time_base.den,
+            avframe->pts, avframe->pkt_dts, *frame_duration);
+    }
+
     if (avframe->pts <= self->pts) {
         tvh_context_log(self, LOG_WARNING,
                         "Invalid pts (%"PRId64") <= last (%"PRId64"), dropping frame",
@@ -380,6 +452,11 @@ tvh_video_context_ship(TVHContext *self, AVPacket *avpkt)
         tvh_context_log(self, LOG_ERR, "encode failed");
         return -1;
     }
+
+    tvh_context_log(self, LOG_TRACE,
+        "Encoded packet for shipping: pts=%" PRId64 ", dts=%" PRId64 ", duration=%" PRId64,
+        avpkt->pts, avpkt->dts, avpkt->duration);
+
     return avpkt->size;
 }
 
index 78cb2ce0e1aea876fbebdddcb48150356ca02ed7..4378a5854d8af652b34746caeffe380c19a09a7a 100644 (file)
@@ -49,6 +49,9 @@ function updateHWFilters(form) {
         form.findField('hw_denoise').setDisabled(!form.findField('hwaccel').getValue());
         form.findField('hw_sharpness').setDisabled(!form.findField('hwaccel').getValue());
         form.findField('hwaccel_details').setDisabled(!form.findField('hwaccel').getValue());
+        form.findField('deinterlace_field_rate').setDisabled(!form.findField('deinterlace').getValue());
+        form.findField('deinterlace_enable_auto').setDisabled(!form.findField('deinterlace').getValue());
+        form.findField('deinterlace_vaapi_mode').setDisabled(!form.findField('hwaccel').getValue() || !form.findField('deinterlace').getValue());
     }
 }
 
@@ -238,6 +241,12 @@ function update_vaapi_ui(form) {
             updateHWFilters(form);
         });
     
+    // on deinterlace change
+    if (form.findField('deinterlace'))
+        form.findField('deinterlace').on('check', function(checkbox, value) {
+            updateHWFilters(form);
+        });
+
     // on desired_b_depth change
     if (form.findField('desired_b_depth'))
         form.findField('desired_b_depth').on('spin', function(spinner, direction, eOpts) {