[PR] avformat/mov: derive video packet durations in presentation order (PR #23936)

panboxiaosa via ffmpeg-devel <[email protected]> Tue, 28 Jul 2026 05:34:48 -0000
Newsgroups gmane.comp.video.ffmpeg.devel
Message-ID <178521688874.51.12498433028190360259@29965ddac10e>
PR #23936 opened by panboxiaosa
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23936
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23936.patch

Supersedes #23919. This PR uses a contributor-controlled branch, as suggested by James Almer.

STTS sample deltas follow decode order, while AVPacket.duration is defined as the interval to the next PTS in presentation order. Assigning the deltas directly therefore produces incorrect packet durations for VFR video with reordered frames.

After index construction and edit-list processing, sort samples by PTS and derive every duration with a following PTS from adjacent presentation timestamps. Keep the original timing table if allocation fails or timestamps are invalid.

FATE coverage includes:
- the VFR H.264 sample suggested during review, with valid STTS/CTTS values;
- a generated three-frame MPEG-4 case whose presentation intervals are not a permutation of its STTS deltas.

Tests:
- `make -k -j8 fate-mov fate-h264-bsf-dts2pts`


>From 7bbb795ee38958c3f23d02ffebbafdf0aaeeb3fb Mon Sep 17 00:00:00 2001
From: panboxiaosa <[email protected]>
Date: Tue, 28 Jul 2026 11:47:36 +0800
Subject: [PATCH] avformat/mov: derive video packet durations in presentation
 order

STTS sample deltas follow decode order, while AVPacket.duration is
defined as the interval to the next PTS in presentation order. Assigning
the deltas directly therefore produces incorrect packet durations for
VFR video with reordered frames.

After index construction and edit-list processing, sort samples by PTS
and derive every duration with a following PTS from adjacent
presentation timestamps. Keep the original timing table if allocation
fails or timestamps are invalid.

Add FATE coverage for the official VFR H.264 sample with CTTS
reordering and for a generated MPEG-4 case whose presentation intervals
are not a permutation of its STTS deltas.

Signed-off-by: panboxiaosa <[email protected]>
---
 libavformat/mov.c                             | 105 ++++++++++++++++++
 tests/fate/mov.mak                            |  21 ++++
 .../ref/fate/mov-vfr-bframes-derived-duration |   3 +
 tests/ref/fate/mov-vfr-bframes-duration       |  12 ++
 4 files changed, 141 insertions(+)
 create mode 100644 tests/ref/fate/mov-vfr-bframes-derived-duration
 create mode 100644 tests/ref/fate/mov-vfr-bframes-duration

diff --git a/libavformat/mov.c b/libavformat/mov.c
index bd5f456032..3a07b02969 100644
--- a/libavformat/mov.c
+++ b/libavformat/mov.c
@@ -28,6 +28,7 @@
 #include <inttypes.h>
 #include <limits.h>
 #include <stdint.h>
+#include <stdlib.h>
 
 #include "libavutil/attributes.h"
 #include "libavutil/bprint.h"
@@ -5269,6 +5270,109 @@ static void mov_build_index(MOVContext *mov, AVStream *st)
     mov_estimate_video_delay(mov, st);
 }
 
