[PR] avformat/mov: derive packet durations from presentation order (PR #23919)

James Almer via ffmpeg-devel <[email protected]> Sun, 26 Jul 2026 13:31:51 -0000
Newsgroups gmane.comp.video.ffmpeg.devel
Message-ID <178507271215.59.8135142091150792139@29965ddac10e>
PR #23919 opened by James Almer (jamrial)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23919
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23919.patch

Some MOV files combine variable frame rate and B-frames. Their samples
are stored in decode order, but a packet duration describes the interval
until the next frame in presentation order. Using adjacent decode-order
timestamps can therefore assign a duration from an unrelated frame and
cause players to hold the wrong frame after a seek.

For fully indexed video tracks with composition time offsets, build a
table from index timestamps plus CTTS offsets, sort it by presentation
timestamp, and use adjacent presentation timestamps when finalizing
packet durations.

Keep the existing STTS-derived duration for incomplete indexes, invalid
or duplicate timestamps, the final sample, and fragmented inputs whose
index changes after parsing. This preserves existing behavior whenever
a complete and unambiguous presentation-order mapping is unavailable.

(Patch originally sent to the ML by its author)


>From c83821b58a74f5c89cd877696e52da71e5855c19 Mon Sep 17 00:00:00 2001
From: panboxiaosa <[email protected]>
Date: Sun, 26 Jul 2026 12:34:20 +0800
Subject: [PATCH] avformat/mov: derive packet durations from presentation order

Some MOV files combine variable frame rate and B-frames. Their samples
are stored in decode order, but a packet duration describes the interval
until the next frame in presentation order. Using adjacent decode-order
timestamps can therefore assign a duration from an unrelated frame and
cause players to hold the wrong frame after a seek.

For fully indexed video tracks with composition time offsets, build a
table from index timestamps plus CTTS offsets, sort it by presentation
timestamp, and use adjacent presentation timestamps when finalizing
packet durations.

Keep the existing STTS-derived duration for incomplete indexes, invalid
or duplicate timestamps, the final sample, and fragmented inputs whose
index changes after parsing. This preserves existing behavior whenever
a complete and unambiguous presentation-order mapping is unavailable.

Add a self-contained VFR B-frame FATE regression test.

Signed-off-by: panboxiaosa <[email protected]>
Signed-off-by: James Almer <[email protected]>
---
 libavformat/isom.h                      |  2 +
 libavformat/mov.c                       | 88 +++++++++++++++++++++++++
 tests/fate/mov.mak                      |  6 ++
 tests/ref/fate/mov-vfr-bframes-duration |  8 +++
 4 files changed, 104 insertions(+)
 create mode 100644 tests/ref/fate/mov-vfr-bframes-duration

diff --git a/libavformat/isom.h b/libavformat/isom.h
index e02af09aaa..f128bec991 100644
--- a/libavformat/isom.h
+++ b/libavformat/isom.h
@@ -206,6 +206,8 @@ typedef struct MOVStreamContext {
     unsigned int elst_count;
     int tts_index;
     int tts_sample;
+    int64_t *presentation_durations;
+    unsigned int presentation_durations_count;
     unsigned int sample_size; ///< may contain value calculated from stsd or value from stsz atom
     unsigned int stsz_sample_size; ///< always contains sample size from stsz atom
     unsigned int sample_count;
diff --git a/libavformat/mov.c b/libavformat/mov.c
index bd5f456032..aaf33cddd1 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,81 @@ 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);
+}
+
+static void mov_build_presentation_durations(AVStream *st)
+{
+    MOVStreamContext *sc = st->priv_data;
+    FFStream *const sti = ffstream(st);
+    MOVPresentationSample *samples;
+    unsigned int tts_index = 0, tts_sample = 0;
+    int count = 0;
+
+    av_freep(&sc->presentation_durations);
+    sc->presentation_durations_count = 0;
+    if (st->codecpar->codec_type != AVMEDIA_TYPE_VIDEO || !sc->ctts_count ||
+        !sc->tts_data || sti->nb_index_entries < 2)
+        return;
+
+    samples = av_malloc_array(sti->nb_index_entries, sizeof(*samples));
+    if (!samples)
+        goto fail;
+
+    while (count < sti->nb_index_entries && tts_index < sc->tts_count) {
+        int64_t pts_offset = (int64_t)sc->dts_shift + sc->tts_data[tts_index].offset;
+        int64_t dts = sti->index_entries[count].timestamp;
+
+        if (dts == AV_NOPTS_VALUE || !sc->tts_data[tts_index].count ||
+            (pts_offset > 0 && dts > INT64_MAX - pts_offset) ||
+            (pts_offset < 0 && dts < INT64_MIN - pts_offset))
+            goto fail;
+        samples[count].index = count;
+        samples[count].pts = dts + pts_offset;
+        count++;
+        if (++tts_sample == sc->tts_data[tts_index].count) {
+            tts_sample = 0;
+            tts_index++;
+        }
+    }
+    if (count != sti->nb_index_entries || tts_index != sc->tts_count)
+        goto fail;
+
+    qsort(samples, count, sizeof(*samples), mov_compare_presentation_samples);
+    sc->presentation_durations = av_calloc(sti->nb_index_entries,
+                                           sizeof(*sc->presentation_durations));
+    if (!sc->presentation_durations)
+        goto fail;
+    for (int i = 0; i + 1 < count; i++) {
+        if ((!i || samples[i - 1].pts != samples[i].pts) &&
+            samples[i].pts < samples[i + 1].pts &&
+            (samples[i].pts >= 0 ||
+             samples[i + 1].pts <= INT64_MAX + samples[i].pts)) {
+            sc->presentation_durations[samples[i].index] =
+                samples[i + 1].pts - samples[i].pts;
+        }
+    }
+    sc->presentation_durations_count = sti->nb_index_entries;
+    av_free(samples);
+    return;
+
+fail:
+    av_free(samples);
+    av_freep(&sc->presentation_durations);
+}
+
 static int test_same_origin(const char *src, const char *ref) {
     char src_proto[64];
     char ref_proto[64];
@@ -5497,6 +5573,7 @@ static int mov_read_trak(MOVContext *c, AVIOContext *pb, MOVAtom atom)
     }
 
     mov_build_index(c, st);
