[vlc-devel] [PATCH v2 2/3] core: Add a thumbnailing API

Hugo Beauzée-Luyssen hugo at beauzee.fr
Mon Oct 15 16:32:07 CEST 2018


ref #17368
---
 include/vlc_input.h        |  11 ++
 include/vlc_thumbnailer.h  | 107 +++++++++++++++++++
 src/Makefile.am            |   2 +
 src/input/decoder.c        |  45 +++++++-
 src/input/es_out.c         |   5 +-
 src/input/input.c          |  19 +++-
 src/input/input_internal.h |   1 +
 src/input/thumbnailer.c    | 206 +++++++++++++++++++++++++++++++++++++
 src/input/var.c            |   2 +
 src/libvlccore.sym         |   5 +
 10 files changed, 395 insertions(+), 8 deletions(-)
 create mode 100644 include/vlc_thumbnailer.h
 create mode 100644 src/input/thumbnailer.c

diff --git a/include/vlc_input.h b/include/vlc_input.h
index 8f6a7be472..36be1236ac 100644
--- a/include/vlc_input.h
+++ b/include/vlc_input.h
@@ -388,6 +388,9 @@ typedef enum input_event_type_e
     /* (pre-)parsing events */
     INPUT_EVENT_SUBITEMS,
 
+    /* Thumbnail generation */
+    INPUT_EVENT_THUMBNAIL_READY,
+
 } input_event_type_e;
 
 #define VLC_INPUT_CAPABILITIES_SEEKABLE (1<<0)
@@ -515,6 +518,8 @@ struct vlc_input_event
         struct vlc_input_event_vout vout;
         /* INPUT_EVENT_SUBITEMS */
         input_item_node_t *subitems;
+        /* INPUT_EVENT_THUMBNAIL_READY */
+        picture_t *thumbnail;
     };
 };
 
@@ -608,6 +613,12 @@ VLC_API input_thread_t *input_CreatePreparser(vlc_object_t *obj,
                                               void *events_data, input_item_t *item)
 VLC_USED;
 
+VLC_API
+input_thread_t *input_CreateThumbnailer(vlc_object_t *obj,
+                                        input_thread_events_cb events_cb,
+                                        void *events_data, input_item_t *item)
+VLC_USED;
+
 VLC_API int input_Start( input_thread_t * );
 
 VLC_API void input_Stop( input_thread_t * );
