PR #24946 opened by Zhao Zhili (quink)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24946
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24946.patch

# Summary of changes

Encoders that wrap an external API often get no usable decode
timestamp back, and each of them derives dts from pts in its own way.
Add a helper implementing the ordered window that
compute_muxer_pkt_fields() uses when muxing, so those encoders can
share it, along with the reorder_delay option they offer.

<!--
If this PR requires new FATE test samples, attach them to the PR and
list their target paths below (relative to the fate-suite root).

Attached filenames must match the sample's filename:

```fate-samples
# e.g. vorbis/new-sample.ogg
```
-->



>From 2f846652b070d76260351ab453e282efaf82c5c2 Mon Sep 17 00:00:00 2001
From: Zhao Zhili <[email protected]>
Date: Thu, 1 Oct 2026 18:05:09 +0800
Subject: [PATCH 1/4] avcodec/dts_gen: add a helper deriving dts from the pts
 of packets

Encoders that wrap an external API often get no usable decode
timestamp back, and each of them derives dts from pts in its own way.
Add a helper implementing the ordered window that
compute_muxer_pkt_fields() uses when muxing, so those encoders can
share it, along with the reorder_delay option they offer.

The caller supplies an upper bound on the reorder depth. A smaller
value yields dts values that do not increase, which the helper logs; a
larger one lowers every dts by the excess frames. Packets carry no
reliable duration when the helper runs, so the dts values of the first
packets are extrapolated with the interval between the first input
frames, which the caller records.

A FATE test checks the generated dts against exact values with and
without reordering, at a variable frame rate, and with a delay above
the real depth. It also covers the three ways the first dts values are
spaced, a repeated pts, delay skipping, a codec that does not reorder,
reset, and rejection of invalid delays.

Signed-off-by: Zhao Zhili <[email protected]>
---
 libavcodec/Makefile        |   2 +
 libavcodec/dts_gen.c       | 127 ++++++++++++++++++++++++
 libavcodec/dts_gen.h       | 106 ++++++++++++++++++++
 libavcodec/tests/dts_gen.c | 192 +++++++++++++++++++++++++++++++++++++
 tests/fate/libavcodec.mak  |   5 +
 5 files changed, 432 insertions(+)
 create mode 100644 libavcodec/dts_gen.c
 create mode 100644 libavcodec/dts_gen.h
 create mode 100644 libavcodec/tests/dts_gen.c

diff --git a/libavcodec/Makefile b/libavcodec/Makefile
index 3d2200af8c..be63f45131 100644
--- a/libavcodec/Makefile
+++ b/libavcodec/Makefile
@@ -43,6 +43,7 @@ OBJS = ac3_parser.o                                           
          \
        d3d11va.o                                                        \
        decode.o                                                         \
        dirac.o                                                          \
+       dts_gen.o                                                        \
        dv_profile.o                                                     \
        encode.o                                                         \
        exif.o                                                           \
@@ -1385,6 +1386,7 @@ TESTPROGS = avcodec                                       
              \
             bitstream_be                                                \
             bitstream_le                                                \
             codec_desc                                                  \
+            dts_gen                                                     \
             htmlsubtitles                                               \
             jpeg2000dwt                                                 \
             mathops                                                    \
diff --git a/libavcodec/dts_gen.c b/libavcodec/dts_gen.c
new file mode 100644
index 0000000000..902e7dfe44
--- /dev/null
+++ b/libavcodec/dts_gen.c
@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) 2026 Zhao Zhili <[email protected]>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "libavutil/common.h"
+#include "libavutil/log.h"
+#include "libavutil/mathematics.h"
+
+#include "codec_desc.h"
+#include "dts_gen.h"
+
+av_cold int ff_dts_gen_init(const AVCodecContext *avctx, FFDTSGen *g,
+                            int delay)
+{
+    if (delay < FF_DTS_GEN_DELAY_SKIP || delay > FF_DTS_GEN_DELAY_MAX ||
+        delay == FF_DTS_GEN_DELAY_AUTO)
+        return AVERROR(EINVAL);
+
+    /* Without reordering dts equals pts, and receive_packet encoders get no
+     * generic fallback that sets it, so FF_DTS_GEN_DELAY_SKIP is not honoured.
+     */
+    if (!(avctx->codec_descriptor->props & AV_CODEC_PROP_REORDER))
+        delay = 0;
+
+    *g = (FFDTSGen) {
+        .delay       = delay,
+        .prev_dts    = AV_NOPTS_VALUE,
+        .prev_in_pts = AV_NOPTS_VALUE,
+    };
+
+    return 0;
+}
+
+void ff_dts_gen_record_pts(FFDTSGen *g, int64_t pts)
+{
+    if (pts == AV_NOPTS_VALUE)
+        return;
+
+    if (!g->step && g->prev_in_pts != AV_NOPTS_VALUE && pts > g->prev_in_pts)
+        g->step = av_sat_sub64(pts, g->prev_in_pts);
+
+    g->prev_in_pts = pts;
+}
+
+static int64_t dts_gen_step(const AVCodecContext *avctx)
+{
+    if (avctx->framerate.num > 0 && avctx->framerate.den > 0) {
+        int64_t step = av_rescale_q(1,
+                                    (AVRational) { avctx->framerate.den,
+                                                   avctx->framerate.num },
+                                    avctx->time_base);
+        if (step > 0)
+            return step;
+    }
+
+    return 1;
+}
+
+void ff_dts_gen_fill_pkt(AVCodecContext *avctx, FFDTSGen *g, AVPacket *pkt)
+{
+    int64_t dts;
+
+    if (g->delay == FF_DTS_GEN_DELAY_SKIP || pkt->pts == AV_NOPTS_VALUE) {
+        pkt->dts = AV_NOPTS_VALUE;
+        return;
+    }
+
+    if (!g->started) {
+        /* A decoder needs delay frames of reordering buffer, so the first
+         * delay packets decode before the first frame is presented and get
+         * timestamps extrapolated from the interval between input frames.
+         */
+        int64_t step = g->step > 0 ? g->step : dts_gen_step(avctx);
+
+        /* Any positive step keeps these timestamps increasing, so clamping
+         * an absurd interval only keeps the product in range.
+         */
+        step = FFMIN(step, INT64_MAX / (FF_DTS_GEN_DELAY_MAX + 1));
+
+        for (int i = 1; i <= g->delay; i++)
+            g->window[i] = av_sat_sub64(pkt->pts, (g->delay + 1 - i) * step);
+        g->started = 1;
+    }
+
+    g->window[0] = pkt->pts;
+    for (int i = 0; i < g->delay && g->window[i] > g->window[i + 1]; i++)
+        FFSWAP(int64_t, g->window[i], g->window[i + 1]);
+
+    dts = g->window[0];
+
+    /* Reached when delay is below the real reorder depth, or when two input
+     * frames share a pts, which no delay can fix.
+     */
+    if (g->prev_dts != AV_NOPTS_VALUE && dts <= g->prev_dts)
+        av_log_once(avctx, AV_LOG_ERROR, AV_LOG_VERBOSE, &g->warned,
+                    "Generated dts %"PRId64" does not increase over %"PRId64"; 
"
+                    "raise reorder_delay, or check that the input pts values "
+                    "are strictly increasing.\n", dts, g->prev_dts);
+
+    g->prev_dts = dts;
+    pkt->dts    = dts;
+}
+
+av_cold void ff_dts_gen_reset(FFDTSGen *g)
+{
+    g->prev_in_pts = AV_NOPTS_VALUE;
+    g->step        = 0;
+    g->prev_dts    = AV_NOPTS_VALUE;
+    g->started     = 0;
+    g->warned      = 0;
+}
diff --git a/libavcodec/dts_gen.h b/libavcodec/dts_gen.h
new file mode 100644
index 0000000000..1a3d00ff6c
--- /dev/null
+++ b/libavcodec/dts_gen.h
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2026 Zhao Zhili <[email protected]>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_DTS_GEN_H
+#define AVCODEC_DTS_GEN_H
+
+#include <stdint.h>
+
+#include "libavutil/opt.h"
+
+#include "avcodec.h"
+
+// Special value for reorder delay.
+// Asks the encoder to pick a value itself.
+#define FF_DTS_GEN_DELAY_AUTO -1
+// Leaves pkt->dts unset.
+#define FF_DTS_GEN_DELAY_SKIP -2
+#define FF_DTS_GEN_DELAY_MAX   16
+
+/* AVOption entries for the reorder_delay option. The encoder resolves
+ * FF_DTS_GEN_DELAY_AUTO before passing the value to ff_dts_gen_init().
+ */
+#define FF_DTS_GEN_REORDER_DELAY_OPTION(offset, flags)                         
                                                                 \
