Steve Lhomme pushed to branch master at VideoLAN / VLC
Commits:
3cfdf08e by Alaric Senat at 2024-02-10T11:44:19+00:00
sout: hls: move segment extraction functions
- - - - -
593e01e5 by Alaric Senat at 2024-02-10T11:44:19+00:00
sout: hls: avoid segment config shallow copy
Let's just store the parameters directly in the segment structure. This
is far more flexible and de-couple the stored config info in the segment
from what's passed to the init function.
- - - - -
532df37c by Alaric Senat at 2024-02-10T11:44:19+00:00
sout: hls: introduce a playlist type enum
Preparation work for the introduction of mp4frag and webVTT segments.
- - - - -
1d592200 by Alaric Senat at 2024-02-10T11:44:19+00:00
sout: hls: return err code on segment extraction
- - - - -
8e0f73b0 by Alaric Senat at 2024-02-10T11:44:19+00:00
sout: hls: simplify segment extraction params
- - - - -
673e3679 by Alaric Senat at 2024-02-10T11:44:19+00:00
sout: hls: change access OOM error logger
Write them as a global access error.
- - - - -
9cfe3909 by Alaric Senat at 2024-02-10T11:44:19+00:00
sout: hls: give access visibility of the whole HLS context
This is needed in future patches notably to switch to segment extraction
at access output level. Visibility of all the playlists will be needed
to write all the segments at the same time.
- - - - -
e97a5de0 by Alaric Senat at 2024-02-10T11:44:19+00:00
sout: hls: store muxed output length
- - - - -
0335dee9 by Alaric Senat at 2024-02-10T11:44:19+00:00
sout: hls: move segmentation to access output
The segmentation was previously done in SetPCR to output segments at
precise stream times instead of reliying on the quantity of data output
by the muxers.
This had two main issues:
- SetPCR does not report errors and the segment creation phase can fail
at multiple stages.
- The calculated stream time did not actually reflect the quantity of
data that left muxers. Muxers can be delayed by mux caching and are
allowed to have an internal queue. The previous algorithm was
outputting smaller segments due to the muxer delay.
Those issues were fine with TS as a single muxer but with the future
introduction of VTT/FMP4 segmentation that both make choice base on
segment times, we needed to change the segmenting strategy.
Segmentation is now done at muxer output, bigger segments are output
when all the muxers sent the right amount of data.
- - - - -
86755ae7 by Alaric Senat at 2024-02-10T11:44:19+00:00
sout: hls: keep track of output header blocks
Those will be used as a delimitation hint from muxers. WebVTT, TS and
mp4frag will use those eventually.
- - - - -
b0c1db5b by Alaric Senat at 2024-02-10T11:44:19+00:00
mux: webvtt: signal header blocks
This will be used by HLS to properly segment subtitles.
- - - - -
659a9c53 by Alaric Senat at 2024-02-10T11:44:19+00:00
sout: hls: add a subtitle segmenter
The subtitle segmenter is a meta-muxer used to create subtitles
segments. It handles subtitle frames splitting, re-creation of the
subtitles muxer and empty segments creation when no data is available.
- - - - -
f8c79b09 by Alaric Senat at 2024-02-10T11:44:19+00:00
test: hls: add subtitle segmentation unit tests
- - - - -
bcfdf128 by Alaric Senat at 2024-02-10T11:44:19+00:00
sout: hls: add webvtt support
WebVTT and plain subtitle tracks can now be exposed in HLS. an extra
media rendition is created for each subtitle ES and the HLS server
creates max-sized vtt segments following the pace of the other ES.
WebVTT segmentation in scenario where no subtitle is output for a while
is handled using the PCR of the stream. Having reliable clock info helps
a lot to create empty subtitle segments at the correct time.
- - - - -
39a6f9b4 by Alaric Senat at 2024-02-10T11:44:19+00:00
sout: hls: remove unneeded reference
Holding this reference that breaks encapsulation can easily be avoided
now by passing the sout context to the extraction function.
- - - - -
10 changed files:
- modules/mux/webvtt.c
- modules/stream_out/Makefile.am
- modules/stream_out/hls/codecs.c
- modules/stream_out/hls/hls.c
- modules/stream_out/hls/hls.h
- modules/stream_out/hls/segments.c
- modules/stream_out/hls/segments.h
- + modules/stream_out/hls/subtitles_segmenter.c
- test/Makefile.am
- + test/modules/stream_out/hls/subtitles_segmenter.c
Changes:
=====================================
modules/mux/webvtt.c
=====================================
@@ -188,7 +188,10 @@ static int Mux(sout_mux_t *mux)
}
if (data)
+ {
+ data->i_flags |= BLOCK_FLAG_HEADER;
sout_AccessOutWrite(mux->p_access, data);
+ }
sys->header_done = true;
}
=====================================
modules/stream_out/Makefile.am
=====================================
@@ -40,7 +40,8 @@ libstream_out_hls_plugin_la_SOURCES = \
stream_out/hls/variant_maps.h stream_out/hls/variant_maps.c \
stream_out/hls/storage.h stream_out/hls/storage.c \
stream_out/hls/segments.h stream_out/hls/segments.c \
- stream_out/hls/codecs.h stream_out/hls/codecs.c
+ stream_out/hls/codecs.h stream_out/hls/codecs.c \
+ stream_out/hls/subtitles_segmenter.c
libstream_out_hls_plugin_la_LIBADD = libvlc_hxxxhelper.la
sout_LTLIBRARIES = \
=====================================
modules/stream_out/hls/codecs.c
=====================================
@@ -55,6 +55,12 @@ static int FormatMP4A(struct vlc_memstream *ms, const
es_format_t *fmt)
return (wrote == -1) ? VLC_ENOMEM : VLC_SUCCESS;
}
+static int FormatWebVTT(struct vlc_memstream *ms)
+{
+ const int written = vlc_memstream_puts(ms, "wvtt");
+ return (written <= 0) ? VLC_ENOMEM : VLC_SUCCESS;
+}
+
int hls_codec_Format(struct vlc_memstream *ms, const es_format_t *fmt)
{
switch (fmt->i_codec)
@@ -63,6 +69,9 @@ int hls_codec_Format(struct vlc_memstream *ms, const
es_format_t *fmt)
return FormatAVC1(ms, fmt);
case VLC_CODEC_MP4A:
return FormatMP4A(ms, fmt);
+ case VLC_CODEC_TEXT:
+ case VLC_CODEC_WEBVTT:
+ return FormatWebVTT(ms);
default:
return VLC_ENOTSUP;
}
@@ -70,5 +79,6 @@ int hls_codec_Format(struct vlc_memstream *ms, const
es_format_t *fmt)
bool hls_codec_IsSupported(const es_format_t *fmt)
{
- return fmt->i_codec == VLC_CODEC_H264 || fmt->i_codec == VLC_CODEC_MP4A;
+ return fmt->i_codec == VLC_CODEC_H264 || fmt->i_codec == VLC_CODEC_MP4A ||
+ fmt->i_codec == VLC_CODEC_TEXT || fmt->i_codec == VLC_CODEC_WEBVTT;
}
=====================================
modules/stream_out/hls/hls.c
=====================================
@@ -47,6 +47,7 @@ typedef struct
block_t *begin;
block_t **end;
vlc_tick_t length;
+ block_t *last_header;
} hls_block_chain_t;
static inline void hls_block_chain_Reset(hls_block_chain_t *chain)
@@ -54,6 +55,7 @@ static inline void hls_block_chain_Reset(hls_block_chain_t
*chain)
chain->begin = NULL;
chain->end = &chain->begin;
chain->length = 0;
+ chain->last_header = NULL;
}
/**
@@ -64,7 +66,7 @@ typedef struct hls_playlist
unsigned int id;
const struct hls_config *config;
- size_t *current_memory_cached_ref;
+ enum hls_playlist_type type;
sout_access_out_t *access;
sout_mux_t *mux;
@@ -145,8 +147,6 @@ typedef struct
httpd_url_t *http_manifest;
vlc_tick_t first_pcr;
- vlc_tick_t last_pcr;
- vlc_tick_t last_segment;
size_t current_memory_cached;
} sout_stream_sys_t;
@@ -301,10 +301,12 @@ static struct hls_storage *GenerateMainManifest(const
sout_stream_sys_t *sys)
static const char *const TRACK_TYPES[] = {
[VIDEO_ES] = "VIDEO",
[AUDIO_ES] = "AUDIO",
+ [SPU_ES] = "SUBTITLES",
};
static const char *const GROUP_IDS[] = {
[VIDEO_ES] = "video",
[AUDIO_ES] = "audio",
+ [SPU_ES] = "subtitles",
};
const hls_playlist_t *playlist;
@@ -312,7 +314,8 @@ static struct hls_storage *GenerateMainManifest(const
sout_stream_sys_t *sys)
{
const hls_track_t *track = MediaGetTrack(playlist);
const es_format_t *fmt = &track->input->fmt;
- assert(fmt->i_cat == VIDEO_ES || fmt->i_cat == AUDIO_ES);
+ assert(fmt->i_cat == VIDEO_ES || fmt->i_cat == AUDIO_ES ||
+ fmt->i_cat == SPU_ES);
MANIFEST_START_TAG("#EXT-X-MEDIA")
const char *track_type = TRACK_TYPES[fmt->i_cat];
@@ -360,6 +363,7 @@ static struct hls_storage *GenerateMainManifest(const
sout_stream_sys_t *sys)
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]);
MANIFEST_END_TAG
if (vlc_memstream_printf(&out, "%s\n", playlist->url) < 0)
@@ -462,35 +466,161 @@ static int UpdatePlaylistManifest(hls_playlist_t
*playlist)
return VLC_SUCCESS;
}
+static hls_block_chain_t ExtractCommonSegment(hls_block_chain_t *muxed_output,
+ vlc_tick_t max_segment_length)
+{
+ hls_block_chain_t segment = {.begin = muxed_output->begin};
+
+ block_t *prev = NULL;
+ for (block_t *it = muxed_output->begin; it != NULL; it = it->p_next)
+ {
+ if (segment.length + it->i_length > max_segment_length)
+ {
+ muxed_output->begin = it;
+
+ if (prev != NULL)
+ prev->p_next = NULL;
+ return segment;
+ }
+ segment.length += it->i_length;
+ muxed_output->length -= it->i_length;
+ prev = it;
+ }
+
+ hls_block_chain_Reset(muxed_output);
+ return segment;
+}
+
+static hls_block_chain_t ExtractSubtitleSegment(hls_block_chain_t
*muxed_output,
+ vlc_tick_t segment_length)
+{
+ hls_block_chain_t segment = {.begin = muxed_output->begin,
+ .length = segment_length};
+ for (block_t *it = muxed_output->begin; it != NULL; it = it->p_next)
+ {
+ /* Subtitle segments are segmented at mux level by the
+ * hls_sub_segmenter. They have varying length so we use the header
flag
+ * to extract them properly. */
+ if (it->p_next != NULL && it->p_next->i_flags & BLOCK_FLAG_HEADER)
+ {
+ muxed_output->begin = it->p_next;
+ muxed_output->last_header = it->p_next;
+ it->p_next = NULL;
+ return segment;
+ }
+ muxed_output->length -= it->i_length;
+ }
+ hls_block_chain_Reset(muxed_output);
+ return segment;
+}
+
+static hls_block_chain_t ExtractSegment(hls_playlist_t *playlist)
+{
+ const vlc_tick_t seglen = playlist->config->segment_length;
+ if (playlist->type == HLS_PLAYLIST_TYPE_WEBVTT)
+ return ExtractSubtitleSegment(&playlist->muxed_output, seglen);
+ return ExtractCommonSegment(&playlist->muxed_output, seglen);
+}
+
+static int ExtractAndAddSegment(hls_playlist_t *playlist,
+ sout_stream_sys_t *sys)
+{
+ hls_block_chain_t segment = ExtractSegment(playlist);
+
+ if (hls_config_IsMemStorageEnabled(&sys->config) &&
+ hls_segment_queue_IsAtMaxCapacity(&playlist->segments))
+ {
+ const hls_segment_t *to_be_removed =
+ hls_segment_GetFirst(&playlist->segments);
+ sys->current_memory_cached -=
+ hls_storage_GetSize(to_be_removed->storage);
+ }
+
+ const int status = hls_segment_queue_NewSegment(
+ &playlist->segments, segment.begin, segment.length);
+ if (unlikely(status != VLC_SUCCESS))
+ {
+ vlc_error(playlist->logger,
+ "Segment '%u' creation failed",
+ playlist->segments.total_segments + 1);
+ return status;
+ }
+
+ vlc_debug(playlist->logger,
+ "Segment '%u' created",
+ playlist->segments.total_segments);
+
+ return UpdatePlaylistManifest(playlist);
+}
+
+static bool IsSegmentReady(enum hls_playlist_type type,
+ hls_block_chain_t *buffer,
+ vlc_tick_t seglen)
+{
+ /* 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)
+ return buffer->begin != buffer->last_header;
+
+ /* Only consider full segments as ready for now. */
+ return buffer->length >= seglen;
+}
+
static ssize_t AccessOutWrite(sout_access_out_t *access, block_t *block)
{
- hls_playlist_t *playlist = access->p_sys;
+ sout_stream_sys_t *sys = access->p_sys;
size_t size = 0;
- block_ChainProperties(block, NULL, &size, NULL);
+ vlc_tick_t length;
+ block_ChainProperties(block, NULL, &size, &length);
- if (hls_config_IsMemStorageEnabled(playlist->config))
+ if (hls_config_IsMemStorageEnabled(&sys->config))
{
- *playlist->current_memory_cached_ref += size;
- if (*playlist->current_memory_cached_ref >=
- playlist->config->max_memory)
+ sys->current_memory_cached += size;
+ if (sys->current_memory_cached >= sys->config.max_memory)
{
- vlc_error(playlist->logger,
- "Maximum memory capacity (%zuKb) for segment storage was
"
- "reached. The HLS server will stop creating segments. "
- "Please refer to the max-memory option for more info.",
- BYTES_TO_KB(playlist->config->max_memory));
+ msg_Err(access,
+ "Maximum memory capacity (%zuKb) for segment storage was "
+ "reached. The HLS server will stop creating segments. "
+ "Please refer to the max-memory option for more info.",
+ BYTES_TO_KB(sys->config.max_memory));
block_ChainRelease(block);
return -1;
}
}
- block_ChainLastAppend(&playlist->muxed_output.end, block);
+ bool segments_ready = true;
+ hls_playlist_t *it;
+ hls_playlists_foreach(it)
+ {
+ /* Append the muxed output to the playlist tied to this access call. */
+ if (it->access == access)
+ {
+ block_ChainLastAppend(&it->muxed_output.end, block);
+ it->muxed_output.length += length;
+ if (block->i_flags & BLOCK_FLAG_HEADER)
+ it->muxed_output.last_header = block;
+ }
+
+ if (!IsSegmentReady(
+ it->type, &it->muxed_output, sys->config.segment_length))
+ segments_ready = false;
+ }
+
+
+ if (segments_ready)
+ {
+ hls_playlists_foreach (it)
+ {
+ if (ExtractAndAddSegment(it, sys) != VLC_SUCCESS)
+ return -1;
+ }
+ }
return size;
}
-static sout_access_out_t *CreateAccessOut(sout_stream_t *stream,
- hls_playlist_t *sys)
+static sout_access_out_t *CreateAccessOut(sout_stream_t *stream)
{
sout_access_out_t *access = vlc_object_create(stream, sizeof(*access));
if (unlikely(access == NULL))
@@ -505,7 +635,7 @@ static sout_access_out_t *CreateAccessOut(sout_stream_t
*stream,
access->p_cfg = NULL;
access->p_module = NULL;
- access->p_sys = sys;
+ access->p_sys = stream->p_sys;
access->psz_path = NULL;
access->pf_control = NULL;
@@ -527,7 +657,22 @@ static inline char *FormatPlaylistManifestURL(const
hls_playlist_t *playlist)
return url;
}
-static hls_playlist_t *CreatePlaylist(sout_stream_t *stream)
+static sout_mux_t *CreatePlaylistMuxer(sout_access_out_t *access,
+ enum hls_playlist_type type,
+ const struct hls_config *config)
+{
+ switch(type)
+ {
+ case HLS_PLAYLIST_TYPE_TS:
+ return sout_MuxNew(access, "ts");
+ case HLS_PLAYLIST_TYPE_WEBVTT:
+ return CreateSubtitleSegmenter(access, config);
+ }
+ return NULL;
+}
+
+static hls_playlist_t *CreatePlaylist(sout_stream_t *stream,
+ enum hls_playlist_type type)
{
sout_stream_sys_t *sys = stream->p_sys;
@@ -535,18 +680,18 @@ static hls_playlist_t *CreatePlaylist(sout_stream_t
*stream)
if (unlikely(playlist == NULL))
return NULL;
- playlist->access = CreateAccessOut(stream, playlist);
+ playlist->access = CreateAccessOut(stream);
if (unlikely(playlist->access == NULL))
goto access_err;
- playlist->mux = sout_MuxNew(playlist->access, "ts");
+ playlist->mux = CreatePlaylistMuxer(playlist->access, type, &sys->config);
if (unlikely(playlist->mux == NULL))
goto mux_err;
playlist->id = sys->playlist_created_count;
+ playlist->type = type;
playlist->config = &sys->config;
playlist->ended = false;
- playlist->current_memory_cached_ref = &sys->current_memory_cached;
playlist->url = FormatPlaylistManifestURL(playlist);
if (unlikely(playlist->url == NULL))
@@ -560,6 +705,7 @@ static hls_playlist_t *CreatePlaylist(sout_stream_t *stream)
struct hls_segment_queue_config config = {
.playlist_id = playlist->id,
+ .playlist_type = type,
.httpd_ref = sys->http_host,
.httpd_callback = HTTPCallback,
};
@@ -626,9 +772,11 @@ static void DeletePlaylist(hls_playlist_t *playlist)
free(playlist);
}
-static hls_playlist_t *AddPlaylist(sout_stream_t *stream, struct vlc_list
*head)
+static hls_playlist_t *AddPlaylist(sout_stream_t *stream,
+ enum hls_playlist_type type,
+ struct vlc_list *head)
{
- hls_playlist_t *variant = CreatePlaylist(stream);
+ hls_playlist_t *variant = CreatePlaylist(stream, type);
if (variant == NULL)
return NULL;
@@ -654,11 +802,15 @@ Add(sout_stream_t *stream, const es_format_t *fmt, const
char *es_id)
if (map != NULL)
{
if (map->playlist_ref == NULL)
- map->playlist_ref = AddPlaylist(stream, &sys->variant_playlists);
+ map->playlist_ref = AddPlaylist(stream, HLS_PLAYLIST_TYPE_TS,
&sys->variant_playlists);
playlist = map->playlist_ref;
}
+ else if (fmt->i_cat == SPU_ES)
+ playlist = AddPlaylist(
+ stream, HLS_PLAYLIST_TYPE_WEBVTT, &sys->media_playlists);
else
- playlist = AddPlaylist(stream, &sys->media_playlists);
+ playlist =
+ AddPlaylist(stream, HLS_PLAYLIST_TYPE_TS, &sys->media_playlists);
if (playlist == NULL)
return NULL;
@@ -709,61 +861,6 @@ error:
return NULL;
}
-static hls_block_chain_t ExtractSegment(hls_playlist_t *playlist,
- vlc_tick_t max_segment_length)
-{
- hls_block_chain_t segment = {.begin = playlist->muxed_output.begin};
-
- block_t *prev = NULL;
- for (block_t *it = playlist->muxed_output.begin; it != NULL;
- it = it->p_next)
- {
- if (segment.length + it->i_length > max_segment_length)
- {
- playlist->muxed_output.begin = it;
-
- if (prev != NULL)
- prev->p_next = NULL;
- return segment;
- }
- segment.length += it->i_length;
- prev = it;
- }
-
- hls_block_chain_Reset(&playlist->muxed_output);
- return segment;
-}
-
-static void ExtractAndAddSegment(hls_playlist_t *playlist,
- vlc_tick_t last_segment_time)
-{
- hls_block_chain_t segment = ExtractSegment(playlist, last_segment_time);
-
- if (hls_config_IsMemStorageEnabled(playlist->config) &&
- hls_segment_queue_IsAtMaxCapacity(&playlist->segments))
- {
- const hls_segment_t *to_be_removed =
- hls_segment_GetFirst(&playlist->segments);
- *playlist->current_memory_cached_ref -=
- hls_storage_GetSize(to_be_removed->storage);
- }
-
- const int status = hls_segment_queue_NewSegment(
- &playlist->segments, segment.begin, segment.length);
- if (unlikely(status != VLC_SUCCESS))
- {
- vlc_error(playlist->logger,
- "Segment '%u' creation failed",
- playlist->segments.total_segments + 1);
- return;
- }
-
- vlc_debug(playlist->logger,
- "Segment '%u' created",
- playlist->segments.total_segments);
-
- UpdatePlaylistManifest(playlist);
-}
static void Del(sout_stream_t *stream, void *id)
{
sout_stream_sys_t *sys = stream->p_sys;
@@ -780,7 +877,7 @@ static void Del(sout_stream_t *stream, void *id)
map->playlist_ref = NULL;
track->playlist_ref->ended = true;
- ExtractAndAddSegment(track->playlist_ref, sys->config.segment_length);
+ ExtractAndAddSegment(track->playlist_ref, sys);
UpdatePlaylistManifest(track->playlist_ref);
DeletePlaylist(track->playlist_ref);
@@ -796,17 +893,11 @@ static int Send(sout_stream_t *stream, void *id,
vlc_frame_t *frame)
(void)stream;
}
-/**
- * PCR events are used to have a reliable stream time status. Segmenting is
done
- * after a PCR testifying that we are above the segment limit arrives.
- */
+/** PCR events are used to have a reliable stream time status. */
static void SetPCR(sout_stream_t *stream, vlc_tick_t pcr)
{
sout_stream_sys_t *sys = stream->p_sys;
- const vlc_tick_t last_pcr = sys->last_pcr;
- sys->last_pcr = pcr;
-
if (sys->first_pcr == VLC_TICK_INVALID)
{
sys->first_pcr = pcr;
@@ -814,23 +905,13 @@ static void SetPCR(sout_stream_t *stream, vlc_tick_t pcr)
}
const vlc_tick_t stream_time = pcr - sys->first_pcr;
- const vlc_tick_t current_seglen = stream_time - sys->last_segment;
-
- const vlc_tick_t pcr_gap = pcr - last_pcr;
- /* PCR and segment length aren't necessarily aligned. Testing segment
length
- * with a **next** PCR approximation will avoid piling up data:
- *
- * |------x#|-----x##|----x###| time
- * ^ PCR ^ Segment end ^ Buffer expanding
- *
- * The segments are then a little shorter than they could be.
- */
- if (current_seglen + pcr_gap >= sys->config.segment_length)
+ const hls_playlist_t *playlist;
+ vlc_list_foreach_const (playlist, &sys->media_playlists, node)
{
- hls_playlist_t *playlist;
- hls_playlists_foreach (playlist)
- ExtractAndAddSegment(playlist, sys->config.segment_length);
- sys->last_segment = stream_time;
+ if (playlist->type != HLS_PLAYLIST_TYPE_WEBVTT)
+ continue;
+
+ hls_sub_segmenter_SignalStreamUpdate(playlist->mux, stream_time);
}
}
@@ -974,8 +1055,6 @@ static int Open(vlc_object_t *this)
vlc_list_init(&sys->media_playlists);
sys->first_pcr = VLC_TICK_INVALID;
- sys->last_pcr = VLC_TICK_INVALID;
- sys->last_segment = 0;
sys->current_memory_cached = 0;
=====================================
modules/stream_out/hls/hls.h
=====================================
@@ -20,6 +20,12 @@
#ifndef HLS_H
#define HLS_H
+enum hls_playlist_type
+{
+ HLS_PLAYLIST_TYPE_TS,
+ HLS_PLAYLIST_TYPE_WEBVTT,
+};
+
struct hls_config
{
char *base_url;
@@ -45,4 +51,9 @@ hls_config_IsMemStorageEnabled(const struct hls_config
*config)
return config->outdir == NULL;
}
+struct hls_sub_segmenter;
+sout_mux_t *CreateSubtitleSegmenter(sout_access_out_t *access,
+ const struct hls_config *config);
+void hls_sub_segmenter_SignalStreamUpdate(sout_mux_t *, vlc_tick_t);
+
#endif
=====================================
modules/stream_out/hls/segments.c
=====================================
@@ -42,12 +42,33 @@ static void hls_segment_Destroy(hls_segment_t *segment)
free(segment);
}
+static const char *
+hls_segment_queue_GetFileExtension(enum hls_playlist_type type)
+{
+ switch (type)
+ {
+ case HLS_PLAYLIST_TYPE_TS:
+ return "ts";
+ case HLS_PLAYLIST_TYPE_WEBVTT:
+ return "vtt";
+ default:
+ vlc_assert_unreachable();
+ }
+}
+
void hls_segment_queue_Init(hls_segment_queue_t *queue,
const struct hls_segment_queue_config *config,
const struct hls_config *hls_config)
{
- memcpy(&queue->config, config, sizeof(*config));
+ queue->playlist_id = config->playlist_id;
queue->total_segments = 0;
+
+ queue->httpd_ref = config->httpd_ref;
+ queue->httpd_callback = config->httpd_callback;
+
+ queue->file_extension =
+ hls_segment_queue_GetFileExtension(config->playlist_type);
+
queue->hls_config = hls_config;
vlc_list_init(&queue->segments);
@@ -71,10 +92,11 @@ int hls_segment_queue_NewSegment(hls_segment_queue_t *queue,
segment->length = length;
if (asprintf(&segment->url,
- "%s/playlist-%u-%u.ts",
+ "%s/playlist-%u-%u.%s",
queue->hls_config->base_url,
- queue->config.playlist_id,
- segment->id) == -1)
+ queue->playlist_id,
+ segment->id,
+ queue->file_extension) == -1)
{
segment->url = NULL;
goto nomem;
@@ -89,16 +111,16 @@ int hls_segment_queue_NewSegment(hls_segment_queue_t
*queue,
if (unlikely(segment->storage == NULL))
goto nomem;
- if (queue->config.httpd_ref != NULL)
+ if (queue->httpd_ref != NULL)
{
segment->http_url =
- httpd_UrlNew(queue->config.httpd_ref, segment->url, NULL, NULL);
+ httpd_UrlNew(queue->httpd_ref, segment->url, NULL, NULL);
if (segment->http_url == NULL)
goto nomem;
httpd_UrlCatch(segment->http_url,
HTTPD_MSG_GET,
- queue->config.httpd_callback,
+ queue->httpd_callback,
(httpd_callback_sys_t *)segment->storage);
}
else
=====================================
modules/stream_out/hls/segments.h
=====================================
@@ -39,6 +39,7 @@ typedef struct hls_segment
struct hls_segment_queue_config
{
unsigned int playlist_id;
+ enum hls_playlist_type playlist_type;
httpd_host_t *httpd_ref;
httpd_callback_t httpd_callback;
@@ -46,9 +47,14 @@ struct hls_segment_queue_config
typedef struct
{
- struct hls_segment_queue_config config;
+ unsigned int playlist_id;
unsigned int total_segments;
+ httpd_host_t *httpd_ref;
+ httpd_callback_t httpd_callback;
+
+ const char *file_extension;
+
const struct hls_config *hls_config;
struct vlc_list segments;
=====================================
modules/stream_out/hls/subtitles_segmenter.c
=====================================
@@ -0,0 +1,335 @@
+/*****************************************************************************
+ * subtitle_segmenter.c: Create subtitle segments
+ *****************************************************************************
+ * Copyright (C) 2024 VLC authors and VideoLAN
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <assert.h>
+
+#include <vlc_common.h>
+
+#include <vlc_block.h>
+#include <vlc_frame.h>
+#include <vlc_sout.h>
+#include <vlc_tick.h>
+
+#include "hls.h"
+
+/**
+ * The hls_sub_segmenter is a meta-muxer used to create subtitles segments. It
+ * handles subtitle frames splitting, re-creation of the subtitles muxer and
+ * empty segments creation when no data is available.
+ */
+struct hls_sub_segmenter
+{
+ sout_mux_t owner;
+
+ const struct hls_config *config;
+ sout_mux_t *spu_muxer;
+ sout_input_t *spu_muxer_input;
+ bool segment_empty;
+ vlc_tick_t last_segment;
+ vlc_tick_t last_stream_update;
+};
+
+static int hls_sub_segmenter_ResetMuxer(struct hls_sub_segmenter *segmenter,
+ sout_input_t *owner_input)
+{
+ if (segmenter->spu_muxer_input != NULL)
+ sout_MuxDeleteStream(segmenter->spu_muxer, segmenter->spu_muxer_input);
+ if (segmenter->spu_muxer != NULL)
+ sout_MuxDelete(segmenter->spu_muxer);
+
+ segmenter->segment_empty = true;
+
+ segmenter->spu_muxer = sout_MuxNew(segmenter->owner.p_access, "webvtt");
+ if (segmenter->spu_muxer == NULL)
+ return VLC_EGENERIC;
+
+ segmenter->spu_muxer_input =
+ sout_MuxAddStream(segmenter->spu_muxer, &owner_input->fmt);
+
+ // Disable mux caching.
+ segmenter->spu_muxer->b_waiting_stream = false;
+ return VLC_SUCCESS;
+}
+
+static int hls_sub_segmenter_EndSegment(struct hls_sub_segmenter *segmenter,
+ sout_input_t *input)
+{
+ segmenter->last_segment += segmenter->config->segment_length;
+ return hls_sub_segmenter_ResetMuxer(segmenter, input);
+}
+
+static int hls_sub_segmenter_EndEmptySegment(
+ struct hls_sub_segmenter *segmenter, sout_input_t *input, vlc_tick_t len)
+{
+ char *vtt_header = strdup("WEBVTT\n\n");
+ if (unlikely(vtt_header == NULL))
+ return VLC_ENOMEM;
+
+ block_t *empty_segment = block_heap_Alloc(vtt_header, strlen(vtt_header));
+ if (unlikely(empty_segment == NULL))
+ return VLC_ENOMEM;
+
+ empty_segment->i_flags |= BLOCK_FLAG_HEADER;
+ empty_segment->i_length = len;
+ if (sout_AccessOutWrite(segmenter->spu_muxer->p_access, empty_segment) < 0)
+ return VLC_EGENERIC;
+
+ return hls_sub_segmenter_EndSegment(segmenter, input);
+}
+
+static int hls_sub_segmenter_Add(sout_mux_t *mux, sout_input_t *input)
+{
+ struct hls_sub_segmenter *segmenter = mux->p_sys;
+ hls_sub_segmenter_ResetMuxer(segmenter, input);
+ return VLC_SUCCESS;
+}
+
+static int hls_sub_segmenter_MuxSend(struct hls_sub_segmenter *segmenter,
+ vlc_frame_t *spu)
+{
+ segmenter->segment_empty = false;
+ return sout_MuxSendBuffer(
+ segmenter->spu_muxer, segmenter->spu_muxer_input, spu);
+}
+
+static void hls_sub_segmenter_Del(sout_mux_t *mux, sout_input_t *input)
+{
+ struct hls_sub_segmenter *segmenter = mux->p_sys;
+
+ if (segmenter->last_segment != segmenter->last_stream_update)
+ {
+ const vlc_tick_t seglen =
+ segmenter->last_stream_update - segmenter->last_segment;
+
+ if (!vlc_fifo_IsEmpty(input->p_fifo))
+ {
+ /* Drain the last ephemeral SPU. */
+ vlc_frame_t *ephemeral = vlc_fifo_Get(input->p_fifo);
+ assert(ephemeral->i_length == VLC_TICK_INVALID);
+ ephemeral->i_length = seglen;
+ hls_sub_segmenter_MuxSend(segmenter, ephemeral);
+ }
+ else if (segmenter->segment_empty)
+ {
+ /* Drain an empty last segment. */
+ hls_sub_segmenter_EndEmptySegment(segmenter, input, seglen);
+ }
+ }
+
+ if (segmenter->spu_muxer_input != NULL)
+ sout_MuxDeleteStream(segmenter->spu_muxer, segmenter->spu_muxer_input);
+ if (segmenter->spu_muxer != NULL)
+ sout_MuxDelete(segmenter->spu_muxer);
+}
+
+static vlc_frame_t* hls_sub_segmenter_CutEphemeral(vlc_frame_t *ephemeral,
vlc_tick_t length)
+{
+ vlc_frame_t *cut = vlc_frame_Duplicate(ephemeral);
+ if (unlikely(cut == NULL))
+ return NULL;
+
+ cut->i_length = length;
+ ephemeral->i_pts += cut->i_length;
+ return cut;
+}
+
+/**
+ * Should be called at frequent PCR interval.
+ *
+ * Creates empty segments when no data is sent to the muxer for a time superior
+ * to segment length.
+ */
+void hls_sub_segmenter_SignalStreamUpdate(sout_mux_t *mux,
+ vlc_tick_t stream_time)
+{
+ if (mux->i_nb_inputs == 0)
+ return;
+
+ sout_input_t *owner_input = mux->pp_inputs[0];
+
+ struct hls_sub_segmenter *segmenter = mux->p_sys;
+ segmenter->last_stream_update = stream_time;
+ if (stream_time - segmenter->last_segment <
+ segmenter->config->segment_length)
+ return;
+
+ if (!vlc_fifo_IsEmpty(owner_input->p_fifo))
+ {
+ vlc_frame_t *ephemeral = vlc_fifo_Show(owner_input->p_fifo);
+
+ /* The segmenter should only keep ephemeral SPUs in queue. */
+ assert(ephemeral->i_length == VLC_TICK_INVALID);
+
+ const vlc_tick_t eph_len =
+ (segmenter->last_segment + segmenter->config->segment_length) -
+ ephemeral->i_pts;
+ vlc_frame_t *cut = hls_sub_segmenter_CutEphemeral(ephemeral, eph_len);
+ if (likely(cut != NULL))
+ hls_sub_segmenter_MuxSend(segmenter, cut);
+ }
+
+ if (!segmenter->segment_empty)
+ {
+ hls_sub_segmenter_EndSegment(segmenter, owner_input);
+ return;
+ }
+
+ hls_sub_segmenter_EndEmptySegment(
+ segmenter, owner_input, segmenter->config->segment_length);
+}
+
+static int
+hls_sub_segmenter_ProcessSingleFrame(struct hls_sub_segmenter *segmenter,
+ sout_input_t *owner_input,
+ vlc_frame_t *spu)
+{
+ const vlc_tick_t seglen = segmenter->config->segment_length;
+ if (spu->i_pts >= segmenter->last_segment + seglen)
+ hls_sub_segmenter_EndSegment(segmenter, owner_input);
+
+ // If the subtitle overlaps between segments, we have to split it.
+ while (spu->i_pts + spu->i_length > segmenter->last_segment + seglen)
+ {
+ vlc_frame_t *capped_spu = vlc_frame_Duplicate(spu);
+ if (unlikely(capped_spu == NULL))
+ {
+ vlc_frame_Release(spu);
+ return VLC_ENOMEM;
+ }
+
+ // Extra time not fitting in the current segment.
+ const vlc_tick_t extra_time =
+ (spu->i_pts - VLC_TICK_0 + spu->i_length) -
+ segmenter->last_segment - seglen;
+
+ // Shorten the copy length so it fits the current segment.
+ capped_spu->i_length -= extra_time;
+
+ if (spu->i_dts != VLC_TICK_INVALID)
+ spu->i_dts += capped_spu->i_length;
+ spu->i_pts += capped_spu->i_length;
+ spu->i_length = extra_time;
+
+ const int status = hls_sub_segmenter_MuxSend(segmenter, capped_spu);
+ if (status != VLC_SUCCESS)
+ {
+ vlc_frame_Release(spu);
+ return status;
+ }
+ hls_sub_segmenter_EndSegment(segmenter, owner_input);
+ }
+ return hls_sub_segmenter_MuxSend(segmenter, spu);
+}
+
+static int hls_sub_segmenter_Process(sout_mux_t *mux)
+{
+ struct hls_sub_segmenter *segmenter = mux->p_sys;
+
+ if (mux->i_nb_inputs == 0)
+ return VLC_SUCCESS;
+
+ sout_input_t *input = mux->pp_inputs[0];
+
+ vlc_fifo_Lock(input->p_fifo);
+ vlc_frame_t *chain = vlc_fifo_DequeueAllUnlocked(input->p_fifo);
+
+ int status = VLC_SUCCESS;
+ while (chain != NULL)
+ {
+ if (chain->i_length == VLC_TICK_INVALID)
+ {
+ /* Ephemeral SPUs length depends on the next SPUs timestamps. */
+ if (chain->p_next == NULL)
+ {
+ vlc_fifo_QueueUnlocked(input->p_fifo, chain);
+ break;
+ }
+ else
+ {
+ chain->i_length = chain->p_next->i_pts - chain->i_pts;
+ }
+ }
+
+ vlc_frame_t *next = chain->p_next;
+ chain->p_next = NULL;
+ status =
+ hls_sub_segmenter_ProcessSingleFrame(segmenter, input, chain);
+ if (status != VLC_SUCCESS)
+ {
+ vlc_frame_ChainRelease(next);
+ break;
+ }
+ chain = next;
+ }
+ vlc_fifo_Unlock(input->p_fifo);
+ return status;
+}
+
+static int hls_sub_segmenter_Control(sout_mux_t *mux, int query, va_list args)
+{
+ if (query != MUX_CAN_ADD_STREAM_WHILE_MUXING)
+ return VLC_ENOTSUP;
+
+ *(va_arg(args, bool *)) = false;
+ return VLC_SUCCESS;
+ (void)mux;
+}
+
+sout_mux_t *CreateSubtitleSegmenter(sout_access_out_t *access,
+ const struct hls_config *config)
+{
+ struct hls_sub_segmenter *segmenter =
+ vlc_object_create(access, sizeof(*segmenter));
+ if (unlikely(segmenter == NULL))
+ return NULL;
+
+ sout_mux_t *mux = &segmenter->owner;
+ mux->psz_mux = strdup("hls-sub-segmenter");
+ if (unlikely(mux->psz_mux == NULL))
+ {
+ vlc_object_delete(mux);
+ return NULL;
+ }
+
+ mux->p_module = NULL;
+ mux->p_cfg = NULL;
+ mux->i_nb_inputs = 0;
+ mux->pp_inputs = NULL;
+ mux->b_add_stream_any_time = false;
+ mux->b_waiting_stream = true;
+ mux->i_add_stream_start = VLC_TICK_INVALID;
+ mux->p_sys = segmenter;
+ mux->p_access = access;
+
+ mux->pf_addstream = hls_sub_segmenter_Add;
+ mux->pf_delstream = hls_sub_segmenter_Del;
+ mux->pf_mux = hls_sub_segmenter_Process;
+ mux->pf_control = hls_sub_segmenter_Control;
+
+ segmenter->config = config;
+ segmenter->spu_muxer = NULL;
+ segmenter->segment_empty = true;
+ segmenter->last_segment = VLC_TICK_0;
+ segmenter->last_stream_update = VLC_TICK_INVALID;
+ return &segmenter->owner;
+}
=====================================
test/Makefile.am
=====================================
@@ -57,6 +57,7 @@ check_PROGRAMS = \
test_modules_tls \
test_modules_stream_out_transcode \
test_modules_mux_webvtt \
+ test_modules_stream_out_hls_subtitles_segmenter \
$(NULL)
if HAVE_GL
@@ -303,6 +304,12 @@ test_modules_stream_out_pcr_sync_LDADD = $(LIBVLCCORE)
test_modules_mux_webvtt_SOURCES = modules/mux/webvtt.c
test_modules_mux_webvtt_LDADD = $(LIBVLCCORE) $(LIBVLC)
+test_modules_stream_out_hls_subtitles_segmenter_SOURCES = \
+ modules/stream_out/hls/subtitles_segmenter.c \
+ ../modules/stream_out/hls/hls.h \
+ ../modules/stream_out/hls/subtitles_segmenter.c
+test_modules_stream_out_hls_subtitles_segmenter_LDADD = $(LIBVLCCORE) $(LIBVLC)
+
checkall:
$(MAKE) check_PROGRAMS="$(check_PROGRAMS) $(EXTRA_PROGRAMS)" check
=====================================
test/modules/stream_out/hls/subtitles_segmenter.c
=====================================
@@ -0,0 +1,461 @@
+/*****************************************************************************
+ * subtitles_segmenter.c: HLS subtitle segmentation unit tests
+ *****************************************************************************
+ * Copyright (C) 2024 VLC authors and VideoLAN
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <vlc_common.h>
+
+#include <vlc_block.h>
+#include <vlc_plugin.h>
+#include <vlc_sout.h>
+
+#include "../../../libvlc/test.h"
+#include "../../modules/stream_out/hls/hls.h"
+#include "../lib/libvlc_internal.h"
+
+struct test_scenario
+{
+ vlc_tick_t segment_length;
+
+ unsigned bytes_reported;
+ const char *expected;
+
+ void (*report_sub)(block_t *);
+ void (*send_sub)(sout_mux_t *, sout_input_t *);
+};
+
+static struct test_scenario *CURRENT_SCENARIO = NULL;
+
+static ssize_t AccessOutWrite(sout_access_out_t *access, block_t *block)
+{
+ struct test_scenario *cb = access->p_sys;
+ assert(block->p_next == NULL);
+
+ cb->report_sub(block);
+
+ const ssize_t r = block->i_buffer;
+ block_ChainRelease(block);
+ return r;
+}
+
+static sout_access_out_t *CreateAccessOut(vlc_object_t *parent,
+ const struct test_scenario *cb)
+{
+ sout_access_out_t *access = vlc_object_create(parent, sizeof(*access));
+ if (unlikely(access == NULL))
+ return NULL;
+
+ access->psz_access = strdup("mock");
+ if (unlikely(access->psz_access == NULL))
+ {
+ vlc_object_delete(access);
+ return NULL;
+ }
+
+ access->p_cfg = NULL;
+ access->p_module = NULL;
+ access->p_sys = (void *)cb;
+ access->psz_path = NULL;
+
+ access->pf_control = NULL;
+ access->pf_read = NULL;
+ access->pf_seek = NULL;
+ access->pf_write = AccessOutWrite;
+ return access;
+}
+
+static vlc_frame_t *
+make_spu(const char *text, vlc_tick_t start, vlc_tick_t stop)
+{
+ char *owned_txt = strdup(text);
+ assert(owned_txt != NULL);
+
+ vlc_frame_t *spu = vlc_frame_heap_Alloc(owned_txt, strlen(owned_txt));
+ spu->i_pts = spu->i_dts = start + VLC_TICK_0;
+ if (stop != VLC_TICK_INVALID)
+ spu->i_length = stop - start;
+ else
+ spu->i_length = VLC_TICK_INVALID;
+ return spu;
+}
+static vlc_frame_t *make_ephemer_spu(const char *text, vlc_tick_t start)
+{
+ return make_spu(text, start, VLC_TICK_INVALID);
+}
+
+#define NO_SPLIT_EXPECTED
\
+ "WEBVTT\n\n"
\
+ "00:00:00.000 --> 00:00:03.000\n"
\
+ "Hello world\n\n"
+static void send_no_split(sout_mux_t *mux, sout_input_t *input)
+{
+ vlc_frame_t *spu = make_spu("Hello world", 0, VLC_TICK_FROM_SEC(3));
+ const int status = sout_MuxSendBuffer(mux, input, spu);
+ assert(status == VLC_SUCCESS);
+}
+
+#define ONE_SPLIT_EXPECTED
\
+ "WEBVTT\n\n"
\
+ "00:00:00.000 --> 00:00:04.000\n"
\
+ "Hello world\n\n"
\
+ "WEBVTT\n\n"
\
+ "00:00:04.000 --> 00:00:06.000\n"
\
+ "Hello world\n\n"
+static void send_one_split(sout_mux_t *mux, sout_input_t *input)
+{
+ vlc_frame_t *spu = make_spu("Hello world", 0, VLC_TICK_FROM_SEC(6));
+ const int status = sout_MuxSendBuffer(mux, input, spu);
+ assert(status == VLC_SUCCESS);
+}
+
+#define FIVE_SPLITS_EXPECTED
\
+ "WEBVTT\n\n"
\
+ "00:00:01.500 --> 00:00:02.000\n"
\
+ "Hello world\n\n"
\
+ "WEBVTT\n\n"
\
+ "00:00:02.000 --> 00:00:04.000\n"
\
+ "Hello world\n\n"
\
+ "WEBVTT\n\n"
\
+ "00:00:04.000 --> 00:00:06.000\n"
\
+ "Hello world\n\n"
\
+ "WEBVTT\n\n"
\
+ "00:00:06.000 --> 00:00:08.000\n"
\
+ "Hello world\n\n"
\
+ "WEBVTT\n\n"
\
+ "00:00:08.000 --> 00:00:09.500\n"
\
+ "Hello world\n\n"
+static void send_five_splits(sout_mux_t *mux, sout_input_t *input)
+{
+ vlc_frame_t *spu =
+ make_spu("Hello world", VLC_TICK_FROM_MS(1500),
VLC_TICK_FROM_MS(9500));
+ const int status = sout_MuxSendBuffer(mux, input, spu);
+ assert(status == VLC_SUCCESS);
+}
+
+#define NOTHING_FOR_FIVE_SEC_EXPECTED
\
+ "WEBVTT\n\n"
\
+ "WEBVTT\n\n"
\
+ "WEBVTT\n\n"
+static void send_nothing_for_five_sec(sout_mux_t *mux, sout_input_t *input)
+{
+ for (vlc_tick_t stream_time = VLC_TICK_0;
+ stream_time < VLC_TICK_FROM_MS(5300);
+ stream_time += VLC_TICK_FROM_MS(150))
+ {
+ hls_sub_segmenter_SignalStreamUpdate(mux, stream_time);
+ }
+ (void)input;
+}
+
+#define BEGIN_HOLE_EXPECTED
\
+ "WEBVTT\n\n"
\
+ "WEBVTT\n\n"
\
+ "WEBVTT\n\n"
\
+ "WEBVTT\n\n"
\
+ "00:00:07.500 --> 00:00:08.000\n"
\
+ "Hello world\n\n"
\
+ "WEBVTT\n\n"
\
+ "00:00:08.300 --> 00:00:10.000\n"
\
+ "Hello world\n\n"
\
+ "WEBVTT\n\n"
\
+ "00:00:10.000 --> 00:00:11.500\n"
\
+ "Hello world\n\n"
+static void send_begin_hole(sout_mux_t *mux, sout_input_t *input)
+{
+ for (vlc_tick_t stream_time = VLC_TICK_0;
+ stream_time < VLC_TICK_FROM_MS(6150);
+ stream_time += VLC_TICK_FROM_MS(150))
+ {
+ hls_sub_segmenter_SignalStreamUpdate(mux, stream_time);
+ }
+
+ vlc_frame_t *spu =
+ make_spu("Hello world", VLC_TICK_FROM_MS(7500),
VLC_TICK_FROM_MS(8000));
+ int status = sout_MuxSendBuffer(mux, input, spu);
+ assert(status == VLC_SUCCESS);
+
+ spu = make_spu(
+ "Hello world", VLC_TICK_FROM_MS(8300), VLC_TICK_FROM_MS(11500));
+ status = sout_MuxSendBuffer(mux, input, spu);
+ assert(status == VLC_SUCCESS);
+}
+
+#define MIDDLE_HOLE_EXPECTED
\
+ "WEBVTT\n\n"
\
+ "00:00:01.500 --> 00:00:02.000\n"
\
+ "Hello world\n\n"
\
+ "WEBVTT\n\n"
\
+ "00:00:02.000 --> 00:00:02.300\n"
\
+ "Hello world\n\n"
\
+ "00:00:02.300 --> 00:00:03.000\n"
\
+ "Hello world\n\n"
\
+ "WEBVTT\n\n"
\
+ "WEBVTT\n\n"
\
+ "WEBVTT\n\n"
\
+ "00:00:11.000 --> 00:00:11.500\n"
\
+ "Hello world\n\n"
+static void send_middle_hole(sout_mux_t *mux, sout_input_t *input)
+{
+ vlc_frame_t *spu =
+ make_spu("Hello world", VLC_TICK_FROM_MS(1500),
VLC_TICK_FROM_MS(2300));
+ int status = sout_MuxSendBuffer(mux, input, spu);
+ assert(status == VLC_SUCCESS);
+
+ spu =
+ make_spu("Hello world", VLC_TICK_FROM_MS(2300),
VLC_TICK_FROM_MS(3000));
+ status = sout_MuxSendBuffer(mux, input, spu);
+ assert(status == VLC_SUCCESS);
+
+ for (vlc_tick_t stream_time = VLC_TICK_FROM_MS(3000) + VLC_TICK_0;
+ stream_time < VLC_TICK_FROM_MS(10000);
+ stream_time += VLC_TICK_FROM_MS(150))
+ {
+ hls_sub_segmenter_SignalStreamUpdate(mux, stream_time);
+ }
+
+ spu = make_spu(
+ "Hello world", VLC_TICK_FROM_MS(11000), VLC_TICK_FROM_MS(11500));
+ status = sout_MuxSendBuffer(mux, input, spu);
+ assert(status == VLC_SUCCESS);
+}
+
+#define END_HOLE_EXPECTED
\
+ "WEBVTT\n\n"
\
+ "00:00:01.500 --> 00:00:02.000\n"
\
+ "Hello world\n\n"
\
+ "WEBVTT\n\n"
\
+ "00:00:02.000 --> 00:00:02.300\n"
\
+ "Hello world\n\n"
\
+ "00:00:02.300 --> 00:00:03.000\n"
\
+ "Hello world\n\n"
\
+ "WEBVTT\n\n"
\
+ "WEBVTT\n\n"
\
+ "WEBVTT\n\n"
+static void send_end_hole(sout_mux_t *mux, sout_input_t *input)
+{
+ vlc_frame_t *spu =
+ make_spu("Hello world", VLC_TICK_FROM_MS(1500),
VLC_TICK_FROM_MS(2300));
+ int status = sout_MuxSendBuffer(mux, input, spu);
+ assert(status == VLC_SUCCESS);
+
+ spu =
+ make_spu("Hello world", VLC_TICK_FROM_MS(2300),
VLC_TICK_FROM_MS(3000));
+ status = sout_MuxSendBuffer(mux, input, spu);
+ assert(status == VLC_SUCCESS);
+
+ for (vlc_tick_t stream_time = VLC_TICK_FROM_MS(3000) + VLC_TICK_0;
+ stream_time < VLC_TICK_FROM_MS(10000);
+ stream_time += VLC_TICK_FROM_MS(150))
+ {
+ hls_sub_segmenter_SignalStreamUpdate(mux, stream_time);
+ }
+}
+
+#define EPHEMER_NO_SPLIT_EXPECTED
\
+ "WEBVTT\n\n"
\
+ "00:00:01.500 --> 00:00:02.000\n"
\
+ "Hello world\n\n"
\
+ "WEBVTT\n\n"
\
+ "00:00:02.000 --> 00:00:02.300\n"
\
+ "Hello world\n\n"
\
+ "00:00:02.300 --> 00:00:03.100\n"
\
+ "Hello world\n\n"
\
+ "00:00:03.100 --> 00:00:04.000\n"
\
+ "Hello world\n\n"
\
+ "WEBVTT\n\n"
\
+ "00:00:04.000 --> 00:00:05.500\n"
\
+ "Hello world\n\n"
+static void send_ephemer_no_split(sout_mux_t *mux, sout_input_t *input)
+{
+ vlc_frame_t *spu = make_ephemer_spu("Hello world", VLC_TICK_FROM_MS(1500));
+ int status = sout_MuxSendBuffer(mux, input, spu);
+ assert(status == VLC_SUCCESS);
+
+ spu = make_ephemer_spu("Hello world", VLC_TICK_FROM_MS(2300));
+ status = sout_MuxSendBuffer(mux, input, spu);
+ assert(status == VLC_SUCCESS);
+ hls_sub_segmenter_SignalStreamUpdate(mux,
+ VLC_TICK_FROM_MS(2300) + VLC_TICK_0);
+
+ spu = make_ephemer_spu("Hello world", VLC_TICK_FROM_MS(3100));
+ status = sout_MuxSendBuffer(mux, input, spu);
+ assert(status == VLC_SUCCESS);
+ hls_sub_segmenter_SignalStreamUpdate(mux,
+ VLC_TICK_FROM_MS(3100) + VLC_TICK_0);
+
+ spu = make_ephemer_spu("Hello world", VLC_TICK_FROM_MS(4000));
+ status = sout_MuxSendBuffer(mux, input, spu);
+ assert(status == VLC_SUCCESS);
+ hls_sub_segmenter_SignalStreamUpdate(mux,
+ VLC_TICK_FROM_MS(4000) + VLC_TICK_0);
+
+ hls_sub_segmenter_SignalStreamUpdate(mux,
+ VLC_TICK_FROM_MS(5500) + VLC_TICK_0);
+}
+
+#define LONG_EPHEMER_EXPECTED
\
+ "WEBVTT\n\n"
\
+ "00:00:01.500 --> 00:00:02.000\n"
\
+ "Hello world\n\n"
\
+ "WEBVTT\n\n"
\
+ "00:00:02.000 --> 00:00:04.000\n"
\
+ "Hello world\n\n"
\
+ "WEBVTT\n\n"
\
+ "00:00:04.000 --> 00:00:06.000\n"
\
+ "Hello world\n\n"
\
+ "WEBVTT\n\n"
\
+ "00:00:06.000 --> 00:00:08.000\n"
\
+ "Hello world\n\n"
\
+ "WEBVTT\n\n"
\
+ "00:00:08.000 --> 00:00:09.500\n"
\
+ "Hello world\n\n"
+static void send_long_ephemer(sout_mux_t *mux, sout_input_t *input)
+{
+ vlc_frame_t *spu = make_ephemer_spu("Hello world", VLC_TICK_FROM_MS(1500));
+ int status = sout_MuxSendBuffer(mux, input, spu);
+ assert(status == VLC_SUCCESS);
+
+ for (vlc_tick_t stream_time = VLC_TICK_FROM_MS(1500);
+ stream_time <= VLC_TICK_FROM_MS(9500);
+ stream_time += VLC_TICK_FROM_MS(100))
+ {
+ hls_sub_segmenter_SignalStreamUpdate(mux, stream_time + VLC_TICK_0);
+ }
+}
+
+static void default_report(block_t *sub)
+{
+ const char *expected =
+ CURRENT_SCENARIO->expected + CURRENT_SCENARIO->bytes_reported;
+
+ const int cmp =
+ strncmp(expected, (const char *)sub->p_buffer, sub->i_buffer);
+ assert(cmp == 0);
+ CURRENT_SCENARIO->bytes_reported += sub->i_buffer;
+}
+
+static struct test_scenario TEST_SCENARIOS[] = {
+ {
+ .segment_length = VLC_TICK_FROM_SEC(4),
+ .send_sub = send_no_split,
+ .report_sub = default_report,
+ .expected = NO_SPLIT_EXPECTED,
+ },
+ {
+ .segment_length = VLC_TICK_FROM_SEC(4),
+ .send_sub = send_one_split,
+ .report_sub = default_report,
+ .expected = ONE_SPLIT_EXPECTED,
+ },
+ {
+ .segment_length = VLC_TICK_FROM_SEC(2),
+ .send_sub = send_five_splits,
+ .report_sub = default_report,
+ .expected = FIVE_SPLITS_EXPECTED,
+ },
+ {
+ .segment_length = VLC_TICK_FROM_SEC(2),
+ .send_sub = send_nothing_for_five_sec,
+ .report_sub = default_report,
+ .expected = NOTHING_FOR_FIVE_SEC_EXPECTED,
+ },
+ {
+ .segment_length = VLC_TICK_FROM_SEC(2),
+ .send_sub = send_begin_hole,
+ .report_sub = default_report,
+ .expected = BEGIN_HOLE_EXPECTED,
+ },
+ {
+ .segment_length = VLC_TICK_FROM_SEC(2),
+ .send_sub = send_middle_hole,
+ .report_sub = default_report,
+ .expected = MIDDLE_HOLE_EXPECTED,
+ },
+ {
+ .segment_length = VLC_TICK_FROM_SEC(2),
+ .send_sub = send_end_hole,
+ .report_sub = default_report,
+ .expected = END_HOLE_EXPECTED,
+ },
+ {
+ .segment_length = VLC_TICK_FROM_SEC(2),
+ .send_sub = send_ephemer_no_split,
+ .report_sub = default_report,
+ .expected = EPHEMER_NO_SPLIT_EXPECTED,
+ },
+ {
+ .segment_length = VLC_TICK_FROM_SEC(2),
+ .send_sub = send_long_ephemer,
+ .report_sub = default_report,
+ .expected = LONG_EPHEMER_EXPECTED,
+ },
+};
+
+static void RunTests(libvlc_instance_t *instance)
+{
+ for (size_t i = 0; i < ARRAY_SIZE(TEST_SCENARIOS); ++i)
+ {
+ CURRENT_SCENARIO = &TEST_SCENARIOS[i];
+ sout_access_out_t *access = CreateAccessOut(
+ VLC_OBJECT(instance->p_libvlc_int), CURRENT_SCENARIO);
+ assert(access != NULL);
+
+ const struct hls_config config = {
+ .segment_length = CURRENT_SCENARIO->segment_length,
+ };
+ sout_mux_t *mux = CreateSubtitleSegmenter(access, &config);
+ assert(mux != NULL);
+
+ es_format_t fmt;
+ es_format_Init(&fmt, SPU_ES, VLC_CODEC_TEXT);
+ sout_input_t *input = sout_MuxAddStream(mux, &fmt);
+ assert(input != NULL);
+
+ // Disable mux caching.
+ mux->b_waiting_stream = false;
+
+ CURRENT_SCENARIO->send_sub(mux, input);
+
+ sout_MuxDeleteStream(mux, input);
+
+ assert(CURRENT_SCENARIO->bytes_reported ==
+ strlen(CURRENT_SCENARIO->expected));
+
+ sout_MuxDelete(mux);
+ sout_AccessOutDelete(access);
+ }
+}
+
+int main(void)
+{
+ test_init();
+
+ const char *const args[] = {
+ "-vvv",
+ };
+ libvlc_instance_t *vlc = libvlc_new(ARRAY_SIZE(args), args);
+ if (vlc == NULL)
+ return 1;
+
+ RunTests(vlc);
+
+ libvlc_release(vlc);
+}
View it on GitLab:
https://code.videolan.org/videolan/vlc/-/compare/ef35d253cbac5ab9b902807761d807ac6a4c1348...39a6f9b4aeef8b06800d0b481b0f58be81d25bd0
--
View it on GitLab:
https://code.videolan.org/videolan/vlc/-/compare/ef35d253cbac5ab9b902807761d807ac6a4c1348...39a6f9b4aeef8b06800d0b481b0f58be81d25bd0
You're receiving this email because of your account on code.videolan.org.
VideoLAN code repository instance
_______________________________________________
vlc-commits mailing list
vlc-commits@videolan.org
https://mailman.videolan.org/listinfo/vlc-commits