diff --git a/include/vlc_thumbnailer.h b/include/vlc_thumbnailer.h
new file mode 100644
index 0000000000..122acdaf3d
--- /dev/null
+++ b/include/vlc_thumbnailer.h
@@ -0,0 +1,107 @@
+/*****************************************************************************
+ * vlc_thumbnailer.h: Thumbnailing API
+ *****************************************************************************
+ * Copyright (C) 1998-2018 VLC authors and VideoLAN
+ *
+ * Authors: Hugo Beauzée-Luyssen <hugo at beauzee.fr>
+ *
+ * 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.
+ *****************************************************************************/
+
+#ifndef VLC_THUMBNAILER_H
+#define VLC_THUMBNAILER_H
+
+#include <vlc_common.h>
+
+typedef struct vlc_thumbnailer_t vlc_thumbnailer_t;
+typedef struct vlc_thumbnailer_request_t vlc_thumbnailer_request_t;
+
+/**
+ * \brief vlc_thumbnailer_cb defines a callback invoked on thumbnailing completion or error
+ *
+ * This callback will always be called, provided vlc_thumbnailer_Request returned
+ * a non NULL request, and provided the request is not cancelled before its
+ * completion.
+ * In case of failure, p_thumbnail will be NULL.
+ * The picture, if any, is owned by the thumbnail, and must be acquired by using
+ * \link picture_Hold \endlink to use it passed the callback's scope.
+ *
+ * \param data Is the opaque pointer passed as vlc_thumbnailer_Request last parameter
+ * \param p_thumbnail The generated thumbnail, or NULL in case of failure
+ */
+typedef void(*vlc_thumbnailer_cb)( void* data, picture_t* p_thumbnail );
+
+typedef struct vlc_thumbnailer_params_t
+{
+    union
+    {
+        vlc_tick_t i_time;
+        float f_pos;
+    };
+    enum
+    {
+        VLC_THUMBNAILER_SEEK_TIME,
+        VLC_THUMBNAILER_SEEK_POS,
+    } i_type;
+    bool b_fast_seek;
+    input_item_t* p_input_item;
+    vlc_thumbnailer_cb p_cb;
+    void* p_user_data;
+} vlc_thumbnailer_params_t;
+
+/**
+ * \brief vlc_thumbnailer_Create Creates a thumbnailer object
+ * \param p_parent A VLC object
+ * \return A thumbnailer object, or NULL in case of failure
+ */
+VLC_API vlc_thumbnailer_t*
+vlc_thumbnailer_Create( vlc_object_t* p_parent )
+VLC_USED;
+
+/**
+ * \brief vlc_thumbnailer_Request
+ * \param p_thumbnailer A thumbnailer object
+ * \param p_params a pointer to a thumbnail parameter instance
+ * \return An opaque request object, or NULL in case of failure
+ *
+ * If this function returns a valid request object, the callback is guaranteed
+ * to be called, even in case of later failure.
+ * The returned request object must not be used after the callback has been
+ * invoked. That request object is owned by the thumbnailer, and must not be
+ * released.
+ */
+VLC_API vlc_thumbnailer_request_t*
+vlc_thumbnailer_Request( vlc_thumbnailer_t *p_thumbnailer,
+                         const vlc_thumbnailer_params_t* p_params );
+
+/**
+ * \brief vlc_thumbnailer_Cancel Cancel a thumbnail request
+ * \param p_thumbnailer A thumbnailer object
+ * \param p_request An opaque thumbnail request object
+ *
+ * Cancelling a request will *not* invoke the completion callback.
+ * The behavior is undefined if the request is cancelled after its completion.
+ */
+VLC_API void
+vlc_thumbnailer_Cancel( vlc_thumbnailer_t* p_thumbnailer,
+                        vlc_thumbnailer_request_t* p_request );
+
+/**
+ * \brief vlc_thumbnailer_Release releases a thumbnailer and cancel all pending requests
+ * \param p_thumbnailer A thumbnailer object
+ */
+VLC_API void vlc_thumbnailer_Release( vlc_thumbnailer_t* p_thumbnailer );
+
+#endif // VLC_THUMBNAILER_H
diff --git a/src/Makefile.am b/src/Makefile.am
index c1452b17bc..bdb751a7c4 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -94,6 +94,7 @@ pluginsinclude_HEADERS = \
 	../include/vlc_threads.h \
 	../include/vlc_tick.h \
 	../include/vlc_timestamp_helper.h \
+	../include/vlc_thumbnailer.h \
 	../include/vlc_tls.h \
 	../include/vlc_url.h \
 	../include/vlc_variables.h \
@@ -267,6 +268,7 @@ libvlccore_la_SOURCES = \
 	input/stream_filter.c \
 	input/stream_memory.c \
 	input/subtitles.c \
+	input/thumbnailer.c \
 	input/var.c \
 	audio_output/aout_internal.h \
 	audio_output/common.c \
diff --git a/src/input/decoder.c b/src/input/decoder.c
index b39c704bfa..524fac7b64 100644
--- a/src/input/decoder.c
+++ b/src/input/decoder.c
@@ -1157,6 +1157,37 @@ static void DecoderQueueVideo( decoder_t *p_dec, picture_t *p_pic )
     p_owner->pf_update_stat( p_owner, 1, i_lost );
 }
 