+    { "reorder_delay", "Reorder delay in frames used to generate dts; a value 
below the real reorder depth makes dts non-monotonic",            \
+      offset, AV_OPT_TYPE_INT, { .i64 = FF_DTS_GEN_DELAY_AUTO }, 
FF_DTS_GEN_DELAY_SKIP, FF_DTS_GEN_DELAY_MAX, flags, .unit = "reorder_delay" }, \
+    { "auto", "Derive the delay from the encoder configuration",               
                                                                 \
+        0, AV_OPT_TYPE_CONST, { .i64 = FF_DTS_GEN_DELAY_AUTO }, 0, 0, flags, 
.unit = "reorder_delay" },                                         \
+    { "skip", "Leave dts unset",                                               
                                                                 \
+        0, AV_OPT_TYPE_CONST, { .i64 = FF_DTS_GEN_DELAY_SKIP }, 0, 0, flags, 
.unit = "reorder_delay" }
+
+/* Derives pkt->dts for packets emitted in decode order from their pts and
+ * the reorder delay. Packets are not reordered.
+ */
+typedef struct FFDTSGen {
+    /* Configuration, fixed by ff_dts_gen_init(). */
+    int     delay;
+
+    /* Input side: the pts of input frames. */
+    int64_t prev_in_pts;
+    /* First increasing interval between two input pts. */
+    int64_t step;
+
+    /* Output side: the dts of output packets. */
+    /* pts values kept in ascending order; window[0] is the next dts. */
+    int64_t window[FF_DTS_GEN_DELAY_MAX + 1];
+    int64_t prev_dts;
+    int     started;
+    int     warned;
+} FFDTSGen;
+
+/* Prepares g for packets emitted in decode order. delay is the reorder
+ * depth: the largest number of frames that precede a frame in decode
+ * order but follow it in display order. It is FF_DTS_GEN_DELAY_SKIP or a
+ * count in [0, FF_DTS_GEN_DELAY_MAX]; the caller resolves
+ * FF_DTS_GEN_DELAY_AUTO.
+ *
+ * Codecs without AV_CODEC_PROP_REORDER get dts == pts whatever delay is.
+ * g->delay holds the effective value, which a caller can publish as
+ * avctx->has_b_frames. A count above the real depth lowers every dts by
+ * the excess frames; a count below it produces dts values that do not
+ * increase, which is logged.
+ *
+ * Returns 0 on success or AVERROR(EINVAL) for any other delay.
+ */
+av_cold int ff_dts_gen_init(const AVCodecContext *avctx, FFDTSGen *g,
+                            int delay);
+
+/* Records the pts of an input frame, in display order. The first
+ * increasing interval spaces the dts values of the first packets, so it is
+ * latched. AV_NOPTS_VALUE is ignored.
+ */
+void ff_dts_gen_record_pts(FFDTSGen *g, int64_t pts);
+
+/* Sets pkt->dts. Call once per output packet, in output order, after
+ * setting pkt->pts. pkt->dts becomes AV_NOPTS_VALUE when g uses
+ * FF_DTS_GEN_DELAY_SKIP or pkt->pts is AV_NOPTS_VALUE.
+ *
+ * The first delay packets get dts values below the first pts, spaced by
+ * the interval recorded by ff_dts_gen_record_pts(), or by
+ * avctx->framerate, or by one tick when neither is known.
+ */
+void ff_dts_gen_fill_pkt(AVCodecContext *avctx, FFDTSGen *g, AVPacket *pkt);
+
+/* Drops the buffered timestamps and the recorded interval, keeping the
+ * configured delay.
+ */
+av_cold void ff_dts_gen_reset(FFDTSGen *g);
+
+#endif /* AVCODEC_DTS_GEN_H */
diff --git a/libavcodec/tests/dts_gen.c b/libavcodec/tests/dts_gen.c
new file mode 100644
index 0000000000..1f1d3dfa10
--- /dev/null
+++ b/libavcodec/tests/dts_gen.c
@@ -0,0 +1,192 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <stdio.h>
+
+#include "libavutil/macros.h"
+
+#include "libavcodec/avcodec.h"
+#include "libavcodec/codec_desc.h"
+#include "libavcodec/dts_gen.h"
+
+#define NP AV_NOPTS_VALUE
+
+/* Pts of the input frames, in display order. */
+static const int64_t cfr[] = { 0, 10, 20, 30, 40, 50, 60, 70, 80 };
+static const int64_t vfr[] = { 0, 5, 25, 45, 65, 85, 105, 125, 145 };
+
+/* Pts of the output packets, in decode order. */
+static const int64_t cfr_out[] = { 0, 40, 20, 10, 30, 80, 60, 50, 70 };
+static const int64_t vfr_out[] = { 0, 65, 25, 5, 45, 145, 105, 85, 125 };
+
+typedef struct Case {
+    const char *name;
+    enum AVCodecID id;
+    int delay;
+    AVRational framerate;
+    const int64_t *input;   /* passed to ff_dts_gen_record_pts() */
+    int nb_input;
+    const int64_t *pts;     /* pts of the output packets */
+    int nb;
+    const int64_t *dts;     /* expected dts of each output packet */
+    int warned;             /* whether a dts that does not increase is 
reported */
+} Case;
+
+#define ARRAY(a)  a, FF_ARRAY_ELEMS(a)
+#define SEQ(...)  (const int64_t[]) { __VA_ARGS__ }, \
+                  FF_ARRAY_ELEMS(((const int64_t[]) { __VA_ARGS__ }))
+#define DTS(...)  (const int64_t[]) { __VA_ARGS__ }
+
+static const Case cases[] = {
+    { "no reordering", AV_CODEC_ID_H264, 0, { 0, 0 },
+      ARRAY(cfr), ARRAY(cfr), DTS(0, 10, 20, 30, 40, 50, 60, 70, 80) },
+    { "reordering", AV_CODEC_ID_H264, 2, { 0, 0 },
+      ARRAY(cfr), ARRAY(cfr_out), DTS(-20, -10, 0, 10, 20, 30, 40, 50, 60) },
+    { "variable frame rate", AV_CODEC_ID_H264, 2, { 0, 0 },
+      ARRAY(vfr), ARRAY(vfr_out), DTS(-10, -5, 0, 5, 25, 45, 65, 85, 105) },
+    { "delay above depth", AV_CODEC_ID_H264, 4, { 0, 0 },
+      ARRAY(cfr), ARRAY(cfr_out), DTS(-40, -30, -20, -10, 0, 10, 20, 30, 40) },
+    { "skip", AV_CODEC_ID_H264, FF_DTS_GEN_DELAY_SKIP, { 0, 0 },
+      ARRAY(cfr), ARRAY(cfr_out), DTS(NP, NP, NP, NP, NP, NP, NP, NP, NP) },
+    /* The window reproduces the repeated pts, so dts stalls. */
+    { "repeated pts", AV_CODEC_ID_H264, 1, { 0, 0 },
+      SEQ(0, 1, 1, 2), SEQ(0, 1, 1, 2), DTS(-1, 0, 1, 1), 1 },
+    /* The first dts values step by the recorded interval, else by the
+     * frame rate, else by one tick.
+     */
+    { "recorded interval", AV_CODEC_ID_H264, 2, { 50, 1 },
+      ARRAY(cfr), ARRAY(cfr_out), DTS(-20, -10, 0, 10, 20, 30, 40, 50, 60) },
+    { "frame rate", AV_CODEC_ID_H264, 2, { 50, 1 },
+      NULL, 0, ARRAY(cfr_out), DTS(-40, -20, 0, 10, 20, 30, 40, 50, 60) },
+    { "one tick", AV_CODEC_ID_H264, 2, { 0, 0 },
+      NULL, 0, ARRAY(cfr_out), DTS(-2, -1, 0, 10, 20, 30, 40, 50, 60) },
+    /* AV1 does not reorder, so dts equals pts whatever the delay. */
+    { "codec without reordering", AV_CODEC_ID_AV1, FF_DTS_GEN_DELAY_SKIP,
+      { 0, 0 }, ARRAY(cfr), ARRAY(cfr), DTS(0, 10, 20, 30, 40, 50, 60, 70, 80) 
},
+};
+
+static AVCodecContext *alloc_context(enum AVCodecID id, AVRational framerate)
+{
+    AVCodecContext *avctx = avcodec_alloc_context3(NULL);
+
+    if (avctx) {
+        avctx->codec_id         = id;
+        avctx->codec_descriptor = avcodec_descriptor_get(id);
+        avctx->time_base        = (AVRational) { 1, 1000 };
+        avctx->framerate        = framerate;
+    }
+    return avctx;
+}
+
+static int run_case(const Case *c)
+{
+    AVCodecContext *avctx = alloc_context(c->id, c->framerate);
+    FFDTSGen g;
+    int err = 0;
+
+    if (!avctx || ff_dts_gen_init(avctx, &g, c->delay) < 0) {
+        avcodec_free_context(&avctx);
+        return 1;
+    }
+
+    for (int i = 0; i < c->nb_input; i++)
+        ff_dts_gen_record_pts(&g, c->input[i]);
+
+    for (int i = 0; i < c->nb; i++) {
+        AVPacket pkt = { .pts = c->pts[i] };
+
+        ff_dts_gen_fill_pkt(avctx, &g, &pkt);
+        if (pkt.dts != c->dts[i]) {
+            fprintf(stderr, "%s: packet %d: dts %"PRId64", expected 
%"PRId64"\n",
+                    c->name, i, pkt.dts, c->dts[i]);
+            err = 1;
+        }
+    }
+    if (g.warned != c->warned) {
+        fprintf(stderr, "%s: warned %d, expected %d\n",
+                c->name, g.warned, c->warned);
+        err = 1;
+    }
+
+    avcodec_free_context(&avctx);
+    return err;
+}
+
+/* ff_dts_gen_reset() must drop the recorded interval and the window. */
+static int run_reset(void)
+{
+    AVCodecContext *avctx = alloc_context(AV_CODEC_ID_H264, (AVRational) { 50, 
1 });
+    AVPacket pkt = { .pts = 0 };
+    FFDTSGen g;
+    int err = 0;
+
+    if (!avctx || ff_dts_gen_init(avctx, &g, 1) < 0) {
+        avcodec_free_context(&avctx);
+        return 1;
+    }
+
+    ff_dts_gen_record_pts(&g, 0);
+    ff_dts_gen_record_pts(&g, 40);
+    ff_dts_gen_fill_pkt(avctx, &g, &pkt);
+
+    ff_dts_gen_reset(&g);
+    pkt.pts = 100;
+    ff_dts_gen_fill_pkt(avctx, &g, &pkt);
+    if (pkt.dts != 80) {
+        fprintf(stderr, "reset: dts %"PRId64", expected 80\n", pkt.dts);
+        err = 1;
+    }
+
+    avcodec_free_context(&avctx);
+    return err;
+}
+
+static int run_invalid_delays(void)
+{
+    static const int delays[] = { FF_DTS_GEN_DELAY_AUTO,
+                                  FF_DTS_GEN_DELAY_SKIP - 1,
+                                  FF_DTS_GEN_DELAY_MAX + 1 };
+    AVCodecContext *avctx = alloc_context(AV_CODEC_ID_H264, (AVRational) { 0, 
0 });
+    FFDTSGen g;
+    int err = 0;
+
+    if (!avctx)
+        return 1;
+
+    for (int i = 0; i < FF_ARRAY_ELEMS(delays); i++) {
+        if (ff_dts_gen_init(avctx, &g, delays[i]) != AVERROR(EINVAL)) {
+            fprintf(stderr, "init accepted delay %d\n", delays[i]);
+            err = 1;
+        }
+    }
+
+    avcodec_free_context(&avctx);
+    return err;
+}
+
+int main(void)
+{
+    int err = 0;
+
+    for (int i = 0; i < FF_ARRAY_ELEMS(cases); i++)
+        err |= run_case(&cases[i]);
+    err |= run_reset();
+    err |= run_invalid_delays();
+
+    return err;
+}
diff --git a/tests/fate/libavcodec.mak b/tests/fate/libavcodec.mak
index 3cde003212..2257542a32 100644
--- a/tests/fate/libavcodec.mak
+++ b/tests/fate/libavcodec.mak
@@ -47,6 +47,11 @@ fate-golomb: libavcodec/tests/golomb$(EXESUF)
 fate-golomb: CMD = run libavcodec/tests/golomb$(EXESUF)
 fate-golomb: CMP = null
 
