PR #25021 opened by superuser404
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25021
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25021.patch

`mov_flush_fragment()` fills in the end of each track from the first packet 
still queued for it in the interleaver (`ff_interleaved_peek()`). `check_pkt()` 
may already have moved the written samples past that packet's dts: the first 
sample of a fragment is placed at the end the previous fragment declared 
(`start_dts + track_duration`), and every later sample that still sits below it 
is clamped to the one before plus one ("Packet duration ... is out of range"). 
When a sample's duration reaches beyond its successor, the written timeline 
therefore runs ahead of the real one, the queued packet lies before the last 
written sample, and `get_cluster_duration()` computes a negative duration for 
it and aborts the process on `av_assert0(next_dts >= 0)`.

A queued packet behind the written samples says nothing about where the track 
ends, so this skips it, and the end stays what the last sample's own duration 
gave it (the existing estimation below then applies as before).

### How it shows up

A fragmented-MP4 producer using `frag_custom` that flushes with 
`av_write_frame(NULL)` without draining the interleaver first. Reported from an 
iOS IPTV app as a crash on its segment producer thread:

```
Assertion next_dts >= 0 failed at libavformat/movenc.c:1247
abort
```

(`n8.1.3`; `movenc.c:1326` on master.)

### Reproduction

The new `api-movenc` case `frag-end-behind-queued`: video plus audio, 
`+frag_custom+empty_moov+default_base_moof`, all through 
`av_interleaved_write_frame()`.

1. Video at 0, 1, 2 frames, the last one with a duration of ten frames; audio 
at 0 and 2.5 frames. Drain and flush.
2. Video at 3 to 6 frames (each clamped past the declared end), audio at 6.5 
frames (which releases them), then video at 7 frames, which stays queued. 
`av_write_frame(NULL)`.

On master the test binary aborts at the second flush with the assertion above. 
With the patch it completes, and the earlier cases in 
`tests/ref/fate/api-movenc` are byte-identical; the reference only gains the 
new case.

### Tests

`fate-api-movenc` (reference extended), plus the 58 FATE targets matching mov, 
mp4, ismv, dash, hls or frag (`fate-lavf-mov`, `fate-lavf-mp4`, 
`fate-lavf-ismv`, `fate-lavf-mov_hybrid_frag`, `fate-mov-mp4-frag-flush`, 
`fate-mov-trun-large-sample-duration`, `fate-mov-vfr`, 
`fate-hls-fmp4-big-init`, `fate-dash-mpd-timing`, ...), all passing on aarch64 
macOS.



>From 7292b7cbb822e762c55dddceadb95123d69eaa60 Mon Sep 17 00:00:00 2001
From: Vincent Herbst <[email protected]>
Date: Mon, 12 Oct 2026 11:39:47 +0200
Subject: [PATCH] avformat/movenc: don't end a fragment's track before its
 written samples

When a fragment is flushed while packets are still queued in the
interleaver, mov_flush_fragment() takes the first queued packet of each
track as that track's end. check_pkt() may already have moved the written
samples past that packet: the first sample of a fragment is placed at the
end the previous fragment declared, and every following sample that is
still below it is clamped to the one before plus one. With a sample whose
duration reaches beyond its successor, the track end then lies before the
last written sample, and get_cluster_duration() aborts the process on
av_assert0(next_dts >= 0).

Such a queued packet says nothing about where the track ends, so skip it
and keep the end the last sample's own duration gave it.

A fragmented-MP4 producer that flushes with av_write_frame(NULL) without
draining the interleaver first hits this in the field. The new
api-movenc case reproduces it: two fragments, the first ending on a video
sample that claims ten frames, then a flush with one more video packet
queued. Without the change the test aborts; the existing cases are
unchanged.

Signed-off-by: Vincent Herbst <[email protected]>
---
 libavformat/movenc.c        |  4 ++++
 tests/api/api-movenc-test.c | 40 +++++++++++++++++++++++++++++++++++++
 tests/ref/fate/api-movenc   |  7 +++++++
 3 files changed, 51 insertions(+)