+typedef struct MOVPresentationSample {
+    int index;
+    int64_t pts;
+} MOVPresentationSample;
+
+static int mov_compare_presentation_samples(const void *a, const void *b)
+{
+    const MOVPresentationSample *sa = a;
+    const MOVPresentationSample *sb = b;
+
+    if (sa->pts != sb->pts)
+        return (sa->pts > sb->pts) - (sa->pts < sb->pts);
+    return (sa->index > sb->index) - (sa->index < sb->index);
+}
+
+/*
+ * Set sample durations from adjacent presentation timestamps.
+ */
+static void mov_update_sample_durations(MOVContext *mov, AVStream *st)
+{
+    MOVStreamContext *sc = st->priv_data;
+    FFStream *const sti = ffstream(st);
+    MOVPresentationSample *samples = NULL;
+    MOVTimeToSample *tts_data = NULL;
+    unsigned int tts_index = 0, tts_sample = 0;
+    int count = sti->nb_index_entries;
+
+    /* A single STTS entry describes a fixed sample delta. */
+    if (st->codecpar->codec_type != AVMEDIA_TYPE_VIDEO ||
+        !sc->ctts_count || sc->stts_count < 2 ||
+        !sc->tts_data || count < 2 ||
+        count >= UINT_MAX / sizeof(*tts_data))
+        return;
+
+    samples   = av_malloc_array(count, sizeof(*samples));
+    tts_data  = av_malloc_array(count, sizeof(*tts_data));
+    if (!samples || !tts_data)
+        goto fail;
+
+    for (int i = 0; i < count; i++) {
+        int64_t dts, offset;
+
+        if (tts_index >= sc->tts_count || !sc->tts_data[tts_index].count)
+            goto fail;
+
+        tts_data[i] = sc->tts_data[tts_index];
+        tts_data[i].count = 1;
+
+        dts = sti->index_entries[i].timestamp;
+        offset = (int64_t)sc->dts_shift + tts_data[i].offset;
+        if (dts == AV_NOPTS_VALUE ||
+            (offset > 0 && dts > INT64_MAX - offset) ||
+            (offset < 0 && dts < INT64_MIN - offset))
+            goto fail;
+
+        samples[i].index = i;
+        samples[i].pts = dts + offset;
+
+        if (++tts_sample == sc->tts_data[tts_index].count) {
+            tts_index++;
+            tts_sample = 0;
+        }
+    }
+    if (tts_index != sc->tts_count || tts_sample)
+        goto fail;
+
+    qsort(samples, count, sizeof(*samples), mov_compare_presentation_samples);
+
+    for (int i = 0; i + 1 < count; i++) {
+        uint64_t duration;
+
+        if (samples[i].pts >= samples[i + 1].pts)
+            goto fail;
+
+        /*
+         * In VFR streams with reordered frames, STTS deltas follow decode
+         * order while AVPacket.duration follows presentation order. CTTS
+         * may produce presentation intervals that cannot be obtained by
+         * merely permuting the STTS deltas, so derive each known duration
+         * from adjacent PTS.
+         */
+        duration = (uint64_t)samples[i + 1].pts - samples[i].pts;
+        if (!duration || duration > UINT_MAX)
+            goto fail;
+
+        tts_data[samples[i].index].duration = (unsigned int)duration;
+    }
+
+    av_log(mov->fc, AV_LOG_DEBUG,
+           "Updated sample durations in presentation order for stream %d\n",
+           st->index);
+
+    av_freep(&sc->tts_data);
+    sc->tts_data = tts_data;
+    sc->tts_count = count;
+    sc->tts_allocated_size = count * sizeof(*tts_data);
+    tts_data = NULL;
+
+fail:
+    av_free(samples);
+    av_free(tts_data);
+}
+
 static int test_same_origin(const char *src, const char *ref) {
     char src_proto[64];
     char ref_proto[64];
@@ -5497,6 +5601,7 @@ static int mov_read_trak(MOVContext *c, AVIOContext *pb, MOVAtom atom)
     }
 
     mov_build_index(c, st);