+FATE_LIBAVCODEC-yes += fate-dts-gen
+fate-dts-gen: libavcodec/tests/dts_gen$(EXESUF)
+fate-dts-gen: CMD = run libavcodec/tests/dts_gen$(EXESUF)
+fate-dts-gen: CMP = null
+
 FATE_LIBAVCODEC-$(CONFIG_DXV_ENCODER) += fate-hashtable
 fate-hashtable: libavcodec/tests/hashtable$(EXESUF)
 fate-hashtable: CMD = run libavcodec/tests/hashtable$(EXESUF)
-- 
2.52.0


>From 4b376ce7a4447af4509b7d1585bd0d565089e426 Mon Sep 17 00:00:00 2001
From: Zhao Zhili <[email protected]>
Date: Thu, 1 Oct 2026 18:06:32 +0800
Subject: [PATCH 2/4] avcodec/videotoolboxenc: derive dts with the shared
 helper

The encoder took dts from VideoToolbox and subtracted a fixed offset,
measured once from the first input frames. The offset only fits a
constant frame interval: when the interval grows later in the stream,
dts ends up above pts.

Derive dts from the pts of the output packets instead. The result
follows the actual pts values, so it also holds at a variable frame
rate. The first packets are no longer withheld until the offset has
been measured.

VideoToolbox reports whether it reorders but not the depth, so
REORDER_DELAY is the delay when reorder_delay is auto. Add the
reorder_delay option to override it.