diff --git a/libavformat/movenc.c b/libavformat/movenc.c
index 6492f4ffb3..4b214524d7 100644
--- a/libavformat/movenc.c
+++ b/libavformat/movenc.c
@@ -6794,6 +6794,10 @@ static int mov_flush_fragment(AVFormatContext *s, int 
force)
                 dts = pkt->dts + offset;
                 if (track->dts_shift != AV_NOPTS_VALUE)
                     dts += track->dts_shift;
+                /* check_pkt() may have moved the written samples past this
+                 * packet; it cannot end the track before them. */
+                if (track->entry && dts < track->cluster[track->entry - 1].dts)
+                    continue;
                 track->track_duration = dts - track->start_dts;
                 if (pts != AV_NOPTS_VALUE)
                     track->end_pts = pts;
diff --git a/tests/api/api-movenc-test.c b/tests/api/api-movenc-test.c
index 39494748fd..a4eab1f4c8 100644
--- a/tests/api/api-movenc-test.c
+++ b/tests/api/api-movenc-test.c
@@ -345,6 +345,23 @@ static void mux_id3(void)
     av_write_frame(ctx, pkt);
 }
 
+static void mux_raw(int stream_index, int64_t dts, int64_t pkt_duration)
+{
+    uint8_t pktdata[8] = { 0 };
+    av_packet_unref(pkt);
+
+    pkt->dts = pkt->pts = dts;
+    pkt->stream_index = stream_index;
+    pkt->duration = pkt_duration;
+    pkt->flags |= AV_PKT_FLAG_KEY;
+
+    AV_WB32(pktdata + 4, pkt->pts);
+    pkt->data = pktdata;
+    pkt->size = 8;
+
+    av_interleaved_write_frame(ctx, pkt);
+}
+
 static void mux_gops(int n)
 {
     mux_frames(gop_size * n, 0);
@@ -843,6 +860,29 @@ int main(int argc, char **argv)
     finish();
     close_out();
 
+    // Flush a fragment while a packet is still queued in the interleaver
+    // behind the samples already written for its track. The first fragment
+    // ends on a video sample claiming a duration of ten frames, so the next
+    // fragment's samples are moved past their real dts when written. The
+    // queued video packet must not end the track before them.
+    init_out("frag-end-behind-queued");
+    av_dict_set(&opts, "movflags", 
"+frag_custom+empty_moov+default_base_moof", 0);
+    init(0, 0);
+    mux_raw(1, 0, audio_duration);
+    mux_raw(0, 0, duration);
+    mux_raw(0, duration, duration);
+    mux_raw(0, 2 * duration, 10 * duration);
+    mux_raw(1, 3675, audio_duration);
+    av_interleaved_write_frame(ctx, NULL);
+    av_write_frame(ctx, NULL);
+    for (int i = 3; i < 7; i++)
+        mux_raw(0, i * duration, duration);
+    mux_raw(1, 9555, audio_duration);
+    mux_raw(0, 7 * duration, duration);
+    av_write_frame(ctx, NULL);
+    finish();
+    close_out();
+
     av_free(md5);
     av_packet_free(&pkt);
 
diff --git a/tests/ref/fate/api-movenc b/tests/ref/fate/api-movenc
index 20996b8910..61a3f5e8bc 100644
--- a/tests/ref/fate/api-movenc
+++ b/tests/ref/fate/api-movenc
@@ -163,3 +163,10 @@ write_data len 70, time 2000000, type boundary atom emsg
 write_data len 1840, time 2000000, type sync atom moof
 write_data len 148, time nopts, type trailer atom -
 43f9d447aa759ec494e20d9c8d149488 5111 emsg
+write_data len 28, time nopts, type header atom ftyp
+write_data len 1123, time nopts, type header atom -
+write_data len 244, time 0, type sync atom moof
+write_data len 240, time 400000, type sync atom moof
+write_data len 120, time 433529, type sync atom moof
+write_data len 167, time nopts, type trailer atom -
+92d293d2d51563c9b3d464f8bdd3ae9f 1922 frag-end-behind-queued
-- 
2.52.0

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

Reply via email to