+static int thumbnailer_update_format( decoder_t *p_dec )
+{
+    VLC_UNUSED(p_dec);
+    return 0;
+}
+
+static picture_t *thumbnailer_buffer_new( decoder_t *p_dec )
+{
+    struct decoder_owner *p_owner = dec_get_owner( p_dec );
+    /* Avoid decoding more than one frame when a thumbnail was
+     * already generated */
+    if( !p_owner->b_first )
+        return NULL;
+    return picture_NewFromFormat( &p_dec->fmt_out.video );
+}
+
+static void DecoderQueueThumbnail( decoder_t *p_dec, picture_t *p_pic )
+{
+    struct decoder_owner *p_owner = dec_get_owner( p_dec );
+    if( !p_owner->b_first )
+    {
+        picture_Release( p_pic );
+        return;
+    }
+    input_SendEvent(p_owner->p_input, &(struct vlc_input_event) {
+        .type = INPUT_EVENT_THUMBNAIL_READY,
+        .thumbnail = p_pic
+    });
+    p_owner->b_first = false;
+}
+
 static void DecoderPlayAudio( decoder_t *p_dec, block_t *p_audio,
                              unsigned *restrict pi_lost_sum )
 {
@@ -1726,6 +1757,15 @@ static const struct decoder_owner_callbacks dec_video_cbs =
     },
     .get_attachments = DecoderGetInputAttachments,
 };