Signed-off-by: Zhao Zhili <[email protected]>
---
 libavcodec/version.h         |  2 +-
 libavcodec/videotoolboxenc.c | 60 +++++++++++++++---------------------
 2 files changed, 25 insertions(+), 37 deletions(-)

diff --git a/libavcodec/version.h b/libavcodec/version.h
index 65bc52fb24..d8604fd00c 100644
--- a/libavcodec/version.h
+++ b/libavcodec/version.h
@@ -30,7 +30,7 @@
 #include "version_major.h"
 
 #define LIBAVCODEC_VERSION_MINOR  18
-#define LIBAVCODEC_VERSION_MICRO 100
+#define LIBAVCODEC_VERSION_MICRO 101
 
 #define LIBAVCODEC_VERSION_INT  AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \
                                                LIBAVCODEC_VERSION_MINOR, \
diff --git a/libavcodec/videotoolboxenc.c b/libavcodec/videotoolboxenc.c
index d01a091728..86cbe57c42 100644
--- a/libavcodec/videotoolboxenc.c
+++ b/libavcodec/videotoolboxenc.c
@@ -36,6 +36,7 @@
 
 #include "atsc_a53.h"
 #include "codec_internal.h"
+#include "dts_gen.h"
 #include "encode.h"
 #include "h264.h"
 #include "h264_sei.h"
@@ -66,18 +67,17 @@ enum { kVTQPModulationLevel_Disable = 0 };
 
 /*
  * Upper bound of the reorder depth of VideoToolbox H.264 and HEVC
- * output, in frames. The encoder shifts the decode timestamps
- * VideoToolbox reports by the pts distance of this many frames, and
- * the value is published as AVCodecContext.has_b_frames; understating
- * it outputs frames with dts greater than pts.
+ * output, in frames. It is published as AVCodecContext.has_b_frames
+ * and is the reorder delay used to derive dts when reorder_delay is
+ * auto.
  *
  * The VideoToolbox API does not report the depth, so the value was
  * measured from encoder output on macOS 15 (Apple silicon). It bounds
  * the real depth instead of matching it everywhere: the software
  * H.264 encoder (require_sw) reorders by one frame, and untested
  * devices may reorder less. Keeping one value for every encoder is
- * safe because overstating only shifts dts earlier, and it is simpler
- * than branching per encoder.
+ * safe because overstating only lowers dts, while understating makes
+ * dts non-monotonic, and it is simpler than branching per encoder.
  */
 #define REORDER_DELAY 2
 
@@ -280,8 +280,7 @@ typedef struct VTEncContext {
     int64_t frame_ct_out;
     int64_t frame_ct_in;
 
-    int64_t first_pts;
-    int64_t dts_delta;
+    FFDTSGen dts_gen;
 
     int profile;
     int level;
@@ -307,6 +306,7 @@ typedef struct VTEncContext {
     int power_efficient;
     int max_ref_frames;
     int spatialaq;
+    int reorder_delay;
 } VTEncContext;
 
 static void vtenc_free_buf_node(BufNode *info)
@@ -1742,8 +1742,6 @@ static int vtenc_configure_encoder(AVCodecContext *avctx)
             goto init_cleanup;
     }
 
-    vtctx->dts_delta = vtctx->has_b_frames ? -1 : 0;
-
     get_cv_gamma(avctx, &gamma_level);
     vtctx->transfer_function = 
av_map_videotoolbox_color_trc_from_av(avctx->color_trc);
     vtctx->ycbcr_matrix = 
av_map_videotoolbox_color_matrix_from_av(avctx->colorspace);
@@ -1787,6 +1785,7 @@ static av_cold int vtenc_init(AVCodecContext *avctx)
     VTEncContext    *vtctx = avctx->priv_data;
     CFBooleanRef    has_b_frames_cfbool;
     int             status;
+    int             delay;
 
     pthread_once(&once_ctrl, loadVTEncSymbols);
 
@@ -1814,6 +1813,17 @@ static av_cold int vtenc_init(AVCodecContext *avctx)
     }
     avctx->has_b_frames = vtctx->has_b_frames;
 
+    delay = vtctx->reorder_delay;
+    if (delay == FF_DTS_GEN_DELAY_AUTO)
+        delay = vtctx->has_b_frames;
+
+    status = ff_dts_gen_init(avctx, &vtctx->dts_gen, delay);
+    if (status < 0)
+        return status;
+
+    if (vtctx->dts_gen.delay >= 0)
+        avctx->has_b_frames = vtctx->dts_gen.delay;
+
     return 0;
 }
 
