[vlc-devel] [PATCH] [WIP] Partial UPnP rewrite

Hugo Beauzée-Luyssen hugo at beauzee.fr
Tue Mar 10 23:40:19 CET 2015


On 03/10/2015 07:25 PM, Tristan Matthews wrote:
> On Tue, Mar 10, 2015 at 1:59 PM, Hugo Beauzée-Luyssen <hugo at beauzee.fr> wrote:
>> This is splitting the UPnP module upon 2 parts:
>> - A service discovery module that is solely responsible for discovering
>> UPnP devices on the network
>> - An access module, that will leverage the recently introduced
>> pf_readdir calback to list directories.
>>
>> This removes the need for recursion and handling of all the items from
>> withing the SD module.
>> It hasn't been tested thoroughly yet, though all comments are more than
>> welcome.
>> ---
>>  modules/services_discovery/upnp.cpp | 1229 ++++++++++++-----------------------
>>  modules/services_discovery/upnp.hpp |  181 ++----
>>  2 files changed, 487 insertions(+), 923 deletions(-)
>>
>> diff --git a/modules/services_discovery/upnp.cpp b/modules/services_discovery/upnp.cpp
>> index ab765aa..4f252cc 100644
>> --- a/modules/services_discovery/upnp.cpp
>> +++ b/modules/services_discovery/upnp.cpp
>> @@ -7,6 +7,7 @@
>>   * Authors: Rémi Denis-Courmont <rem # videolan.org> (original plugin)
>>   *          Christian Henz <henz # c-lab.de>
>>   *          Mirsal Ennaime <mirsal dot ennaime at gmail dot com>
>> + *          Hugo Beauzée-Luyssen <hugo at beauzee.fr>
>>   *
>>   * UPnP Plugin using the Intel SDK (libupnp) instead of CyberLink
>>   *
>> @@ -32,13 +33,16 @@
>>  # include "config.h"
>>  #endif
>>
>> -#include "services_discovery/upnp.hpp"
>> +#include "upnp.hpp"
>>
>> +#include <vlc_access.h>
>>  #include <vlc_plugin.h>
>>  #include <vlc_services_discovery.h>
>> +#include <vlc_url.h>
>>
>>  #include <assert.h>
>>  #include <limits.h>
>> +#include <algorithm>
>>
>>  /*
>>   * Constants
>> @@ -51,139 +55,63 @@ const char* CONTENT_DIRECTORY_SERVICE_TYPE = "urn:schemas-upnp-org:service:Conte
>>   */
>>  struct services_discovery_sys_t
>>  {
>> -    UpnpClient_Handle client_handle;
>> -    MediaServerList* p_server_list;
>> -    vlc_mutex_t callback_lock;
>> +    SD::MediaServerList* p_server_list;
>> +    UpnpInstanceWrapper* p_upnp;
>>  };
>>
>> +struct access_sys_t
>> +{
>> +    UpnpInstanceWrapper* p_upnp;
>> +};
>> +
>> +UpnpInstanceWrapper* UpnpInstanceWrapper::s_instance;
>> +vlc_mutex_t UpnpInstanceWrapper::s_lock = VLC_STATIC_MUTEX;
>> +
>>  /*
>>   * VLC callback prototypes
>>   */
>> -static int Open( vlc_object_t* );
>> -static void Close( vlc_object_t* );
>> +namespace SD
>> +{
>> +    static int Open( vlc_object_t* );
>> +    static void Close( vlc_object_t* );
>> +}
>> +
>> +namespace Access
>> +{
>> +    static int Open( vlc_object_t* );
>> +    static void Close( vlc_object_t* );
>> +}
>> +
>>  VLC_SD_PROBE_HELPER( "upnp", "Universal Plug'n'Play", SD_CAT_LAN )
>>
>>  /*
>>   * Module descriptor
>>   */
>> -vlc_module_begin();
>> +vlc_module_begin()
>>      set_shortname( "UPnP" );
>>      set_description( N_( "Universal Plug'n'Play" ) );
>>      set_category( CAT_PLAYLIST );
>>      set_subcategory( SUBCAT_PLAYLIST_SD );
>>      set_capability( "services_discovery", 0 );
>> -    set_callbacks( Open, Close );
>> +    set_callbacks( SD::Open, SD::Close );
>> +
>> +    add_submodule()
>> +        set_category( CAT_INPUT )
>> +        set_subcategory( SUBCAT_INPUT_ACCESS )
>> +        set_callbacks( Access::Open, Access::Close )
>> +        set_capability( "access", 0 )
>>
>>      VLC_SD_PROBE_SUBMODULE
>> -vlc_module_end();
>> +vlc_module_end()
>>
>> -/*
>> - * Local prototypes
>> - */
>> -static int Callback( Upnp_EventType event_type, void* p_event, void* p_user_data );
>>
>> +/* XML utility functions */
>>  const char* xml_getChildElementValue( IXML_Element* p_parent,
>>                                        const char*   psz_tag_name );
>>
>>  const char* xml_getChildElementValue( IXML_Document* p_doc,
>>                                        const char*    psz_tag_name );
>>
>> -const char* xml_getChildElementAttributeValue( IXML_Element* p_parent,
>> -                                        const char* psz_tag_name,
>> -                                        const char* psz_attribute );
>> -
>> -int xml_getNumber( IXML_Document* p_doc,
>> -                   const char*    psz_tag_name );
>> -
>> -IXML_Document* parseBrowseResult( IXML_Document* p_doc );
>> -
>> -/*
>> - * Initializes UPNP instance.
>> - */
>> -static int Open( vlc_object_t *p_this )
>> -{
>> -    int i_res;
>> -    services_discovery_t *p_sd = ( services_discovery_t* )p_this;
>> -    services_discovery_sys_t *p_sys  = ( services_discovery_sys_t * )
>> -            calloc( 1, sizeof( services_discovery_sys_t ) );
>> -
>> -    if( !( p_sd->p_sys = p_sys ) )
>> -        return VLC_ENOMEM;
>> -
>> -#ifdef UPNP_ENABLE_IPV6
>> -    char* psz_miface;
>> -    psz_miface = var_InheritString( p_sd, "miface" );
>> -    msg_Info( p_sd, "Initializing libupnp on '%s' interface", psz_miface );
>> -    i_res = UpnpInit2( psz_miface, 0 );
>> -    free( psz_miface );
>> -#else
>> -    /* If UpnpInit2 isnt available, initialize on first IPv4-capable interface */
>> -    i_res = UpnpInit( 0, 0 );
>> -#endif
>> -    if( i_res != UPNP_E_SUCCESS )
>> -    {
>> -        msg_Err( p_sd, "Initialization failed: %s", UpnpGetErrorMessage( i_res ) );
>> -        free( p_sys );
>> -        return VLC_EGENERIC;
>> -    }
>> -
>> -    ixmlRelaxParser( 1 );
>> -
>> -    p_sys->p_server_list = new MediaServerList( p_sd );
>> -    vlc_mutex_init( &p_sys->callback_lock );
>> -
>> -    /* Register a control point */
>> -    i_res = UpnpRegisterClient( Callback, p_sd, &p_sys->client_handle );
>> -    if( i_res != UPNP_E_SUCCESS )
>> -    {
>> -        msg_Err( p_sd, "Client registration failed: %s", UpnpGetErrorMessage( i_res ) );
>> -        Close( (vlc_object_t*) p_sd );
>> -        return VLC_EGENERIC;
>> -    }
>> -
>> -    /* Search for media servers */
>> -    i_res = UpnpSearchAsync( p_sys->client_handle, 5,
>> -            MEDIA_SERVER_DEVICE_TYPE, p_sd );
>> -    if( i_res != UPNP_E_SUCCESS )
>> -    {
>> -        msg_Err( p_sd, "Error sending search request: %s", UpnpGetErrorMessage( i_res ) );
>> -        Close( (vlc_object_t*) p_sd );
>> -        return VLC_EGENERIC;
>> -    }
>> -
>> -    /* libupnp does not treat a maximum content length of 0 as unlimited
>> -     * until 64dedf (~ pupnp v1.6.7) and provides no sane way to discriminate
>> -     * between versions */
>> -    if( (i_res = UpnpSetMaxContentLength( INT_MAX )) != UPNP_E_SUCCESS )
>> -    {
>> -        msg_Err( p_sd, "Failed to set maximum content length: %s",
>> -                UpnpGetErrorMessage( i_res ));
>> -
>> -        Close( (vlc_object_t*) p_sd );
>> -        return VLC_EGENERIC;
>> -    }
>> -
>> -    return VLC_SUCCESS;
>> -}
>> -
>> -/*
>> - * Releases resources.
>> - */
>> -static void Close( vlc_object_t *p_this )
>> -{
>> -    services_discovery_t *p_sd = ( services_discovery_t* )p_this;
>> -
>> -    UpnpUnRegisterClient( p_sd->p_sys->client_handle );
>> -    UpnpFinish();
>> -
>> -    delete p_sd->p_sys->p_server_list;
>> -    vlc_mutex_destroy( &p_sd->p_sys->callback_lock );
>> -
>> -    free( p_sd->p_sys );
>> -}
>> -
>> -/* XML utility functions */
>> -
>>  /*
>>   * Returns the value of a child element, or NULL on error
>>   */
>> @@ -208,28 +136,6 @@ const char* xml_getChildElementValue( IXML_Element* p_parent,
>>  }
>>
>>  /*
>> - * Returns the value of a child element's attribute, or NULL on error
>> - */
>> -const char* xml_getChildElementAttributeValue( IXML_Element* p_parent,
>> -                                        const char* psz_tag_name,
>> -                                        const char* psz_attribute )
>> -{
>> -    assert( p_parent );
>> -    assert( psz_tag_name );
>> -    assert( psz_attribute );
>> -
>> -    IXML_NodeList* p_node_list;
>> -    p_node_list = ixmlElement_getElementsByTagName( p_parent, psz_tag_name );
>> -    if ( !p_node_list )   return NULL;
>> -
>> -    IXML_Node* p_element = ixmlNodeList_item( p_node_list, 0 );
>> -    ixmlNodeList_free( p_node_list );
>> -    if ( !p_element )     return NULL;
>> -
>> -    return ixmlElement_getAttribute( (IXML_Element*) p_element, psz_attribute );
>> -}
>> -
>> -/*
>>   * Returns the value of a child element, or NULL on error
>>   */
>>  const char* xml_getChildElementValue( IXML_Document*  p_doc,
>> @@ -302,171 +208,173 @@ IXML_Document* parseBrowseResult( IXML_Document* p_doc )
>>      return (IXML_Document*)p_node;
>>  }
>>
>> +namespace SD
>> +{
>> +
>>  /*
>> - * Get the number value from a SOAP response
>> + * Initializes UPNP instance.
>>   */
>> -int xml_getNumber( IXML_Document* p_doc,
>> -                   const char* psz_tag_name )
>> +static int Open( vlc_object_t *p_this )
>>  {
>> -    assert( p_doc );
>> -    assert( psz_tag_name );
>> +    services_discovery_t *p_sd = ( services_discovery_t* )p_this;
>> +    services_discovery_sys_t *p_sys  = ( services_discovery_sys_t * )
>> +            calloc( 1, sizeof( services_discovery_sys_t ) );
>>
>> -    const char* psz = xml_getChildElementValue( p_doc, psz_tag_name );
>> +    if( !( p_sd->p_sys = p_sys ) )
>> +        return VLC_ENOMEM;
>>
>> -    if( !psz )
>> -        return 0;
>> +    p_sys->p_server_list = new(std::nothrow) SD::MediaServerList( p_sd );
>> +    if ( unlikely( p_sys->p_server_list == NULL ) )
>> +    {
>> +        return VLC_ENOMEM;
>> +    }
>>
>> -    char *psz_end;
>> -    long l = strtol( psz, &psz_end, 10 );
>> +    p_sys->p_upnp = UpnpInstanceWrapper::get( p_this, SD::MediaServerList::Callback, p_sys->p_server_list );
>> +    if ( !p_sys->p_upnp )
>> +    {
>> +        Close( p_this );
>> +        return VLC_EGENERIC;
>> +    }
>>
>> -    if( *psz_end || l < 0 || l > INT_MAX )
>> -        return 0;
>> +    /* Search for media servers */
>> +    int i_res = UpnpSearchAsync( p_sys->p_upnp->handle(), 5,
>> +            MEDIA_SERVER_DEVICE_TYPE, p_sys->p_server_list );
>> +    if( i_res != UPNP_E_SUCCESS )
>> +    {
>> +        msg_Err( p_sd, "Error sending search request: %s", UpnpGetErrorMessage( i_res ) );
>> +        Close( (vlc_object_t*) p_sd );
> 
> Why not just Close( p_this ); like above?

Ahem, because... reasons? :)
I'll fix this.

> 
>> +        return VLC_EGENERIC;
>> +    }
>>
>> -    return (int)l;
>> +    return VLC_SUCCESS;
>>  }
>>
>>  /*
>> - * Handles all UPnP events
>> + * Releases resources.
>>   */
>> -static int Callback( Upnp_EventType event_type, void* p_event, void* p_user_data )
>> +static void Close( vlc_object_t *p_this )
>>  {
>> -    services_discovery_t* p_sd = ( services_discovery_t* ) p_user_data;
>> -    services_discovery_sys_t* p_sys = p_sd->p_sys;
>> -    vlc_mutex_locker locker( &p_sys->callback_lock );
>> -
>> -    switch( event_type )
>> -    {
>> -    case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
>> -    case UPNP_DISCOVERY_SEARCH_RESULT:
>> -    {
>> -        struct Upnp_Discovery* p_discovery = ( struct Upnp_Discovery* )p_event;
>> +    services_discovery_t *p_sd = ( services_discovery_t* )p_this;
>> +    services_discovery_sys_t *p_sys = p_sd->p_sys;
>>
>> -        IXML_Document *p_description_doc = 0;
>> +    if (p_sys->p_upnp)
>> +        p_sys->p_upnp->release( true );
>> +    delete p_sys->p_server_list;
>> +    free( p_sys );
>> +}
>>
>> -        int i_res;
>> -        i_res = UpnpDownloadXmlDoc( p_discovery->Location, &p_description_doc );
>> -        if ( i_res != UPNP_E_SUCCESS )
>> -        {
>> -            msg_Warn( p_sd, "Could not download device description! "
>> -                            "Fetching data from %s failed: %s",
>> -                            p_discovery->Location, UpnpGetErrorMessage( i_res ) );
>> -            return i_res;
>> -        }
>> +MediaServerDesc::MediaServerDesc(const std::string& udn, const std::string& fName, const std::string& loc)
>> +    : UDN( udn )
>> +    , friendlyName( fName )
>> +    , location( loc )
>> +    , inputItem( NULL )
>> +{
>> +}
>>
>> -        MediaServer::parseDeviceDescription( p_description_doc,
>> -                p_discovery->Location, p_sd );
>> +MediaServerDesc::~MediaServerDesc()
>> +{
>> +    if (inputItem)
>> +        vlc_gc_decref( inputItem );
>> +}
>>
>> -        ixmlDocument_free( p_description_doc );
>> -    }
>> -    break;
>> +/*
>> + * MediaServerList class
>> + */
>> +MediaServerList::MediaServerList( services_discovery_t* p_sd )
>> +    : _p_sd( p_sd )
>> +{
>> +    vlc_mutex_init( &_lock );
>> +}
>>
>> -    case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE:
>> -    {
>> -        struct Upnp_Discovery* p_discovery = ( struct Upnp_Discovery* )p_event;
>> +MediaServerList::~MediaServerList()
>> +{
>> +    vlc_delete_all(_list);
>> +    vlc_mutex_destroy( &_lock );
>> +}
>>
>> -        p_sys->p_server_list->removeServer( p_discovery->DeviceId );
>> +bool MediaServerList::addServer( MediaServerDesc* desc )
>> +{
>> +    vlc_mutex_locker lock( &_lock );
>> +    input_item_t* p_input_item = NULL;
>> +    if ( getServer( desc->UDN ) != 0 )
>> +        return false;
>>
>> -    }
>> -    break;
>> +    msg_Dbg( _p_sd, "Adding server '%s' with uuid '%s'", desc->friendlyName.c_str(), desc->UDN.c_str() );
>>
>> -    case UPNP_EVENT_RECEIVED:
>> -    {
>> -        Upnp_Event* p_e = ( Upnp_Event* )p_event;
>> +    char* psz_mrl;
>> +    if( asprintf(&psz_mrl, "upnp://%s?ObjectID=%s", desc->location.c_str(), desc->UDN.c_str() ) < 0 )
>> +        return false;
>>
>> -        MediaServer* p_server = p_sys->p_server_list->getServerBySID( p_e->Sid );
>> -        if ( p_server ) p_server->fetchContents();
>> -    }
>> -    break;
>> +    p_input_item = input_item_NewWithType( psz_mrl, desc->friendlyName.c_str(), 0,
>> +                                           NULL, 0, -1, ITEM_TYPE_NODE );
>> +    free( psz_mrl );
>> +    if ( !p_input_item )
>> +        return false;
>> +    desc->inputItem = p_input_item;
>> +    input_item_SetDescription( p_input_item, desc->UDN.c_str() );
>> +    services_discovery_AddItem( _p_sd, p_input_item, NULL );
>> +    _list.push_back( desc );
>> +    return true;
>> +}
>>
>> -    case UPNP_EVENT_AUTORENEWAL_FAILED:
>> -    case UPNP_EVENT_SUBSCRIPTION_EXPIRED:
>> +MediaServerDesc* MediaServerList::getServer( const std::string& udn )
>> +{
>> +    for ( unsigned int i = 0; i < _list.size(); i++ )
>>      {
>> -        /* Re-subscribe. */
>> -
>> -        Upnp_Event_Subscribe* p_s = ( Upnp_Event_Subscribe* )p_event;
>> -
>> -        MediaServer* p_server = p_sys->p_server_list->getServerBySID( p_s->Sid );
>> -        if ( p_server ) p_server->subscribeToContentDirectory();
>> -    }
>> -    break;
>> -
>> -    case UPNP_EVENT_SUBSCRIBE_COMPLETE:
>> -        msg_Warn( p_sd, "subscription complete" );
>> -        break;
>> -
>> -    case UPNP_DISCOVERY_SEARCH_TIMEOUT:
>> -        msg_Warn( p_sd, "search timeout" );
>> -        break;
>> -
>> -    default:
>> -        msg_Err( p_sd, "Unhandled event, please report ( type=%d )", event_type );
>> -        break;
>> +        if( udn == _list[i]->UDN )
>> +        {
>> +            return _list[i];
>> +        }
>>      }
>> -
>> -    return UPNP_E_SUCCESS;
>> +    return NULL;
>>  }
>>
>> -
>> -/*
>> - * Local class implementations.
>> - */
>> -
>> -/*
>> - * MediaServer
>> - */
>> -
>> -void MediaServer::parseDeviceDescription( IXML_Document* p_doc,
>> -                                          const char*    p_location,
>> -                                          services_discovery_t* p_sd )
>> +void MediaServerList::parseNewServer( IXML_Document *doc, const std::string &location )
>>  {
>> -    if ( !p_doc )
>> +    if ( !doc )
>>      {
>> -        msg_Err( p_sd, "Null IXML_Document" );
>> +        msg_Err( _p_sd, "Null IXML_Document" );
>>          return;
>>      }
>>
>> -    if ( !p_location )
>> +    if ( location.empty() )
>>      {
>> -        msg_Err( p_sd, "Null location" );
>> +        msg_Err( _p_sd, "Empty location" );
>>          return;
>>      }
>>
>> -    const char* psz_base_url = p_location;
>> +    const char* psz_base_url = location.c_str();
>>
>>      /* Try to extract baseURL */
>> -    IXML_NodeList* p_url_list = ixmlDocument_getElementsByTagName( p_doc, "URLBase" );
>> +    IXML_NodeList* p_url_list = ixmlDocument_getElementsByTagName( doc, "URLBase" );
>>      if ( p_url_list )
>>      {
>> -
>>          if ( IXML_Node* p_url_node = ixmlNodeList_item( p_url_list, 0 ) )
>>          {
>>              IXML_Node* p_text_node = ixmlNode_getFirstChild( p_url_node );
>> -            if ( p_text_node ) psz_base_url = ixmlNode_getNodeValue( p_text_node );
>> +            if ( p_text_node )
>> +                psz_base_url = ixmlNode_getNodeValue( p_text_node );
>>          }
>> -
>>          ixmlNodeList_free( p_url_list );
>>      }
>>
>>      /* Get devices */
>> -    IXML_NodeList* p_device_list =
>> -                ixmlDocument_getElementsByTagName( p_doc, "device" );
>> +    IXML_NodeList* p_device_list = ixmlDocument_getElementsByTagName( doc, "device" );
>>
>>      if ( p_device_list )
>>      {
>>          for ( unsigned int i = 0; i < ixmlNodeList_length( p_device_list ); i++ )
>>          {
>> -            IXML_Element* p_device_element =
>> -                   ( IXML_Element* ) ixmlNodeList_item( p_device_list, i );
>> +            IXML_Element* p_device_element = ( IXML_Element* ) ixmlNodeList_item( p_device_list, i );
>>
>>              if( !p_device_element )
>>                  continue;
>>
>> -            const char* psz_device_type =
>> -                xml_getChildElementValue( p_device_element, "deviceType" );
>> +            const char* psz_device_type = xml_getChildElementValue( p_device_element, "deviceType" );
>>
>>              if ( !psz_device_type )
>>              {
>> -                msg_Warn( p_sd, "No deviceType found!" );
>> +                msg_Warn( _p_sd, "No deviceType found!" );
>>                  continue;
>>              }
>>
>> @@ -478,14 +386,14 @@ void MediaServer::parseDeviceDescription( IXML_Document* p_doc,
>>                                                              "UDN" );
>>              if ( !psz_udn )
>>              {
>> -                msg_Warn( p_sd, "No UDN!" );
>> +                msg_Warn( _p_sd, "No UDN!" );
>>                  continue;
>>              }
>>
>>              /* Check if server is already added */
>> -            if ( p_sd->p_sys->p_server_list->getServer( psz_udn ) != 0 )
>> +            if ( _p_sd->p_sys->p_server_list->getServer( psz_udn ) != 0 )
>>              {
>> -                msg_Warn( p_sd, "Server with uuid '%s' already exists.", psz_udn );
>> +                msg_Warn( _p_sd, "Server with uuid '%s' already exists.", psz_udn );
>>                  continue;
>>              }
>>
>> @@ -495,38 +403,25 @@ void MediaServer::parseDeviceDescription( IXML_Document* p_doc,
>>
>>              if ( !psz_friendly_name )
>>              {
>> -                msg_Dbg( p_sd, "No friendlyName!" );
>> +                msg_Dbg( _p_sd, "No friendlyName!" );
>>                  continue;
>>              }
>>
>> -            MediaServer* p_server = new MediaServer( psz_udn,
>> -                    psz_friendly_name, p_sd );
>> -
>> -            if ( !p_sd->p_sys->p_server_list->addServer( p_server ) )
>> -            {
>> -                delete p_server;
>> -                p_server = 0;
>> -                continue;
>> -            }
>> +            // We now have basic info, we need to get the content browsing url
>> +            // so the access module can browse without fetching the manifest again
>>
>>              /* Check for ContentDirectory service. */
>> -            IXML_NodeList* p_service_list =
>> -                       ixmlElement_getElementsByTagName( p_device_element,
>> -                                                         "service" );
>> +            IXML_NodeList* p_service_list = ixmlElement_getElementsByTagName( p_device_element, "service" );
>>              if ( p_service_list )
>>              {
>> -                for ( unsigned int j = 0;
>> -                      j < ixmlNodeList_length( p_service_list ); j++ )
>> +                for ( unsigned int j = 0; j < ixmlNodeList_length( p_service_list ); j++ )
>>                  {
>> -                    IXML_Element* p_service_element =
>> -                       ( IXML_Element* ) ixmlNodeList_item( p_service_list, j );
>> +                    IXML_Element* p_service_element = (IXML_Element*)ixmlNodeList_item( p_service_list, j );
>>
>> -                    const char* psz_service_type =
>> -                        xml_getChildElementValue( p_service_element,
>> -                                                  "serviceType" );
>> +                    const char* psz_service_type = xml_getChildElementValue( p_service_element, "serviceType" );
>>                      if ( !psz_service_type )
>>                      {
>> -                        msg_Warn( p_sd, "No service type found." );
>> +                        msg_Warn( _p_sd, "No service type found." );
>>                          continue;
>>                      }
>>
>> @@ -535,241 +430,256 @@ void MediaServer::parseDeviceDescription( IXML_Document* p_doc,
>>                                  psz_service_type, k ) != 0 )
>>                          continue;
>>
>> -                   p_server->_i_content_directory_service_version =
>> -                       psz_service_type[k];
>> -
>> -                    const char* psz_event_sub_url =
>> -                        xml_getChildElementValue( p_service_element,
>> -                                                  "eventSubURL" );
>> -                    if ( !psz_event_sub_url )
>> -                    {
>> -                        msg_Warn( p_sd, "No event subscription url found." );
>> -                        continue;
>> -                    }
>> -
>> -                    const char* psz_control_url =
>> -                        xml_getChildElementValue( p_service_element,
>> +                    const char* psz_control_url = xml_getChildElementValue( p_service_element,
>>                                                    "controlURL" );
>>                      if ( !psz_control_url )
>>                      {
>> -                        msg_Warn( p_sd, "No control url found." );
>> +                        msg_Warn( _p_sd, "No control url found." );
>>                          continue;
>>                      }
>>
>> -                    /* Try to subscribe to ContentDirectory service */
>> -
>> -                    char* psz_url = ( char* ) malloc( strlen( psz_base_url ) +
>> -                            strlen( psz_event_sub_url ) + 1 );
>> -                    if ( psz_url )
>> -                    {
>> -                        if ( UpnpResolveURL( psz_base_url, psz_event_sub_url, psz_url ) ==
>> -                                UPNP_E_SUCCESS )
>> -                        {
>> -                            p_server->setContentDirectoryEventURL( psz_url );
>> -                            p_server->subscribeToContentDirectory();
>> -                        }
>> -
>> -                        free( psz_url );
>> -                    }
>> -
>>                      /* Try to browse content directory. */
>> -
>> -                    psz_url = ( char* ) malloc( strlen( psz_base_url ) +
>> +                    char* psz_url = ( char* ) malloc( strlen( psz_base_url ) +
> 
> It was already like this before, but in a separate patch perhaps this
> function should error out if malloc fails.
> 

Fair point. I don't think it makes sense to split this out of another
patch at this point though.

>>                              strlen( psz_control_url ) + 1 );
>>                      if ( psz_url )
>>                      {
>> -                        if ( UpnpResolveURL( psz_base_url, psz_control_url, psz_url ) ==
>> -                                UPNP_E_SUCCESS )
>> +                        if ( UpnpResolveURL( psz_base_url, psz_control_url, psz_url ) == UPNP_E_SUCCESS )
>>                          {
>> -                            p_server->setContentDirectoryControlURL( psz_url );
>> -                            p_server->fetchContents();
>> +                            SD::MediaServerDesc* p_server = new SD::MediaServerDesc( psz_udn,
>> +                                    psz_friendly_name, psz_url );
> 
> Any reason for using nothrow elsewhere but not here?
> 

I have plenty of reasons, but no good one :)
Will fix.

>> +
>> +                            if ( !addServer( p_server ) )
>> +                            {
>> +                                delete p_server;
>> +                                continue;
>> +                            }
>>                          }
>>
>>                          free( psz_url );
>>                      }
>>                 }
>>                 ixmlNodeList_free( p_service_list );
>> -           }
>> +            }
>>         }
>>         ixmlNodeList_free( p_device_list );
>>      }
>>  }
>>
>> -MediaServer::MediaServer( const char* psz_udn,
>> -                          const char* psz_friendly_name,
>> -                          services_discovery_t* p_sd )
>> +void MediaServerList::removeServer( const std::string& udn )
>>  {
>> -    _p_sd = p_sd;
>> +    vlc_mutex_locker lock( &_lock );
>>
>> -    _UDN = psz_udn;
>> -    _friendly_name = psz_friendly_name;
>> +    MediaServerDesc* p_server = getServer( udn );
>> +    if ( !p_server )
>> +        return;
>>
>> -    _p_contents = NULL;
>> -    _p_input_item = NULL;
>> -    _i_content_directory_service_version = 1;
>> -}
>> +    msg_Dbg( _p_sd, "Removing server '%s'", p_server->friendlyName.c_str() );
>>
>> -MediaServer::~MediaServer()
>> -{
>> -    delete _p_contents;
>> -}
>> +    assert(p_server->inputItem);
>> +    services_discovery_RemoveItem( _p_sd, p_server->inputItem );
>>
>> -const char* MediaServer::getUDN() const
>> -{
>> -    return _UDN.c_str();
>> +    std::vector<MediaServerDesc*>::iterator it = std::find(_list.begin(), _list.end(), p_server);
>> +    if (it != _list.end())
>> +    {
>> +        _list.erase( it );
>> +    }
>> +    delete p_server;
>>  }
>>
>> -const char* MediaServer::getFriendlyName() const
>> +/*
>> + * Handles servers listing UPnP events
>> + */
>> +int MediaServerList::Callback( Upnp_EventType event_type, void* p_event, void* p_user_data )
>>  {
>> -    return _friendly_name.c_str();
>> -}
>> +    MediaServerList* self = static_cast<MediaServerList*>( p_user_data );
>> +    services_discovery_t* p_sd = self->_p_sd;
>>
>> -void MediaServer::setContentDirectoryEventURL( const char* psz_url )
>> -{
>> -    _content_directory_event_url = psz_url;
>> +    switch( event_type )
>> +    {
>> +    case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
>> +    case UPNP_DISCOVERY_SEARCH_RESULT:
>> +    {
>> +        struct Upnp_Discovery* p_discovery = ( struct Upnp_Discovery* )p_event;
>> +
>> +        IXML_Document *p_description_doc = 0;
>> +
>> +        int i_res;
>> +        i_res = UpnpDownloadXmlDoc( p_discovery->Location, &p_description_doc );
>> +        if ( i_res != UPNP_E_SUCCESS )
>> +        {
>> +            msg_Warn( p_sd, "Could not download device description! "
>> +                            "Fetching data from %s failed: %s",
>> +                            p_discovery->Location, UpnpGetErrorMessage( i_res ) );
>> +            return i_res;
>> +        }
>> +        self->parseNewServer( p_description_doc, p_discovery->Location );
>> +        ixmlDocument_free( p_description_doc );
>> +    }
>> +    break;
>> +
>> +    case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE:
>> +    {
>> +        struct Upnp_Discovery* p_discovery = ( struct Upnp_Discovery* )p_event;
>> +
>> +        self->removeServer( p_discovery->DeviceId );
>> +
>> +    }
>> +    break;
>> +
>> +    case UPNP_EVENT_SUBSCRIBE_COMPLETE:
>> +        msg_Warn( p_sd, "subscription complete" );
>> +        break;
>> +
>> +    case UPNP_DISCOVERY_SEARCH_TIMEOUT:
>> +        msg_Warn( p_sd, "search timeout" );
>> +        break;
>> +
>> +    case UPNP_EVENT_RECEIVED:
>> +    case UPNP_EVENT_AUTORENEWAL_FAILED:
>> +    case UPNP_EVENT_SUBSCRIPTION_EXPIRED:
>> +        // Those are for the access part
>> +        break;
>> +
>> +    default:
>> +        msg_Err( p_sd, "Unhandled event, please report ( type=%d )", event_type );
>> +        break;
>> +    }
>> +
>> +    return UPNP_E_SUCCESS;
>>  }
>>
>> -const char* MediaServer::getContentDirectoryEventURL() const
>> -{
>> -    return _content_directory_event_url.c_str();
>>  }
>>
>> -void MediaServer::setContentDirectoryControlURL( const char* psz_url )
>> +namespace Access
>>  {
>> -    _content_directory_control_url = psz_url;
>> -}
>>
>> -const char* MediaServer::getContentDirectoryControlURL() const
>> +MediaServer::MediaServer(const char *psz_url, access_t *p_access, input_item_node_t *node)
>> +    : _url( psz_url )
>> +    , _access( p_access )
>> +    , _node( node )
>>  {
>> -    return _content_directory_control_url.c_str();
>>  }
>>
>> -/**
>> - * Subscribes current client handle to Content Directory Service.
>> - * CDS exports the server shares to clients.
>> - */
>> -void MediaServer::subscribeToContentDirectory()
>> +void MediaServer::addItem(const char *objectID, const char *title )
>>  {
>> -    const char* psz_url = getContentDirectoryEventURL();
>> -    if ( !psz_url )
>> +    vlc_url_t url;
>> +    vlc_UrlParse( &url, _url.c_str(), '?' );
>> +    char* psz_url;
>> +
>> +    if (asprintf( &psz_url, "upnp://%s://%s:%u%s?ObjectID=%s", url.psz_protocol,
>> +                  url.psz_host, url.i_port ? url.i_port : 80, url.psz_path, objectID ) < 0 )
>>      {
>> -        msg_Dbg( _p_sd, "No subscription url set!" );
>> -        return;
>> +        vlc_UrlClean( &url );
>> +        return ;
>>      }
>> +    vlc_UrlClean( &url );
>>
>> -    int i_timeout = 1810;
>> -    Upnp_SID sid;
>> -
>> -    int i_res = UpnpSubscribe( _p_sd->p_sys->client_handle, psz_url, &i_timeout, sid );
>> +    input_item_t* p_item = input_item_NewWithType( psz_url, title, 0, NULL,
>> +                                                   0, -1, ITEM_TYPE_NODE );
>> +    free( psz_url);
>> +    if ( !p_item )
>> +        return;
>> +    input_item_CopyOptions( _node->p_item, p_item );
>> +    input_item_node_AppendItem( _node, p_item );
>> +    input_item_Release( p_item );
>> +}
>>
>> -    if ( i_res == UPNP_E_SUCCESS )
>> -    {
>> -        _i_subscription_timeout = i_timeout;
>> -        memcpy( _subscription_id, sid, sizeof( Upnp_SID ) );
>> -    }
>> -    else
>> -    {
>> -        msg_Dbg( _p_sd, "Subscribe failed: '%s': %s",
>> -                getFriendlyName(), UpnpGetErrorMessage( i_res ) );
>> -    }
>> +void MediaServer::addItem(const char* title, const char*, const char*,
>> +                          mtime_t duration, const char* psz_url)
>> +{
>> +    input_item_t* p_item = input_item_NewExt( psz_url, title, 0, NULL, 0, duration );
>> +    input_item_node_AppendItem( _node, p_item );
>> +    input_item_Release( p_item );
>>  }
>> -/*
>> - * Constructs UpnpAction to browse available content.
>> - */
>> +
>> +/* Access part */
>>  IXML_Document* MediaServer::_browseAction( const char* psz_object_id_,
>>                                             const char* psz_browser_flag_,
>>                                             const char* psz_filter_,
>> -                                           const char* psz_starting_index_,
>>                                             const char* psz_requested_count_,
>>                                             const char* psz_sort_criteria_ )
>>  {
>>      IXML_Document* p_action = 0;
>>      IXML_Document* p_response = 0;
>> -    const char* psz_url = getContentDirectoryControlURL();
>> +    const char* psz_url = _url.c_str();
>>
>> -    if ( !psz_url )
>> +    if ( _url.empty() )
>>      {
>> -        msg_Dbg( _p_sd, "No subscription url set!" );
>> +        msg_Dbg( _access, "No subscription url set!" );
>>          return 0;
>>      }
>>
>> -    char* psz_service_type = strdup( CONTENT_DIRECTORY_SERVICE_TYPE );
>> -
>> -    psz_service_type[strlen( psz_service_type ) - 1] =
>> -       _i_content_directory_service_version;
>> -
>>      int i_res;
>>
>>      i_res = UpnpAddToAction( &p_action, "Browse",
>> -            psz_service_type, "ObjectID", psz_object_id_ );
>> +            CONTENT_DIRECTORY_SERVICE_TYPE, "ObjectID", psz_object_id_ );
>>
>>      if ( i_res != UPNP_E_SUCCESS )
>>      {
>> -        msg_Dbg( _p_sd, "AddToAction 'ObjectID' failed: %s",
>> +        msg_Dbg( _access, "AddToAction 'ObjectID' failed: %s",
>>                  UpnpGetErrorMessage( i_res ) );
>>          goto browseActionCleanup;
>>      }
>>
>>      i_res = UpnpAddToAction( &p_action, "Browse",
>> -            psz_service_type, "BrowseFlag", psz_browser_flag_ );
>> -
>> +            CONTENT_DIRECTORY_SERVICE_TYPE, "StartingIndex", "0" );
>>      if ( i_res != UPNP_E_SUCCESS )
>>      {
>> -        msg_Dbg( _p_sd, "AddToAction 'BrowseFlag' failed: %s",
>> +        msg_Dbg( _access, "AddToAction 'StartingIndex' failed: %s",
>>                  UpnpGetErrorMessage( i_res ) );
>>          goto browseActionCleanup;
>>      }
>>
>>      i_res = UpnpAddToAction( &p_action, "Browse",
>> -            psz_service_type, "Filter", psz_filter_ );
>> +            CONTENT_DIRECTORY_SERVICE_TYPE, "BrowseFlag", psz_browser_flag_ );
>>
>>      if ( i_res != UPNP_E_SUCCESS )
>>      {
>> -        msg_Dbg( _p_sd, "AddToAction 'Filter' failed: %s",
>> +        msg_Dbg( _access, "AddToAction 'BrowseFlag' failed: %s",
>>                  UpnpGetErrorMessage( i_res ) );
>>          goto browseActionCleanup;
>>      }
>>
>>      i_res = UpnpAddToAction( &p_action, "Browse",
>> -            psz_service_type, "StartingIndex", psz_starting_index_ );
>> +            CONTENT_DIRECTORY_SERVICE_TYPE, "Filter", psz_filter_ );
>>
>>      if ( i_res != UPNP_E_SUCCESS )
>>      {
>> -        msg_Dbg( _p_sd, "AddToAction 'StartingIndex' failed: %s",
>> +        msg_Dbg( _access, "AddToAction 'Filter' failed: %s",
>>                  UpnpGetErrorMessage( i_res ) );
>>          goto browseActionCleanup;
>>      }
>>
>>      i_res = UpnpAddToAction( &p_action, "Browse",
>> -            psz_service_type, "RequestedCount", psz_requested_count_ );
>> +            CONTENT_DIRECTORY_SERVICE_TYPE, "RequestedCount", psz_requested_count_ );
>>
>>      if ( i_res != UPNP_E_SUCCESS )
>>      {
>> -        msg_Dbg( _p_sd, "AddToAction 'RequestedCount' failed: %s",
>> +        msg_Dbg( _access, "AddToAction 'RequestedCount' failed: %s",
>>                  UpnpGetErrorMessage( i_res ) );
>>          goto browseActionCleanup;
>>      }
>>
>>      i_res = UpnpAddToAction( &p_action, "Browse",
>> -            psz_service_type, "SortCriteria", psz_sort_criteria_ );
>> +            CONTENT_DIRECTORY_SERVICE_TYPE, "SortCriteria", psz_sort_criteria_ );
>>
>>      if ( i_res != UPNP_E_SUCCESS )
>>      {
>> -        msg_Dbg( _p_sd, "AddToAction 'SortCriteria' failed: %s",
>> +        msg_Dbg( _access, "AddToAction 'SortCriteria' failed: %s",
>>                  UpnpGetErrorMessage( i_res ) );
>>          goto browseActionCleanup;
>>      }
>>
>> -    i_res = UpnpSendAction( _p_sd->p_sys->client_handle,
>> +    i_res = UpnpSendAction( _access->p_sys->p_upnp->handle(),
>>                psz_url,
>> -              psz_service_type,
>> +              CONTENT_DIRECTORY_SERVICE_TYPE,
>>                0, /* ignored in SDK, must be NULL */
>>                p_action,
>>                &p_response );
>>
>>      if ( i_res != UPNP_E_SUCCESS )
>>      {
>> -        msg_Err( _p_sd, "%s when trying the send() action with URL: %s",
>> +        msg_Err( _access, "%s when trying the send() action with URL: %s",
>>                  UpnpGetErrorMessage( i_res ), psz_url );
>>
>>          ixmlDocument_free( p_response );
>> @@ -777,86 +687,51 @@ IXML_Document* MediaServer::_browseAction( const char* psz_object_id_,
>>      }
>>
>>  browseActionCleanup:
>> -
>> -    free( psz_service_type );
>> -
>>      ixmlDocument_free( p_action );
>>      return p_response;
>>  }
>>
>> -void MediaServer::fetchContents()
>> -{
>> -    /* Delete previous contents to prevent duplicate entries */
>> -    if ( _p_contents )
>> -    {
>> -        delete _p_contents;
>> -        services_discovery_RemoveItem( _p_sd, _p_input_item );
>> -        services_discovery_AddItem( _p_sd, _p_input_item, NULL );
>> -    }
>> -
>> -    Container* root = new Container( 0, "0", getFriendlyName() );
>> -
>> -    _fetchContents( root, 0 );
>> -
>> -    _p_contents = root;
>> -    _p_contents->setInputItem( _p_input_item );
>> -
>> -    _buildPlaylist( _p_contents, NULL );
>> -}
>> -
>>  /*
>>   * Fetches and parses the UPNP response
>>   */
>> -bool MediaServer::_fetchContents( Container* p_parent, int i_offset )
>> +bool MediaServer::fetchContents()
>>  {
>> -    if (!p_parent)
>> -    {
>> -        msg_Err( _p_sd, "No parent" );
>> -        return false;
>> -    }
>> +    const char* objectID = "";
>> +    vlc_url_t url;
>> +    vlc_UrlParse( &url, _access->psz_location, '?');
>>
>> -    char* psz_starting_index;
>> -    if( asprintf( &psz_starting_index, "%d", i_offset ) < 0 )
>> +    if ( url.psz_option && !strncmp( url.psz_option, "ObjectID=", strlen( "ObjectID=" ) ) )
>>      {
>> -        msg_Err( _p_sd, "asprintf error:%d", i_offset );
>> -        return false;
>> +        objectID = &url.psz_option[strlen( "ObjectID=" )];
>>      }
>>
>> -    IXML_Document* p_response = _browseAction( p_parent->getObjectID(),
>> +    IXML_Document* p_response = _browseAction( objectID,
>>                                        "BrowseDirectChildren",
>>                                        "id,dc:title,res," /* Filter */
>>                                        "sec:CaptionInfo,sec:CaptionInfoEx,"
>>                                        "pv:subtitlefile",
>> -                                      psz_starting_index, /* StartingIndex */
>>                                        "0", /* RequestedCount */
>>                                        "" /* SortCriteria */
>>                                        );
>> -    free( psz_starting_index );
>> +    vlc_UrlClean( &url );
>>      if ( !p_response )
>>      {
>> -        msg_Err( _p_sd, "No response from browse() action" );
>> +        msg_Err( _access, "No response from browse() action" );
>>          return false;
>>      }
>>
>>      IXML_Document* p_result = parseBrowseResult( p_response );
>> -    int i_number_returned = xml_getNumber( p_response, "NumberReturned" );
>> -    int i_total_matches   = xml_getNumber( p_response , "TotalMatches" );
>> -
>> -#ifndef NDEBUG
>> -    msg_Dbg( _p_sd, "i_offset[%d]i_number_returned[%d]_total_matches[%d]\n",
>> -             i_offset, i_number_returned, i_total_matches );
>> -#endif
>>
>>      ixmlDocument_free( p_response );
>>
>>      if ( !p_result )
>>      {
>> -        msg_Err( _p_sd, "browse() response parsing failed" );
>> +        msg_Err( _access, "browse() response parsing failed" );
>>          return false;
>>      }
>>
>>  #ifndef NDEBUG
>> -    msg_Dbg( _p_sd, "Got DIDL document: %s", ixmlPrintDocument( p_result ) );
>> +    msg_Dbg( _access, "Got DIDL document: %s", ixmlPrintDocument( p_result ) );
>>  #endif
>>
>>      IXML_NodeList* containerNodeList =
>> @@ -864,11 +739,9 @@ bool MediaServer::_fetchContents( Container* p_parent, int i_offset )
>>
>>      if ( containerNodeList )
>>      {
>> -        for ( unsigned int i = 0;
>> -                i < ixmlNodeList_length( containerNodeList ); i++ )
>> +        for ( unsigned int i = 0; i < ixmlNodeList_length( containerNodeList ); i++ )
>>          {
>> -            IXML_Element* containerElement =
>> -                  ( IXML_Element* )ixmlNodeList_item( containerNodeList, i );
>> +            IXML_Element* containerElement = (IXML_Element*)ixmlNodeList_item( containerNodeList, i );
>>
>>              const char* objectID = ixmlElement_getAttribute( containerElement,
>>                                                               "id" );
>> @@ -877,13 +750,9 @@ bool MediaServer::_fetchContents( Container* p_parent, int i_offset )
>>
>>              const char* title = xml_getChildElementValue( containerElement,
>>                                                            "dc:title" );
>> -
>>              if ( !title )
>>                  continue;
>> -
>> -            Container* container = new Container( p_parent, objectID, title );
>> -            p_parent->addContainer( container );
>> -            _fetchContents( container, 0 );
>> +            addItem(objectID, title);
>>          }
>>          ixmlNodeList_free( containerNodeList );
>>      }
>> @@ -944,443 +813,187 @@ bool MediaServer::_fetchContents( Container* p_parent, int i_offset )
>>                                                                i_seconds );
>>                      }
>>
>> -                    Item* item = new Item( p_parent, objectID, title, psz_resource_url, psz_subtitles, i_duration );
>> -                    p_parent->addItem( item );
>> +                    addItem( title, objectID, psz_subtitles, i_duration, psz_resource_url );
>>                  }
>>                  ixmlNodeList_free( p_resource_list );
>>              }
>> -            else continue;
>> +            else
>> +                continue;
>>          }
>>          ixmlNodeList_free( itemNodeList );
>>      }
>>
>>      ixmlDocument_free( p_result );
>> -
>> -    if( i_offset + i_number_returned < i_total_matches )
>> -        return _fetchContents( p_parent, i_offset + i_number_returned );
>> -
>>      return true;
>>  }
>>
>> -// TODO: Create a permanent fix for the item duplication bug. The current fix
>> -// is essentially only a small hack. Although it fixes the problem, it introduces
>> -// annoying cosmetic issues with the playlist. For example, when the UPnP Server
>> -// rebroadcasts it's directory structure, the VLC Client deletes the old directory
>> -// structure, causing the user to go back to the root node of the directory. The
>> -// directory is then rebuilt, and the user is forced to traverse through the directory
>> -// to find the item they were looking for. Some servers may not push the directory
>> -// structure too often, but we cannot rely on this fix.
>> -//
>> -// I have thought up another fix, but this would require certain features to
>> -// be present within the VLC services discovery. Currently, services_discovery_AddItem
>> -// does not allow the programmer to nest items. It only allows a "2 deep" scope.
>> -// An example of the limitation is below:
>> -//
>> -// Root Directory
>> -// + Item 1
>> -// + Item 2
>> -//
>> -// services_discovery_AddItem will not let the programmer specify a child-node to
>> -// insert items into, so we would not be able to do the following:
>> -//
>> -// Root Directory
>> -// + Item 1
>> -//   + Sub Item 1
>> -// + Item 2
>> -//   + Sub Item 1 of Item 2
>> -//     + Sub-Sub Item 1 of Sub Item 1
>> -//
>> -// This creates a HUGE limitation on what we are able to do. If we were able to do
>> -// the above, we could simply preserve the old directory listing, and compare what items
>> -// do not exist in the new directory listing, then remove them from the shown listing using
>> -// services_discovery_RemoveItem. If new files were introduced within an already existing
>> -// container, we could simply do so with services_discovery_AddItem.
>> -
>> -/*
>> - * Builds playlist based on available input items.
>> - */
>> -void MediaServer::_buildPlaylist( Container* p_parent, input_item_node_t *p_input_node )
>> +static int ReadDirectory( access_t *p_access, input_item_node_t* p_node )
>>  {
>> -    bool b_send = p_input_node == NULL;
>> -    if( b_send )
>> -        p_input_node = input_item_node_Create( p_parent->getInputItem() );
>> -
>> -    for ( unsigned int i = 0; i < p_parent->getNumContainers(); i++ )
>> -    {
>> -        Container* p_container = p_parent->getContainer( i );
>> -
>> -        input_item_t* p_input_item = input_item_New( "vlc://nop",
>> -                                                    p_container->getTitle() );
>> -        input_item_node_t *p_new_node =
>> -            input_item_node_AppendItem( p_input_node, p_input_item );
>> +    MediaServer server( p_access->psz_location, p_access, p_node );
>>
>> -        p_container->setInputItem( p_input_item );
>> -        _buildPlaylist( p_container, p_new_node );
>> -    }
>> +    if ( !server.fetchContents() )
>> +        return VLC_EGENERIC;
>> +    return VLC_SUCCESS;
>> +}
>>
>> -    for ( unsigned int i = 0; i < p_parent->getNumItems(); i++ )
>> +static int Control( access_t *, int i_query, va_list args )
>> +{
>> +    switch ( i_query )
>>      {
>> -        Item* p_item = p_parent->getItem( i );
>> -
>> -        char **ppsz_opts = NULL;
>> -        char *psz_input_slave = p_item->buildInputSlaveOption();
>> -        if( psz_input_slave )
>> -        {
>> -            ppsz_opts = (char**)malloc( 2 * sizeof( char* ) );
>> -            ppsz_opts[0] = psz_input_slave;
>> -            ppsz_opts[1] = p_item->buildSubTrackIdOption();
>> -        }
>> -
>> -        input_item_t* p_input_item = input_item_NewExt( p_item->getResource(),
>> -                                           p_item->getTitle(),
>> -                                           psz_input_slave ? 2 : 0,
>> -                                           psz_input_slave ? ppsz_opts : NULL,
>> -                                           VLC_INPUT_OPTION_TRUSTED, /* XXX */
>> -                                           p_item->getDuration() );
>> +    case ACCESS_CAN_SEEK:
>> +    case ACCESS_CAN_FASTSEEK:
>> +    case ACCESS_CAN_PAUSE:
>> +    case ACCESS_CAN_CONTROL_PACE:
>> +        *va_arg( args, bool* ) = false;
>> +        break;
>>
>> -        assert( p_input_item );
>> -        if( ppsz_opts )
>> -        {
>> -            free( ppsz_opts[0] );
>> -            free( ppsz_opts[1] );
>> -            free( ppsz_opts );
>> +    case ACCESS_GET_SIZE:
>> +    {
>> +        *va_arg( args, uint64_t * ) = 0;
>> +        break;
>> +    }
>> +    case ACCESS_GET_PTS_DELAY:
>> +        *va_arg( args, int64_t * ) = 0;
>> +        break;
>>
>> -            psz_input_slave = NULL;
>> -        }
>> +    case ACCESS_SET_PAUSE_STATE:
>> +        /* Nothing to do */
>> +        break;
>>
>> -        input_item_node_AppendItem( p_input_node, p_input_item );
>> -        p_item->setInputItem( p_input_item );
>> +    default:
>> +        return VLC_EGENERIC;
>>      }
>> -
>> -    if( b_send )
>> -        input_item_node_PostAndDelete( p_input_node );
>> +    return VLC_SUCCESS;
>>  }
>>
>> -void MediaServer::setInputItem( input_item_t* p_input_item )
>> +static int Open( vlc_object_t *p_this )
>>  {
>> -    if( _p_input_item == p_input_item )
>> -        return;
>> +    access_t* p_access = (access_t*)p_this;
>> +    access_sys_t* p_sys = new(std::nothrow) access_sys_t;
>> +    if ( unlikely( !p_sys ) )
>> +        return VLC_ENOMEM;
>>
>> -    if( _p_input_item )
>> -        vlc_gc_decref( _p_input_item );
>> +    p_access->p_sys = p_sys;
>> +    p_sys->p_upnp = UpnpInstanceWrapper::get( p_this, NULL, NULL );
>> +    if ( !p_sys->p_upnp )
>> +    {
>> +        delete p_sys;
>> +        return VLC_EGENERIC;
>> +    }
>>
>> -    vlc_gc_incref( p_input_item );
>> -    _p_input_item = p_input_item;
>> -}
>> +    p_access->pf_readdir = ReadDirectory;
>> +    ACCESS_SET_CALLBACKS( NULL, NULL, Control, NULL );
>>
>> -input_item_t* MediaServer::getInputItem() const
>> -{
>> -    return _p_input_item;
>> +    return VLC_SUCCESS;
>>  }
>>
>> -bool MediaServer::compareSID( const char* psz_sid )
>> +static void Close( vlc_object_t* p_this )
>>  {
>> -    return ( strncmp( _subscription_id, psz_sid, sizeof( Upnp_SID ) ) == 0 );
>> +    access_t* p_access = (access_t*)p_this;
>> +    p_access->p_sys->p_upnp->release( false );
>> +    delete p_access->p_sys;
>>  }
>>
>> -
>> -/*
>> - * MediaServerList class
>> - */
>> -MediaServerList::MediaServerList( services_discovery_t* p_sd )
>> -{
>> -    _p_sd = p_sd;
>>  }
>>
>> -MediaServerList::~MediaServerList()
>> +UpnpInstanceWrapper::UpnpInstanceWrapper()
>> +    : _opaque( NULL )
>> +    , _callback( NULL )
>> +    , _refcount( 0 )
>>  {
>> -    for ( unsigned int i = 0; i < _list.size(); i++ )
>> -    {
>> -        delete _list[i];
>> -    }
>>  }
>>
>> -bool MediaServerList::addServer( MediaServer* p_server )
>> +UpnpInstanceWrapper::~UpnpInstanceWrapper()
>>  {
>> -    input_item_t* p_input_item = NULL;
>> -    if ( getServer( p_server->getUDN() ) != 0 ) return false;
>> -
>> -    msg_Dbg( _p_sd, "Adding server '%s' with uuid '%s'", p_server->getFriendlyName(), p_server->getUDN() );
>> -
>> -    p_input_item = input_item_New( "vlc://nop", p_server->getFriendlyName() );
>> -
>> -    input_item_SetDescription( p_input_item, p_server->getUDN() );
>> -
>> -    p_server->setInputItem( p_input_item );
>> -
>> -    services_discovery_AddItem( _p_sd, p_input_item, NULL );
>> -
>> -    _list.push_back( p_server );
>> -
>> -    return true;
>> +    UpnpUnRegisterClient( _handle );
>> +    UpnpFinish();
>>  }
>>
>> -MediaServer* MediaServerList::getServer( const char* psz_udn )
>> +UpnpInstanceWrapper *UpnpInstanceWrapper::get(vlc_object_t *p_obj, Upnp_FunPtr callback, SD::MediaServerList *opaque)
>>  {
>> -    MediaServer* p_result = 0;
>> -
>> -    for ( unsigned int i = 0; i < _list.size(); i++ )
>> +    vlc_mutex_locker lock( &s_lock );
>> +    if ( s_instance == NULL )
>>      {
>> -        if( strcmp( psz_udn, _list[i]->getUDN() ) == 0 )
>> +        UpnpInstanceWrapper* instance = new(std::nothrow) UpnpInstanceWrapper;
>> +        if ( unlikely( !instance ) )
>> +            return NULL;
>> +
>> +    #ifdef UPNP_ENABLE_IPV6
>> +        char* psz_miface = var_InheritString( p_obj, "miface" );
>> +        msg_Info( p_obj, "Initializing libupnp on '%s' interface", psz_miface );
>> +        int i_res = UpnpInit2( psz_miface, 0 );
>> +        free( psz_miface );
>> +    #else
>> +        /* If UpnpInit2 isnt available, initialize on first IPv4-capable interface */
>> +        int i_res = UpnpInit( 0, 0 );
>> +    #endif
>> +        if( i_res != UPNP_E_SUCCESS )
>>          {
>> -            p_result = _list[i];
>> -            break;
>> +            msg_Err( p_obj, "Initialization failed: %s", UpnpGetErrorMessage( i_res ) );
>> +            delete instance;
>> +            return NULL;
>>          }
>> -    }
>>
>> -    return p_result;
>> -}
>> -
>> -MediaServer* MediaServerList::getServerBySID( const char* psz_sid )
>> -{
>> -    MediaServer* p_server = 0;
>> +        ixmlRelaxParser( 1 );
>>
>> -    for ( unsigned int i = 0; i < _list.size(); i++ )
>> -    {
>> -        if ( _list[i]->compareSID( psz_sid ) )
>> +        /* Register a control point */
>> +        i_res = UpnpRegisterClient( Callback, instance, &instance->_handle );
>> +        if( i_res != UPNP_E_SUCCESS )
>>          {
>> -            p_server = _list[i];
>> -            break;
>> +            msg_Err( p_obj, "Client registration failed: %s", UpnpGetErrorMessage( i_res ) );
>> +            delete instance;
>> +            return NULL;
>>          }
>> -    }
>> -
>> -    return p_server;
>> -}
>> -
>> -void MediaServerList::removeServer( const char* psz_udn )
>> -{
>> -    MediaServer* p_server = getServer( psz_udn );
>> -    if ( !p_server ) return;
>> -
>> -    msg_Dbg( _p_sd, "Removing server '%s'", p_server->getFriendlyName() );
>>
>> -    services_discovery_RemoveItem( _p_sd, p_server->getInputItem() );
>> -
>> -    std::vector<MediaServer*>::iterator it;
>> -    for ( it = _list.begin(); it != _list.end(); ++it )
>> -    {
>> -        if ( *it == p_server )
>> +        /* libupnp does not treat a maximum content length of 0 as unlimited
>> +         * until 64dedf (~ pupnp v1.6.7) and provides no sane way to discriminate
>> +         * between versions */
>> +        if( (i_res = UpnpSetMaxContentLength( INT_MAX )) != UPNP_E_SUCCESS )
>>          {
>> -            _list.erase( it );
>> -            delete p_server;
>> -            break;
>> +            msg_Err( p_obj, "Failed to set maximum content length: %s",
>> +                    UpnpGetErrorMessage( i_res ));
>> +            delete instance;
>> +            return NULL;
>>          }
>> +        s_instance = instance;
>>      }
>> -}
>> -
>> -
>> -/*
>> - * Item class
>> - */
>> -Item::Item( Container* p_parent,
>> -        const char* psz_object_id, const char* psz_title,
>> -        const char* psz_resource, const char* psz_subtitles,
>> -        mtime_t i_duration )
>> -{
>> -    _parent = p_parent;
>> -
>> -    _objectID = psz_object_id;
>> -    _title = psz_title;
>> -    _resource = psz_resource;
>> -    _subtitles = psz_subtitles ? psz_subtitles : "";
>> -    _duration = i_duration;
>> -
>> -    _p_input_item = NULL;
>> -}
>> -
>> -Item::~Item()
>> -{
>> -    if( _p_input_item )
>> -        vlc_gc_decref( _p_input_item );
>> -}
>> -
>> -const char* Item::getObjectID() const
>> -{
>> -    return _objectID.c_str();
>> -}
>> -
>> -const char* Item::getTitle() const
>> -{
>> -    return _title.c_str();
>> -}
>> -
>> -const char* Item::getResource() const
>> -{
>> -    return _resource.c_str();
>> -}
>> -
>> -const char* Item::getSubtitles() const
>> -{
>> -    if( !_subtitles.size() )
>> -        return NULL;
>> -
>> -    return _subtitles.c_str();
>> -}
>> -
>> -mtime_t Item::getDuration() const
>> -{
>> -    return _duration;
>> -}
>> -
>> -char* Item::buildInputSlaveOption() const
>> -{
>> -    const char *psz_subtitles    = getSubtitles();
>> -
>> -    const char *psz_scheme_delim = "://";
>> -    const char *psz_sub_opt_fmt  = ":input-slave=%s/%s://%s";
>> -    const char *psz_demux        = "subtitle";
>> -
>> -    char       *psz_uri_scheme   = NULL;
>> -    const char *psz_scheme_end   = NULL;
>> -    const char *psz_uri_location = NULL;
>> -    char       *psz_input_slave  = NULL;
>> -
>> -    size_t i_scheme_len;
>> -
>> -    if( !psz_subtitles )
>> -        return NULL;
>> -
>> -    psz_scheme_end = strstr( psz_subtitles, psz_scheme_delim );
>> -
>> -    /* subtitles not being an URI would make no sense */
>> -    if( !psz_scheme_end )
>> -        return NULL;
>> -
>> -    i_scheme_len   = psz_scheme_end - psz_subtitles;
>> -    psz_uri_scheme = (char*)malloc( i_scheme_len + 1 );
>> -
>> -    if( !psz_uri_scheme )
>> -        return NULL;
>> -
>> -    memcpy( psz_uri_scheme, psz_subtitles, i_scheme_len );
>> -    psz_uri_scheme[i_scheme_len] = '\0';
>> -
>> -    /* If the subtitles try to force a vlc demux,
>> -     * then something is very wrong */
>> -    if( strchr( psz_uri_scheme, '/' ) )
>> +    s_instance->_refcount++;
>> +    // This assumes a single UPNP SD instance
>> +    if (callback && opaque)
>>      {
>> -        free( psz_uri_scheme );
>> -        return NULL;
>> +        assert(!s_instance->_callback && !s_instance->_opaque);
>> +        s_instance->_opaque = opaque;
>> +        s_instance->_callback = callback;
>>      }
>> -
>> -    psz_uri_location = psz_scheme_end + strlen( psz_scheme_delim );
>> -
>> -    if( -1 == asprintf( &psz_input_slave, psz_sub_opt_fmt,
>> -            psz_uri_scheme, psz_demux, psz_uri_location ) )
>> -        psz_input_slave = NULL;
>> -
>> -    free( psz_uri_scheme );
>> -    return psz_input_slave;
>> +    return s_instance;
>>  }
>>
>> -char* Item::buildSubTrackIdOption() const
>> +void UpnpInstanceWrapper::release(bool isSd)
>>  {
>> -    return strdup( ":sub-track-id=2" );
>> -}
>> -
>> -void Item::setInputItem( input_item_t* p_input_item )
>> -{
>> -    if( _p_input_item == p_input_item )
>> -        return;
>> -
>> -    if( _p_input_item )
>> -        vlc_gc_decref( _p_input_item );
>> -
>> -    vlc_gc_incref( p_input_item );
>> -    _p_input_item = p_input_item;
>> -}
>> -
>> -/*
>> - * Container class
>> - */
>> -Container::Container( Container*  p_parent,
>> -                      const char* psz_object_id,
>> -                      const char* psz_title )
>> -{
>> -    _parent = p_parent;
>> -
>> -    _objectID = psz_object_id;
>> -    _title = psz_title;
>> -
>> -    _p_input_item = NULL;
>> -}
>> -
>> -Container::~Container()
>> -{
>> -    for ( unsigned int i = 0; i < _containers.size(); i++ )
>> +    vlc_mutex_locker lock( &s_lock );
>> +    if ( isSd )
>>      {
>> -        delete _containers[i];
>> +        _callback = NULL;
>> +        _opaque = NULL;
>>      }
>> -
>> -    for ( unsigned int i = 0; i < _items.size(); i++ )
>> +    if (--s_instance->_refcount == 0)
>>      {
>> -        delete _items[i];
>> +        delete s_instance;
>> +        s_instance = NULL;
>>      }
>> -
>> -    if( _p_input_item )
>> -        vlc_gc_decref( _p_input_item );
>> -}
>> -
>> -void Container::addItem( Item* item )
>> -{
>> -    _items.push_back( item );
>> -}
>> -
>> -void Container::addContainer( Container* p_container )
>> -{
>> -    _containers.push_back( p_container );
>> -}
>> -
>> -const char* Container::getObjectID() const
>> -{
>> -    return _objectID.c_str();
>>  }
>>
>> -const char* Container::getTitle() const
>> +UpnpClient_Handle UpnpInstanceWrapper::handle() const
>>  {
>> -    return _title.c_str();
>> +    return _handle;
>>  }
>>
>> -unsigned int Container::getNumItems() const
>> +int UpnpInstanceWrapper::Callback(Upnp_EventType event_type, void *p_event, void *p_user_data)
>>  {
>> -    return _items.size();
>> -}
>> -
>> -unsigned int Container::getNumContainers() const
>> -{
>> -    return _containers.size();
>> -}
>> -
>> -Item* Container::getItem( unsigned int i_index ) const
>> -{
>> -    if ( i_index < _items.size() ) return _items[i_index];
>> -    return 0;
>> -}
>> -
>> -Container* Container::getContainer( unsigned int i_index ) const
>> -{
>> -    if ( i_index < _containers.size() ) return _containers[i_index];
>> +    UpnpInstanceWrapper* self = static_cast<UpnpInstanceWrapper*>( p_user_data );
>> +    vlc_mutex_locker lock( &self->s_lock );
>> +    if ( !self->_callback )
>> +        return 0;
>> +    self->_callback( event_type, p_event, self->_opaque );
>>      return 0;
>>  }
>> -
>> -Container* Container::getParent()
>> -{
>> -    return _parent;
>> -}
>> -
>> -void Container::setInputItem( input_item_t* p_input_item )
>> -{
>> -    if( _p_input_item == p_input_item )
>> -        return;
>> -
>> -    if( _p_input_item )
>> -        vlc_gc_decref( _p_input_item );
>> -
>> -    vlc_gc_incref( p_input_item );
>> -    _p_input_item = p_input_item;
>> -}
>> -
>> -input_item_t* Container::getInputItem() const
>> -{
>> -    return _p_input_item;
>> -}
>> diff --git a/modules/services_discovery/upnp.hpp b/modules/services_discovery/upnp.hpp
>> index 23fe4db..971b709 100644
>> --- a/modules/services_discovery/upnp.hpp
>> +++ b/modules/services_discovery/upnp.hpp
>> @@ -7,6 +7,7 @@
>>   * Authors: Rémi Denis-Courmont <rem # videolan.org> (original plugin)
>>   *          Christian Henz <henz # c-lab.de>
>>   *          Mirsal Ennaime <mirsal dot ennaime at gmail dot com>
>> + *          Hugo Beauzée-Luyssen <hugo at beauzee.fr>
>>   *
>>   * UPnP Plugin using the Intel SDK (libupnp) instead of CyberLink
>>   *
>> @@ -33,62 +34,55 @@
>>
>>  #include <vlc_common.h>
>>
>> -// Classes
>> -class Container;
>> -
>> -class MediaServer
>> +namespace SD
>>  {
>> -public:
>> -
>> -    static void parseDeviceDescription( IXML_Document* p_doc,
>> -                                        const char*    psz_location,
>> -                                        services_discovery_t* p_sd );
>> -
>> -    MediaServer( const char* psz_udn,
>> -                 const char* psz_friendly_name,
>> -                 services_discovery_t* p_sd );
>> -
>> -    ~MediaServer();
>> -
>> -    const char* getUDN() const;
>> -    const char* getFriendlyName() const;
>> -
>> -    void setContentDirectoryEventURL( const char* psz_url );
>> -    const char* getContentDirectoryEventURL() const;
>> -
>> -    void setContentDirectoryControlURL( const char* psz_url );
>> -    const char* getContentDirectoryControlURL() const;
>> -
>> -    void subscribeToContentDirectory();
>> -    void fetchContents();
>> -
>> -    void setInputItem( input_item_t* p_input_item );
>> -    input_item_t* getInputItem() const;
>> -
>> -    bool compareSID( const char* psz_sid );
>> +    class MediaServerList;
>> +}
>> +
>> +/*
>> + * libUpnp allows only one instance per process, so we have to share one for
>> + * both SD & Access module
>> + * Since the callback is bound to the UpnpClient_Handle, we have to register
>> + * a wrapper callback, in order for the access module to be able to initialize
>> + * libUpnp first.
>> + * When a SD wishes to use libUpnp, it will provide its own callback, that the
>> + * wrapper will forward.
>> + * This way, we always have a register callback & a client handle.
>> + */
>> +class UpnpInstanceWrapper
>> +{
>> +public:
>> +    // This increases the refcount before returning the instance
>> +    static UpnpInstanceWrapper* get(vlc_object_t* p_obj, Upnp_FunPtr callback, SD::MediaServerList *opaque);
>> +    void release(bool isSd);
>> +    UpnpClient_Handle handle() const;
>>
>>  private:
>> +    static int Callback( Upnp_EventType event_type, void* p_event, void* p_user_data );
>>
>> -    bool _fetchContents( Container* p_parent, int i_starting_index );
>> -    void _buildPlaylist( Container* p_container, input_item_node_t *p_item_node );
>> -
>> -    IXML_Document* _browseAction( const char*, const char*,
>> -            const char*, const char*, const char*, const char* );
>> -
>> -    services_discovery_t* _p_sd;
>> -
>> -    Container* _p_contents;
>> -    input_item_t* _p_input_item;
>> +    UpnpInstanceWrapper();
>> +    ~UpnpInstanceWrapper();
>>
>> -    std::string _UDN;
>> -    std::string _friendly_name;
>> +private:
>> +    static UpnpInstanceWrapper* s_instance;
>> +    static vlc_mutex_t s_lock;
>> +    UpnpClient_Handle _handle;
>> +    SD::MediaServerList* _opaque;
>> +    Upnp_FunPtr _callback;
>> +    int _refcount;
> 
> Identifiers with leading underscores are reserved for the implementation.
> 

In all fairness, that's only true for the global namespace,
nevertheless, I agreed and prefer to use "m_", though I tried to follow
the existing convention. I'll gladly replace it by something else.

>> +};
>>
>> -    std::string _content_directory_event_url;
>> -    std::string _content_directory_control_url;
>> +namespace SD
>> +{
>>
>> -    int _i_subscription_timeout;
>> -    int _i_content_directory_service_version;
>> -    Upnp_SID _subscription_id;
>> +struct MediaServerDesc
>> +{
>> +    MediaServerDesc(const std::string& udn, const std::string& fName, const std::string& loc);
>> +    ~MediaServerDesc();
>> +    std::string UDN;
>> +    std::string friendlyName;
>> +    std::string location;
>> +    input_item_t* inputItem;
>>  };
>>
>>
>> @@ -99,87 +93,44 @@ public:
>>      MediaServerList( services_discovery_t* p_sd );
>>      ~MediaServerList();
>>
>> -    bool addServer( MediaServer* p_server );
>> -    void removeServer( const char* psz_udn );
>> -
>> -    MediaServer* getServer( const char* psz_udn );
>> -    MediaServer* getServerBySID( const char* psz_sid );
>> +    bool addServer(MediaServerDesc *desc );
>> +    void removeServer(const std::string &udn );
>> +    MediaServerDesc* getServer( const std::string& udn );
>> +    static int Callback( Upnp_EventType event_type, void* p_event, void* p_user_data );
>>
>>  private:
>> +    void parseNewServer( IXML_Document* doc, const std::string& location );
>>
>> +private:
>>      services_discovery_t* _p_sd;
>> -
>> -    std::vector<MediaServer*> _list;
>> +    std::vector<MediaServerDesc*> _list;
>> +    vlc_mutex_t _lock;
> 
> same
> 
>>  };
>>
>> +}
>>
>> -class Item
>> +namespace Access
>>  {
>> -public:
>> -
>> -    Item( Container*  parent,
>> -          const char* objectID,
>> -          const char* title,
>> -          const char* subtitles,
>> -          const char* resource,
>> -          mtime_t duration );
>> -    ~Item();
>> -
>> -    const char* getObjectID() const;
>> -    const char* getTitle() const;
>> -    const char* getResource() const;
>> -    const char* getSubtitles() const;
>> -    char* buildInputSlaveOption() const;
>> -    char* buildSubTrackIdOption() const;
>> -    mtime_t getDuration() const;
>> -
>> -    void setInputItem( input_item_t* p_input_item );
>> -
>> -private:
>> -
>> -    input_item_t* _p_input_item;
>>
>> -    Container* _parent;
>> -    std::string _objectID;
>> -    std::string _title;
>> -    std::string _resource;
>> -    std::string _subtitles;
>> -    mtime_t _duration;
>> -};
>> -
>> -
>> -class Container
>> +class MediaServer
>>  {
>>  public:
>> -
>> -    Container( Container* parent, const char* objectID, const char* title );
>> -    ~Container();
>> -
>> -    void addItem( Item* item );
>> -    void addContainer( Container* container );
>> -
>> -    const char* getObjectID() const;
>> -    const char* getTitle() const;
>> -
>> -    unsigned int getNumItems() const;
>> -    unsigned int getNumContainers() const;
>> -
>> -    Item* getItem( unsigned int i ) const;
>> -    Container* getContainer( unsigned int i ) const;
>> -    Container* getParent();
>> -
>> -    void setInputItem( input_item_t* p_input_item );
>> -    input_item_t* getInputItem() const;
>> +    MediaServer( const char* psz_url, access_t* p_access, input_item_node_t* node );
>> +    bool fetchContents();
>>
>>  private:
>> +    MediaServer(const MediaServer&);
>> +    MediaServer& operator=(const MediaServer&);
>>
>> -    input_item_t* _p_input_item;
>> +    void addItem(const char* objectID, const char* title);
>> +    void addItem(const char* title, const char* psz_objectID, const char* psz_subtitles, mtime_t duration, const char* psz_url );
>>
>> -    Container* _parent;
>> +    IXML_Document* _browseAction(const char*, const char*,
>> +            const char*, const char*, const char* );
>>
>> -    std::string _objectID;
>> -    std::string _title;
>> -    std::vector<Item*> _items;
>> -    std::vector<Container*> _containers;
>> +    const std::string _url;
>> +    access_t* _access;
>> +    input_item_node_t* _node;
> 
> same
> 
>>  };
>>
>> +}
>> --
>> 2.1.0
>>
>> _______________________________________________
>> vlc-devel mailing list
>> To unsubscribe or modify your subscription options:
>> https://mailman.videolan.org/listinfo/vlc-devel
> _______________________________________________
> vlc-devel mailing list
> To unsubscribe or modify your subscription options:
> https://mailman.videolan.org/listinfo/vlc-devel
> 

Thanks for reviewing!

Regards,




More information about the vlc-devel mailing list