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

# avformat/hlsenc: provide next packet timing before fMP4 fragment flush

This pull request addresses [FFmpeg issue 
#24207](https://code.ffmpeg.org/FFmpeg/FFmpeg/issues/24207).

## What the patch changes

The HLS muxer currently flushes the child fragmented-MOV muxer before the
boundary packet is forwarded to it.  When the preceding Matroska packet has a
two-frame duration, MOV's accumulated `track_duration` can extend past the DTS
of the next HLS boundary packet.  The subsequent `check_pkt()` path then
rewrites that packet's DTS and clears its PTS.

This patch:

* adds the internal helper `ff_mov_set_fragment_end_hint()`;
* passes the next reference packet's DTS/PTS to the child MOV muxer immediately
  before the fMP4 fragment flush;
* converts the hint into the child MOV time base with the muxer offset and
  `dts_shift` applied;
* shrinks the accumulated fragment duration only when the next DTS is
  monotonic and overlaps the current duration;
* leaves the boundary packet itself unchanged;
* does not modify Matroska demuxing or generic `check_pkt()` behavior.

The change is narrowly scoped and is a no-op when the accumulated fragment 
duration does not overlap the next packet DTS. No regressions were observed in 
the completed controls and tests.

## Validation

All measurements below use the clean upstream master at
`564f92cce23ae95399476617b8a1dc357f002a47` and the three-file patch in this
branch.

### Minimal MKV reproducer

The unmodified build reports `1` `Packet duration` warning and `1` `pts has
no value` warning.  The patched build reports `0/0` and keeps two HLS media
segments with the same `7.175000` second playlist duration.

At the first affected boundary, the unmodified output changes the incoming
packet from PTS/DTS `112112/109440` to `110097/110097`.  With the patch, the
previous sample's effective duration is `672` ticks and the boundary packet
remains PTS/DTS `112112/109440`.

### Full MKV

The unmodified build reports `336/336` warnings.  The patched build reports
`0/0`, keeps `518` media segments, and keeps the `3108.396000` second playlist
duration.  All former affected boundaries are covered by the same
shrink-only accounting.

### Controls and payloads

The minimal MP4 no-overlap control reports `0/0` before and after the patch;
its playlist, video packet table, audio packet table, and both media segments
are byte-identical.  MPEG-TS and static MP4 controls are byte-identical in
the tested minimal and full cases.  The full MP4 artifact is not a no-overlap
control in this test set: clean master reports `81/81` warnings, so its
patched fMP4 output is expected to change.

The independently extracted Annex-B HEVC payload of the minimal MKV and MP4
controls matches:

`d4d92728d6455781e213252330c93c2cfa27e539a89ea90186b4fccc5027f502`

The baseline and patched HLS video payloads also match, and the tested audio
packet tables are identical.

### Tests

Targeted HLS fMP4, MOV fragment-flush, VFR/B-frame duration, and hybrid
fragmented-MOV FATE tests passed on both clean and patched builds.  The full
`make fate` run reached the same pre-existing failure,
`mov-reenc-delete-format-metadata`, on both builds; no additional failure was
observed.  Two optional full-input `hls_time=10` matrix runs were not
completed because the WSL input path returned I/O errors after many segments.

The complete validation report and reproducible artifacts are available in
the accompanying research package.


>From 51ccfb7f0ee1f9321ebd0eaa0d3af08be7e3f147 Mon Sep 17 00:00:00 2001
From: Kostazol <[email protected]>
Date: Wed, 19 Aug 2026 23:00:57 +0700
Subject: [PATCH] avformat/hlsenc: provide next packet timing before fMP4
 fragment flush

Pass the next HLS boundary packet timing to the child MOV muxer before flushing 
an fMP4 fragment. The MOV helper only shrinks an overlapping accumulated 
fragment duration; normal boundaries remain unchanged and the boundary packet 
itself is not modified.
---
 libavformat/hlsenc.c |  8 ++++++++
 libavformat/movenc.c | 42 ++++++++++++++++++++++++++++++++++++++++++
 libavformat/movenc.h |  3 +++
 3 files changed, 53 insertions(+)

diff --git a/libavformat/hlsenc.c b/libavformat/hlsenc.c
index 50f19f9694..4726a4a708 100644
--- a/libavformat/hlsenc.c
+++ b/libavformat/hlsenc.c
@@ -53,6 +53,9 @@
 #include "hlsplaylist.h"
 #include "internal.h"
 #include "mux.h"
+#if CONFIG_MP4_MUXER
+#include "movenc.h"
+#endif
 #include "os_support.h"
 #include "url.h"
 
@@ -2520,6 +2523,11 @@ static int hls_write_packet(AVFormatContext *s, AVPacket 
*pkt)
         int byterange_mode = (hls->flags & HLS_SINGLE_FILE) || 
(hls->max_seg_size > 0);
         double cur_duration;
 
+#if CONFIG_MP4_MUXER
+        if (hls->segment_type == SEGMENT_TYPE_FMP4 && is_ref_pkt &&
+            pkt->dts != AV_NOPTS_VALUE)
+            ff_mov_set_fragment_end_hint(oc, stream_index, pkt, st->time_base);
+#endif
         av_write_frame(oc, NULL); /* Flush any buffered data */
         new_start_pos = avio_tell(oc->pb);
         vs->size = new_start_pos - vs->start_pos;
diff --git a/libavformat/movenc.c b/libavformat/movenc.c
index 2a3226c70f..367caecee9 100644
--- a/libavformat/movenc.c
+++ b/libavformat/movenc.c
@@ -6948,6 +6948,48 @@ static int check_pkt(AVFormatContext *s, MOVTrack *trk, 
AVPacket *pkt)
     return 0;
 }
 