@@ -2231,10 +2241,8 @@ static int vtenc_cm_to_avpacket(
     size_t  in_buf_size;
     size_t  out_buf_size;
     size_t  sei_nalu_size = 0;
-    int64_t dts_delta;
     int nalu_count;
     CMTime  pts;
-    CMTime  dts;
     CMVideoFormatDescriptionRef vid_fmt;
 
     vtenc_get_frame_info(sample_buffer, &is_key_frame);
@@ -2321,26 +2329,14 @@ static int vtenc_cm_to_avpacket(
     }
 
     pts = CMSampleBufferGetPresentationTimeStamp(sample_buffer);
-    dts = CMSampleBufferGetDecodeTimeStamp      (sample_buffer);
 
     if (CMTIME_IS_INVALID(pts)) {
         av_log(avctx, AV_LOG_ERROR, "PTS is invalid.\n");
         return AVERROR_EXTERNAL;
     }
 
-    if (CMTIME_IS_INVALID(dts)) {
-        if (!vtctx->has_b_frames) {
-            dts = pts;
-        } else {
-            av_log(avctx, AV_LOG_ERROR, "DTS is invalid.\n");
-            return AVERROR_EXTERNAL;
-        }
-    }
-
-    dts_delta = vtctx->dts_delta >= 0 ? vtctx->dts_delta : 0;
     pkt->pts = av_rescale_q(pts.value, (AVRational){1, pts.timescale}, 
avctx->time_base);
-    pkt->dts = av_rescale_q(dts.value, (AVRational){1, dts.timescale}, 
avctx->time_base)
-               - dts_delta;
+    ff_dts_gen_fill_pkt(avctx, &vtctx->dts_gen, pkt);
 
     return 0;
 }
@@ -2554,7 +2550,6 @@ static av_cold int vtenc_frame(
     int            *got_packet)
 {
     VTEncContext *vtctx = avctx->priv_data;
-    bool get_frame;
     int status;
     CMSampleBufferRef buf = NULL;
     ExtraSEI sei = {0};
@@ -2567,11 +2562,7 @@ static av_cold int vtenc_frame(
             goto end_nopkt;
         }
 
-        if (vtctx->frame_ct_in == 0) {
-            vtctx->first_pts = frame->pts;
-        } else if(vtctx->frame_ct_in == vtctx->has_b_frames) {
-            vtctx->dts_delta = frame->pts - vtctx->first_pts;
-        }
+        ff_dts_gen_record_pts(&vtctx->dts_gen, frame->pts);
 
         vtctx->frame_ct_in++;
     } else if(!vtctx->flushing) {
@@ -2588,11 +2579,6 @@ static av_cold int vtenc_frame(
     }
 
     *got_packet = 0;
-    get_frame = vtctx->dts_delta >= 0 || !frame;
-    if (!get_frame) {
-        status = 0;
-        goto end_nopkt;
-    }
 
     status = vtenc_q_pop(vtctx, !frame, &buf, &sei);
     if (status) goto end_nopkt;
@@ -2850,6 +2836,7 @@ static const AVOption h264_options[] = {
 
     { "constant_bit_rate", "Require constant bit rate (macOS 13 or newer)", 
OFFSET(constant_bit_rate), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, VE },
     { "max_slice_bytes", "Set the maximum number of bytes in an H.264 slice.", 
OFFSET(max_slice_bytes), AV_OPT_TYPE_INT, { .i64 = -1 }, -1, INT_MAX, VE },
+    FF_DTS_GEN_REORDER_DELAY_OPTION(OFFSET(reorder_delay), VE),
     COMMON_OPTIONS
     { NULL },
 };
@@ -2898,6 +2885,7 @@ static const AVOption hevc_options[] = {
 
     { "constant_bit_rate", "Require constant bit rate (macOS 13 or newer)", 
OFFSET(constant_bit_rate), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, VE },
 
+    FF_DTS_GEN_REORDER_DELAY_OPTION(OFFSET(reorder_delay), VE),
     COMMON_OPTIONS
     { NULL },
 };
-- 
2.52.0


>From a2b28a3c1f7832c3001de0ee7cfa20f39cdee062 Mon Sep 17 00:00:00 2001
From: Zhao Zhili <[email protected]>
Date: Thu, 1 Oct 2026 18:07:30 +0800
Subject: [PATCH 3/4] avcodec/nvenc: derive dts with the shared helper

nvenc already derives dts from the input pts, delayed by the reorder
depth, which works at a variable frame rate. Move it onto the shared
helper so the private timestamp queue and the bookkeeping for the first
packets can go. The reorder depth is computed as before.

Add the reorder_delay option, so a user can override the depth.

Signed-off-by: Zhao Zhili <[email protected]>
---
 libavcodec/nvenc.c      | 132 ++++++++--------------------------------
 libavcodec/nvenc.h      |   8 +--
 libavcodec/nvenc_h264.c |   1 +
 libavcodec/nvenc_hevc.c |   1 +
 4 files changed, 33 insertions(+), 109 deletions(-)

diff --git a/libavcodec/nvenc.c b/libavcodec/nvenc.c
index 6e7331405f..e53dec4ed7 100644
--- a/libavcodec/nvenc.c
+++ b/libavcodec/nvenc.c
@@ -1965,11 +1965,6 @@ static av_cold int nvenc_setup_surfaces(AVCodecContext 
*avctx)
     if (!ctx->frame_data_array)
         return AVERROR(ENOMEM);
 
-    ctx->timestamp_list = av_fifo_alloc2(ctx->nb_surfaces + 
ctx->encode_config.frameIntervalP,
-                                         sizeof(int64_t), 0);
-    if (!ctx->timestamp_list)
-        return AVERROR(ENOMEM);
-
     ctx->unused_surface_queue = av_fifo_alloc2(ctx->nb_surfaces, 
sizeof(NvencSurface*), 0);
     if (!ctx->unused_surface_queue)
         return AVERROR(ENOMEM);
@@ -2051,7 +2046,6 @@ av_cold int ff_nvenc_encode_close(AVCodecContext *avctx)
         p_nvenc->nvEncEncodePicture(ctx->nvencoder, &params);
     }
 
-    av_fifo_freep2(&ctx->timestamp_list);
     av_fifo_freep2(&ctx->output_surface_ready_queue);
     av_fifo_freep2(&ctx->output_surface_queue);
     av_fifo_freep2(&ctx->unused_surface_queue);
@@ -2121,7 +2115,7 @@ av_cold int ff_nvenc_encode_close(AVCodecContext *avctx)
 av_cold int ff_nvenc_encode_init(AVCodecContext *avctx)
 {
     NvencContext *ctx = avctx->priv_data;
-    int ret;
+    int ret, delay;
 
     if (IS_HWACCEL(avctx->pix_fmt)) {
         AVHWFramesContext *frames_ctx;
@@ -2167,7 +2161,21 @@ av_cold int ff_nvenc_encode_init(AVCodecContext *avctx)
             return ret;
     }
 
-    return 0;
+    if (ctx->reorder_delay != FF_DTS_GEN_DELAY_AUTO) {
+        delay = ctx->reorder_delay;
+    } else {
+        /* frameIntervalP - 1 B pictures sit between two anchors, which bounds
+         * the depth. MV-HEVC interleaves two views that reorder
+         * independently, so the depth doubles.
+         */
+        delay = FFMAX(ctx->encode_config.frameIntervalP - 1, 0);
+#ifdef NVENC_HAVE_MVHEVC
+        delay *= ctx->multiview ? 2 : 1;
+#endif
+        delay = FFMIN(delay, FF_DTS_GEN_DELAY_MAX);
+    }
+
+    return ff_dts_gen_init(avctx, &ctx->dts_gen, delay);
 }
 
 static NvencSurface *get_free_frame(NvencContext *ctx)
