PR #23262 opened by Artem Smorodin (artem.smorodin) URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23262 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23262.patch
According to [RFC 8216](https://datatracker.ietf.org/doc/html/rfc8216): > If the server wishes to remove segments from a Media Playlist containing an EXT-X-DISCONTINUITY tag, the Media Playlist MUST contain an EXT-X-DISCONTINUITY-SEQUENCE tag. Without the EXT-X- DISCONTINUITY-SEQUENCE tag, it can be impossible for a client to locate corresponding segments between Renditions. > > If the server removes an EXT-X-DISCONTINUITY tag from the Media Playlist, it MUST increment the value of the EXT-X-DISCONTINUITY- SEQUENCE tag so that the Discontinuity Sequence Numbers of the segments still in the Media Playlist remain unchanged. The value of the EXT-X-DISCONTINUITY-SEQUENCE tag MUST NOT decrease or wrap. Clients can malfunction if each Media Segment does not have a consistent Discontinuity Sequence Number. This means that FFmpeg currently generates an m3u8 playlist that does not comply with the standard. This PR is intended to fix this by adding support for EXT-X-DISCONTINUITY-SEQUENCE. To avoid breaking the current behavior, this tag is added only when the `discont_sequence` flag is enabled. >From 470f1accc04df3fdb71ea7182113594216d279db Mon Sep 17 00:00:00 2001 From: Artem Smorodin <[email protected]> Date: Thu, 28 May 2026 17:20:52 +0300 Subject: [PATCH] avformat/hlsenc: add EXT-X-DISCONTINUITY-SEQUENCE support --- doc/muxers.texi | 3 +++ libavformat/hlsenc.c | 57 +++++++++++++++++++++++++++++++++++++++ libavformat/hlsplaylist.c | 11 ++++++++ libavformat/hlsplaylist.h | 2 ++ 4 files changed, 73 insertions(+) diff --git a/doc/muxers.texi b/doc/muxers.texi index 26199ad836..f2ec9fa7c5 100644 --- a/doc/muxers.texi +++ b/doc/muxers.texi @@ -2225,6 +2225,9 @@ then this will allow @command{ffmpeg} to output a HLS version 2 m3u8. Add the @code{#EXT-X-DISCONTINUITY} tag to the playlist, before the first segment's information. +@item discont_sequence +Add the @code{#EXT-X-DISCONTINUITY-SEQUENCE} tag to the playlist. + @item omit_endlist Do not append the @code{EXT-X-ENDLIST} tag at the end of the playlist. diff --git a/libavformat/hlsenc.c b/libavformat/hlsenc.c index 6fd99e811b..6adb82cd0e 100644 --- a/libavformat/hlsenc.c +++ b/libavformat/hlsenc.c @@ -110,6 +110,7 @@ typedef enum HLSFlags { HLS_PERIODIC_REKEY = (1 << 12), HLS_INDEPENDENT_SEGMENTS = (1 << 13), HLS_I_FRAMES_ONLY = (1 << 14), + HLS_DISCONT_SEQUENCE = (1 << 15), } HLSFlags; typedef enum { @@ -149,6 +150,9 @@ typedef struct VariantStream { int nb_entries; int discontinuity_set; int discontinuity; + int64_t discontinuity_sequence; + int discontinuity_sequence_set; + int discont_start_in_playlist; int reference_stream_index; int64_t total_size; @@ -1038,6 +1042,24 @@ static int sls_flag_use_localtime_filename(AVFormatContext *oc, HLSContext *c, V return 0; } +static void hls_increment_discontinuity_sequence(VariantStream *vs) +{ + if (vs->discontinuity_sequence < INT64_MAX) + vs->discontinuity_sequence++; +} + +static void hls_update_discontinuity_sequence(VariantStream *vs, + const HLSSegment *segment) +{ + if (vs->discont_start_in_playlist) { + hls_increment_discontinuity_sequence(vs); + vs->discont_start_in_playlist = 0; + } + + if (segment && segment->discont) + hls_increment_discontinuity_sequence(vs); +} + /* Create a new segment and append it to the segment list */ static int hls_append_segment(struct AVFormatContext *s, HLSContext *hls, VariantStream *vs, double duration, int64_t pos, @@ -1123,6 +1145,7 @@ static int hls_append_segment(struct AVFormatContext *s, HLSContext *hls, en = vs->segments; if (!en->next->discont_program_date_time && !en->discont_program_date_time) vs->initial_prog_date_time += en->duration; + hls_update_discontinuity_sequence(vs, en); vs->segments = en->next; if (en && hls->flags & HLS_DELETE_SEGMENTS && !(hls->flags & HLS_SINGLE_FILE)) { @@ -1193,6 +1216,19 @@ static int parse_playlist(AVFormatContext *s, const char *url, VariantStream *vs av_log(hls, AV_LOG_DEBUG, "Found playlist sequence number: %"PRId64"\n", tmp_sequence); vs->sequence = tmp_sequence; } + } else if (av_strstart(line, "#EXT-X-DISCONTINUITY-SEQUENCE:", &ptr)) { + int64_t tmp_sequence = strtoll(ptr, NULL, 10); + if (tmp_sequence < 0) { + av_log(hls, AV_LOG_WARNING, + "Found playlist discontinuity sequence number was negative: %"PRId64", omitting\n", + tmp_sequence); + } else { + av_log(hls, AV_LOG_DEBUG, + "Found playlist discontinuity sequence number: %"PRId64"\n", + tmp_sequence); + vs->discontinuity_sequence = tmp_sequence; + vs->discontinuity_sequence_set = 1; + } } else if (av_strstart(line, "#EXT-X-DISCONTINUITY", &ptr)) { is_segment = 1; vs->discontinuity = 1; @@ -1545,6 +1581,8 @@ static int hls_window(AVFormatContext *s, int last, VariantStream *vs) double prog_date_time = vs->initial_prog_date_time; double *prog_date_time_p = (hls->flags & HLS_PROGRAM_DATE_TIME) ? &prog_date_time : NULL; int byterange_mode = (hls->flags & HLS_SINGLE_FILE) || (hls->max_seg_size > 0); + int write_discontinuity_sequence = (hls->flags & HLS_DISCONT_SEQUENCE) || vs->discontinuity_sequence_set; + int discont_start_written = 0; hls->version = 2; if (!(hls->flags & HLS_ROUND_DURATIONS)) { @@ -1588,9 +1626,16 @@ static int hls_window(AVFormatContext *s, int last, VariantStream *vs) ff_hls_write_playlist_header(byterange_mode ? hls->m3u8_out : vs->out, hls->version, hls->allowcache, target_duration, sequence, hls->pl_type, hls->flags & HLS_I_FRAMES_ONLY); + if (write_discontinuity_sequence) { + ff_hls_write_discontinuity_sequence(byterange_mode ? hls->m3u8_out : vs->out, vs->discontinuity_sequence); + } + if ((hls->flags & HLS_DISCONT_START) && sequence==hls->start_sequence && vs->discontinuity_set==0) { avio_printf(byterange_mode ? hls->m3u8_out : vs->out, "#EXT-X-DISCONTINUITY\n"); vs->discontinuity_set = 1; + vs->discont_start_in_playlist = 1; + + discont_start_written = 1; } if (vs->has_video && (hls->flags & HLS_INDEPENDENT_SEGMENTS)) { avio_printf(byterange_mode ? hls->m3u8_out : vs->out, "#EXT-X-INDEPENDENT-SEGMENTS\n"); @@ -1637,6 +1682,17 @@ static int hls_window(AVFormatContext *s, int last, VariantStream *vs) } ff_hls_write_playlist_header(hls->sub_m3u8_out, hls->version, hls->allowcache, target_duration, sequence, PLAYLIST_TYPE_NONE, 0); + + if (write_discontinuity_sequence) { + ff_hls_write_discontinuity_sequence(hls->sub_m3u8_out, + vs->discontinuity_sequence); + } + + // To sync discontinuity sequence between main and sub playlists + if (write_discontinuity_sequence && discont_start_written) { + avio_printf(hls->sub_m3u8_out, "#EXT-X-DISCONTINUITY\n"); + } + for (en = vs->segments; en; en = en->next) { ret = ff_hls_write_file_entry(hls->sub_m3u8_out, en->discont, byterange_mode, en->duration, 0, en->size, en->pos, @@ -3148,6 +3204,7 @@ static const AVOption options[] = { {"delete_segments", "delete segment files that are no longer part of the playlist", 0, AV_OPT_TYPE_CONST, {.i64 = HLS_DELETE_SEGMENTS }, 0, UINT_MAX, E, .unit = "flags"}, {"round_durations", "round durations in m3u8 to whole numbers", 0, AV_OPT_TYPE_CONST, {.i64 = HLS_ROUND_DURATIONS }, 0, UINT_MAX, E, .unit = "flags"}, {"discont_start", "start the playlist with a discontinuity tag", 0, AV_OPT_TYPE_CONST, {.i64 = HLS_DISCONT_START }, 0, UINT_MAX, E, .unit = "flags"}, + {"discont_sequence", "add EXT-X-DISCONTINUITY-SEQUENCE tag to playlists", 0, AV_OPT_TYPE_CONST, {.i64 = HLS_DISCONT_SEQUENCE }, 0, UINT_MAX, E, .unit = "flags"}, {"omit_endlist", "Do not append an endlist when ending stream", 0, AV_OPT_TYPE_CONST, {.i64 = HLS_OMIT_ENDLIST }, 0, UINT_MAX, E, .unit = "flags"}, {"split_by_time", "split the hls segment by time which user set by hls_time", 0, AV_OPT_TYPE_CONST, {.i64 = HLS_SPLIT_BY_TIME }, 0, UINT_MAX, E, .unit = "flags"}, {"append_list", "append the new segments into old hls segment list", 0, AV_OPT_TYPE_CONST, {.i64 = HLS_APPEND_LIST }, 0, UINT_MAX, E, .unit = "flags"}, diff --git a/libavformat/hlsplaylist.c b/libavformat/hlsplaylist.c index 5c6a384d84..55add2bbae 100644 --- a/libavformat/hlsplaylist.c +++ b/libavformat/hlsplaylist.c @@ -131,6 +131,17 @@ void ff_hls_write_playlist_header(AVIOContext *out, int version, int allowcache, } } +void ff_hls_write_discontinuity_sequence(AVIOContext *out, + int64_t discontinuity_sequence) +{ + if (!out) + return; + avio_printf(out, "#EXT-X-DISCONTINUITY-SEQUENCE:%"PRId64"\n", + discontinuity_sequence); + av_log(NULL, AV_LOG_VERBOSE, "EXT-X-DISCONTINUITY-SEQUENCE:%"PRId64"\n", + discontinuity_sequence); +} + void ff_hls_write_init_file(AVIOContext *out, const char *filename, int byterange_mode, int64_t size, int64_t pos) { diff --git a/libavformat/hlsplaylist.h b/libavformat/hlsplaylist.h index ec44e5a0ae..92995d7ebe 100644 --- a/libavformat/hlsplaylist.h +++ b/libavformat/hlsplaylist.h @@ -50,6 +50,8 @@ void ff_hls_write_stream_info(AVStream *st, AVIOContext *out, int bandwidth, void ff_hls_write_playlist_header(AVIOContext *out, int version, int allowcache, int target_duration, int64_t sequence, uint32_t playlist_type, int iframe_mode); +void ff_hls_write_discontinuity_sequence(AVIOContext *out, + int64_t discontinuity_sequence); void ff_hls_write_init_file(AVIOContext *out, const char *filename, int byterange_mode, int64_t size, int64_t pos); int ff_hls_write_file_entry(AVIOContext *out, int insert_discont, -- 2.52.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
