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]