@@ -2488,81 +2496,14 @@ static void 
nvenc_codec_specific_pic_params(AVCodecContext *avctx, const AVFrame
     }
 }
 
-static inline void timestamp_queue_enqueue(AVFifo *queue, int64_t timestamp)
-{
-    av_fifo_write(queue, &timestamp, 1);
-}
-
-static inline int64_t timestamp_queue_dequeue(AVFifo *queue)
-{
-    int64_t timestamp = AV_NOPTS_VALUE;
-    // The following call might fail if the queue is empty.
-    av_fifo_read(queue, &timestamp, 1);
-
-    return timestamp;
-}
-
-static inline int64_t timestamp_queue_peek(AVFifo *queue, size_t index)
-{
-    int64_t timestamp = AV_NOPTS_VALUE;
-    av_fifo_peek(queue, &timestamp, 1, index);
-
-    return timestamp;
-}
-
 static int nvenc_set_timestamp(AVCodecContext *avctx,
                                NV_ENC_LOCK_BITSTREAM *params,
                                AVPacket *pkt)
 {
     NvencContext *ctx = avctx->priv_data;
-    unsigned int delay;
-    int64_t delay_time;
 
     pkt->pts = params->outputTimeStamp;
-
-    if (!(avctx->codec_descriptor->props & AV_CODEC_PROP_REORDER)) {
-        pkt->dts = pkt->pts;
-        return 0;
-    }
-
-    // This can be more than necessary, but we don't know the real reorder 
delay.
-    delay = FFMAX(ctx->encode_config.frameIntervalP - 1, 0);
-#ifdef NVENC_HAVE_MVHEVC
-    delay *= ctx->multiview ? 2 : 1;
-#endif
-    if (ctx->output_frame_num >= delay) {
-        pkt->dts = timestamp_queue_dequeue(ctx->timestamp_list);
-        ctx->output_frame_num++;
-        return 0;
-    }
-
-    delay_time = ctx->initial_delay_time;
-    if (!delay_time) {
-        int64_t t1, t2, t3;
-        t1 = timestamp_queue_peek(ctx->timestamp_list, delay);
-        t2 = timestamp_queue_peek(ctx->timestamp_list, 0);
-        t3 = (delay > 1) ? timestamp_queue_peek(ctx->timestamp_list, 1) : t1;
-
-        if (t1 != AV_NOPTS_VALUE) {
-            delay_time = t1 - t2;
-        } else if (avctx->framerate.num > 0 && avctx->framerate.den > 0) {
-            delay_time = av_rescale_q(delay, (AVRational) 
{avctx->framerate.den, avctx->framerate.num},
-                                      avctx->time_base);
-        } else if (t3 != AV_NOPTS_VALUE) {
-            delay_time = delay * (t3 - t2);
-        } else {
-            delay_time = delay;
-        }
-        ctx->initial_delay_time = delay_time;
-    }
-
-    /* The following method is simple, but doesn't guarantee monotonic with VFR
-     * when delay_time isn't accurate (that is, t1 == AV_NOPTS_VALUE)
-     *
-     * dts = timestamp_queue_peek(ctx->timestamp_list, ctx->output_frame_num) 
- delay_time
-     */
-    pkt->dts = timestamp_queue_peek(ctx->timestamp_list, 0) - delay_time * 
(delay - ctx->output_frame_num) / delay;
-    ctx->output_frame_num++;
+    ff_dts_gen_fill_pkt(avctx, &ctx->dts_gen, pkt);
 
     return 0;
 }