+static const struct decoder_owner_callbacks dec_thumbnailer_cbs =
+{
+    .video = {
+        .format_update = thumbnailer_update_format,
+        .buffer_new = thumbnailer_buffer_new,
+        .queue = DecoderQueueThumbnail,
+    },
+    .get_attachments = DecoderGetInputAttachments,
+};
 static const struct decoder_owner_callbacks dec_audio_cbs =
 {
     .audio = {
@@ -1841,7 +1881,10 @@ static decoder_t * CreateDecoder( vlc_object_t *p_parent,
     switch( fmt->i_cat )
     {
         case VIDEO_ES:
-            p_dec->cbs = &dec_video_cbs;
+            if( !input_priv( p_input )->b_thumbnailing )
+                p_dec->cbs = &dec_video_cbs;
+            else
+                p_dec->cbs = &dec_thumbnailer_cbs;
             p_owner->pf_update_stat = DecoderUpdateStatVideo;
             break;
         case AUDIO_ES:
diff --git a/src/input/es_out.c b/src/input/es_out.c
index 7e1480fad4..0b4ec13881 100644
--- a/src/input/es_out.c
+++ b/src/input/es_out.c
@@ -1788,6 +1788,7 @@ static void EsOutSelectEs( es_out_t *out, es_out_id_t *es )
 {
     es_out_sys_t *p_sys = container_of(out, es_out_sys_t, out);
     input_thread_t *p_input = p_sys->p_input;
+    bool b_thumbnailing = input_priv(p_input)->b_thumbnailing;
 
     if( EsIsSelected( es ) )
     {
@@ -1826,7 +1827,7 @@ static void EsOutSelectEs( es_out_t *out, es_out_id_t *es )
             }
             else if( es->fmt.i_cat == AUDIO_ES )
             {
-                if( !var_GetBool( p_input, b_sout ? "sout-audio" : "audio" ) )
+                if( !var_GetBool( p_input, b_sout ? "sout-audio" : "audio" ) || b_thumbnailing )
                 {
                     msg_Dbg( p_input, "audio is disabled, not selecting ES 0x%x",
                              es->fmt.i_id );
@@ -1835,7 +1836,7 @@ static void EsOutSelectEs( es_out_t *out, es_out_id_t *es )
             }
             if( es->fmt.i_cat == SPU_ES )
             {
-                if( !var_GetBool( p_input, b_sout ? "sout-spu" : "spu" ) )
+                if( !var_GetBool( p_input, b_sout ? "sout-spu" : "spu" ) || b_thumbnailing )
                 {
                     msg_Dbg( p_input, "spu is disabled, not selecting ES 0x%x",
                              es->fmt.i_id );
diff --git a/src/input/input.c b/src/input/input.c
index ef2e8c1475..043c841f23 100644
--- a/src/input/input.c
+++ b/src/input/input.c
@@ -63,7 +63,7 @@ static  void *Run( void * );
 static  void *Preparse( void * );
 
 static input_thread_t * Create  ( vlc_object_t *, input_thread_events_cb, void *,
-                                  input_item_t *, const char *, bool,
+                                  input_item_t *, const char *, bool, bool,
                                   input_resource_t *, vlc_renderer_item_t * );
 static  int             Init    ( input_thread_t *p_input );
 static void             End     ( input_thread_t *p_input );
@@ -132,7 +132,7 @@ input_thread_t *input_Create( vlc_object_t *p_parent,
                               vlc_renderer_item_t *p_renderer )
 {
     return Create( p_parent, events_cb, events_data, p_item, psz_log, false,
-                   p_resource, p_renderer );
+                   false, p_resource, p_renderer );
 }
 
 #undef input_Read
@@ -147,7 +147,7 @@ int input_Read( vlc_object_t *p_parent, input_item_t *p_item,
                 input_thread_events_cb events_cb, void *events_data )
 {
     input_thread_t *p_input = Create( p_parent, events_cb, events_data, p_item,
-                                      NULL, false, NULL, NULL );
+                                      NULL, false, false, NULL, NULL );
     if( !p_input )
         return VLC_EGENERIC;
 
@@ -165,7 +165,14 @@ input_thread_t *input_CreatePreparser( vlc_object_t *parent,
                                        input_thread_events_cb events_cb,
                                        void *events_data, input_item_t *item )
 {
-    return Create( parent, events_cb, events_data, item, NULL, true, NULL, NULL );
+    return Create( parent, events_cb, events_data, item, NULL, true, false, NULL, NULL );
+}
+
+input_thread_t *input_CreateThumbnailer(vlc_object_t *obj,
+                                        input_thread_events_cb events_cb,
+                                        void *events_data, input_item_t *item)
+{
+    return Create( obj, events_cb, events_data, item, NULL, false, true, NULL, NULL );
 }
 
 /**
@@ -308,7 +315,8 @@ input_item_t *input_GetItem( input_thread_t *p_input )
 static input_thread_t *Create( vlc_object_t *p_parent,
                                input_thread_events_cb events_cb, void *events_data,
                                input_item_t *p_item, const char *psz_header,
-                               bool b_preparsing, input_resource_t *p_resource,
+                               bool b_preparsing, bool b_thumbnailing,
+                               input_resource_t *p_resource,
                                vlc_renderer_item_t *p_renderer )
 {
     /* Allocate descriptor */
@@ -345,6 +353,7 @@ static input_thread_t *Create( vlc_object_t *p_parent,
     priv->is_running = false;
     priv->is_stopped = false;
     priv->b_recording = false;
+    priv->b_thumbnailing = b_thumbnailing;
     priv->i_rate = INPUT_RATE_DEFAULT;
     memset( &priv->bookmark, 0, sizeof(priv->bookmark) );
     TAB_INIT( priv->i_bookmark, priv->pp_bookmark );
diff --git a/src/input/input_internal.h b/src/input/input_internal.h
index dbc79fb109..afb38c076d 100644
--- a/src/input/input_internal.h
+++ b/src/input/input_internal.h
@@ -121,6 +121,7 @@ typedef struct input_thread_private_t
     bool        is_running;
     bool        is_stopped;
     bool        b_recording;
+    bool        b_thumbnailing;
     int         i_rate;
 
     /* Playtime configuration and state */
diff --git a/src/input/thumbnailer.c b/src/input/thumbnailer.c
new file mode 100644
index 0000000000..8653b4277b
--- /dev/null
+++ b/src/input/thumbnailer.c
@@ -0,0 +1,206 @@
+/*****************************************************************************
+ * thumbnailer.c: Thumbnailing API
+ *****************************************************************************
+ * Copyright (C) 1998-2018 VLC authors and VideoLAN
+ *
+ * Authors: Hugo Beauzée-Luyssen <hugo at beauzee.fr>
+ *
+ * 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_thumbnailer.h>
+#include <vlc_input.h>
+#include "misc/background_worker.h"
+
+struct vlc_thumbnailer_t
+{
+    vlc_object_t* p_parent;
+    struct background_worker* worker;
+};
+
+struct vlc_thumbnailer_request_t
+{
+    vlc_thumbnailer_t *p_thumbnailer;
+    input_thread_t *p_input_thread;
+
+    vlc_thumbnailer_params_t params;
+
+    vlc_mutex_t lock;
+    bool b_done;
+    picture_t* p_picture;
+};
+
+static void
+on_thumbnailer_input_event( input_thread_t *input,
+                            const struct vlc_input_event *event, void *userdata )
+{
+    VLC_UNUSED(input);
+    if ( event->type != INPUT_EVENT_THUMBNAIL_READY &&
+         ( event->type != INPUT_EVENT_STATE || event->state != ERROR_S ) )
+         return;
+
+    vlc_thumbnailer_request_t* p_request = userdata;
+    picture_t *p_pic = NULL;
+
+    if ( event->type == INPUT_EVENT_THUMBNAIL_READY )
+    {
+        // Stop the input thread ASAP, delegate its release to
+        // thumbnailer_request_Release
+        input_Stop( p_request->p_input_thread );
+        p_pic = event->thumbnail;
+    }
+    vlc_mutex_lock( &p_request->lock );
+    p_request->b_done = true;
+    p_request->p_picture = p_pic;
+    // If the request has not been cancelled, we can invoke the completion
+    // callback. A request that was cancelled early in its processing might
+    // send an error state, which we don't want to propagate.
+    if ( p_request->params.p_cb )
+        p_request->params.p_cb( p_request->params.p_user_data,
+                                p_request->p_picture );
+    vlc_mutex_unlock( &p_request->lock );
+    background_worker_RequestProbe( p_request->p_thumbnailer->worker );
+}
+
+static void thumbnailer_request_Hold( void* p_data )
+{
+    VLC_UNUSED(p_data);
+}
+
+static void thumbnailer_request_Release( void* p_data )
+{
+    vlc_thumbnailer_request_t* p_request = p_data;
+    if ( p_request->p_input_thread )
+        input_Close( p_request->p_input_thread );
+
+    input_item_Release( p_request->params.p_input_item );
+    vlc_mutex_destroy( &p_request->lock );
+    if ( p_request->p_picture )
+        picture_Release( p_request->p_picture );
+    free( p_request );
+}
+
+static int thumbnailer_request_Start( void* owner, void* entity, void** out )
+{
+    vlc_thumbnailer_t* p_thumbnailer = owner;
+    vlc_thumbnailer_request_t* p_request = entity;
+    input_thread_t* p_input = p_request->p_input_thread =
+            input_CreateThumbnailer( p_thumbnailer->p_parent,
+                                     on_thumbnailer_input_event, p_request,
+                                     p_request->params.p_input_item );
+    if ( unlikely( p_input == NULL ) )
+        return VLC_EGENERIC;
+    if ( p_request->params.i_type == VLC_THUMBNAILER_SEEK_TIME )
+    {
+        input_SetTime( p_input, p_request->params.i_time,
+                       p_request->params.b_fast_seek );
+    }
+    else
+    {
+        assert( p_request->params.i_type == VLC_THUMBNAILER_SEEK_POS );
+        input_SetPosition( p_input, p_request->params.f_pos,
+                       p_request->params.b_fast_seek );
+    }
+    if ( input_Start( p_input ) != VLC_SUCCESS )
+        return VLC_EGENERIC;
+    *out = p_request;
+    return VLC_SUCCESS;
+}
+
+static void thumbnailer_request_Stop( void* owner, void* handle )
+{
+    VLC_UNUSED(owner);
+    vlc_thumbnailer_request_t *p_request = handle;
+    assert( p_request->p_input_thread != NULL );
+    input_Stop( p_request->p_input_thread );
+}
+
+static int thumbnailer_request_Probe( void* owner, void* handle )
+{
+    VLC_UNUSED(owner);
+    vlc_thumbnailer_request_t *p_request = handle;
+    vlc_mutex_lock( &p_request->lock );
+    int res = p_request->b_done;
+    vlc_mutex_unlock( &p_request->lock );
+    return res;
+}
+
+vlc_thumbnailer_request_t*
+vlc_thumbnailer_Request( vlc_thumbnailer_t* p_thumbnailer,
+                         const vlc_thumbnailer_params_t* params )
+{
+    vlc_thumbnailer_request_t *p_request = malloc( sizeof( *p_request ) );
+    if ( unlikely( p_request == NULL ) )
+        return NULL;
+    p_request->p_thumbnailer = p_thumbnailer;
+    p_request->p_input_thread = NULL;
+    p_request->params = *(vlc_thumbnailer_params_t*)params;
+    p_request->b_done = false;
+    p_request->p_picture = NULL;
+    input_item_Hold( p_request->params.p_input_item );
+    vlc_mutex_init( &p_request->lock );
+
+    if ( background_worker_Push( p_thumbnailer->worker, p_request,
+                                 p_request, -1 ) != VLC_SUCCESS )
+    {
+        thumbnailer_request_Release( p_request );
+        return NULL;
+    }
+    return p_request;
+}
+
+void vlc_thumbnailer_Cancel( vlc_thumbnailer_t* p_thumbnailer,
+                             vlc_thumbnailer_request_t* p_req )
+{
+    vlc_mutex_lock( &p_req->lock );
+    // Ensure we won't invoke the callback if the input was running.
+    p_req->params.p_cb = NULL;
+    vlc_mutex_unlock( &p_req->lock );
+    background_worker_Cancel( p_thumbnailer->worker, p_req );
+}
+
+vlc_thumbnailer_t *vlc_thumbnailer_Create( vlc_object_t* p_parent)
+{
+    vlc_thumbnailer_t *p_thumbnailer = malloc( sizeof( *p_thumbnailer ) );
+    if ( unlikely( p_thumbnailer == NULL ) )
+        return NULL;
+    p_thumbnailer->p_parent = p_parent;
+    struct background_worker_config cfg = {
+        .default_timeout = vlc_tick_from_secf( 5.f ),
+        .max_threads = 1,
+        .pf_release = thumbnailer_request_Release,
+        .pf_hold = thumbnailer_request_Hold,
+        .pf_start = thumbnailer_request_Start,
+        .pf_probe = thumbnailer_request_Probe,
+        .pf_stop = thumbnailer_request_Stop,
+    };
+    p_thumbnailer->worker = background_worker_New( p_thumbnailer, &cfg );
+    if ( unlikely( p_thumbnailer->worker == NULL ) )
+    {
+        free( p_thumbnailer );
+        return NULL;
+    }
+    return p_thumbnailer;
+}
+
+void vlc_thumbnailer_Release( vlc_thumbnailer_t *p_thumbnailer )
+{
+    background_worker_Delete( p_thumbnailer->worker );
+    free( p_thumbnailer );
+}
diff --git a/src/input/var.c b/src/input/var.c
index 3ef8c0fa31..22170e31a0 100644
--- a/src/input/var.c
+++ b/src/input/var.c
@@ -421,6 +421,8 @@ void input_LegacyEvents( input_thread_t *p_input,
             break;
         case INPUT_EVENT_SUBITEMS:
             break;
+        case INPUT_EVENT_THUMBNAIL_READY:
+            break;
     }
     Trigger( p_input, event->type );
 }
diff --git a/src/libvlccore.sym b/src/libvlccore.sym
index a1709cb9f6..292a775b9b 100644
--- a/src/libvlccore.sym
+++ b/src/libvlccore.sym
@@ -154,6 +154,7 @@ input_Control
 input_Create
 input_CreateFilename
 input_CreatePreparser
+input_CreateThumbnailer
 input_DecoderCreate
 input_DecoderDelete
 input_DecoderDecode
@@ -784,3 +785,7 @@ vlc_es_id_Hold
 vlc_es_id_Release
 vlc_es_id_GetInputId
 vlc_es_id_GetCat
+vlc_thumbnailer_Create
+vlc_thumbnailer_Request
+vlc_thumbnailer_Cancel
+vlc_thumbnailer_Release
-- 
2.19.1



More information about the vlc-devel mailing list