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