@@ -2633,23 +2574,21 @@ static int process_output_surface(AVCodecContext 
*avctx, AVPacket *pkt, NvencSur
 
     nv_status = p_nvenc->nvEncLockBitstream(ctx->nvencoder, &lock_params);
     if (nv_status != NV_ENC_SUCCESS) {
-        res = nvenc_print_error(avctx, nv_status, "Failed locking bitstream 
buffer");
-        goto error;
+        return nvenc_print_error(avctx, nv_status, "Failed locking bitstream 
buffer");
     }
 
     res = ff_get_encode_buffer(avctx, pkt, lock_params.bitstreamSizeInBytes, 
0);
 
     if (res < 0) {
         p_nvenc->nvEncUnlockBitstream(ctx->nvencoder, 
tmpoutsurf->output_surface);
-        goto error;
+        return res;
     }
 
     memcpy(pkt->data, lock_params.bitstreamBufferPtr, 
lock_params.bitstreamSizeInBytes);
 
     nv_status = p_nvenc->nvEncUnlockBitstream(ctx->nvencoder, 
tmpoutsurf->output_surface);
     if (nv_status != NV_ENC_SUCCESS) {
-        res = nvenc_print_error(avctx, nv_status, "Failed unlocking bitstream 
buffer, expect the gates of mordor to open");
-        goto error;
+        return nvenc_print_error(avctx, nv_status, "Failed unlocking bitstream 
buffer, expect the gates of mordor to open");
     }
 
 
@@ -2658,12 +2597,10 @@ static int process_output_surface(AVCodecContext 
*avctx, AVPacket *pkt, NvencSur
         if (ctx->registered_frames[tmpoutsurf->reg_idx].mapped == 0) {
             nv_status = p_nvenc->nvEncUnmapInputResource(ctx->nvencoder, 
ctx->registered_frames[tmpoutsurf->reg_idx].in_map.mappedResource);
             if (nv_status != NV_ENC_SUCCESS) {
-                res = nvenc_print_error(avctx, nv_status, "Failed unmapping 
input resource");
-                goto error;
+                return nvenc_print_error(avctx, nv_status, "Failed unmapping 
input resource");
             }
         } else if (ctx->registered_frames[tmpoutsurf->reg_idx].mapped < 0) {
-            res = AVERROR_BUG;
-            goto error;
+            return AVERROR_BUG;
         }
 
         av_frame_unref(tmpoutsurf->in_ref);
@@ -2690,8 +2627,7 @@ static int process_output_surface(AVCodecContext *avctx, 
AVPacket *pkt, NvencSur
     default:
         av_log(avctx, AV_LOG_ERROR, "Unknown picture type encountered, expect 
the output to be broken.\n");
         av_log(avctx, AV_LOG_ERROR, "Please report this error and include as 
much information on how to reproduce it as possible.\n");
-        res = AVERROR_EXTERNAL;
-        goto error;
+        return AVERROR_EXTERNAL;
     }
 
     ff_encode_add_stats_side_data(pkt,
@@ -2699,19 +2635,9 @@ static int process_output_surface(AVCodecContext *avctx, 
AVPacket *pkt, NvencSur
 
     res = nvenc_set_timestamp(avctx, &lock_params, pkt);
     if (res < 0)
-        goto error2;
+        return res;
 
-    res = nvenc_retrieve_frame_data(avctx, &lock_params, pkt);
-    if (res < 0)
-        goto error2;
-
-    return 0;
-
-error:
-    timestamp_queue_dequeue(ctx->timestamp_list);
-
-error2:
-    return res;
+    return nvenc_retrieve_frame_data(avctx, &lock_params, pkt);
 }
 
 static int output_ready(AVCodecContext *avctx, int flush)
@@ -3163,9 +3089,7 @@ static int nvenc_send_frame(AVCodecContext *avctx, const 
AVFrame *frame)
 
     if (frame && frame->buf[0]) {
         av_fifo_write(ctx->output_surface_queue, &in_surf, 1);
-
-        if (avctx->codec_descriptor->props & AV_CODEC_PROP_REORDER)
-            timestamp_queue_enqueue(ctx->timestamp_list, frame->pts);
+        ff_dts_gen_record_pts(&ctx->dts_gen, frame->pts);
     }
 
     /* all the pending buffers are now ready for output */
@@ -3233,7 +3157,5 @@ av_cold void ff_nvenc_encode_flush(AVCodecContext *avctx)
     NvencContext *ctx = avctx->priv_data;
 
     nvenc_send_frame(avctx, NULL);
-    av_fifo_reset2(ctx->timestamp_list);
-    ctx->output_frame_num = 0;
-    ctx->initial_delay_time = 0;
+    ff_dts_gen_reset(&ctx->dts_gen);
 }
diff --git a/libavcodec/nvenc.h b/libavcodec/nvenc.h
index acde2a42c1..eeaf187b70 100644
--- a/libavcodec/nvenc.h
+++ b/libavcodec/nvenc.h
@@ -37,6 +37,7 @@ typedef void ID3D11Device;
 #include "hwconfig.h"
 
 #include "avcodec.h"
+#include "dts_gen.h"
 
 #define MAX_REGISTERED_FRAMES 64
 
@@ -191,10 +192,8 @@ typedef struct NvencContext
     AVFifo *unused_surface_queue;
     AVFifo *output_surface_queue;
     AVFifo *output_surface_ready_queue;
-    AVFifo *timestamp_list;
-    // This is for DTS calculating, reset after flush
-    uint64_t output_frame_num;
-    int64_t initial_delay_time;
+
+    FFDTSGen dts_gen;
 
     NV_ENC_SEI_PAYLOAD *sei_data;
     int sei_data_size;
@@ -276,6 +275,7 @@ typedef struct NvencContext
     int cbr_padding;
     int multiview, multiview_supported;
     int display_sei_sent;
+    int reorder_delay;
 } NvencContext;
 
 int ff_nvenc_encode_init(AVCodecContext *avctx);
diff --git a/libavcodec/nvenc_h264.c b/libavcodec/nvenc_h264.c
index 103c9a95fd..7a0cb8e28c 100644
--- a/libavcodec/nvenc_h264.c
+++ b/libavcodec/nvenc_h264.c
@@ -176,6 +176,7 @@ static const AVOption options[] = {
                                                             
OFFSET(constrained_encoding), AV_OPT_TYPE_BOOL,  { .i64 = 0 }, 0, 1, VE },
     { "cbr_padding",  "Pad the bitstream to ensure bitrate does not drop below 
the target in CBR mode",
                                                             
OFFSET(cbr_padding),  AV_OPT_TYPE_BOOL,  { .i64 = 0 }, 0, 1, VE },
+    FF_DTS_GEN_REORDER_DELAY_OPTION(OFFSET(reorder_delay), VE),
 #ifdef NVENC_HAVE_H264_AND_AV1_TEMPORAL_FILTER
     { "tf_level",     "Specifies the strength of the temporal filtering",
                                                             OFFSET(tf_level),  
   AV_OPT_TYPE_INT,   { .i64 = -1 }, -1, INT_MAX, VE, .unit = "tf_level" },
diff --git a/libavcodec/nvenc_hevc.c b/libavcodec/nvenc_hevc.c
index 94ba34c48b..17ec674d9d 100644
--- a/libavcodec/nvenc_hevc.c
+++ b/libavcodec/nvenc_hevc.c
@@ -162,6 +162,7 @@ static const AVOption options[] = {
                                                             
OFFSET(constrained_encoding), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, VE },
     { "cbr_padding",  "Pad the bitstream to ensure bitrate does not drop below 
the target in CBR mode",
                                                             
OFFSET(cbr_padding),  AV_OPT_TYPE_BOOL,  { .i64 = 0 }, 0, 1, VE },
+    FF_DTS_GEN_REORDER_DELAY_OPTION(OFFSET(reorder_delay), VE),
 #ifdef NVENC_HAVE_TEMPORAL_FILTER
     { "tf_level",     "Specifies the strength of the temporal filtering",
                                                             OFFSET(tf_level),  
   AV_OPT_TYPE_INT,   { .i64 = -1 }, -1, INT_MAX, VE, .unit = "tf_level" },
-- 
2.52.0


>From 266156a6b782a7cd1f00df0d925aad5fe0c7c037 Mon Sep 17 00:00:00 2001
From: Zhao Zhili <[email protected]>
Date: Thu, 1 Oct 2026 18:09:14 +0800
Subject: [PATCH 4/4] avcodec/mediacodecenc: derive dts with the shared helper

MediaCodec reports only a presentation timestamp. The encoder left dts
unset when B frames were enabled and therefore demanded
-strict experimental.

Derive dts with the helper, using max_b_frames as the reorder depth:
the "max-bframes" format key limits the B frames between two anchors,
which bounds the depth. The -strict experimental requirement is no
longer needed. With pts_as_dts set, which is the default without B
frames, the depth stays zero and dts equals pts as before. Add the
reorder_delay option to override the depth.

Signed-off-by: Zhao Zhili <[email protected]>
---
 doc/encoders.texi          | 17 +++++++++++++++--
 libavcodec/mediacodecenc.c | 38 +++++++++++++++++++++++++-------------
 2 files changed, 40 insertions(+), 15 deletions(-)

diff --git a/doc/encoders.texi b/doc/encoders.texi
index fcb085f2b8..fb282d1b1d 100644
--- a/doc/encoders.texi
+++ b/doc/encoders.texi
@@ -3554,8 +3554,21 @@ Constant bitrate mode with frame drops
 @end table
 
 @item pts_as_dts @var{boolean}
-Use PTS as DTS. This is a workaround since MediaCodec API doesn't provide
-decoding timestamp. It is enabled automatically if B frame is 0.
+Use PTS as DTS when @option{reorder_delay} is @samp{auto}. It is enabled
+automatically if B frame is 0.
+
+@item reorder_delay @var{integer}
+Reorder delay in frames used to generate DTS, since MediaCodec API doesn't
+provide decoding timestamp. A value below the real reorder depth makes DTS
+non-monotonic, a larger value lowers every DTS by the excess frames.
+
+@table @samp
+@item auto
+Use the number of B frames, or 0 when @option{pts_as_dts} is enabled. This is
+the default.
+@item skip
+Leave DTS unset.
+@end table
 
 @item operating_rate @var{integer}
 The desired operating rate that the codec will need to operate at, zero for
diff --git a/libavcodec/mediacodecenc.c b/libavcodec/mediacodecenc.c
index 507c48df9c..f5ef99a015 100644
--- a/libavcodec/mediacodecenc.c
+++ b/libavcodec/mediacodecenc.c
@@ -34,6 +34,7 @@
 #include "avcodec.h"
 #include "bsf.h"
 #include "codec_internal.h"
+#include "dts_gen.h"
 #include "encode.h"
 #include "hwconfig.h"
 #include "jni.h"
@@ -78,11 +79,13 @@ typedef struct MediaCodecEncContext {
 
     AVFrame *frame;
     AVBSFContext *bsf;
+    FFDTSGen dts_gen;
 
     int bitrate_mode;
     int level;
     int pts_as_dts;
     int extract_extradata;
+    int reorder_delay;
     // Ref. MediaFormat KEY_OPERATING_RATE
     int operating_rate;
     int async_mode;
@@ -398,6 +401,7 @@ static av_cold int mediacodec_init(AVCodecContext *avctx)
     FFAMediaFormat *format = NULL;
     int ret;
     int gop;
+    int delay;
 
     // Init async state first, so we can do cleanup safely on error path.
     ret = mediacodec_init_async_state(avctx);
@@ -555,18 +559,22 @@ static av_cold int mediacodec_init(AVCodecContext *avctx)
         av_log(avctx, AV_LOG_DEBUG, "set level to 0x%x\n", s->level);
         ff_AMediaFormat_setInt32(format, "level", s->level);
     }
-    if (avctx->max_b_frames > 0) {
-        if (avctx->strict_std_compliance > FF_COMPLIANCE_EXPERIMENTAL) {
-            av_log(avctx, AV_LOG_ERROR,
-                    "Enabling B frames will produce packets with no DTS. "
-                    "Use -strict experimental to use it anyway.\n");
-            ret = AVERROR(EINVAL);
-            goto bailout;
-        }
+    if (avctx->max_b_frames > 0)
         ff_AMediaFormat_setInt32(format, "max-bframes", avctx->max_b_frames);
-    }
     if (s->pts_as_dts == -1)
         s->pts_as_dts = avctx->max_b_frames <= 0;
+
+    /* MediaCodec reports neither a decode timestamp nor a reorder depth.
+     * "max-bframes" limits the B pictures between two anchors, which bounds
+     * the depth.
+     */
+    delay = s->reorder_delay;
+    if (delay == FF_DTS_GEN_DELAY_AUTO)
+        delay = s->pts_as_dts ? 0 : av_clip(avctx->max_b_frames, 0,
+                                            FF_DTS_GEN_DELAY_MAX);
+    if ((ret = ff_dts_gen_init(avctx, &s->dts_gen, delay)) < 0)
+        goto bailout;
+
     if (s->operating_rate > 0)
         ff_AMediaFormat_setInt32(format, "operating-rate", s->operating_rate);
 
@@ -737,8 +745,7 @@ static int mediacodec_receive(AVCodecContext *avctx, 
AVPacket *pkt)
     }
     memcpy(pkt->data + extradata_size, out_buf + out_info.offset, 
out_info.size);
     pkt->pts = av_rescale_q(out_info.presentationTimeUs, AV_TIME_BASE_Q, 
avctx->time_base);
-    if (s->pts_as_dts)
-        pkt->dts = pkt->pts;
+    ff_dts_gen_fill_pkt(avctx, &s->dts_gen, pkt);
     if (out_info.flags & ff_AMediaCodec_getBufferFlagKeyFrame(codec))
         pkt->flags |= AV_PKT_FLAG_KEY;
     ret = 0;
@@ -805,6 +812,7 @@ static int mediacodec_send(AVCodecContext *avctx,
             return ff_AMediaCodec_signalEndOfInputStream(codec);
         }
 
+        ff_dts_gen_record_pts(&s->dts_gen, frame->pts);
         if (frame->data[3])
             av_mediacodec_release_buffer((AVMediaCodecBuffer *)frame->data[3], 
1);
         return 0;
@@ -827,6 +835,7 @@ static int mediacodec_send(AVCodecContext *avctx,
         copy_frame_to_buffer(avctx, frame, input_buf, input_size);
 
         pts = av_rescale_q(frame->pts, avctx->time_base, AV_TIME_BASE_Q);
+        ff_dts_gen_record_pts(&s->dts_gen, frame->pts);
     } else {
         flags |= ff_AMediaCodec_getBufferFlagEndOfStream(codec);
         s->eof_sent = 1;
@@ -1016,6 +1025,7 @@ static av_cold void mediacodec_flush(AVCodecContext 
*avctx)
         av_bsf_flush(s->bsf);
     av_frame_unref(s->frame);
     ff_AMediaCodec_flush(s->codec);
+    ff_dts_gen_reset(&s->dts_gen);
 }
 
 static const AVCodecHWConfigInternal *const mediacodec_hw_configs[] = {
@@ -1056,8 +1066,9 @@ static const FFCodecDefault mediacodec_defaults[] = {
                     0, AV_OPT_TYPE_CONST, {.i64 = BITRATE_MODE_CBR}, 0, 0, VE, 
.unit = "bitrate_mode" },            \
     { "cbr_fd", "Constant bitrate mode with frame drops",                      
                                     \
                     0, AV_OPT_TYPE_CONST, {.i64 = BITRATE_MODE_CBR_FD}, 0, 0, 
VE, .unit = "bitrate_mode" },         \
-    { "pts_as_dts", "Use PTS as DTS. It is enabled automatically if avctx 
max_b_frames <= 0, "              \
-                    "since most of Android devices don't output B frames by 
default.",                      \
+    { "pts_as_dts", "Use PTS as DTS when reorder_delay is auto. It is enabled 
automatically if "            \
+                    "avctx max_b_frames <= 0, since most of Android devices 
don't output B frames "         \
+                    "by default.",                                             
                             \
                     OFFSET(pts_as_dts), AV_OPT_TYPE_BOOL, {.i64 = -1}, -1, 1, 
VE },                         \
     { "operating_rate", "The desired operating rate that the codec will need 
to operate at, zero for unspecified",    \
                     OFFSET(operating_rate), AV_OPT_TYPE_INT, {.i64 = 0}, 0, 
INT_MAX, VE },                  \
@@ -1073,6 +1084,7 @@ static const FFCodecDefault mediacodec_defaults[] = {
                     OFFSET(qp_p_max), AV_OPT_TYPE_INT, {.i64 = -1}, -1, 
INT_MAX, VE },                      \
     { "qp_b_max", "maximum quantization parameter for B frame",                
                             \
                     OFFSET(qp_b_max), AV_OPT_TYPE_INT, {.i64 = -1}, -1, 
INT_MAX, VE },                      \
+    FF_DTS_GEN_REORDER_DELAY_OPTION(OFFSET(reorder_delay), VE),                
                             \
 
 
 #define MEDIACODEC_ENCODER_CLASS(name)              \
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to