+    mov_update_sample_durations(c, st);
 
 #if CONFIG_IAMFDEC
     if (sc->iamf) {
diff --git a/tests/fate/mov.mak b/tests/fate/mov.mak
index 1629796f77..5c399f4020 100644
--- a/tests/fate/mov.mak
+++ b/tests/fate/mov.mak
@@ -44,6 +44,8 @@ FATE_MOV_FFPROBE-$(call FRAMEMD5, MOV, MPEG4, H264_PARSER) += fate-mov-mp4-exten
 
 FATE_MOV_FFPROBE-$(call DEMDEC, MOV, HEVC) += fate-mov-dovi-hvce-mp4-read
 
+FATE_MOV_FFPROBE-$(call DEMDEC, MOV, H264) += fate-mov-vfr-bframes-duration
+
 FATE_MOV_FASTSTART = fate-mov-faststart-4gb-overflow \
 
 FATE_SAMPLES_FFMPEG += $(FATE_MOV-yes) $(FATE_MOV_REMUX-yes)
@@ -148,6 +150,8 @@ fate-mov-spherical-mono: CMD = run ffprobe$(PROGSSUF)$(EXESUF) -show_entries str
 
 fate-mov-dovi-hvce-mp4-read: CMD = run ffprobe$(PROGSSUF)$(EXESUF) -show_entries stream_side_data_list -select_streams v -v 0 $(TARGET_SAMPLES)/mov/dovi-p7-hvce.mp4
 
+fate-mov-vfr-bframes-duration: CMD = run ffprobe$(PROGSSUF)$(EXESUF) -show_packets -show_entries packet=pts,dts,duration -print_format compact -select_streams v -v 0 $(TARGET_SAMPLES)/mov/vfr-7-12-1-sequence.mp4
+
 fate-mov-gpmf-remux: CMD = md5 -i $(TARGET_SAMPLES)/mov/fake-gp-media-with-real-gpmf.mp4 -map 0 -c copy -fflags +bitexact -f mp4
 fate-mov-gpmf-remux: CMP = oneline
 fate-mov-gpmf-remux: REF = e919915c5cd22c849e2aba281ddaf0c8
@@ -302,6 +306,23 @@ fate-mov-vfr: CMD = md5 -filter_complex testsrc=size=2x2:duration=1,setpts=N*N:s
 fate-mov-vfr: CMP = oneline
 fate-mov-vfr: REF = 1558b4a9398d8635783c93f84eb5a60d
 
+FATE_MOV_FFMPEG_FFPROBE-$(call ALLYES, COLOR_FILTER SETPTS_FILTER MPEG4_ENCODER \
+                                      MOV_MUXER MOV_DEMUXER FILE_PROTOCOL)      \
+                                      += fate-mov-vfr-bframes-derived-duration
+
+# Create VFR B-frames whose presentation durations are not a permutation of
+# the STTS sample deltas.
+tests/data/mov-vfr-bframes-derived-duration.mov: TAG = GEN
+tests/data/mov-vfr-bframes-derived-duration.mov: ffmpeg$(PROGSSUF)$(EXESUF) | tests/data
+	$(M)$(TARGET_EXEC) $(TARGET_PATH)/$< -nostdin -v error \
+	    -filter_complex "color=c=black:s=2x2:r=1,setpts=N+N*N" \
+	    -frames:v 3 -fps_mode vfr -c:v mpeg4 -bf 2 -q:v 2 -threads 1 \
+	    -flags +bitexact -fflags +bitexact \
+	    -f mov $(TARGET_PATH)/$@ -y
+
+fate-mov-vfr-bframes-derived-duration: tests/data/mov-vfr-bframes-derived-duration.mov
+fate-mov-vfr-bframes-derived-duration: CMD = run ffprobe$(PROGSSUF)$(EXESUF) -show_packets -show_entries packet=pts,dts,duration -print_format compact -select_streams v -v 0 $(TARGET_PATH)/tests/data/mov-vfr-bframes-derived-duration.mov
+
 FATE_MOV_FFMPEG_FFPROBE-$(call TRANSCODE, FLAC, MP4 MOV, WAV_DEMUXER PCM_S16LE_DECODER) += fate-mov-mp4-iamf-stereo
 fate-mov-mp4-iamf-stereo: tests/data/asynth-44100-2.wav tests/data/streamgroups/audio_element-stereo tests/data/streamgroups/mix_presentation-stereo
 fate-mov-mp4-iamf-stereo: SRC = $(TARGET_PATH)/tests/data/asynth-44100-2.wav
diff --git a/tests/ref/fate/mov-vfr-bframes-derived-duration b/tests/ref/fate/mov-vfr-bframes-derived-duration
new file mode 100644
index 0000000000..9f62978780
--- /dev/null
+++ b/tests/ref/fate/mov-vfr-bframes-derived-duration
@@ -0,0 +1,3 @@
+packet|pts=0|dts=-32768|duration=32768
+packet|pts=98304|dts=0|duration=32768
+packet|pts=32768|dts=32768|duration=65536
diff --git a/tests/ref/fate/mov-vfr-bframes-duration b/tests/ref/fate/mov-vfr-bframes-duration
new file mode 100644
index 0000000000..18f54b7e63
--- /dev/null
+++ b/tests/ref/fate/mov-vfr-bframes-duration
@@ -0,0 +1,12 @@
+packet|pts=0|dts=-1710000|duration=630000
+packet|pts=3510000|dts=-1080000|duration=90000
+packet|pts=1710000|dts=0|duration=90000
+packet|pts=630000|dts=630000|duration=1080000
+packet|pts=1800000|dts=1710000|duration=630000
+packet|pts=2430000|dts=1800000|duration=1080000
+packet|pts=5400000|dts=2430000|duration=630000
+packet|pts=4230000|dts=3510000|duration=1080000
+packet|pts=3600000|dts=3600000|duration=630000
+packet|pts=5310000|dts=4230000|duration=90000
+packet|pts=6030000|dts=5310000|duration=1080000
+packet|pts=7110000|dts=5400000|duration=90000
-- 
2.52.0

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