PR #25706 opened by Baptiste Coudurier (bcoudurier)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25706
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25706.patch

With `-hls_segment_options movflags=+cmaf+frag_keyframe`, the MP4 muxer can 
write media fragments before the first HLS segment boundary. hlsenc treats the 
entire buffered output as initialization data, producing an oversized 
`EXT-X-MAP` range and a zero-byte first segment.

Record the end of the initial `moov` in the MOV muxer and use that boundary to 
separate initialization data from media. Retain early fragments for the first 
HLS segment, and share this handling between normal segmentation and trailer 
initialization for short outputs.

Add two FATE tests covering internal keyframes with both multiple HLS segments 
and an output shorter than the first segment interval.

Validation:

- Both new FATE tests pass and reproduce the bug on the original code.
- Existing single-file fMP4, large-init, and MOV muxer API tests pass.
- Sixteen audio/video cases cover single-file and separate-file output, normal 
and short clips, aligned and internal keyframes, and init-file resends. The 
simplification preserves playlists and media byte-for-byte, with all video 
packets retained.
- hlsenc compiles with MP4 registration disabled; HLS already selects the 
shared MOV muxer implementation.



>From 98dd9a2110f7316015c72cbacd4622cc84a062b9 Mon Sep 17 00:00:00 2001
From: Baptiste Coudurier <[email protected]>
Date: Mon, 2 Nov 2026 23:43:32 -0800
Subject: [PATCH] avformat/hlsenc: keep early fragments out of fMP4 init data

Automatic MP4 fragment cuts can write media before the first HLS segment 
boundary. Record the end of the initial moov in the MOV muxer and use that 
boundary to separate initialization data from media, retaining early fragments 
for the first HLS segment.

Share initialization handling between normal segmentation and the trailer, and 
add FATE coverage for both paths.
---
 libavformat/hlsenc.c                        | 65 ++++++++++++++-------
 libavformat/movenc.c                        |  2 +
 libavformat/movenc.h                        |  1 +
 tests/fate/hlsenc.mak                       | 23 ++++++++
 tests/ref/fate/hls-fmp4-frag-keyframe       |  3 +
 tests/ref/fate/hls-fmp4-frag-keyframe-short |  2 +
 6 files changed, 75 insertions(+), 21 deletions(-)
 create mode 100644 tests/ref/fate/hls-fmp4-frag-keyframe
 create mode 100644 tests/ref/fate/hls-fmp4-frag-keyframe-short

diff --git a/libavformat/hlsenc.c b/libavformat/hlsenc.c
index e35af34d230..1c227a0870e 100644
--- a/libavformat/hlsenc.c
+++ b/libavformat/hlsenc.c
@@ -53,9 +53,7 @@
 #include "hlsplaylist.h"
 #include "internal.h"
 #include "mux.h"
-#if CONFIG_MP4_MUXER
 #include "movenc.h"
-#endif
 #include "os_support.h"
 #include "url.h"
 
@@ -505,6 +503,44 @@ static void reflush_dynbuf(VariantStream *vs, int 
*range_length)
     avio_write(vs->out, vs->temp_buffer, *range_length);
 }
 
