[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