[vlc-commits] [Git][videolan/vlc][master] 7 commits: sout: hls: only expose existing renditions
Steve Lhomme (@robUx4)
gitlab at videolan.org
Fri Aug 21 08:34:18 UTC 2026
Steve Lhomme pushed to branch master at VideoLAN / VLC
Commits:
134db034 by Alaric Senat at 2026-08-21T08:22:26+00:00
sout: hls: only expose existing renditions
- - - - -
74a17d34 by Alaric Senat at 2026-08-21T08:22:26+00:00
sout: hls: flag first extra renditions as "default"
- - - - -
6b9946e9 by Alaric Senat at 2026-08-21T08:22:26+00:00
sout: hls: make the int-to-enum conversion explicit
Avoid implicit int-to-enum conversion warnings.
- - - - -
d7728715 by Alaric Senat at 2026-08-21T08:22:26+00:00
sout: hls: fix inverted condition
Segments are destroyed only when a max number of segments is defined.
This led to playlists being marked as EVENT instead of LIVE.
- - - - -
a016e802 by Alaric Senat at 2026-08-21T08:22:26+00:00
sout: hls: reset muxed output tail after a full segment cut
When ExtractAVSegment cuts a segment that consumes the whole muxed
chain, muxed_output->begin becomes NULL but the tail pointer and
last_header still reference the extracted blocks.
- - - - -
cb8392fc by Alaric Senat at 2026-08-21T08:22:26+00:00
sout: hls: fix crash on empty seg-type option
var_GetNonEmptyString() returns NULL for an empty option value , which
hls_playlist_type_FromString() passes straight to strcmp() and crashes.
- - - - -
e8df37d2 by Alaric Senat at 2026-08-21T08:22:26+00:00
sout: hls: add a maximum segment length
Segments were floored to the target length: the segmenter cut on the
last keyframe at or before "seg-len", producing segments often shorter
than asked for.
Introduce "max-seg-len", a hard cap advertised as EXT-X-TARGETDURATION.
When it is larger than the target, the segmenter cuts on the first
keyframe at or after the target instead, extending up to the cap.
- - - - -
2 changed files:
- modules/stream_out/hls/hls.c
- modules/stream_out/hls/hls.h
Changes:
=====================================
modules/stream_out/hls/hls.c
=====================================
@@ -290,6 +290,19 @@ error:
return NULL;
}
+/* Whether an EXT-X-MEDIA rendition of the given category exists. */
+static bool HasMediaRendition(const sout_stream_sys_t *sys,
+ enum es_format_category_e cat)
+{
+ const hls_playlist_t *playlist;
+ vlc_list_foreach_const (playlist, &sys->media_playlists, node)
+ {
+ if (MediaGetTrack(playlist)->input->fmt.i_cat == cat)
+ return true;
+ }
+ return false;
+}
+
static int GenerateMainManifest(const sout_stream_sys_t *sys,
struct hls_storage **storage_out)
{
@@ -332,6 +345,10 @@ static int GenerateMainManifest(const sout_stream_sys_t *sys,
[SPU_ES] = "subtitles",
};
+ /* At most one rendition per group may be DEFAULT (RFC 8216 4.3.4.1). */
+ bool audio_default_set = false;
+ bool spu_default_set = false;
+
const hls_playlist_t *playlist;
vlc_list_foreach_const (playlist, &sys->media_playlists, node)
{
@@ -347,6 +364,25 @@ static int GenerateMainManifest(const sout_stream_sys_t *sys,
const char *group_id = GROUP_IDS[fmt->i_cat];
MANIFEST_ADD_ATTRIBUTE("GROUP-ID=\"%s\"", group_id);
+ if (fmt->i_cat == AUDIO_ES)
+ {
+ if (!audio_default_set)
+ {
+ MANIFEST_ADD_ATTRIBUTE("DEFAULT=%s", "YES");
+ audio_default_set = true;
+ }
+ MANIFEST_ADD_ATTRIBUTE("AUTOSELECT=%s", "YES");
+ }
+ else if (fmt->i_cat == SPU_ES)
+ {
+ if (!spu_default_set)
+ {
+ MANIFEST_ADD_ATTRIBUTE("DEFAULT=%s", "YES");
+ spu_default_set = true;
+ }
+ MANIFEST_ADD_ATTRIBUTE("AUTOSELECT=%s", "YES");
+ }
+
const iso639_lang_t *lang =
(fmt->psz_language != NULL)
? vlc_find_iso639(fmt->psz_language, false)
@@ -384,9 +420,12 @@ static int GenerateMainManifest(const sout_stream_sys_t *sys,
MANIFEST_ADD_ATTRIBUTE("CODECS=\"%s\"", codecs);
free(codecs);
- MANIFEST_ADD_ATTRIBUTE("VIDEO=\"%s\"", GROUP_IDS[VIDEO_ES]);
- MANIFEST_ADD_ATTRIBUTE("AUDIO=\"%s\"", GROUP_IDS[AUDIO_ES]);
- MANIFEST_ADD_ATTRIBUTE("SUBTITLES=\"%s\"", GROUP_IDS[SPU_ES]);
+ if (HasMediaRendition(sys, VIDEO_ES))
+ MANIFEST_ADD_ATTRIBUTE("VIDEO=\"%s\"", GROUP_IDS[VIDEO_ES]);
+ if (HasMediaRendition(sys, AUDIO_ES))
+ MANIFEST_ADD_ATTRIBUTE("AUDIO=\"%s\"", GROUP_IDS[AUDIO_ES]);
+ if (HasMediaRendition(sys, SPU_ES))
+ MANIFEST_ADD_ATTRIBUTE("SUBTITLES=\"%s\"", GROUP_IDS[SPU_ES]);
MANIFEST_END_TAG
if (vlc_memstream_printf(&out, "%s\n", playlist->url) < 0)
@@ -428,12 +467,12 @@ GeneratePlaylistManifest(const hls_playlist_t *playlist,
MANIFEST_ADD_TAG("#EXTM3U");
const double seg_duration =
- secf_from_vlc_tick(playlist->config->segment_length);
+ secf_from_vlc_tick(playlist->config->max_segment_length);
MANIFEST_ADD_TAG("#EXT-X-TARGETDURATION:%.0f", seg_duration);
// First version adding CMAF fragments support.
MANIFEST_ADD_TAG("#EXT-X-VERSION:7");
- const bool will_destroy_segments = playlist->config->max_segments == 0;
+ const bool will_destroy_segments = playlist->config->max_segments != 0;
if (playlist->ended)
MANIFEST_ADD_TAG("#EXT-X-PLAYLIST-TYPE:VOD");
else if (!will_destroy_segments)
@@ -505,6 +544,7 @@ typedef struct
} hls_cut_t;
static hls_cut_t CutTsSegment(hls_block_chain_t *muxed_output,
+ vlc_tick_t min_length,
vlc_tick_t max_length)
{
hls_cut_t iframe_cut = {0};
@@ -514,7 +554,11 @@ static hls_cut_t CutTsSegment(hls_block_chain_t *muxed_output,
for (block_t *it = muxed_output->begin; it != NULL; it = it->p_next)
{
if (prev != NULL && (it->i_flags & BLOCK_FLAG_HEADER))
+ {
+ if (total >= min_length)
+ return (hls_cut_t){.last = prev, .length = total};
iframe_cut = (hls_cut_t){.last = prev, .length = total};
+ }
if (total + it->i_length > max_length)
break;
@@ -528,9 +572,10 @@ static hls_cut_t CutTsSegment(hls_block_chain_t *muxed_output,
}
static hls_cut_t CutMP4Segment(hls_block_chain_t *muxed_output,
+ vlc_tick_t min_length,
vlc_tick_t max_length)
{
- hls_cut_t iframe_cut = {0};
+ hls_cut_t aligned_cut = {0};
hls_cut_t moof_cut = {0};
block_t *prev = NULL;
@@ -538,7 +583,11 @@ static hls_cut_t CutMP4Segment(hls_block_chain_t *muxed_output,
for (block_t *it = muxed_output->begin; it != NULL; it = it->p_next)
{
if (prev != NULL && (it->i_flags & MP4_MUX_BLOCK_FLAG_SYNC))
- iframe_cut = (hls_cut_t){.last = prev, .length = total};
+ {
+ if (total >= min_length)
+ return (hls_cut_t){.last = prev, .length = total};
+ aligned_cut = (hls_cut_t){.last = prev, .length = total};
+ }
if (prev != NULL && (it->i_flags & MP4_MUX_BLOCK_FLAG_BOUNDARY))
moof_cut = (hls_cut_t){.last = prev, .length = total};
@@ -549,26 +598,44 @@ static hls_cut_t CutMP4Segment(hls_block_chain_t *muxed_output,
prev = it;
}
- /* Prioritize iframe alignment cuts, align on the MP4 MOOF box otherwise,
- * and cut anywhere worst case. */
- return iframe_cut.last ? iframe_cut
- : moof_cut.last ? moof_cut
- : (hls_cut_t){.last = prev, .length = total};
+ /* Prioritize aligned cuts, align on the MP4 MOOF box otherwise, and cut
+ * anywhere worst case. */
+ return aligned_cut.last ? aligned_cut
+ : moof_cut.last ? moof_cut
+ : (hls_cut_t){.last = prev, .length = total};
}
-static hls_block_chain_t ExtractAVSegment(enum hls_playlist_type type,
+/* Block flag marking a position the playlist can be cut on. */
+static uint32_t PlaylistCutFlag(const hls_playlist_t *playlist)
+{
+ switch (playlist->type)
+ {
+ case HLS_PLAYLIST_TYPE_TS:
+ return BLOCK_FLAG_HEADER;
+ case HLS_PLAYLIST_TYPE_MP4:
+ /* Audio-only muxes have no iframe to align on, but the mp4 muxer
+ * already flags every moof as SYNC in that case. */
+ return MP4_MUX_BLOCK_FLAG_SYNC;
+ case HLS_PLAYLIST_TYPE_WEBVTT:
+ break;
+ }
+ vlc_assert_unreachable();
+}
+
+static hls_block_chain_t ExtractAVSegment(const hls_playlist_t *playlist,
hls_block_chain_t *muxed_output,
+ vlc_tick_t min_length,
vlc_tick_t max_length)
{
hls_block_chain_t segment = {.begin = muxed_output->begin};
hls_cut_t cut;
- switch (type) {
+ switch (playlist->type) {
case HLS_PLAYLIST_TYPE_TS:
- cut = CutTsSegment(muxed_output, max_length);
+ cut = CutTsSegment(muxed_output, min_length, max_length);
break;
case HLS_PLAYLIST_TYPE_MP4:
- cut = CutMP4Segment(muxed_output, max_length);
+ cut = CutMP4Segment(muxed_output, min_length, max_length);
break;
case HLS_PLAYLIST_TYPE_WEBVTT:
vlc_assert_unreachable();
@@ -580,6 +647,11 @@ static hls_block_chain_t ExtractAVSegment(enum hls_playlist_type type,
muxed_output->begin = cut.last->p_next;
cut.last->p_next = NULL;
muxed_output->length -= segment.length;
+ if (muxed_output->begin == NULL)
+ {
+ muxed_output->end = &muxed_output->begin;
+ muxed_output->last_header = NULL;
+ }
}
else
{
@@ -668,15 +740,16 @@ static void PrependSegmentBoxes(hls_block_chain_t *segment,
static hls_block_chain_t ExtractSegment(hls_playlist_t *playlist)
{
- const vlc_tick_t seglen = playlist->config->segment_length;
+ const vlc_tick_t min_length = playlist->config->segment_length;
+ const vlc_tick_t max_length = playlist->config->max_segment_length;
switch (playlist->type)
{
case HLS_PLAYLIST_TYPE_WEBVTT:
- return ExtractSubtitleSegment(&playlist->muxed_output, seglen);
+ return ExtractSubtitleSegment(&playlist->muxed_output, min_length);
case HLS_PLAYLIST_TYPE_MP4:
case HLS_PLAYLIST_TYPE_TS:
return ExtractAVSegment(
- playlist->type, &playlist->muxed_output, seglen);
+ playlist, &playlist->muxed_output, min_length, max_length);
}
vlc_assert_unreachable();
}
@@ -775,18 +848,33 @@ static int ExtractAndAddSegment(hls_playlist_t *playlist,
return UpdatePlaylistManifest(playlist);
}
-static bool IsSegmentReady(enum hls_playlist_type type,
- hls_block_chain_t *buffer,
- vlc_tick_t seglen)
+static bool IsSegmentReady(const hls_playlist_t *playlist,
+ vlc_tick_t min_length,
+ vlc_tick_t max_length)
{
+ const hls_block_chain_t *buffer = &playlist->muxed_output;
+
/* The subtitle header outputs one header per segment. Let's wait until we
* received the next header before considering the current segment
* finished. */
- if( type == HLS_PLAYLIST_TYPE_WEBVTT)
+ if (playlist->type == HLS_PLAYLIST_TYPE_WEBVTT)
return buffer->begin != buffer->last_header;
- /* Only consider full segments as ready for now. */
- return buffer->length >= seglen;
+ if (max_length == min_length)
+ return buffer->length >= min_length;
+
+ if (buffer->length >= max_length)
+ return true;
+
+ const uint32_t cut_flag = PlaylistCutFlag(playlist);
+ vlc_tick_t total = 0;
+ for (const block_t *it = buffer->begin; it != NULL; it = it->p_next)
+ {
+ if (total >= min_length && (it->i_flags & cut_flag))
+ return true;
+ total += it->i_length;
+ }
+ return false;
}
static void ReplaceInitSection(hls_playlist_t *playlist, block_t *content)
@@ -908,8 +996,9 @@ static ssize_t AccessOutWrite(sout_access_out_t *access, block_t *block)
if (it->access == access)
PlaylistWriteMuxedOutput(it, block, length);
- if (!IsSegmentReady(
- it->type, &it->muxed_output, sys->config.segment_length))
+ if (!IsSegmentReady(it,
+ sys->config.segment_length,
+ sys->config.max_segment_length))
segments_ready = false;
}
@@ -918,9 +1007,9 @@ static ssize_t AccessOutWrite(sout_access_out_t *access, block_t *block)
{
hls_playlists_foreach (it)
{
- while (IsSegmentReady(it->type,
- &it->muxed_output,
- sys->config.segment_length) &&
+ while (IsSegmentReady(it,
+ sys->config.segment_length,
+ sys->config.max_segment_length) &&
it->muxed_duration < sys->elapsed_stream_time)
{
if (ExtractAndAddSegment(it, sys) != VLC_SUCCESS)
@@ -1352,6 +1441,7 @@ static int Open(vlc_object_t *this)
"out-dir",
"pace",
"seg-len",
+ "max-seg-len",
"variants",
"seg-type",
NULL,
@@ -1366,6 +1456,16 @@ static int Open(vlc_object_t *this)
sys->config.pace = var_GetBool(stream, SOUT_CFG_PREFIX "pace");
sys->config.segment_length =
VLC_TICK_FROM_SEC(var_GetInteger(stream, SOUT_CFG_PREFIX "seg-len"));
+ sys->config.max_segment_length =
+ VLC_TICK_FROM_SEC(var_GetInteger(stream, SOUT_CFG_PREFIX "max-seg-len"));
+ if (sys->config.max_segment_length < sys->config.segment_length)
+ {
+ if (sys->config.max_segment_length != 0)
+ msg_Warn(stream,
+ "\"" SOUT_CFG_PREFIX "max-seg-len\" is smaller than \""
+ SOUT_CFG_PREFIX "seg-len\"; using the target as the cap");
+ sys->config.max_segment_length = sys->config.segment_length;
+ }
sys->config.max_memory =
BYTES_FROM_KB(var_GetInteger(stream, SOUT_CFG_PREFIX "max-memory"));
@@ -1484,8 +1584,14 @@ variant_error:
#define PACE_LONGTEXT \
N_("Enable input pacing, the media will play at playback rate")
#define PACE_TEXT N_("Enable pacing")
-#define SEGLEN_LONGTEXT N_("Length of segments in seconds")
-#define SEGLEN_TEXT N_("Segment length (sec)")
+#define SEGLEN_LONGTEXT \
+ N_("Target length of segments in seconds. The segmenter cuts on a keyframe "\
+ "near this duration.")
+#define SEGLEN_TEXT N_("Target segment length (sec)")
+#define MAXSEGLEN_LONGTEXT \
+ N_("Maximum length of a segment in seconds, A segment never " \
+ "exceeds this value. Defaults to 0, implying the target length.")
+#define MAXSEGLEN_TEXT N_("Maximum segment length (sec)")
#define SEGTYPE_LONGTEXT N_("Specifies the segments container")
#define SEGTYPE_TEXT N_("Segment muxed format")
@@ -1518,6 +1624,8 @@ vlc_module_begin()
add_bool(SOUT_CFG_PREFIX "pace", false, PACE_TEXT, PACE_LONGTEXT)
add_integer(SOUT_CFG_PREFIX "seg-len", 4, SEGLEN_TEXT, SEGLEN_LONGTEXT)
change_integer_range(1, 60)
+ add_integer(SOUT_CFG_PREFIX "max-seg-len", 0, MAXSEGLEN_TEXT, MAXSEGLEN_LONGTEXT)
+ change_integer_range(0, 60)
set_callback(Open)
vlc_module_end()
=====================================
modules/stream_out/hls/hls.h
=====================================
@@ -36,11 +36,14 @@ static const char *const HLS_PLAYLIST_TYPE_STRINGS[] = {
static inline int hls_playlist_type_FromString(const char *str,
enum hls_playlist_type *out)
{
+ if (str == NULL)
+ return -ENOENT;
+
for (unsigned i = 0; i < ARRAY_SIZE(HLS_PLAYLIST_TYPE_STRINGS); ++i)
{
if (!strcmp(str, HLS_PLAYLIST_TYPE_STRINGS[i]))
{
- *out = i;
+ *out = (enum hls_playlist_type)i;
return VLC_SUCCESS;
}
}
@@ -54,6 +57,7 @@ struct hls_config
unsigned int max_segments;
bool pace;
vlc_tick_t segment_length;
+ vlc_tick_t max_segment_length;
size_t max_memory;
enum hls_playlist_type preferred_type;
};
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/010eed83fcfd40bb1e390697094faa19e35d5a38...e8df37d26aa1717461444f168766945f5003729e
--
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/010eed83fcfd40bb1e390697094faa19e35d5a38...e8df37d26aa1717461444f168766945f5003729e
You're receiving this email because of your account on code.videolan.org. Manage all notifications: https://code.videolan.org/-/profile/notifications | Help: https://code.videolan.org/help
More information about the vlc-commits
mailing list