+static int hls_write_init_file(AVFormatContext *s, VariantStream *vs)
+{
+    HLSContext *hls = s->priv_data;
+    AVFormatContext *oc = vs->avf;
+    const MOVMuxContext *mov = oc->priv_data;
+    uint8_t *buffer;
+    int buffer_size, ret;
+    int64_t init_size = mov->moov_end;
+
+    buffer_size = avio_get_dyn_buf(oc->pb, &buffer);
+    if (init_size <= 0 || init_size > buffer_size)
+        return AVERROR_INVALIDDATA;
+
+    /* Automatic fragment cuts may have already written media after moov.
+     * Keep that media in the dynamic buffer for the first HLS segment. */
+    buffer_size = avio_close_dyn_buf(oc->pb, &buffer);
+    oc->pb = NULL;
+    ret = avio_open_dyn_buf(&oc->pb);
+    if (ret < 0) {
+        av_free(buffer);
+        return ret;
+    }
+    avio_write(oc->pb, buffer + init_size, buffer_size - init_size);
+    avio_write(vs->out, buffer, init_size);
+    if (hls->resend_init_file) {
+        vs->init_buffer = av_memdup(buffer, init_size);
+        if (!vs->init_buffer) {
+            av_free(buffer);
+            return AVERROR(ENOMEM);
+        }
+    }
+    av_free(buffer);
+    vs->init_range_length = init_size;
+    vs->start_pos = init_size;
+    vs->packets_written = 0;
+    return 0;
+}
+
 static int hls_delete_file(HLSContext *hls, AVFormatContext *avf,
                            char *path, const char *proto)
 {
@@ -2542,17 +2578,9 @@ static int hls_write_packet(AVFormatContext *s, AVPacket 
*pkt)
         avio_flush(oc->pb);
         if (hls->segment_type == SEGMENT_TYPE_FMP4) {
             if (!vs->init_range_length) {
-                range_length = avio_close_dyn_buf(oc->pb, &vs->init_buffer);
-                oc->pb = NULL;
-                if (range_length <= 0)
-                    return AVERROR(EINVAL);
-                avio_write(vs->out, vs->init_buffer, range_length);
-                if (!hls->resend_init_file)
-                    av_freep(&vs->init_buffer);
-                vs->init_range_length = range_length;
-                avio_open_dyn_buf(&oc->pb);
-                vs->packets_written = 0;
-                vs->start_pos = range_length;
+                ret = hls_write_init_file(s, vs);
+                if (ret < 0)
+                    return ret;
                 if (!byterange_mode) {
                     hlsenc_io_close(s, &vs->out, vs->base_output_dirname);
                 }
@@ -2795,16 +2823,11 @@ static int hls_write_trailer(struct AVFormatContext *s)
 
         if (hls->segment_type == SEGMENT_TYPE_FMP4) {
             if (!vs->init_range_length) {
-                uint8_t *buffer = NULL;
                 av_write_frame(oc, NULL); /* Flush any buffered data */
 
-                int init_range_length = avio_close_dyn_buf(oc->pb, &buffer);
-                avio_write(vs->out, buffer, init_range_length);
-                av_freep(&buffer);
-                vs->init_range_length = init_range_length;
-                avio_open_dyn_buf(&oc->pb);
-                vs->packets_written = 0;
-                vs->start_pos = init_range_length;
+                ret = hls_write_init_file(s, vs);
+                if (ret < 0)
+                    goto failed;
                 byterange_mode = (hls->flags & HLS_SINGLE_FILE) || 
(hls->max_seg_size > 0);
                 if (!byterange_mode) {
                     ff_format_io_close(s, &vs->out);
diff --git a/libavformat/movenc.c b/libavformat/movenc.c
index 2c349ef663b..7624c5cd458 100644
--- a/libavformat/movenc.c
+++ b/libavformat/movenc.c
@@ -6854,6 +6854,7 @@ static int mov_flush_fragment(AVFormatContext *s, int 
force)
             mov_write_identification(s->pb, s);
         if ((ret = mov_write_moov_tag(s->pb, mov, s)) < 0)
             return ret;
+        mov->moov_end = avio_tell(s->pb);
 
         if (mov->flags & FF_MOV_FLAG_DELAY_MOOV) {
             if (mov->flags & FF_MOV_FLAG_GLOBAL_SIDX)
@@ -9055,6 +9056,7 @@ static int mov_write_header(AVFormatContext *s)
         if ((ret = mov_write_moov_tag(pb, mov, s)) < 0)
             return ret;
         mov->moov_written = 1;
+        mov->moov_end = avio_tell(pb);
         if (mov->flags & FF_MOV_FLAG_GLOBAL_SIDX)
             mov->reserved_header_pos = avio_tell(pb);
     }
diff --git a/libavformat/movenc.h b/libavformat/movenc.h
index c375659c93e..004b37fa57a 100644
--- a/libavformat/movenc.h
+++ b/libavformat/movenc.h
@@ -228,6 +228,7 @@ typedef struct MOVMuxContext {
     int iods_audio_profile;
 
     int moov_written;
+    int64_t moov_end; ///< End of the initial moov atom.
     int fragments;
     int max_fragment_duration;
     int min_fragment_duration;
diff --git a/tests/fate/hlsenc.mak b/tests/fate/hlsenc.mak
index 26b029caf0b..1be95eb518f 100644
--- a/tests/fate/hlsenc.mak
+++ b/tests/fate/hlsenc.mak
@@ -265,6 +265,29 @@ fate-hls-fmp4-big-init: tests/data/hls_fmp4_big_init.m3u8
 fate-hls-fmp4-big-init: CLEANFILES = tests/data/hls_fmp4_big_init.m3u8 
tests/data/hls_fmp4_big_init.mp4 tests/data/hls_fmp4_big_init_*.m4s
 fate-hls-fmp4-big-init: CMD = framecrc -i 
$(TARGET_PATH)/tests/data/hls_fmp4_big_init.m3u8 -map 0:a:99 -c copy
 
+# Keyframes inside an HLS segment must not become part of EXT-X-MAP.
+# A segment longer than the input exercises initialization in the trailer.
+tests/data/hls_fmp4_frag_keyframe.m3u8 
tests/data/hls_fmp4_frag_keyframe_short.m3u8: ffmpeg$(PROGSSUF)$(EXESUF) | 
tests/data
+       $(M)$(TARGET_EXEC) $(TARGET_PATH)/$< -nostdin \
+       -f lavfi -i "testsrc2=size=128x72:rate=2:d=3" -map 0:v \
+       -c:v mpeg2video -g 1 -bitexact -dct int -f hls \
+       -hls_time $(if $(findstring short,$@),10,2) -hls_list_size 0 \
+       -hls_segment_type fmp4 -hls_flags single_file \
+       -hls_segment_options movflags=+cmaf+frag_keyframe \
+       -hls_segment_filename $(TARGET_PATH)/$(@:.m3u8=.mp4) \
+       $(TARGET_PATH)/$@ 2>/dev/null
+
+FATE_HLSENC_LAVFI-$(call ALLYES, TESTSRC2_FILTER LAVFI_INDEV 
MPEG2VIDEO_ENCODER HLS_MUXER MP4_MUXER FILE_PROTOCOL) += 
fate-hls-fmp4-frag-keyframe fate-hls-fmp4-frag-keyframe-short
+fate-hls-fmp4-frag-keyframe: tests/data/hls_fmp4_frag_keyframe.m3u8
+fate-hls-fmp4-frag-keyframe: CLEANFILES = 
tests/data/hls_fmp4_frag_keyframe.m3u8 tests/data/hls_fmp4_frag_keyframe.mp4
+fate-hls-fmp4-frag-keyframe: CMD = sed -n -e /^\#EXT-X-MAP:/p -e 
/^\#EXT-X-BYTERANGE:/p $(TARGET_PATH)/tests/data/hls_fmp4_frag_keyframe.m3u8
+fate-hls-fmp4-frag-keyframe: CMP = diff
+
+fate-hls-fmp4-frag-keyframe-short: tests/data/hls_fmp4_frag_keyframe_short.m3u8
+fate-hls-fmp4-frag-keyframe-short: CLEANFILES = 
tests/data/hls_fmp4_frag_keyframe_short.m3u8 
tests/data/hls_fmp4_frag_keyframe_short.mp4
+fate-hls-fmp4-frag-keyframe-short: CMD = sed -n -e /^\#EXT-X-MAP:/p -e 
/^\#EXT-X-BYTERANGE:/p 
$(TARGET_PATH)/tests/data/hls_fmp4_frag_keyframe_short.m3u8
+fate-hls-fmp4-frag-keyframe-short: CMP = diff
+
 FATE_HLSENC_LAVFI-yes := $(if $(call FRAMECRC), $(FATE_HLSENC_LAVFI-yes))
 
 FATE_FFMPEG += $(FATE_HLSENC_LAVFI-yes)
diff --git a/tests/ref/fate/hls-fmp4-frag-keyframe 
b/tests/ref/fate/hls-fmp4-frag-keyframe
new file mode 100644
index 00000000000..de0719118c2
--- /dev/null
+++ b/tests/ref/fate/hls-fmp4-frag-keyframe
@@ -0,0 +1,3 @@
+#EXT-X-MAP:URI="hls_fmp4_frag_keyframe.mp4",BYTERANGE="814@0"
+#EXT-X-BYTERANGE:20959@814
+#EXT-X-BYTERANGE:11213@21773
diff --git a/tests/ref/fate/hls-fmp4-frag-keyframe-short 
b/tests/ref/fate/hls-fmp4-frag-keyframe-short
new file mode 100644
index 00000000000..db63e27456d
--- /dev/null
+++ b/tests/ref/fate/hls-fmp4-frag-keyframe-short
@@ -0,0 +1,2 @@
+#EXT-X-MAP:URI="hls_fmp4_frag_keyframe_short.mp4",BYTERANGE="814@0"
+#EXT-X-BYTERANGE:32172@814
-- 
2.54.0

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

Reply via email to