[vlc-devel] [PATCH] misc: background_worker: make the background worker multithreaded
Rémi Denis-Courmont
remi at remlab.net
Tue Jul 17 18:06:19 CEST 2018
Hello,
Any default limit larger than one will, with large enough user base, cause failures and performance problems. Of course, a default limit of one makes the feature mostly useless. A rock and a hard place...
However, the first problem is the lack of timeout IIRC - not the lack of parallelism. Spawn however many threads, and without timeout, you might not make any progress anyway.
Le 17 juillet 2018 14:24:56 GMT+03:00, Romain Vimont <rom1v at videolabs.io> a écrit :
>A way to speed up the preparsing consists in preparsing several inputs
>in parallel.
>
>For this purpose, make the background worker (used by the preparser and
>fetcher) execute tasks from (possibly) several threads.
>
>Apart from adding a new field "max_threads" in the
>background_worker_config structure, the background worker API is kept
>unchanged.
>
>Two new options are added to configure the maximum number of threads
>used for preparsing and fetching:
> - preparse-threads
> - fetch-art-threads
>---
>This patch depends on:
> input: preparser: prepare for multithreaded background worker
><https://mailman.videolan.org/pipermail/vlc-devel/2018-July/120394.html>
>
> src/libvlc-module.c | 14 ++
> src/misc/background_worker.c | 435 +++++++++++++++++++++--------------
> src/misc/background_worker.h | 20 ++
> src/preparser/fetcher.c | 1 +
> src/preparser/preparser.c | 1 +
> 5 files changed, 303 insertions(+), 168 deletions(-)
>
>diff --git a/src/libvlc-module.c b/src/libvlc-module.c
>index 82862ab4745..cac63239c65 100644
>--- a/src/libvlc-module.c
>+++ b/src/libvlc-module.c
>@@ -1125,6 +1125,14 @@ static const char *const ppsz_prefres[] = {
> #define PREPARSE_TIMEOUT_LONGTEXT N_( \
> "Maximum time allowed to preparse an item, in milliseconds" )
>
>+#define PREPARSE_THREADS_TEXT N_( "Preparsing threads" )
>+#define PREPARSE_THREADS_LONGTEXT N_( \
>+ "Maximum number of threads used to preparse items" )
>+
>+#define FETCH_ART_THREADS_TEXT N_( "Fetch-art threads" )
>+#define FETCH_ART_THREADS_LONGTEXT N_( \
>+ "Maximum number of threads used to fetch art" )
>+
> #define METADATA_NETWORK_TEXT N_( "Allow metadata network access" )
>
> static const char *const psz_recursive_list[] = {
>@@ -2110,6 +2118,12 @@ vlc_module_begin ()
> add_integer( "preparse-timeout", 5000, PREPARSE_TIMEOUT_TEXT,
> PREPARSE_TIMEOUT_LONGTEXT, false )
>
>+ add_integer( "preparse-threads", 2, PREPARSE_THREADS_TEXT,
>+ PREPARSE_THREADS_LONGTEXT, false )
>+
>+ add_integer( "fetch-art-threads", 2, FETCH_ART_THREADS_TEXT,
>+ FETCH_ART_THREADS_LONGTEXT, false )
>+
> add_obsolete_integer( "album-art" )
> add_bool( "metadata-network-access", false, METADATA_NETWORK_TEXT,
> METADATA_NETWORK_TEXT, false )
>diff --git a/src/misc/background_worker.c
>b/src/misc/background_worker.c
>index 2f21e5006a3..1f2796e5d39 100644
>--- a/src/misc/background_worker.c
>+++ b/src/misc/background_worker.c
>@@ -22,246 +22,345 @@
>
> #include <assert.h>
> #include <vlc_common.h>
>+#include <vlc_atomic.h>
>+#include <vlc_list.h>
> #include <vlc_threads.h>
>-#include <vlc_arrays.h>
>
> #include "libvlc.h"
> #include "background_worker.h"
>
>-struct bg_queued_item {
>+struct task {
>+ struct vlc_list node;
> void* id; /**< id associated with entity */
> void* entity; /**< the entity to process */
>- int timeout; /**< timeout duration in microseconds */
>+ int timeout; /**< timeout duration in milliseconds */
>+};
>+
>+struct background_worker;
>+
>+struct background_thread {
>+ struct background_worker *owner;
>+ vlc_cond_t probe_cancel_wait; /**< wait for probe request or
>cancelation */
>+ bool probe; /**< true if a probe is requested */
>+ bool cancel; /**< true if a cancel is requested */
>+ struct task *task; /**< current task */
>+ struct vlc_list node;
> };
>
> struct background_worker {
> void* owner;
> struct background_worker_config conf;
>
>- vlc_mutex_t lock; /**< acquire to inspect members that follow */
>- struct {
>- bool probe_request; /**< true if a probe is requested */
>- vlc_cond_t wait; /**< wait for update in terms of head */
>- vlc_cond_t worker_wait; /**< wait for probe request or
>cancelation */
>- vlc_tick_t deadline; /**< deadline of the current task */
>- void* id; /**< id of the current task */
>- bool active; /**< true if there is an active thread */
>- } head;
>+ vlc_mutex_t lock;
>
>- struct {
>- vlc_cond_t wait; /**< wait for update in terms of tail */
>- vlc_array_t data; /**< queue of pending entities to process */
>- } tail;
>+ int uncompleted; /**< number of tasks requested but not completed
>*/
>+ int nthreads; /**< number of threads in the threads list */
>+ struct vlc_list threads; /**< list of active background_thread
>instances */
>+
>+ struct vlc_list queue; /**< queue of tasks */
>+ vlc_cond_t queue_wait; /**< wait for the queue to be non-empty */
>+
>+ vlc_cond_t nothreads_wait; /**< wait for nthreads == 0 */
>+ bool closing; /**< true if background worker deletion is requested
>*/
> };
>
>+static struct task *task_Create(struct background_worker *worker, void
>*id,
>+ void *entity, int timeout)
>+{
>+ struct task *task = malloc(sizeof(*task));
>+ if (unlikely(!task))
>+ return NULL;
>+
>+ task->id = id;
>+ task->entity = entity;
>+ task->timeout = timeout < 0 ? worker->conf.default_timeout :
>timeout;
>+ worker->conf.pf_hold(task->entity);
>+ return task;
>+}
>+
>+static void task_Destroy(struct background_worker *worker, struct task
>*task)
>+{
>+ worker->conf.pf_release(task->entity);
>+ free(task);
>+}
>+
>+static struct task *QueueTake(struct background_worker *worker, int
>timeout_ms)
>+{
>+ vlc_assert_locked(&worker->lock);
>+
>+ vlc_tick_t deadline = vlc_tick_now() +
>VLC_TICK_FROM_MS(timeout_ms);
>+ bool timeout = false;
>+ while (!timeout && !worker->closing &&
>vlc_list_is_empty(&worker->queue))
>+ timeout = vlc_cond_timedwait(&worker->queue_wait,
>+ &worker->lock, deadline) != 0;
>+
>+ if (worker->closing || timeout)
>+ return NULL;
>+
>+ struct task *task = vlc_list_first_entry_or_null(&worker->queue,
>+ struct task,
>node);
>+ assert(task);
>+ vlc_list_remove(&task->node);
>+
>+ return task;
>+}
>+
>+static void QueuePush(struct background_worker *worker, struct task
>*task)
>+{
>+ vlc_assert_locked(&worker->lock);
>+ vlc_list_append(&task->node, &worker->queue);
>+ vlc_cond_signal(&worker->queue_wait);
>+}
>+
>+static void QueueRemoveAll(struct background_worker *worker, void *id)
>+{
>+ vlc_assert_locked(&worker->lock);
>+ struct task *task;
>+ vlc_list_foreach(task, &worker->queue, node)
>+ {
>+ if (!id || task->id == id)
>+ {
>+ vlc_list_remove(&task->node);
>+ task_Destroy(worker, task);
>+ }
>+ }
>+}
>+
>+static struct background_thread *
>+background_thread_Create(struct background_worker *owner)
>+{
>+ struct background_thread *thread = malloc(sizeof(*thread));
>+ if (!thread)
>+ return NULL;
>+
>+ vlc_cond_init(&thread->probe_cancel_wait);
>+ thread->probe = false;
>+ thread->cancel = false;
>+ thread->task = NULL;
>+ thread->owner = owner;
>+ return thread;
>+}
>+
>+static void background_thread_Destroy(struct background_thread
>*thread)
>+{
>+ vlc_cond_destroy(&thread->probe_cancel_wait);
>+ free(thread);
>+}
>+
>+static struct background_worker *background_worker_Create(void *owner,
>+ struct
>background_worker_config *conf)
>+{
>+ struct background_worker* worker = malloc(sizeof(*worker));
>+ if (unlikely(!worker))
>+ return NULL;
>+
>+ worker->conf = *conf;
>+ worker->owner = owner;
>+
>+ vlc_mutex_init(&worker->lock);
>+ worker->uncompleted = 0;
>+ worker->nthreads = 0;
>+ vlc_list_init(&worker->threads);
>+ vlc_list_init(&worker->queue);
>+ vlc_cond_init(&worker->queue_wait);
>+ vlc_cond_init(&worker->nothreads_wait);
>+ worker->closing = false;
>+ return worker;
>+}
>+
>+static void background_worker_Destroy(struct background_worker
>*worker)
>+{
>+ vlc_cond_destroy(&worker->queue_wait);
>+ vlc_mutex_destroy(&worker->lock);
>+ free(worker);
>+}
>+
>+static void EndTask(struct background_thread *thread, struct task
>*task)
>+{
>+ struct background_worker *worker = thread->owner;
>+ task_Destroy(worker, task);
>+
>+ vlc_mutex_lock(&worker->lock);
>+ thread->task = NULL;
>+ worker->uncompleted--;
>+ assert(worker->uncompleted >= 0);
>+ vlc_mutex_unlock(&worker->lock);
>+}
>+
>+static void RemoveThread(struct background_thread *thread)
>+{
>+ struct background_worker *worker = thread->owner;
>+
>+ vlc_mutex_lock(&worker->lock);
>+
>+ vlc_list_remove(&thread->node);
>+ worker->nthreads--;
>+ assert(worker->nthreads >= 0);
>+ if (!worker->nthreads)
>+ vlc_cond_signal(&worker->nothreads_wait);
>+
>+ vlc_mutex_unlock(&worker->lock);
>+
>+ background_thread_Destroy(thread);
>+}
>+
> static void* Thread( void* data )
> {
>- struct background_worker* worker = data;
>+ struct background_thread *thread = data;
>+ struct background_worker *worker = thread->owner;
>
>- for( ;; )
>+ for (;;)
> {
>- struct bg_queued_item* item = NULL;
>- void* handle;
>-
>- vlc_mutex_lock( &worker->lock );
>- for( ;; )
>- {
>- if( vlc_array_count( &worker->tail.data ) )
>- {
>- item = vlc_array_item_at_index( &worker->tail.data, 0
>);
>- handle = NULL;
>-
>- vlc_array_remove( &worker->tail.data, 0 );
>- }
>-
>- if( worker->head.deadline == VLC_TICK_INVALID && item ==
>NULL )
>- worker->head.active = false;
>- worker->head.id = item ? item->id : NULL;
>- vlc_cond_broadcast( &worker->head.wait );
>-
>- if( item )
>- {
>- if( item->timeout > 0 )
>- worker->head.deadline = vlc_tick_now() +
>item->timeout * 1000;
>- else
>- worker->head.deadline = INT64_MAX;
>- }
>- else if( worker->head.deadline != VLC_TICK_INVALID )
>- {
>- /* Wait 1 seconds for new inputs before terminating */
>- vlc_tick_t deadline = vlc_tick_now() +
>VLC_TICK_FROM_SEC(1);
>- int ret = vlc_cond_timedwait( &worker->tail.wait,
>- &worker->lock, deadline
>);
>- if( ret != 0 )
>- {
>- /* Timeout: if there is still no items, the thread
>will be
>- * terminated at next loop iteration (active =
>false). */
>- worker->head.deadline = VLC_TICK_INVALID;
>- }
>- continue;
>- }
>- break;
>- }
>-
>- if( !worker->head.active )
>+ vlc_mutex_lock(&worker->lock);
>+ struct task *task = QueueTake(worker, 5000);
>+ if (!task)
> {
>- vlc_mutex_unlock( &worker->lock );
>+ vlc_mutex_unlock(&worker->lock);
>+ /* terminate this thread */
> break;
> }
>- vlc_mutex_unlock( &worker->lock );
>
>- assert( item != NULL );
>+ thread->task = task;
>+ thread->cancel = false;
>+ thread->probe = false;
>+ vlc_tick_t deadline;
>+ if (task->timeout > 0)
>+ deadline = vlc_tick_now() +
>VLC_TICK_FROM_MS(task->timeout);
>+ else
>+ deadline = INT64_MAX; /* no deadline */
>+ vlc_mutex_unlock(&worker->lock);
>
>- if( worker->conf.pf_start( worker->owner, item->entity,
>&handle ) )
>+ void *handle;
>+ if (worker->conf.pf_start(worker->owner, task->entity,
>&handle))
> {
>- worker->conf.pf_release( item->entity );
>- free( item );
>+ EndTask(thread, task);
> continue;
> }
>
>- for( ;; )
>+ for (;;)
> {
>- vlc_mutex_lock( &worker->lock );
>+ vlc_mutex_lock(&worker->lock);
>+ bool timeout = false;
>+ while (!timeout && !thread->probe && !thread->cancel)
>+ /* any non-zero return value means timeout */
>+ timeout =
>vlc_cond_timedwait(&thread->probe_cancel_wait,
>+ &worker->lock, deadline)
>!= 0;
>
>- bool const b_timeout = worker->head.deadline <=
>vlc_tick_now();
>- worker->head.probe_request = false;
>+ bool cancel = thread->cancel;
>+ thread->cancel = false;
>+ thread->probe = false;
>+ vlc_mutex_unlock(&worker->lock);
>
>- vlc_mutex_unlock( &worker->lock );
>-
>- if( b_timeout ||
>- worker->conf.pf_probe( worker->owner, handle ) )
>+ if (timeout || cancel
>+ || worker->conf.pf_probe(worker->owner, handle))
> {
>- worker->conf.pf_stop( worker->owner, handle );
>- worker->conf.pf_release( item->entity );
>- free( item );
>+ worker->conf.pf_stop(worker->owner, handle);
>+ EndTask(thread, task);
> break;
> }
>-
>- vlc_mutex_lock( &worker->lock );
>- if( worker->head.probe_request == false &&
>- worker->head.deadline > vlc_tick_now() )
>- {
>- vlc_cond_timedwait( &worker->head.worker_wait,
>&worker->lock,
>- worker->head.deadline );
>- }
>- vlc_mutex_unlock( &worker->lock );
> }
> }
>
>+ RemoveThread(thread);
>+
> return NULL;
> }
>
>-static void BackgroundWorkerCancel( struct background_worker* worker,
>void* id)
>+static bool SpawnThread(struct background_worker *worker)
> {
>- vlc_mutex_lock( &worker->lock );
>- for( size_t i = 0; i < vlc_array_count( &worker->tail.data ); )
>- {
>- struct bg_queued_item* item =
>- vlc_array_item_at_index( &worker->tail.data, i );
>-
>- if( id == NULL || item->id == id )
>- {
>- vlc_array_remove( &worker->tail.data, i );
>- worker->conf.pf_release( item->entity );
>- free( item );
>- continue;
>- }
>+ vlc_assert_locked(&worker->lock);
>
>- ++i;
>- }
>+ struct background_thread *thread =
>background_thread_Create(worker);
>+ if (!thread)
>+ return false;
>
>- while( ( id == NULL && worker->head.active )
>- || ( id != NULL && worker->head.id == id ) )
>+ if (vlc_clone_detach(NULL, Thread, thread,
>VLC_THREAD_PRIORITY_LOW))
> {
>- worker->head.deadline = VLC_TICK_INVALID;
>- vlc_cond_signal( &worker->head.worker_wait );
>- vlc_cond_signal( &worker->tail.wait );
>- vlc_cond_wait( &worker->head.wait, &worker->lock );
>+ free(thread);
>+ return false;
> }
>- vlc_mutex_unlock( &worker->lock );
>+ worker->nthreads++;
>+ vlc_list_append(&thread->node, &worker->threads);
>+
>+ return true;
> }
>
> struct background_worker* background_worker_New( void* owner,
> struct background_worker_config* conf )
> {
>- struct background_worker* worker = malloc( sizeof *worker );
>-
>- if( unlikely( !worker ) )
>- return NULL;
>-
>- worker->conf = *conf;
>- worker->owner = owner;
>- worker->head.id = NULL;
>- worker->head.active = false;
>- worker->head.deadline = VLC_TICK_INVALID;
>-
>- vlc_mutex_init( &worker->lock );
>- vlc_cond_init( &worker->head.wait );
>- vlc_cond_init( &worker->head.worker_wait );
>-
>- vlc_array_init( &worker->tail.data );
>- vlc_cond_init( &worker->tail.wait );
>-
>- return worker;
>+ return background_worker_Create(owner, conf);
> }
>
>int background_worker_Push( struct background_worker* worker, void*
>entity,
> void* id, int timeout )
> {
>- struct bg_queued_item* item = malloc( sizeof( *item ) );
>+ struct task *task = task_Create(worker, id, entity, timeout);
>+ if (unlikely(!task))
>+ return VLC_ENOMEM;
>
>- if( unlikely( !item ) )
>- return VLC_EGENERIC;
>+ vlc_mutex_lock(&worker->lock);
>+ QueuePush(worker, task);
>+ if (++worker->uncompleted > worker->nthreads
>+ && worker->nthreads < worker->conf.max_threads)
>+ SpawnThread(worker);
>+ vlc_mutex_unlock(&worker->lock);
>
>- item->id = id;
>- item->entity = entity;
>- item->timeout = timeout < 0 ? worker->conf.default_timeout :
>timeout;
>+ return VLC_SUCCESS;
>+}
>
>- vlc_mutex_lock( &worker->lock );
>- int i_ret = vlc_array_append( &worker->tail.data, item );
>- vlc_cond_signal( &worker->tail.wait );
>- if( i_ret != 0 )
>- {
>- free( item );
>- return VLC_EGENERIC;
>- }
>+static void BackgroundWorkerCancelLocked(struct background_worker
>*worker,
>+ void *id)
>+{
>+ vlc_assert_locked(&worker->lock);
>+
>+ QueueRemoveAll(worker, id);
>
>- if( worker->head.active == false )
>+ struct background_thread *thread;
>+ vlc_list_foreach(thread, &worker->threads, node)
> {
>- worker->head.probe_request = false;
>- worker->head.active =
>- !vlc_clone_detach( NULL, Thread, worker,
>VLC_THREAD_PRIORITY_LOW );
>+ if (!id || (thread->task && thread->task->id == id &&
>!thread->cancel))
>+ {
>+ thread->cancel = true;
>+ vlc_cond_signal(&thread->probe_cancel_wait);
>+ }
> }
>-
>- if( worker->head.active )
>- worker->conf.pf_hold( item->entity );
>-
>- int ret = worker->head.active ? VLC_SUCCESS : VLC_EGENERIC;
>- vlc_mutex_unlock( &worker->lock );
>-
>- return ret;
> }
>
>void background_worker_Cancel( struct background_worker* worker, void*
>id )
> {
>- BackgroundWorkerCancel( worker, id );
>+ vlc_mutex_lock(&worker->lock);
>+ BackgroundWorkerCancelLocked(worker, id);
>+ vlc_mutex_unlock(&worker->lock);
> }
>
>void background_worker_RequestProbe( struct background_worker* worker )
> {
>- vlc_mutex_lock( &worker->lock );
>- worker->head.probe_request = true;
>- vlc_cond_signal( &worker->head.worker_wait );
>- vlc_mutex_unlock( &worker->lock );
>+ vlc_mutex_lock(&worker->lock);
>+
>+ struct background_thread *thread;
>+ vlc_list_foreach(thread, &worker->threads, node)
>+ {
>+ thread->probe = true;
>+ vlc_cond_signal(&thread->probe_cancel_wait);
>+ }
>+
>+ vlc_mutex_unlock(&worker->lock);
> }
>
> void background_worker_Delete( struct background_worker* worker )
> {
>- BackgroundWorkerCancel( worker, NULL );
>- vlc_array_clear( &worker->tail.data );
>- vlc_mutex_destroy( &worker->lock );
>- vlc_cond_destroy( &worker->head.wait );
>- vlc_cond_destroy( &worker->head.worker_wait );
>- vlc_cond_destroy( &worker->tail.wait );
>- free( worker );
>+ vlc_mutex_lock(&worker->lock);
>+
>+ worker->closing = true;
>+ BackgroundWorkerCancelLocked(worker, NULL);
>+ /* closing is now true, this will wake up any QueueTake() */
>+ vlc_cond_broadcast(&worker->queue_wait);
>+
>+ while (worker->nthreads)
>+ vlc_cond_wait(&worker->nothreads_wait, &worker->lock);
>+
>+ vlc_mutex_unlock(&worker->lock);
>+
>+ /* no threads use the worker anymore, we can destroy it */
>+ background_worker_Destroy(worker);
> }
>diff --git a/src/misc/background_worker.h
>b/src/misc/background_worker.h
>index 049e47d543d..abe65159ff2 100644
>--- a/src/misc/background_worker.h
>+++ b/src/misc/background_worker.h
>@@ -29,6 +29,11 @@ struct background_worker_config {
> **/
> vlc_tick_t default_timeout;
>
>+ /**
>+ * Maximum number of threads used to execute tasks.
>+ */
>+ int max_threads;
>+
> /**
> * Release an entity
> *
>@@ -37,6 +42,9 @@ struct background_worker_config {
> * pf_stop has finished executing, or if the entity is removed from the
> * queue (through \ref background_worker_Cancel)
> *
>+ * \warning As each task might be executed in parallel by
>different threads,
>+ * this callback must be thread-safe.
>+ *
> * \param entity the entity to release
> **/
> void( *pf_release )( void* entity );
>@@ -48,6 +56,9 @@ struct background_worker_config {
> * entity. It will happen when the entity is pushed into the queue of
> * pending tasks as part of \ref background_worker_Push.
> *
>+ * \warning As each task might be executed in parallel by
>different threads,
>+ * this callback must be thread-safe.
>+ *
> * \param entity the entity to hold
> **/
> void( *pf_hold )( void* entity );
>@@ -63,6 +74,9 @@ struct background_worker_config {
>* The value of `*out` will then be the value of the argument named
>`handle`
> * in terms of \ref pf_probe and \ref pf_stop.
> *
>+ * \warning As each task might be executed in parallel by
>different threads,
>+ * this callback must be thread-safe.
>+ *
> * \param owner the owner of the background-worker
> * \param entity the entity for which a task is to be created
>* \param out [out] `*out` shall, on success, refer to the handle
>associated
>@@ -78,6 +92,9 @@ struct background_worker_config {
>* finished or not. It can be called anytime between a successful call
>to
> * \ref pf_start, and the corresponding call to \ref pf_stop.
> *
>+ * \warning As each task might be executed in parallel by
>different threads,
>+ * this callback must be thread-safe.
>+ *
> * \param owner the owner of the background-worker
> * \param handle the handle associated with the running task
> * \return 0 if the task is still running, any other value if finished.
>@@ -95,6 +112,9 @@ struct background_worker_config {
>* that the task has finished, or if the timeout (if any) for
>the
> * task has been reached.
> *
>+ * \warning As each task might be executed in parallel by
>different threads,
>+ * this callback must be thread-safe.
>+ *
> * \param owner the owner of the background-worker
> * \parma handle the handle associated with the task to be stopped
> **/
>diff --git a/src/preparser/fetcher.c b/src/preparser/fetcher.c
>index 16af948551c..6d095963afb 100644
>--- a/src/preparser/fetcher.c
>+++ b/src/preparser/fetcher.c
>@@ -404,6 +404,7 @@ static void WorkerInit( input_fetcher_t* fetcher,
> {
> struct background_worker_config conf = {
> .default_timeout = 0,
>+ .max_threads = var_InheritInteger( fetcher->owner,
>"fetch-art-threads" ),
> .pf_start = starter,
> .pf_probe = ProbeWorker,
> .pf_stop = CloseWorker,
>diff --git a/src/preparser/preparser.c b/src/preparser/preparser.c
>index 529464e4b89..1f3833f5bc5 100644
>--- a/src/preparser/preparser.c
>+++ b/src/preparser/preparser.c
>@@ -153,6 +153,7 @@ input_preparser_t* input_preparser_New(
>vlc_object_t *parent )
>
> struct background_worker_config conf = {
> .default_timeout = var_InheritInteger( parent, "preparse-timeout" ),
>+ .max_threads = var_InheritInteger( parent, "preparse-threads"
>),
> .pf_start = PreparserOpenInput,
> .pf_probe = PreparserProbeInput,
> .pf_stop = PreparserCloseInput,
>--
>2.18.0
>
>_______________________________________________
>vlc-devel mailing list
>To unsubscribe or modify your subscription options:
>https://mailman.videolan.org/listinfo/vlc-devel
--
Envoyé de mon appareil Android avec Courriel K-9 Mail. Veuillez excuser ma brièveté.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mailman.videolan.org/pipermail/vlc-devel/attachments/20180717/b512d970/attachment-0001.html>
More information about the vlc-devel
mailing list