[vlc-devel] [PATCH 2/2] add V4L2 video_output module
Rémi Denis-Courmont
remi at remlab.net
Sat Jan 14 09:42:21 CET 2012
Le samedi 14 janvier 2012 03:45:55 Francois Cartegnie, vous avez écrit :
> ---
> configure.ac | 5 +
> modules/video_output/Modules.am | 3 +
> modules/video_output/v4l2_output.c | 559
> ++++++++++++++++++++++++++++++++++++ 3 files changed, 567 insertions(+), 0
> deletions(-)
> create mode 100644 modules/video_output/v4l2_output.c
>
> diff --git a/configure.ac b/configure.ac
> index e6b30ed..fdaee60 100644
> --- a/configure.ac
> +++ b/configure.ac
> @@ -1840,6 +1840,8 @@ AC_ARG_ENABLE(libv4l2,
> [AS_HELP_STRING([--disable-libv4l2], [disable userspace V4L2 library
> (default auto)])])
> AC_ARG_ENABLE(pvr, [AS_HELP_STRING([--enable-pvr],
> [support PVR V4L2 cards (default disabled)])])
> +AC_ARG_ENABLE(v4l2_output, [AS_HELP_STRING([--enable-v4l2out],
> + [support V4L2 output (default disabled)])])
> have_v4l2="no"
> AC_CHECK_HEADERS([linux/videodev2.h sys/videoio.h], [
> have_v4l2="yes"
> @@ -1855,6 +1857,9 @@ AS_IF([test "$have_v4l2" = "yes"], [
> AS_IF([test "${enable_pvr}" = "yes"], [
> VLC_ADD_PLUGIN([pvr])
> ])
> + AS_IF([test "${enable_v4l2out}" = "yes"], [
> + VLC_ADD_PLUGIN([v4l2_output])
> + ])
> ])
> AM_CONDITIONAL(HAVE_V4L2, [test "${have_v4l2}" != "no"])
Why do you need a configure option?
> +/*************************************************************************
> **** + * v4l2_output.c : V4L2 output module for vlc
> +
> **************************************************************************
> *** + * Copyright (C) 2011 the VideoLAN team
Wrong year.
> + *
> + * This program is free software; you can redistribute it and/or modify
> + * it under the terms of the GNU General Public License as published by
> + * the Free Software Foundation; either version 2 of the License, or
> + * (at your option) any later version.
I wonder if we can relicense the existing V4L2 plugin to LGPL and then merge
this code directly as LGPL ??
> + *
> + * 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 General Public License for more details.
> + *
> + * You should have received a copy of the GNU 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 <stdarg.h>
> +#include <assert.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <fcntl.h>
> +#include <sys/ioctl.h>
> +#include <sys/errno.h>
What's that?
> +#include <unistd.h>
> +#include <sys/mman.h>
> +
> +#include <vlc_common.h>
> +#include <vlc_plugin.h>
> +#include <vlc_vout_display.h>
> +#include <vlc_picture_pool.h>
> +
> +#include "../access/v4l2/v4l2.h"
> +#include "../access/v4l2/v4l2_common.h"
> +
> +#define V4L2_DEVICE_TEXT N_("Device")
> +#define V4L2_DEVICE_LONGTEXT N_("V4L2 output device")
> +#define V4L2_PROFILE_TEXT N_("Loopback application profile")
> +#define V4L2_PROFILE_LONGTEXT N_("Profile (chroma + size) for special apps
> using v4l2loopback") +#define V4L2_CHROMA_TEXT N_("Chroma used")
> +#define V4L2_CHROMA_LONGTEXT N_("Force use of a specific chroma for
> output. Default is I420.") +
> +#define CFG_PREFIX "v4l2_output_"
> +
> +struct vout_display_sys_t {
> + picture_pool_t *pool;
> + int fd;
> + int i_imagesize;
> + bool b_streaming;
> + struct buffer_t *buffers;
> + int i_buffers;
> +};
> +
> +static int Open (vlc_object_t *);
> +static void Close(vlc_object_t *);
> +
> +static picture_pool_t *Pool (vout_display_t *, unsigned);
> +static void PictureDisplayRW(vout_display_t *, picture_t *,
> subpicture_t *); +static void PictureDisplayMMAP(vout_display_t
> *, picture_t *, subpicture_t *); +static int
> Control(vout_display_t *, int, va_list);
> +
> +static const char * const psz_profiles_texts[] = { N_("None"),
> N_("Skype"), N_("Flash Plugin") }; +static const char * const
> psz_profiles_values[] = { NULL, "skype", "flashplugin" }; +
> +/*
> + * Module descriptor
> + */
> +vlc_module_begin ()
> + set_shortname (N_("V4L2 Output"))
> + set_description (N_("V4L2 Output"))
> + set_category (CAT_VIDEO)
> + set_subcategory (SUBCAT_VIDEO_VOUT)
> + set_capability ("vout display", 0)
> + set_callbacks (Open, Close)
> + add_string(CFG_PREFIX "dev", "/dev/video0", V4L2_DEVICE_TEXT,
> V4L2_DEVICE_LONGTEXT, false) + add_string(CFG_PREFIX "profile",
> NULL, V4L2_PROFILE_TEXT, V4L2_PROFILE_LONGTEXT, true) +
> change_string_list( psz_profiles_values, psz_profiles_texts, 0 ) +
> add_string(CFG_PREFIX "chroma", NULL, V4L2_CHROMA_TEXT,
> V4L2_CHROMA_LONGTEXT, true) +vlc_module_end ()
> +
> +/* applications profiles for v4l2loopback output */
> +static struct {
> + const char const * psz_name;
> + const int i_width;
> + const int i_height;
> + const int i_chroma;
> +} const app_profiles[] = {
> + /* apps requested profiles */
> + { "skype", 640, 480, V4L2_PIX_FMT_YUV420 },
> + { "flashplugin", 0, 0, V4L2_PIX_FMT_RGB32 },
> + { NULL, 0, 0, 0 }
> +};
> +
> +/* same/from v4l2 access IsPixelFormatSupported() */
> +inline static bool is_fallback_v4l2_chroma( uint32_t i_pixelformat )
The compiler probably knows better than you whether or not to inline.
> +{
> + const uint32_t const p_chroma_fallbacks[] =
> + {
> + V4L2_PIX_FMT_YUV420,
> + V4L2_PIX_FMT_YVU420,
> + V4L2_PIX_FMT_YUV422P,
> + V4L2_PIX_FMT_YUYV,
> + V4L2_PIX_FMT_UYVY,
> + V4L2_PIX_FMT_BGR24,
> + V4L2_PIX_FMT_BGR32
> + // minus mjpeg/jpeg
> + };
> +
> + size_t n = ARRAY_SIZE( p_chroma_fallbacks );
> + for( size_t j = 0; j < n; j++ )
> + if( i_pixelformat == p_chroma_fallbacks[ j ] )
> + return true;
> + return false;
> +}
> +/* ! from v4l2 access */
> +
> +static bool toggle_stream( vout_display_t *vd, int i_request )
> +{
> + struct v4l2_requestbuffers reqbuf;
> + reqbuf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
> + reqbuf.memory = V4L2_MEMORY_MMAP;
> + reqbuf.count = vd->sys->i_buffers;
> + return ioctl( vd->sys->fd, i_request, & reqbuf.type );
> +}
> +
> +static void free_buffers( vout_display_sys_t *sys )
> +{
> + for( int i=0; i<sys->i_buffers; i++ )
> + munmap( sys->buffers[ i ].start, sys->buffers[ i ].length );
> + free( sys->buffers );
> +}
> +
> +static bool copy_frames( picture_t *picture, struct buffer_t *buffer,
> unsigned int *offset ) +{
> + *offset = 0;
> + for (int i = 0; i < picture->i_planes; i++) {
> + const plane_t *plane = &picture->p[i];
> + for( int y = 0; y < plane->i_visible_lines; y++) {
> + if ( unlikely( *offset + plane->i_visible_pitch >
> buffer->length ) ) + return false;
> + else
> + memcpy( buffer->start + *offset,
> + &plane->p_pixels[y*plane->i_pitch],
> + plane->i_visible_pitch );
> + *offset += plane->i_visible_pitch;
> + }
> + }
> + return true;
> +}
It is dubious that you would need to copy pictures.
> +
> +static bool apply_profile( char *psz_profile, video_format_t *fmt )
> +{
> + if ( psz_profile )
> + {
> + for (int i=0; app_profiles[i].psz_name != NULL ; i++)
> + {
> + if ( strcmp( app_profiles[i].psz_name, psz_profile ) == 0 )
> + {
> + vlc_fourcc_t i_fourcc;
> + int i_foo;
> + if ( app_profiles[i].i_chroma &&
> + get_fourcc_by_v4l2pixelformat( &i_fourcc,
> + &i_foo,
> + &i_foo,
> + &i_foo,
> +
> app_profiles[i].i_chroma ) ) + {
> + fmt->i_chroma = i_fourcc;
> + } else {
> + return false;
> + }
> + if ( app_profiles[i].i_width )
> + {
> + fmt->i_width = fmt->i_visible_width =
> app_profiles[i].i_width; + fmt->i_height =
> fmt->i_visible_height = app_profiles[i].i_height; + }
> + return true;
> + }
> + }
> + }
> + return false;
> +}
> +
> +/* mostly from v4l2 access. should be merged */
> +static inline bool probe_device_v4l2_chromas( vlc_object_t *p_obj, int
> i_fd, + uint32_t type, struct
> v4l2_fmtdesc **p_codecs, + uint32_t
> *ncodec )
> +{
> + /* Probe for available chromas */
> + struct v4l2_fmtdesc codec = {
> + .index = 0,
> + .type = type,
> + };
> + *p_codecs = NULL;
> + struct v4l2_fmtdesc *codecs = NULL;
> +
> + while( ioctl( i_fd, VIDIOC_ENUM_FMT, &codec ) >= 0 )
> + codec.index = ++*ncodec;
> +
> + *p_codecs = malloc( *ncodec * sizeof( struct v4l2_fmtdesc ) );
> + if( unlikely(*p_codecs == NULL) )
> + *ncodec = 0;
> + codecs = *p_codecs;
> +
> + for( uint32_t i = 0; i < *ncodec; i++ )
> + {
> + codecs[i].index = i;
> + codecs[i].type = type;
> +
> + if( ioctl( i_fd, VIDIOC_ENUM_FMT, &codecs[i] ) < 0 )
> + {
> + msg_Err( p_obj, "cannot get codec description: %m" );
> + return false;
> + }
> +
> + /* only print if vlc supports the format */
> + char fourcc_v4l2[5];
> + memset( fourcc_v4l2, 0, sizeof( fourcc_v4l2 ) );
> + vlc_fourcc_to_char( codecs[i].pixelformat, fourcc_v4l2 );
> +
> + bool b_codec_supported = false;
> + vlc_fourcc_t i_fourcc;
> + int i_foo;
> + b_codec_supported = get_fourcc_by_v4l2pixelformat( &i_fourcc,
> + &i_foo,
> + &i_foo,
> + &i_foo,
> +
> codecs[i].pixelformat ); + if( b_codec_supported )
> + {
> + char fourcc[5];
> + memset( fourcc, 0, sizeof( fourcc ) );
> + vlc_fourcc_to_char( i_fourcc, fourcc );
> + msg_Dbg( p_obj, "device supports chroma %4.4s [%s, %s]",
> + fourcc, codecs[i].description, fourcc_v4l2 );
> + /* can store in driver reserved fields as we won't ioctl again
> */ + codecs[i].reserved[0] = true; /* supported */
> + codecs[i].reserved[1] = is_fallback_v4l2_chroma(
> codecs[i].pixelformat ); + } else {
> + msg_Dbg( p_obj, "device codec %4.4s (%s) not supported",
> + fourcc_v4l2, codecs[i].description );
> + codecs[i].reserved[0] = false;
> + }
> + }
> + return true;
> +}
> +
> +static int Open (vlc_object_t *obj)
> +{
> + vout_display_t *vd = (vout_display_t *)obj;
> + struct v4l2_capability vid_caps;
> + struct v4l2_format v;
> + char *video_device = var_InheritString(vd, CFG_PREFIX "dev");
> + char *psz_profile = var_InheritString(vd, CFG_PREFIX "profile");
> + char *psz_reqchroma = var_InheritString( vd, CFG_PREFIX "chroma" );
> + uint32_t i_requested_v4l2_chroma = 0;
> + int i_status;
> + bool b_streamable;
> + struct v4l2_fmtdesc *codecs = NULL;
> + uint32_t ncodecs = 0;
> +
> + vd->sys = calloc( 1, sizeof(vout_display_sys_t) );
> + if ( !vd->sys ) return VLC_EGENERIC;
> +
> + vd->sys->fd = open(video_device, O_RDWR|O_NONBLOCK);
> + if (vd->sys->fd < 0)
> + {
> + msg_Err( obj, "cannot open v4l2 output device (%s)", video_device
> ); + free(vd->sys);
> + return VLC_EGENERIC;
> + }
Missing the close-on-exec flag.
> +
> + i_status = ioctl(vd->sys->fd, VIDIOC_QUERYCAP, &vid_caps);
> + if ( i_status < 0 )
> + {
> + msg_Err( obj, "cannot query v4l2 output device (%s)", video_device
> ); + goto error;
> + }
> +
> + if ( ( vid_caps.capabilities & V4L2_CAP_VIDEO_OUTPUT ) == 0 )
> + {
> + msg_Err( obj, "device (%s) is not an output device", video_device
> ); + goto error;
> + }
When a system call fails, you should show the error message, %m.
> +
> + if ( ( vid_caps.capabilities & (V4L2_CAP_READWRITE|V4L2_CAP_STREAMING)
> ) == 0 ) + {
> + msg_Err( obj, "no supported I/O method" );
> + goto error;
> + }
> +
> + b_streamable = vid_caps.capabilities & V4L2_CAP_STREAMING;
> +
> + v.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
> + i_status = ioctl(vd->sys->fd, VIDIOC_G_FMT, &v);
> + if ( i_status < 0 )
> + {
> + msg_Err( obj, "cannot control v4l2 format" );
> + goto error;
> + }
> +
> + vd->pool = Pool;
> + vd->prepare = NULL;
> + vd->control = Control;
> + vd->manage = NULL;
> + vout_display_SendEventFullscreen(vd, false);
> +
> + if ( apply_profile( psz_profile, & vd->fmt ) ) /* sets
> vd->fmt.i_chroma */ + msg_Info( obj, "using profile %s",
> psz_profile );
> +
> + if( psz_reqchroma != NULL ) /* can override profile */
> + vd->fmt.i_chroma = vlc_fourcc_GetCodecFromString( VIDEO_ES,
> psz_reqchroma ); +
> + /* try to match selection from above or by default module's one first
> */ + if ( ! get_v4l2pixelformat_by_fourcc( & i_requested_v4l2_chroma,
> vd->fmt.i_chroma ) ) + msg_Warn( obj, "can't match requested
> chroma");
> +
> + v.fmt.pix.width = vd->fmt.i_visible_width;
> + v.fmt.pix.height = vd->fmt.i_visible_height;
> + v.fmt.pix.pixelformat = 0;
> +
> + if ( probe_device_v4l2_chromas( obj, vd->sys->fd,
> V4L2_CAP_VIDEO_OUTPUT, + &codecs, &ncodecs )
> )
> + {
> + for( uint32_t i = 0; i < ncodecs; i++ )
> + {
> + if ( codecs[i].reserved[0] == true ) /* supported */
> + {
> + if ( i_requested_v4l2_chroma > 0 )
> + {
> + if ( i_requested_v4l2_chroma == codecs[i].pixelformat
> ) + {
> + v.fmt.pix.pixelformat = i_requested_v4l2_chroma;
> + msg_Dbg( obj, "using chroma %s",
> codecs[i].description ); + break;
> + }
> + } else {
> + /* pick first available fallback */
> + if ( codecs[i].reserved[1] == true ) /* is_fallback */
> + {
> + v.fmt.pix.pixelformat = codecs[i].pixelformat;
> + msg_Dbg( obj, "using fallback chroma %s",
> codecs[i].description ); + break;
> + }
> + }
> + }
> + }
> + free( codecs );
> + } else {
> + msg_Err( obj, "probing device for chromas failed" );
> + }
> +
> + if ( v.fmt.pix.pixelformat == 0 )
> + {
> + msg_Warn( obj, "No valid V4L2 chroma found" );
> + goto error;
> + } else {
> + int foo;
> + /* ensure to have it configured on display's side */
> + get_fourcc_by_v4l2pixelformat( & vd->fmt.i_chroma, &foo, &foo,
> &foo, + v.fmt.pix.pixelformat );
> + }
> +
> + video_format_FixRgb(&vd->fmt);
> + v.fmt.pix.field = V4L2_FIELD_NONE;
> +
> + i_status = ioctl(vd->sys->fd, VIDIOC_S_FMT, &v);
> + if ( i_status < 0 )
> + {
> + msg_Err( obj, "cannot control dst format" );
> + goto error;
> + }
> +
> + /* image size has been computed by driver */
> + vd->sys->i_imagesize = v.fmt.pix.sizeimage;
> +
> + struct v4l2_requestbuffers reqbuf;
> + memset (&reqbuf, 0, sizeof (reqbuf));
> + reqbuf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
> + reqbuf.memory = V4L2_MEMORY_MMAP;
> + reqbuf.count = 8;
This probably belongs in Pool().
> +
> + if ( !b_streamable || ( ioctl(vd->sys->fd, VIDIOC_REQBUFS, &reqbuf) <
> 0 ) ) + {
> + if ( !b_streamable || errno == EINVAL )
> + {
> + msg_Dbg( obj, "Video capturing or mmap-streaming is not
> supported" ); + vd->display = PictureDisplayRW;
> + vd->sys->buffers = calloc( 1, sizeof( struct buffer_t ) );
> + if ( !vd->sys->buffers ) {
> + msg_Err( obj, "can't allocate buffer");
> + goto error;
> + }
> + vd->sys->i_buffers = 1;
> + vd->sys->buffers[0].length = v.fmt.pix.sizeimage;
> + vd->sys->buffers[0].start = calloc( v.fmt.pix.sizeimage,
> sizeof(uint32_t) ); + if ( !vd->sys->buffers[0].start ) goto
> error;
> + } else {
> + msg_Err( obj, "ioctl error with reqbufs");
> + goto error;
> + }
> + } else {
> +
> + if ( reqbuf.count < 2 ) /* should at least have an in and out
> queue */ + {
> + msg_Err( obj, "Not enough buffer memory");
> + free( vd->sys );
> + return VLC_EGENERIC;
> + }
> +
> + vd->display = PictureDisplayMMAP;
> +
> + vd->sys->buffers = calloc( reqbuf.count, sizeof( struct buffer_t )
> ); + if ( !vd->sys->buffers ) {
> + msg_Err( obj, "can't allocate mmap buffers");
> + goto error;
> + }
> +
> + for( unsigned int i=0; i<reqbuf.count; i++ )
> + {
> + struct v4l2_buffer buf;
> + buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
> + buf.memory = V4L2_MEMORY_MMAP;
> + buf.index = i;
> +
> + if ( ioctl (vd->sys->fd, VIDIOC_QUERYBUF, & buf ) < 0 ) {
> + msg_Err( obj, "Error in VIDIOC_QUERYBUF");
> + goto error;
> + }
> +
> + vd->sys->buffers[ i ].start = mmap( NULL, buf.length,
> + PROT_READ | PROT_WRITE,
> + MAP_SHARED,
> + vd->sys->fd, buf.m.offset
> ); + if ( vd->sys->buffers[ i ].start == MAP_FAILED ) {
> + msg_Err( obj, "Can't mmap buffer");
> + goto error;
> + }
> + vd->sys->buffers[ i ].length = buf.length;
> + vd->sys->i_buffers++;
> + }
> +
> + }
> +
> + return VLC_SUCCESS;
> +
> + error:
Jump labels should be more to the left.
> + close( vd->sys->fd );
> + free_buffers( vd->sys );
> + free( vd->sys );
> + return VLC_EGENERIC;
> +}
> +
> +static void Close (vlc_object_t *obj)
> +{
> + vout_display_t *vd = (vout_display_t *)obj;
> +
> + if ( vd->sys->b_streaming )
> + toggle_stream( vd, VIDIOC_STREAMOFF );
> +
> + if (vd->sys->pool)
> + picture_pool_Delete(vd->sys->pool);
> +
> + close( vd->sys->fd );
> + if ( vd->display == PictureDisplayRW )
> + free( vd->sys->buffers[ 0 ].start ); /* free local buffer */
> + free_buffers( vd->sys );
> + free( vd->sys );
> +}
> +
> +static picture_pool_t *Pool(vout_display_t *vd, unsigned count)
> +{
> + vout_display_sys_t *sys = vd->sys;
> +
> + if (!sys->pool)
> + sys->pool = picture_pool_NewFromFormat(&vd->fmt, count);
> + return sys->pool;
> +}
> +
> +static void PictureDisplayRW(vout_display_t *vd, picture_t *picture,
> subpicture_t *subpicture) +{
> + assert( vd->sys->i_buffers );
> + vout_display_sys_t *sys = vd->sys;
> + struct buffer_t *buffer = &vd->sys->buffers[0];
> + unsigned int i_offset;
> +
> + /* we need to merge into memory to make only 1 write */
> + copy_frames( picture, buffer, &i_offset );
> + if ( i_offset != write( sys->fd, buffer->start, i_offset ) )
> + msg_Warn( vd, "can't fully write to device");
> +
> + picture_Release(picture);
> + VLC_UNUSED(subpicture);
> +}
> +
> +static void PictureDisplayMMAP(vout_display_t *vd, picture_t *picture,
> subpicture_t *subpicture) +{
> + vout_display_sys_t *sys = vd->sys;
> + struct v4l2_buffer freebuf;
> +
> + freebuf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
> + freebuf.memory = V4L2_MEMORY_MMAP;
> + freebuf.flags = 0;
> +
> + /* Query the driver for a free buffer */
> + if ( ioctl( sys->fd, VIDIOC_DQBUF, &freebuf ) < 0 )
This looks like something for the picture lock callback.
> + msg_Warn( vd, "can't VIDIOC_DQBUF");
> + else
> + {
> + struct buffer_t *buffer = &vd->sys->buffers[ freebuf.index ];
> + unsigned int i_offset = 0;
> +
> + copy_frames( picture, buffer, &i_offset );
> +
> + struct timeval tv;
> + gettimeofday (&tv, NULL);
> + freebuf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
> + freebuf.memory = V4L2_MEMORY_MMAP;
> + freebuf.timestamp = tv;
> + freebuf.flags = 0;
> + freebuf.reserved = 0;
> + freebuf.field = V4L2_FIELD_NONE;
> + freebuf.bytesused = i_offset;
> + if ( ioctl( sys->fd, VIDIOC_QBUF, &freebuf ) < 0 )
> + msg_Err( vd, "VIDIOC_QBUF");
> + /* we should test for V4L2_BUF_FLAG_QUEUED flag here */
> + /* but this won't work with the v4l2loopback driver which changes
> */ + /* flag immediately to DONE and returns it in the ioctl */ +
> + if ( !sys->b_streaming )
> + {
> + if ( toggle_stream( vd, VIDIOC_STREAMON ) < 0 )
> + msg_Warn( vd, "can't VIDIOC_STREAMON");
> + else
> + sys->b_streaming = true;
> + }
IIUC, streaming should be enabled in Open() to reserve the device.
> +
> + }
> +
> + picture_Release(picture);
> + VLC_UNUSED(subpicture);
> +}
> +
> +static int Control(vout_display_t *vd, int query, va_list args)
> +{
> + VLC_UNUSED(vd);
> + VLC_UNUSED(args);
> + switch (query) {
> + default:
> + return VLC_EGENERIC;
> + }
> +}
--
Rémi Denis-Courmont
http://www.remlab.net/
http://fi.linkedin.com/in/remidenis
More information about the vlc-devel
mailing list