+    mov_build_presentation_durations(st);
 
 #if CONFIG_IAMFDEC
     if (sc->iamf) {
@@ -6138,6 +6215,10 @@ static int mov_read_trun(MOVContext *c, AVIOContext *pb, MOVAtom atom)
 
     if ((uint64_t)entries+sc->tts_count >= UINT_MAX/sizeof(*sc->tts_data))
         return AVERROR_INVALIDDATA;
+    if (entries) {
+        av_freep(&sc->presentation_durations);
+        sc->presentation_durations_count = 0;
+    }
     if (flags & MOV_TRUN_DATA_OFFSET)        data_offset        = avio_rb32(pb);
     if (flags & MOV_TRUN_FIRST_SAMPLE_FLAGS) first_sample_flags = avio_rb32(pb);
 
@@ -10420,6 +10501,7 @@ static void mov_free_stream_context(AVFormatContext *s, AVStream *st)
     av_freep(&sc->sample_sizes);
     av_freep(&sc->keyframes);
     av_freep(&sc->ctts_data);
+    av_freep(&sc->presentation_durations);
     av_freep(&sc->stts_data);
     av_freep(&sc->sdtp_data);
     av_freep(&sc->stps_data);
@@ -11846,6 +11928,10 @@ static int mov_finalize_packet(AVFormatContext *s, AVStream *st, AVIndexEntry *s
         }
         pkt->pts = pkt->dts;
     }
+    if (sc->current_sample > 0 &&
+        (unsigned)sc->current_sample <= sc->presentation_durations_count &&
+        sc->presentation_durations[sc->current_sample - 1] > 0)
+        pkt->duration = sc->presentation_durations[sc->current_sample - 1];
 
     if (st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && !mov->fragment.found_tfhd &&
         sc->current_sample >= ffstream(st)->nb_index_entries) {
@@ -11940,6 +12026,8 @@ static int mov_read_packet(AVFormatContext *s, AVPacket *pkt)
             mov_current_sample_set(msc, 0);
             msc->tts_index = 0;
 
+            av_freep(&msc->presentation_durations);
+            msc->presentation_durations_count = 0;
             // Discard current index entries
             avsti = ffstream(avst);
             if (avsti->index_entries_allocated_size > 0) {
diff --git a/tests/fate/mov.mak b/tests/fate/mov.mak
index 1629796f77..16b800b27b 100644
--- a/tests/fate/mov.mak
+++ b/tests/fate/mov.mak
@@ -302,6 +302,12 @@ 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 TRANSCODE, MPEG4, MP4 MOV, LAVFI_INDEV TESTSRC2_FILTER SETPTS_FILTER) += \
+                            fate-mov-vfr-bframes-duration
+fate-mov-vfr-bframes-duration: CMD = run_with_temp \
+    "$(FFMPEG) -nostdin -hide_banner -loglevel error -f lavfi -i testsrc2=size=16x16:rate=10 -vf setpts=N*N -frames:v 8 -fps_mode vfr -c:v mpeg4 -bf 2 -g 12 -q:v 2 -flags +bitexact -fflags +bitexact -threads 1 -f mp4 -y" \
+    "ffprobe$(PROGSSUF)$(EXESUF) -v error -select_streams v -show_entries packet=pts,dts,duration -of compact=p=0:nk=1" mp4
+
 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-duration b/tests/ref/fate/mov-vfr-bframes-duration
new file mode 100644
index 0000000000..81faebe757
--- /dev/null
+++ b/tests/ref/fate/mov-vfr-bframes-duration
@@ -0,0 +1,8 @@
+0|-1024|1024
+9216|0|7168
+1024|1024|3072
+4096|4096|5120
+36864|9216|13312
+16384|16384|9216
+25600|25600|11264
+50176|36864|1024
-- 
2.52.0

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