+int ff_mov_set_fragment_end_hint(AVFormatContext *s, int stream_index,
+                                 const AVPacket *pkt, AVRational src_time_base)
+{
+    MOVMuxContext *mov = s->priv_data;
+    AVStream *st;
+    MOVTrack *track;
+    int64_t offset, dts, pts, candidate_duration;
+
+    if (!(mov->flags & FF_MOV_FLAG_FRAGMENT) ||
+        stream_index < 0 || stream_index >= s->nb_streams ||
+        pkt->dts == AV_NOPTS_VALUE)
+        return 0;
+
+    st = s->streams[stream_index];
+    track = st->priv_data;
+    if (!track->entry || track->start_dts == AV_NOPTS_VALUE)
+        return 0;
+
+    if (ff_get_muxer_ts_offset(s, stream_index, &offset) < 0)
+        return 0;
+
+    dts = av_rescale_q(pkt->dts, src_time_base, st->time_base) + offset;
+    pts = pkt->pts == AV_NOPTS_VALUE
+        ? AV_NOPTS_VALUE
+        : av_rescale_q(pkt->pts, src_time_base, st->time_base) + offset;
+    if (track->dts_shift != AV_NOPTS_VALUE)
+        dts += track->dts_shift;
+
+    candidate_duration = dts - track->start_dts;
+    if (dts <= track->cluster[track->entry - 1].dts ||
+        candidate_duration < 0 || candidate_duration >= track->track_duration)
+        return 0;
+
+    track->track_duration = candidate_duration;
+    track->end_pts = pts != AV_NOPTS_VALUE ? pts : dts;
+    if (!(pkt->flags & AV_PKT_FLAG_DISCARD))
+        track->elst_end_pts = track->end_pts;
+    track->end_reliable = 1;
+
+    return 1;
+}
+
 int ff_mov_write_packet(AVFormatContext *s, AVPacket *pkt)
 {
     MOVMuxContext *mov = s->priv_data;
diff --git a/libavformat/movenc.h b/libavformat/movenc.h
index 5d1e7099b4..c375659c93 100644
--- a/libavformat/movenc.h
+++ b/libavformat/movenc.h
@@ -303,6 +303,9 @@ typedef struct MOVMuxContext {
 
 int ff_mov_write_packet(AVFormatContext *s, AVPacket *pkt);
 
+int ff_mov_set_fragment_end_hint(AVFormatContext *s, int stream_index,
+                                 const AVPacket *pkt, AVRational 
src_time_base);
+
 int ff_mov_init_hinting(AVFormatContext *s, int index, int src_index);
 int ff_mov_add_hinted_packet(AVFormatContext *s, AVPacket *pkt,
                              int track_index, int sample,
-- 
2.52.0

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

Reply via email to