[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]