[vlc-commits] [Git][videolan/vlc][master] 4 commits: codec: webvtt: move typedefs
Steve Lhomme (@robUx4)
gitlab at videolan.org
Thu Jan 11 21:00:23 UTC 2024
Steve Lhomme pushed to branch master at VideoLAN / VLC
Commits:
132dcb05 by François Cartegnie at 2024-01-11T19:06:39+00:00
codec: webvtt: move typedefs
- - - - -
733f0e5e by François Cartegnie at 2024-01-11T19:06:39+00:00
mux: implement webvtt
A generic raw WebVTT output muxer. It only handles ES encoded with the
VTT encoder for now.
Co-authored-by: Alaric Senat <dev.asenat at posteo.net>
- - - - -
02cb3ffe by Alaric Senat at 2024-01-11T19:06:39+00:00
mux: vtt: allow muxing from raw text
It should be possible to mux raw text directly without having to use a
VTT encoder. For context, both the HLS and whisper stream output modules
will eventually output raw text subtitles.
- - - - -
7f1887b4 by Alaric Senat at 2024-01-11T19:06:39+00:00
test: mux: add webvtt muxer unit tests
- - - - -
9 changed files:
- modules/codec/Makefile.am
- modules/codec/meson.build
- modules/codec/webvtt/subsvtt.c
- modules/codec/webvtt/webvtt.c
- modules/codec/webvtt/webvtt.h
- + modules/mux/webvtt.c
- test/Makefile.am
- test/modules/meson.build
- + test/modules/mux/webvtt.c
Changes:
=====================================
modules/codec/Makefile.am
=====================================
@@ -233,7 +233,8 @@ libwebvtt_plugin_la_SOURCES = codec/webvtt/subsvtt.c \
demux/webvtt.c \
demux/mp4/minibox.h
if ENABLE_SOUT
-libwebvtt_plugin_la_SOURCES += codec/webvtt/encvtt.c
+libwebvtt_plugin_la_SOURCES += codec/webvtt/encvtt.c \
+ mux/webvtt.c
endif
codec_LTLIBRARIES += libwebvtt_plugin.la
if ENABLE_CSS
=====================================
modules/codec/meson.build
=====================================
@@ -400,7 +400,10 @@ webvtt_sources = files(
)
if get_option('stream_outputs')
- webvtt_sources += files('webvtt/encvtt.c')
+ webvtt_sources += files(
+ 'webvtt/encvtt.c',
+ '../mux/webvtt.c',
+ )
endif
# WebVTT CSS engine requires Flex and Bison
=====================================
modules/codec/webvtt/subsvtt.c
=====================================
@@ -161,13 +161,6 @@ typedef struct
#endif
} decoder_sys_t;
-#define ATOM_iden VLC_FOURCC('i', 'd', 'e', 'n')
-#define ATOM_payl VLC_FOURCC('p', 'a', 'y', 'l')
-#define ATOM_sttg VLC_FOURCC('s', 't', 't', 'g')
-#define ATOM_vttc VLC_FOURCC('v', 't', 't', 'c')
-#define ATOM_vtte VLC_FOURCC('v', 't', 't', 'e')
-#define ATOM_vttx VLC_FOURCC('v', 't', 't', 'x')
-
/*****************************************************************************
*
*****************************************************************************/
=====================================
modules/codec/webvtt/webvtt.c
=====================================
@@ -61,6 +61,12 @@ vlc_module_begin ()
set_capability( "spu encoder", 101 )
set_subcategory( SUBCAT_INPUT_SCODEC )
set_callback( webvtt_OpenEncoder )
+ add_submodule()
+ set_description( N_("Raw WebVTT muxer") )
+ set_capability( "sout mux", 0 )
+ set_subcategory( SUBCAT_SOUT_MUX )
+ add_shortcut( "webvtt", "rawvtt" )
+ set_callbacks( webvtt_OpenMuxer, webvtt_CloseMuxer )
#endif
vlc_module_end ()
=====================================
modules/codec/webvtt/webvtt.h
=====================================
@@ -31,8 +31,18 @@ void webvtt_CloseDemux ( vlc_object_t * );
#ifdef ENABLE_SOUT
int webvtt_OpenEncoder ( vlc_object_t * );
+
+int webvtt_OpenMuxer ( vlc_object_t * );
+void webvtt_CloseMuxer ( vlc_object_t * );
#endif
+#define ATOM_iden VLC_FOURCC('i', 'd', 'e', 'n')
+#define ATOM_payl VLC_FOURCC('p', 'a', 'y', 'l')
+#define ATOM_sttg VLC_FOURCC('s', 't', 't', 'g')
+#define ATOM_vttc VLC_FOURCC('v', 't', 't', 'c')
+#define ATOM_vtte VLC_FOURCC('v', 't', 't', 'e')
+#define ATOM_vttx VLC_FOURCC('v', 't', 't', 'x')
+
typedef struct webvtt_text_parser_t webvtt_text_parser_t;
enum webvtt_header_line_e
=====================================
modules/mux/webvtt.c
=====================================
@@ -0,0 +1,280 @@
+/*****************************************************************************
+ * webvtt.c: muxer for raw WEBVTT
+ *****************************************************************************
+ * Copyright (C) 2024 VideoLabs, 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_codec.h>
+#include <vlc_memstream.h>
+#include <vlc_plugin.h>
+#include <vlc_sout.h>
+
+#include "../codec/webvtt/webvtt.h"
+#include "../demux/mp4/minibox.h"
+
+typedef struct
+{
+ bool header_done;
+ const sout_input_t *input;
+} sout_mux_sys_t;
+
+static void OutputTime(struct vlc_memstream *ms, vlc_tick_t time)
+{
+ const vlc_tick_t secs = SEC_FROM_VLC_TICK(time);
+ vlc_memstream_printf(ms,
+ "%02" PRIi64 ":%02" PRIi64 ":%02" PRIi64 ".%03" PRIi64,
+ secs / 3600,
+ secs % 3600 / 60,
+ secs % 60,
+ (time % CLOCK_FREQ) / 1000);
+}
+
+static block_t *FormatCue(const webvtt_cue_t *cue)
+{
+ struct vlc_memstream ms;
+ if (vlc_memstream_open(&ms))
+ return NULL;
+
+ if (cue->psz_id != NULL)
+ vlc_memstream_printf(&ms, "%s\n", cue->psz_id);
+
+ OutputTime(&ms, cue->i_start);
+ vlc_memstream_printf(&ms, " --> ");
+ OutputTime(&ms, cue->i_stop);
+
+ if (cue->psz_attrs != NULL)
+ vlc_memstream_printf(&ms, " %s\n", cue->psz_attrs);
+ else
+ vlc_memstream_putc(&ms, '\n');
+
+ vlc_memstream_printf(&ms, "%s\n\n", cue->psz_text);
+
+ if (vlc_memstream_close(&ms))
+ return NULL;
+
+ block_t *formatted = block_heap_Alloc(ms.ptr, ms.length);
+ formatted->i_length = cue->i_stop - cue->i_start;
+ formatted->i_pts = formatted->i_dts = cue->i_stop - cue->i_start;
+ return formatted;
+}
+
+static void UnpackISOBMFF(const vlc_frame_t *packed, webvtt_cue_t *out)
+{
+ mp4_box_iterator_t it;
+ mp4_box_iterator_Init(&it, packed->p_buffer, packed->i_buffer);
+ while (mp4_box_iterator_Next(&it))
+ {
+ if (it.i_type != ATOM_vttc && it.i_type != ATOM_vttx)
+ continue;
+
+ mp4_box_iterator_t vtcc;
+ mp4_box_iterator_Init(&vtcc, it.p_payload, it.i_payload);
+ while (mp4_box_iterator_Next(&vtcc))
+ {
+ switch (vtcc.i_type)
+ {
+ case ATOM_iden:
+ if (out->psz_id == NULL)
+ out->psz_id =
+ strndup((char *)vtcc.p_payload, vtcc.i_payload);
+ break;
+ case ATOM_sttg:
+ if (out->psz_attrs)
+ {
+ char *dup = strndup((char *)vtcc.p_payload, vtcc.i_payload);
+ if (dup)
+ {
+ char *psz;
+ if (asprintf(&psz, "%s %s", out->psz_attrs, dup) >= 0)
+ {
+ free(out->psz_attrs);
+ out->psz_attrs = psz;
+ }
+ free(dup);
+ }
+ }
+ else
+ out->psz_attrs =
+ strndup((char *)vtcc.p_payload, vtcc.i_payload);
+ break;
+ case ATOM_payl:
+ if (!out->psz_text)
+ out->psz_text =
+ strndup((char *)vtcc.p_payload, vtcc.i_payload);
+ break;
+ }
+ }
+ }
+}
+
+static int Control(sout_mux_t *mux, int query, va_list args)
+{
+ switch (query)
+ {
+ case MUX_CAN_ADD_STREAM_WHILE_MUXING:
+ *(va_arg(args, bool *)) = false;
+ return VLC_SUCCESS;
+ default:
+ return VLC_ENOTSUP;
+ }
+ (void)mux;
+}
+
+static int AddStream(sout_mux_t *mux, sout_input_t *input)
+{
+ sout_mux_sys_t *sys = mux->p_sys;
+ if ((input->fmt.i_codec != VLC_CODEC_WEBVTT &&
+ input->fmt.i_codec != VLC_CODEC_TEXT) ||
+ sys->input)
+ return VLC_ENOTSUP;
+ sys->input = input;
+ return VLC_SUCCESS;
+}
+
+static void DelStream(sout_mux_t *mux, sout_input_t *input)
+{
+ sout_mux_sys_t *sys = mux->p_sys;
+ if (input == sys->input)
+ sys->input = NULL;
+}
+
+static int Mux(sout_mux_t *mux)
+{
+ sout_mux_sys_t *sys = mux->p_sys;
+
+ if (mux->i_nb_inputs == 0)
+ return VLC_SUCCESS;
+
+ sout_input_t *input = mux->pp_inputs[0];
+
+ if (!sys->header_done)
+ {
+ block_t *data = NULL;
+ if (input->fmt.i_extra > 8 && !memcmp(input->fmt.p_extra, "WEBVTT", 6))
+ {
+ data = block_Alloc(input->fmt.i_extra + 2);
+
+ if (unlikely(data == NULL))
+ return VLC_ENOMEM;
+ memcpy(data->p_buffer, input->fmt.p_extra, input->fmt.i_extra);
+ data->p_buffer[data->i_buffer - 2] = '\n';
+ data->p_buffer[data->i_buffer - 1] = '\n';
+ }
+ else
+ {
+ data = block_Alloc(8);
+ if (unlikely(data == NULL))
+ return VLC_ENOMEM;
+ memcpy(data->p_buffer, "WEBVTT\n\n", 8);
+ }
+
+ if (data)
+ sout_AccessOutWrite(mux->p_access, data);
+ sys->header_done = true;
+ }
+
+ vlc_fifo_Lock(input->p_fifo);
+ vlc_frame_t *chain = vlc_fifo_DequeueAllUnlocked(input->p_fifo);
+ int status = VLC_SUCCESS;
+ while (chain != NULL)
+ {
+ // Ephemer subtitles stop being displayed at the next SPU display time.
+ // We need to delay if subsequent SPU aren't available yet.
+ const bool is_ephemer = (chain->i_length == VLC_TICK_INVALID);
+ if (is_ephemer && chain->p_next == NULL)
+ {
+ vlc_fifo_QueueUnlocked(input->p_fifo, chain);
+ chain = NULL;
+ break;
+ }
+
+ webvtt_cue_t cue = {};
+
+ if (sys->input->fmt.i_codec != VLC_CODEC_WEBVTT)
+ cue.psz_text = strndup((char *)chain->p_buffer, chain->i_buffer);
+ else
+ UnpackISOBMFF(chain, &cue);
+
+ if (unlikely(cue.psz_text == NULL))
+ {
+ webvtt_cue_Clean(&cue);
+ status = VLC_ENOMEM;
+ break;
+ }
+
+ cue.i_start = chain->i_pts - VLC_TICK_0;
+ if(is_ephemer)
+ cue.i_stop = chain->p_next->i_pts - VLC_TICK_0;
+ else
+ cue.i_stop = cue.i_start + chain->i_length;
+ block_t *to_write = FormatCue(&cue);
+ webvtt_cue_Clean(&cue);
+
+ ssize_t written = -1;
+ if (likely(to_write != NULL))
+ written = sout_AccessOutWrite(mux->p_access, to_write);
+ if (written == -1)
+ {
+ status = VLC_EGENERIC;
+ break;
+ }
+
+ vlc_frame_t *next = chain->p_next;
+ vlc_frame_Release(chain);
+ chain = next;
+ }
+
+ vlc_frame_ChainRelease(chain);
+ vlc_fifo_Unlock(input->p_fifo);
+ return status;
+}
+/*****************************************************************************
+ * Open:
+ *****************************************************************************/
+int webvtt_OpenMuxer(vlc_object_t *this)
+{
+ sout_mux_t *mux = (sout_mux_t *)this;
+ sout_mux_sys_t *sys;
+
+ mux->p_sys = sys = malloc(sizeof(*sys));
+ if (unlikely(sys == NULL))
+ return VLC_ENOMEM;
+
+ sys->header_done = false;
+ sys->input = NULL;
+
+ mux->pf_control = Control;
+ mux->pf_addstream = AddStream;
+ mux->pf_delstream = DelStream;
+ mux->pf_mux = Mux;
+
+ return VLC_SUCCESS;
+}
+
+/*****************************************************************************
+ * Close:
+ *****************************************************************************/
+void webvtt_CloseMuxer(vlc_object_t *this)
+{
+ sout_mux_t *mux = (sout_mux_t *)this;
+ free(mux->p_sys);
+}
=====================================
test/Makefile.am
=====================================
@@ -56,6 +56,7 @@ check_PROGRAMS = \
test_modules_stream_out_pcr_sync \
test_modules_tls \
test_modules_stream_out_transcode \
+ test_modules_mux_webvtt \
$(NULL)
if HAVE_GL
@@ -299,6 +300,9 @@ test_modules_stream_out_pcr_sync_SOURCES = modules/stream_out/pcr_sync.c \
../modules/stream_out/transcode/pcr_helper.c
test_modules_stream_out_pcr_sync_LDADD = $(LIBVLCCORE)
+test_modules_mux_webvtt_SOURCES = modules/mux/webvtt.c
+test_modules_mux_webvtt_LDADD = $(LIBVLCCORE) $(LIBVLC)
+
checkall:
$(MAKE) check_PROGRAMS="$(check_PROGRAMS) $(EXTRA_PROGRAMS)" check
=====================================
test/modules/meson.build
=====================================
@@ -165,3 +165,11 @@ vlc_tests += {
'link_with' : [libvlccore],
'module_depends' : vlc_plugins_targets.keys()
}
+
+vlc_tests += {
+ 'name' : 'test_modules_mux_webvtt',
+ 'sources' : files('mux/webvtt.c'),
+ 'suite' : ['modules', 'test_modules'],
+ 'link_with' : [libvlc, libvlccore],
+ 'module_depends' : vlc_plugins_targets.keys()
+}
=====================================
test/modules/mux/webvtt.c
=====================================
@@ -0,0 +1,207 @@
+/*****************************************************************************
+ * webvtt.c: WebVTT Muxer unit testing
+ *****************************************************************************
+ * Copyright (C) 2024 VideoLabs, VideoLAN and VLC Authors
+ *
+ * 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 "../lib/libvlc_internal.h"
+
+struct test_scenario
+{
+ vlc_fourcc_t codec;
+
+ 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 void send_basic(sout_mux_t *mux, sout_input_t *input)
+{
+ for (vlc_tick_t time = VLC_TICK_0; time < VLC_TICK_FROM_MS(1300);
+ time += VLC_TICK_FROM_MS(300))
+ {
+ char *txt = strdup("Hello world");
+ assert(txt != NULL);
+
+ vlc_frame_t *spu = vlc_frame_heap_Alloc(txt, strlen(txt));
+ assert(spu != NULL);
+ spu->i_pts = time + VLC_TICK_FROM_SEC(15 * 60);
+ spu->i_length = VLC_TICK_INVALID;
+ const int status = sout_MuxSendBuffer(mux, input, spu);
+ assert(status == VLC_SUCCESS);
+ }
+}
+
+static void send_ephemer(sout_mux_t *mux, sout_input_t *input)
+{
+ for (vlc_tick_t time = VLC_TICK_0; time < VLC_TICK_FROM_MS(300);
+ time += VLC_TICK_FROM_MS(100))
+ {
+ char *txt = strdup("Hello world");
+ assert(txt != NULL);
+
+ vlc_frame_t *spu = vlc_frame_heap_Alloc(txt, strlen(txt));
+ assert(spu != NULL);
+ spu->i_pts = time;
+ spu->i_length = VLC_TICK_INVALID;
+ const int status = sout_MuxSendBuffer(mux, input, spu);
+ assert(status == VLC_SUCCESS);
+ }
+}
+
+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;
+}
+
+#define BASIC_TEXT \
+ "WEBVTT\n\n" \
+ "00:15:00.000 --> 00:15:00.300\n" \
+ "Hello world\n\n" \
+ "00:15:00.300 --> 00:15:00.600\n" \
+ "Hello world\n\n" \
+ "00:15:00.600 --> 00:15:00.900\n" \
+ "Hello world\n\n" \
+ "00:15:00.900 --> 00:15:01.200\n" \
+ "Hello world\n\n"
+
+#define EPHEMER_TEXT \
+ "WEBVTT\n\n" \
+ "00:00:00.000 --> 00:00:00.100\n" \
+ "Hello world\n\n" \
+ "00:00:00.100 --> 00:00:00.200\n" \
+ "Hello world\n\n"
+
+static struct test_scenario TEST_SCENARIOS[] = {
+ {
+ .codec = VLC_CODEC_TEXT,
+ .send_sub = send_basic,
+ .report_sub = default_report,
+ .expected = BASIC_TEXT,
+ },
+ {
+ .codec = VLC_CODEC_TEXT,
+ .send_sub = send_ephemer,
+ .report_sub = default_report,
+ .expected = EPHEMER_TEXT,
+ },
+};
+
+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);
+
+ sout_mux_t *mux = sout_MuxNew(access, "webvtt");
+ assert(mux != NULL);
+
+ es_format_t fmt;
+ es_format_Init(&fmt, SPU_ES, CURRENT_SCENARIO->codec);
+ 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);
+
+ assert(CURRENT_SCENARIO->bytes_reported ==
+ strlen(CURRENT_SCENARIO->expected));
+
+ sout_MuxDeleteStream(mux, input);
+ 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/058ded5ac0047935724fab69aac0f04699cfd6da...7f1887b4c460ee4353c3e5eac5ed258f478f0f63
--
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/058ded5ac0047935724fab69aac0f04699cfd6da...7f1887b4c460ee4353c3e5eac5ed258f478f0f63
You're receiving this email because of your account on code.videolan.org.
VideoLAN code repository instance
More information about the vlc-commits
mailing list