]> git.sesse.net Git - vlc/commitdiff
Add libupnp-based UPnP discovery from Christian Henz
authorRémi Denis-Courmont <rem@videolan.org>
Thu, 2 Feb 2006 10:59:17 +0000 (10:59 +0000)
committerRémi Denis-Courmont <rem@videolan.org>
Thu, 2 Feb 2006 10:59:17 +0000 (10:59 +0000)
with proper configure checks

THANKS
configure.ac
modules/services_discovery/Modules.am
modules/services_discovery/upnp_cc.cpp [moved from modules/services_discovery/upnp.cpp with 99% similarity]
modules/services_discovery/upnp_intel.cpp [new file with mode: 0644]

diff --git a/THANKS b/THANKS
index 172ea99e2511a1c5386982248f7ab5da6180ef38..0d239efa5ae1121a53df45d3846c5b991073761d 100644 (file)
--- a/THANKS
+++ b/THANKS
@@ -30,7 +30,7 @@ Bruno Vella <allevb at tin.it> - Italian localization
 Carlo Calabrò <murray at via.ecp.fr> - Italian localization
 Carsten Gottbehüt <gottbehuet at active-elements dot de> - v4l hotplug fix
 Chris Clepper - OpenGL fix
-Christian Henz - UPnP service discovery fixes
+Christian Henz - libupnp service discovery plugin, CyberLink UPnP fixes
 Christof Baumgaertner - dbox web intf
 Christophe Mutricy <xtophe at nxtelevision dot com> - many fixes (preferences, M3U, ...)
 Christopher Johnson <cjohnson at mint.net> - Qt fix in vlc.spec
index 3d53621162d57eefb4575bcde8dd9330958681d1..e3fd0ab693b2919567657f25b36c8a22b66df502 100644 (file)
@@ -4092,8 +4092,8 @@ AS_IF([test "${CXX}" != "" -a "${enable_cyberlink}" = "yes" || (test "${enable_c
     CPPFLAGS_cyberlink="-I${real_cyberlink_tree}/include"
     CPPFLAGS="${CPPFLAGS} ${CPPFLAGS_cyberlink}"
     AC_CHECK_HEADERS([cybergarage/upnp/MediaServer.h],
-      [ VLC_ADD_CXXFLAGS([upnp], [${CPPFLAGS_cyberlink}])
-        VLC_ADD_PLUGINS([upnp]) 
+      [ VLC_ADD_CXXFLAGS([upnp_cc], [${CPPFLAGS_cyberlink}])
+        VLC_ADD_PLUGINS([upnp_cc]) 
       ],[
         AC_MSG_ERROR([cannot find CyberLink for C++ headers])
       ])
@@ -4130,7 +4130,7 @@ class testclass : public SearchResponseListener, public MediaPlayer
       AS_IF([test "${LIBS_cclink}" == "no"],
         [AC_MSG_FAILURE([cannot find XML parser for CyberLink])])
       AC_MSG_RESULT([${LIBS_cclink}])
-      VLC_ADD_LDFLAGS([upnp], [${real_cyberlink_tree}/lib/unix/libclink.a -lpthread ${LIBS_cclink}])
+      VLC_ADD_LDFLAGS([upnp_cc], [${real_cyberlink_tree}/lib/unix/libclink.a -lpthread ${LIBS_cclink}])
     ], [
       AC_MSG_RESULT(no)
       AC_MSG_ERROR([cannot find ${real_cyberlink_tree}/lib/unix/libclink.a, make sure you compiled CyberLink for C++ in ${with_cyberlink_tree}])
@@ -4140,6 +4140,30 @@ class testclass : public SearchResponseListener, public MediaPlayer
   ])
 ])
 
+dnl
+dnl UPnP Plugin (Intel SDK)
+dnl
+AC_ARG_ENABLE(upnp,
+  [  --enable-upnp           Intel UPnP SDK (default auto)])
+
+VLC_ADD_CXXFLAGS([upnp_intel], [ ])
+AS_IF([test "x${enable_upnp}" != "xno"], [
+  AC_CHECK_LIB([upnp], [UpnpInit], [has_upnp="yes"], [has_upnp="no"], [-lpthread])
+  AS_IF([test "x${enable_upnp}" != "x" && test "${has_upnp}" == "no"], [
+    AC_MSG_ERROR([cannot find Intel UPnP SDK (libupnp)])
+  ])
+  AS_IF([test "${has_upnp}" == "yes"], [
+    VLC_ADD_LDFLAGS([upnp_intel], [-lupnp])
+  ])
+], [
+  has_upnp="no"
+])
+
+AS_IF([test "${has_upnp}" == "yes"], [
+  VLC_ADD_PLUGINS([upnp_intel])
+])
+
+
 dnl
 dnl  Interface plugins
 dnl
index b0f59c4d4b15e2e3dba711be83593855cbe2365f..b5f76ffcd3cf21d99de79e55354cd98a8109e214 100644 (file)
@@ -2,6 +2,7 @@ SOURCES_sap = sap.c
 SOURCES_hal = hal.c
 SOURCES_daap = daap.c
 SOURCES_shout = shout.c
-SOURCES_upnp = upnp.cpp
+SOURCES_upnp_cc = upnp_cc.cpp
+SOURCES_upnp_intel = upnp_intel.cpp
 SOURCES_bonjour = bonjour.c
 SOURCES_podcast = podcast.c
similarity index 99%
rename from modules/services_discovery/upnp.cpp
rename to modules/services_discovery/upnp_cc.cpp
index 468478f70dce1f70cb633118174cc8ff1f4b2675..9e5ebe304c42630f832a13453844ddb5911a6fe7 100644 (file)
@@ -1,5 +1,5 @@
 /*****************************************************************************
- * upnp.cpp :  UPnP discovery module
+ * upnp_cc.cpp :  UPnP discovery module
  *****************************************************************************
  * Copyright (C) 2004-2005 the VideoLAN team
  * $Id$
diff --git a/modules/services_discovery/upnp_intel.cpp b/modules/services_discovery/upnp_intel.cpp
new file mode 100644 (file)
index 0000000..de045bf
--- /dev/null
@@ -0,0 +1,1167 @@
+/*****************************************************************************
+ * Upnp_intell.cpp :  UPnP discovery module (Intel SDK)
+ *****************************************************************************
+ * Copyright (C) 2004-2006 the VideoLAN team
+ * $Id$
+ *
+ * Authors: Rémi Denis-Courmont <rem # videolan.org> (original plugin)
+ *          Christian Henz <henz # c-lab.de> 
+ * 
+ * UPnP Plugin using the Intel SDK (libupnp) instead of CyberLink
+ *
+ * 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.
+ *
+ * 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.
+ *****************************************************************************/
+
+/*
+  \TODO: Debug messages: "__FILE__, __LINE__" ok ???, Wrn/Err ???
+  \TODO: Change names to VLC standard ???
+*/
+
+#include <stdlib.h>
+
+#include <vector>
+#include <string>
+
+#include <upnp/upnp.h>
+#include <upnp/upnptools.h>
+
+#undef PACKAGE_NAME
+#include <vlc/vlc.h>
+#include <vlc/intf.h>
+
+
+// VLC handle
+
+struct services_discovery_sys_t
+{
+    playlist_item_t *p_node;
+    playlist_t *p_playlist;
+};
+
+
+// Constants
+
+const char* MEDIA_SERVER_DEVICE_TYPE = "urn:schemas-upnp-org:device:MediaServer:1";
+const char* CONTENT_DIRECTORY_SERVICE_TYPE = "urn:schemas-upnp-org:service:ContentDirectory:1";
+
+
+// Classes 
+
+class MediaServer;
+class MediaServerList;
+class Item;
+class Container;
+
+// Cookie that is passed to the callback
+
+typedef struct 
+{
+    services_discovery_t* serviceDiscovery;
+    UpnpClient_Handle clientHandle;
+    MediaServerList* serverList; 
+} Cookie;
+
+
+// Class definitions...
+
+class Lockable 
+{  
+public:
+
+    Lockable( Cookie* c ) 
+    {
+       vlc_mutex_init( c->serviceDiscovery, &_mutex );
+    }
+
+    ~Lockable() 
+    {
+       vlc_mutex_destroy( &_mutex );
+    }
+
+    void lock() { vlc_mutex_lock( &_mutex ); }
+    void unlock() { vlc_mutex_unlock( &_mutex ); }
+
+private:
+    
+    vlc_mutex_t _mutex;
+};
+
+
+class Locker 
+{
+public:
+    Locker( Lockable* l ) 
+    {
+       _lockable = l;
+       _lockable->lock();
+    }
+
+    ~Locker() 
+    {
+       _lockable->unlock();
+    }
+
+private:
+    Lockable* _lockable;
+};
+
+
+class MediaServer
+{
+public:
+
+    static void parseDeviceDescription( IXML_Document* doc, const char* location, Cookie* cookie );
+    
+    MediaServer( const char* UDN, const char* friendlyName, Cookie* cookie );
+    ~MediaServer();
+    
+    const char* getUDN() const;
+    const char* getFriendlyName() const;
+
+    void setContentDirectoryEventURL( const char* url );
+    const char* getContentDirectoryEventURL() const;
+
+    void setContentDirectoryControlURL( const char* url );
+    const char* getContentDirectoryControlURL() const;
+
+    void subscribeToContentDirectory();
+    void fetchContents();
+
+    void setPlaylistNode( playlist_item_t* node );
+
+    bool compareSID( const char* sid );
+
+private:
+
+    bool _fetchContents( Container* parent );
+    void _buildPlaylist( Container* container );
+    IXML_Document* _browseAction( const char*, const char*, const char*, const char*, const char*, const char* );
+
+    Cookie* _cookie;
+  
+    Container* _contents;
+    playlist_item_t* _playlistNode;
+
+    std::string _UDN;
+    std::string _friendlyName;
+  
+    std::string _contentDirectoryEventURL;
+    std::string _contentDirectoryControlURL;
+
+    int _subscriptionTimeOut;
+    Upnp_SID _subscriptionID;
+};
+
+
+class MediaServerList
+{
+public:
+
+    MediaServerList( Cookie* cookie );
+    ~MediaServerList();
+
+    bool addServer( MediaServer* s );
+    void removeServer( const char* UDN );
+
+    MediaServer* getServer( const char* UDN );
+    MediaServer* getServerBySID( const char* );
+
+private:
+
+    Cookie* _cookie;
+
+    std::vector<MediaServer*> _list;
+};
+
+
+class Item 
+{
+public:
+
+    Item( Container* parent, const char* objectID, const char* title, const char* resource );
+
+    const char* getObjectID() const;
+    const char* getTitle() const;
+    const char* getResource() const;
+
+    void setPlaylistNode( playlist_item_t* node );
+    playlist_item_t* getPlaylistNode() const ;
+
+private:
+
+    playlist_item_t* _playlistNode;
+    Container* _parent;
+    std::string _objectID;
+    std::string _title;
+    std::string _resource;
+};
+
+
+class Container 
+{
+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;
+
+    void setPlaylistNode( playlist_item_t* node );
+    playlist_item_t* getPlaylistNode() const;
+
+private:
+
+    playlist_item_t* _playlistNode;
+
+    Container* _parent;
+
+    std::string _objectID;
+    std::string _title;
+    std::vector<Item*> _items;
+    std::vector<Container*> _containers;
+};
+
+
+// VLC callback prototypes
+
+static int Open( vlc_object_t* );
+static void Close( vlc_object_t* );
+static void Run( services_discovery_t *p_sd );
+
+// Module descriptor
+
+vlc_module_begin();
+set_shortname( "UPnP" );
+set_description( _( "Universal Plug'n'Play discovery ( Intel SDK )" ) );
+set_category( CAT_PLAYLIST );
+set_subcategory( SUBCAT_PLAYLIST_SD );
+set_capability( "services_discovery", 0 );
+set_callbacks( Open, Close );
+vlc_module_end();
+
+
+// More prototypes...
+
+static Lockable* CallbackLock;
+static int Callback( Upnp_EventType eventType, void* event, void* pCookie );
+
+char* xml_makeSpecialChars( const char* in );
+const char* xml_getChildElementValue( IXML_Element* parent, const char* tagName );
+IXML_Document* parseBrowseResult( IXML_Document* doc );
+
+
+// VLC callbacks...
+
+static int Open( vlc_object_t *p_this )
+{
+    services_discovery_t *p_sd = ( services_discovery_t* )p_this;
+    services_discovery_sys_t *p_sys  = ( services_discovery_sys_t * )
+       malloc( sizeof( services_discovery_sys_t ) );
+    
+    playlist_view_t *p_view;
+    vlc_value_t val;
+
+    p_sd->pf_run = Run;
+    p_sd->p_sys = p_sys;
+
+    /* Create our playlist node */
+    p_sys->p_playlist = ( playlist_t * )vlc_object_find( p_sd,
+                                                        VLC_OBJECT_PLAYLIST,
+                                                        FIND_ANYWHERE );
+    if( !p_sys->p_playlist )
+    {
+       msg_Warn( p_sd, "unable to find playlist, cancelling UPnP listening" );
+       return VLC_EGENERIC;
+    }
+
+    p_view = playlist_ViewFind( p_sys->p_playlist, VIEW_CATEGORY );
+    p_sys->p_node = playlist_NodeCreate( p_sys->p_playlist, VIEW_CATEGORY,
+                                         "UPnP", p_view->p_root );
+    p_sys->p_node->i_flags |= PLAYLIST_RO_FLAG;
+    p_sys->p_node->i_flags &= ~PLAYLIST_SKIP_FLAG;
+    val.b_bool = VLC_TRUE;
+    var_Set( p_sys->p_playlist, "intf-change", val );
+
+    return VLC_SUCCESS;
+}
+
+static void Close( vlc_object_t *p_this )
+{
+    services_discovery_t *p_sd = ( services_discovery_t* )p_this;
+    services_discovery_sys_t *p_sys = p_sd->p_sys;
+
+    if( p_sys->p_playlist )
+    {
+       playlist_NodeDelete( p_sys->p_playlist, p_sys->p_node, VLC_TRUE,
+                            VLC_TRUE );
+       vlc_object_release( p_sys->p_playlist );
+    }
+
+    free( p_sys );
+}
+
+static void Run( services_discovery_t* p_sd )
+{
+    int res;
+  
+    res = UpnpInit( 0, 0 );
+    if( res != UPNP_E_SUCCESS ) 
+    {
+       msg_Err( p_sd, "%s", UpnpGetErrorMessage( res ) );
+       return;
+    }
+
+    Cookie cookie;
+    cookie.serviceDiscovery = p_sd;
+    cookie.serverList = new MediaServerList( &cookie );
+
+    CallbackLock = new Lockable( &cookie );
+
+    res = UpnpRegisterClient( Callback, &cookie, &cookie.clientHandle );
+    if( res != UPNP_E_SUCCESS ) 
+    {
+       msg_Err( p_sd, "%s", UpnpGetErrorMessage( res ) );
+       goto shutDown;
+    }
+
+    res = UpnpSearchAsync( cookie.clientHandle, 5, MEDIA_SERVER_DEVICE_TYPE, &cookie );
+    if( res != UPNP_E_SUCCESS ) 
+    {
+       msg_Err( p_sd, "%s", UpnpGetErrorMessage( res ) );
+       goto shutDown;
+    }
+    msg_Dbg( p_sd, "UPnP discovery started" );
+    while( !p_sd->b_die ) 
+    {
+       msleep( 500 );
+    }
+
+    msg_Dbg( p_sd, "UPnP discovery stopped" );
+
+ shutDown:
+    UpnpFinish();
+    delete cookie.serverList;
+    delete CallbackLock;
+}
+
+
+// XML utility functions:
+
+// Returns the value of a child element, or 0 on error
+const char* xml_getChildElementValue( IXML_Element* parent, const char* tagName ) 
+{
+    if ( !parent ) return 0;
+    if ( !tagName ) return 0;
+
+    char* s = strdup( tagName );
+    IXML_NodeList* nodeList = ixmlElement_getElementsByTagName( parent, s );
+    free( s );
+    if ( !nodeList ) return 0;
+
+    IXML_Node* element = ixmlNodeList_item( nodeList, 0 );
+    ixmlNodeList_free( nodeList );
+    if ( !element ) return 0;
+
+    IXML_Node* textNode = ixmlNode_getFirstChild( element );
+    if ( !textNode ) return 0;
+
+    return ixmlNode_getNodeValue( textNode );
+}
+
+
+// Replaces "&lt;" with "<" etc.
+// Returns a newly created string that has to be freed by the caller.
+// Returns 0 on error ( out of mem )
+// \TODO: Probably not very robust!!!
+char* xml_makeSpecialChars( const char* in ) 
+{
+    if ( !in ) return 0;
+
+    char* result = ( char* )malloc( strlen( in ) + 1 );
+    if ( !result ) return 0;
+
+    char* out = result;
+
+    while( *in ) 
+    {
+       if ( strncmp( "&amp;", in, 5 ) == 0 ) 
+       {
+           *out = '&';
+
+           in += 5;
+           out++;
+       } 
+       else if ( strncmp( "&quot;", in, 6 ) == 0 ) 
+       {
+           *out = '"';
+
+           in += 6;
+           out++;
+       } 
+       else if ( strncmp( "&gt;", in, 4 ) == 0 ) 
+       {
+           *out = '>';
+           
+           in += 4;
+           out++;
+       } 
+       else if ( strncmp( "&lt;", in, 4 ) == 0 ) 
+       {
+           *out = '<';
+
+           in += 4;
+           out++;
+       } 
+       else 
+       {
+           *out = *in;
+
+           in++;
+           out++;
+       }
+    }
+
+    *out = '\0';
+    return result;
+}
+
+
+// Extracts the result document from a SOAP response
+IXML_Document* parseBrowseResult( IXML_Document* doc ) 
+{
+    if ( !doc ) return 0;
+  
+    IXML_NodeList* resultList = ixmlDocument_getElementsByTagName( doc, "Result" );
+    if ( !resultList ) return 0;
+  
+    IXML_Node* resultNode = ixmlNodeList_item( resultList, 0 );
+
+    ixmlNodeList_free( resultList );
+
+    if ( !resultNode ) return 0;
+
+    IXML_Node* textNode = ixmlNode_getFirstChild( resultNode );
+    if ( !textNode ) return 0;
+
+    const char* resultString = ixmlNode_getNodeValue( textNode );
+    char* resultXML = xml_makeSpecialChars( resultString );
+
+    IXML_Document* browseDoc = ixmlParseBuffer( resultXML );
+
+    free( resultXML );
+
+    return browseDoc;
+}
+
+
+// Handles all UPnP events
+static int Callback( Upnp_EventType eventType, void* event, void* pCookie ) 
+{
+    Locker locker( CallbackLock );
+
+    Cookie* cookie = ( Cookie* )pCookie;
+
+    switch( eventType ) {
+    
+    case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
+    case UPNP_DISCOVERY_SEARCH_RESULT:
+       {
+           struct Upnp_Discovery* discovery = ( struct Upnp_Discovery* )event;
+  
+           IXML_Document *descriptionDoc = 0;
+      
+           int res;
+           res = UpnpDownloadXmlDoc( discovery->Location, &descriptionDoc );
+           if ( res != UPNP_E_SUCCESS ) 
+           {
+             msg_Dbg( cookie->serviceDiscovery, "%s:%d: Could not download device description!", __FILE__, __LINE__ );
+             return res;
+           }
+
+           MediaServer::parseDeviceDescription( descriptionDoc, discovery->Location, cookie );
+
+           ixmlDocument_free( descriptionDoc );
+       }
+       break;
+    
+    case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE:
+       {
+           struct Upnp_Discovery* discovery = ( struct Upnp_Discovery* )event;
+
+           cookie->serverList->removeServer( discovery->DeviceId );
+       }
+       break;
+
+    case UPNP_EVENT_RECEIVED:
+       {
+           Upnp_Event* e = ( Upnp_Event* )event;
+
+           MediaServer* server = cookie->serverList->getServerBySID( e->Sid );
+           if ( server ) server->fetchContents(); 
+       }
+       break;
+
+    case UPNP_EVENT_AUTORENEWAL_FAILED:
+    case UPNP_EVENT_SUBSCRIPTION_EXPIRED:
+       {
+           // Re-subscribe...
+
+           Upnp_Event_Subscribe* s = ( Upnp_Event_Subscribe* )event;
+
+           MediaServer* server = cookie->serverList->getServerBySID( s->Sid );
+           if ( server ) server->subscribeToContentDirectory();
+       }
+       break;
+       
+    default:
+       msg_Dbg( cookie->serviceDiscovery, "%s:%d: DEBUG: UNHANDLED EVENT ( TYPE=%d )", __FILE__, __LINE__, eventType );
+       break;
+    }
+
+    return UPNP_E_SUCCESS;
+}
+
+
+// Class implementations...
+
+// MediaServer...
+
+void MediaServer::parseDeviceDescription( IXML_Document* doc, const char* location, Cookie* cookie ) 
+{
+    if ( !doc ) { msg_Dbg( cookie->serviceDiscovery, "%s:%d: NULL", __FILE__, __LINE__ ); return; }
+    if ( !location ) { msg_Dbg( cookie->serviceDiscovery, "%s:%d: NULL", __FILE__, __LINE__ ); return; }
+  
+    const char* baseURL = location;
+
+    // Try to extract baseURL
+
+    IXML_NodeList* urlList = ixmlDocument_getElementsByTagName( doc, "baseURL" );
+    if ( urlList ) 
+    {  
+       if ( IXML_Node* urlNode = ixmlNodeList_item( urlList, 0 ) ) 
+       {
+           IXML_Node* textNode = ixmlNode_getFirstChild( urlNode );
+           if ( textNode ) baseURL = ixmlNode_getNodeValue( textNode );
+       }
+      
+       ixmlNodeList_free( urlList );
+    }
+  
+    // Get devices
+
+    IXML_NodeList* deviceList = ixmlDocument_getElementsByTagName( doc, "device" );
+    if ( deviceList )
+    {
+  
+       for ( unsigned int i = 0; i < ixmlNodeList_length( deviceList ); i++ ) 
+       {
+           IXML_Element* deviceElement = ( IXML_Element* )ixmlNodeList_item( deviceList, i );
+    
+           const char* deviceType = xml_getChildElementValue( deviceElement, "deviceType" );
+           if ( !deviceType ) { msg_Dbg( cookie->serviceDiscovery, "%s:%d: no deviceType!", __FILE__, __LINE__ ); continue; }
+           if ( strcmp( MEDIA_SERVER_DEVICE_TYPE, deviceType ) != 0 ) continue;
+   
+           const char* UDN = xml_getChildElementValue( deviceElement, "UDN" );
+           if ( !UDN ) { msg_Dbg( cookie->serviceDiscovery, "%s:%d: no UDN!", __FILE__, __LINE__ ); continue; }    
+           if ( cookie->serverList->getServer( UDN ) != 0 ) continue;
+    
+           const char* friendlyName = xml_getChildElementValue( deviceElement, "friendlyName" );
+           if ( !friendlyName ) { msg_Dbg( cookie->serviceDiscovery, "%s:%d: no friendlyName!", __FILE__, __LINE__ ); continue; }
+    
+           MediaServer* server = new MediaServer( UDN, friendlyName, cookie );
+           if ( !cookie->serverList->addServer( server ) ) {
+
+               delete server;
+               server = 0;
+               continue;
+           }
+           
+           // Check for ContentDirectory service...
+           
+           IXML_NodeList* serviceList = ixmlElement_getElementsByTagName( deviceElement, "service" );
+           if ( serviceList ) 
+           {
+               for ( unsigned int j = 0; j < ixmlNodeList_length( serviceList ); j++ ) 
+               {
+                   IXML_Element* serviceElement = ( IXML_Element* )ixmlNodeList_item( serviceList, j );
+               
+                   const char* serviceType = xml_getChildElementValue( serviceElement, "serviceType" );
+                   if ( !serviceType ) continue;
+                   if ( strcmp( CONTENT_DIRECTORY_SERVICE_TYPE, serviceType ) != 0 ) continue;
+                   
+                   const char* eventSubURL = xml_getChildElementValue( serviceElement, "eventSubURL" );
+                   if ( !eventSubURL ) continue;
+               
+                   const char* controlURL = xml_getChildElementValue( serviceElement, "controlURL" );
+                   if ( !controlURL ) continue;
+
+                   // Try to subscribe to ContentDirectory service
+      
+                   char* url = ( char* )malloc( strlen( baseURL ) + strlen( eventSubURL ) + 1 );
+                   if ( url ) 
+                   {
+                           char* s1 = strdup( baseURL );
+                           char* s2 = strdup( eventSubURL );
+                   
+                           if ( UpnpResolveURL( s1, s2, url ) == UPNP_E_SUCCESS ) 
+                           {
+                               // msg_Dbg( cookie->serviceDiscovery, "CDS EVENT URL: %s", url );
+                               
+                               server->setContentDirectoryEventURL( url );
+                               server->subscribeToContentDirectory();
+                           }
+
+                           free( s1 );
+                           free( s2 );
+                           free( url );
+                   }
+                   
+                   // Try to browse content directory...
+                   
+                   url = ( char* )malloc( strlen( baseURL ) + strlen( controlURL ) + 1 );
+                   if ( url ) 
+                   {
+                       char* s1 = strdup( baseURL );
+                       char* s2 = strdup( controlURL );
+                   
+                       if ( UpnpResolveURL( s1, s2, url ) == UPNP_E_SUCCESS ) 
+                       {
+                           // msg_Dbg( cookie->serviceDiscovery, "CDS CTRL URL: %s", url );
+                           
+                           server->setContentDirectoryControlURL( url );
+                           server->fetchContents();
+                       }
+       
+                       free( s1 );
+                       free( s2 );
+                       free( url );
+                   }
+               }
+                               
+               ixmlNodeList_free( serviceList );
+           }
+       }
+
+       ixmlNodeList_free( deviceList );
+    }
+}
+
+MediaServer::MediaServer( const char* UDN, const char* friendlyName, Cookie* cookie ) 
+{
+    _cookie = cookie;
+
+    _UDN = UDN;
+    _friendlyName = friendlyName;
+
+    _contents = 0;
+    _playlistNode = 0;
+}
+
+MediaServer::~MediaServer() 
+{ 
+    if ( _contents ) 
+    {
+       playlist_NodeDelete( _cookie->serviceDiscovery->p_sys->p_playlist, 
+                           _playlistNode,
+                           true,
+                           true );
+    }
+
+    delete _contents;
+}
+
+const char* MediaServer::getUDN() const
+{
+  const char* s = _UDN.c_str();
+  return s;
+}
+
+const char* MediaServer::getFriendlyName() const
+{
+    const char* s = _friendlyName.c_str();
+    return s;
+}
+
+void MediaServer::setContentDirectoryEventURL( const char* url ) 
+{
+    _contentDirectoryEventURL = url;
+}
+
+const char* MediaServer::getContentDirectoryEventURL() const
+{
+    const char* s =  _contentDirectoryEventURL.c_str();
+    return s;
+}
+
+void MediaServer::setContentDirectoryControlURL( const char* url ) 
+{
+    _contentDirectoryControlURL = url;
+}
+
+const char* MediaServer::getContentDirectoryControlURL() const
+{
+    return _contentDirectoryControlURL.c_str();
+}
+
+void MediaServer::subscribeToContentDirectory() 
+{
+    const char* url = getContentDirectoryEventURL();
+    if ( !url || strcmp( url, "" ) == 0 ) 
+    { 
+       msg_Dbg( _cookie->serviceDiscovery, "No subscription url set!" ); 
+       return;
+    }
+
+    int timeOut = 1810;
+    Upnp_SID sid;
+  
+    int res = UpnpSubscribe( _cookie->clientHandle, url, &timeOut, sid );
+
+    if ( res == UPNP_E_SUCCESS ) 
+    {
+       _subscriptionTimeOut = timeOut;
+       memcpy( _subscriptionID, sid, sizeof( Upnp_SID ) );
+    } 
+    else 
+    {
+       msg_Dbg( _cookie->serviceDiscovery, "%s:%d: WARNING: '%s': %s", __FILE__, __LINE__, getFriendlyName(), UpnpGetErrorMessage( res ) );
+    }
+}
+
+IXML_Document* MediaServer::_browseAction( const char* pObjectID, const char* pBrowseFlag, const char* pFilter, 
+                                          const char* pStartingIndex, const char* pRequestedCount, const char* pSortCriteria ) 
+{
+    IXML_Document* action = 0;
+    IXML_Document* response = 0;
+
+    const char* url = getContentDirectoryControlURL();
+    if ( !url || strcmp( url, "" ) == 0 ) { msg_Dbg( _cookie->serviceDiscovery, "No subscription url set!" ); return 0; }
+
+    char* ObjectID = strdup( pObjectID );
+    char* BrowseFlag = strdup( pBrowseFlag );
+    char* Filter = strdup( pFilter );
+    char* StartingIndex = strdup( pStartingIndex );
+    char* RequestedCount = strdup( pRequestedCount );
+    char* SortCriteria = strdup( pSortCriteria );
+
+    char* serviceType = strdup( CONTENT_DIRECTORY_SERVICE_TYPE );
+
+    int res;
+
+    res = UpnpAddToAction( &action, "Browse", serviceType, "ObjectID", ObjectID );
+    if ( res != UPNP_E_SUCCESS ) { /* msg_Dbg( _cookie->serviceDiscovery, "%s:%d: ERROR: %s", __FILE__, __LINE__, UpnpGetErrorMessage( res ) ); */ goto browseActionCleanup; }
+
+    res = UpnpAddToAction( &action, "Browse", serviceType, "BrowseFlag", BrowseFlag );
+    if ( res != UPNP_E_SUCCESS ) { /* msg_Dbg( _cookie->serviceDiscovery, "%s:%d: ERROR: %s", __FILE__, __LINE__, UpnpGetErrorMessage( res ) ); */ goto browseActionCleanup; }
+
+    res = UpnpAddToAction( &action, "Browse", serviceType, "Filter", Filter );
+    if ( res != UPNP_E_SUCCESS ) { /* msg_Dbg( _cookie->serviceDiscovery, "%s:%d: ERROR: %s", __FILE__, __LINE__, UpnpGetErrorMessage( res ) ); */ goto browseActionCleanup; }
+
+    res = UpnpAddToAction( &action, "Browse", serviceType, "StartingIndex", StartingIndex );
+    if ( res != UPNP_E_SUCCESS ) { /* msg_Dbg( _cookie->serviceDiscovery, "%s:%d: ERROR: %s", __FILE__, __LINE__, UpnpGetErrorMessage( res ) ); */ goto browseActionCleanup; }
+
+    res = UpnpAddToAction( &action, "Browse", serviceType, "RequestedCount", RequestedCount );
+    if ( res != UPNP_E_SUCCESS ) { /* msg_Dbg( _cookie->serviceDiscovery, "%s:%d: ERROR: %s", __FILE__, __LINE__, UpnpGetErrorMessage( res ) ); */ goto browseActionCleanup; }
+
+    res = UpnpAddToAction( &action, "Browse", serviceType, "SortCriteria", SortCriteria );
+    if ( res != UPNP_E_SUCCESS ) { /* msg_Dbg( _cookie->serviceDiscovery, "%s:%d: ERROR: %s", __FILE__, __LINE__, UpnpGetErrorMessage( res ) ); */ goto browseActionCleanup; }
+  
+    res = UpnpSendAction( _cookie->clientHandle, 
+                         url,
+                         CONTENT_DIRECTORY_SERVICE_TYPE, 
+                         0, 
+                         action, 
+                         &response );
+    if ( res != UPNP_E_SUCCESS ) 
+    { 
+       msg_Dbg( _cookie->serviceDiscovery, "%s:%d: ERROR: %s", __FILE__, __LINE__, UpnpGetErrorMessage( res ) ); 
+       ixmlDocument_free( response );
+       response = 0;
+    }
+
+ browseActionCleanup:
+
+    free( ObjectID );
+    free( BrowseFlag );
+    free( Filter );
+    free( StartingIndex );
+    free( RequestedCount );
+    free( SortCriteria );
+
+    free( serviceType );
+
+    ixmlDocument_free( action );
+    return response;
+}
+
+void MediaServer::fetchContents() 
+{
+    Container* root = new Container( 0, "0", getFriendlyName() );
+    _fetchContents( root );
+
+    if ( _contents ) 
+    {
+       vlc_mutex_lock( &_cookie->serviceDiscovery->p_sys->p_playlist->object_lock );
+
+       playlist_NodeEmpty( _cookie->serviceDiscovery->p_sys->p_playlist, 
+                          _playlistNode,
+                          true );
+
+       vlc_mutex_unlock( &_cookie->serviceDiscovery->p_sys->p_playlist->object_lock );
+    
+       delete _contents;
+    }
+
+    _contents = root;
+    _contents->setPlaylistNode( _playlistNode );
+
+    _buildPlaylist( _contents );
+}
+
+bool MediaServer::_fetchContents( Container* parent ) 
+{
+    if (!parent) { msg_Dbg( _cookie->serviceDiscovery, "%s:%d: parent==NULL", __FILE__, __LINE__ ); return false; }
+
+    IXML_Document* response = _browseAction( parent->getObjectID(), "BrowseDirectChildren", "*", "0", "0", "" );
+    if ( !response ) { msg_Dbg( _cookie->serviceDiscovery, "%s:%d: ERROR!", __FILE__, __LINE__ ); return false; }
+
+    IXML_Document* result = parseBrowseResult( response );
+    ixmlDocument_free( response );
+    if ( !result ) { msg_Dbg( _cookie->serviceDiscovery, "%s:%d: ERROR!", __FILE__, __LINE__ ); return false; }
+
+    IXML_NodeList* containerNodeList = ixmlDocument_getElementsByTagName( result, "container" );
+    if ( containerNodeList ) 
+    {
+       for ( unsigned int i = 0; i < ixmlNodeList_length( containerNodeList ); i++ ) 
+       {
+           IXML_Element* containerElement = ( IXML_Element* )ixmlNodeList_item( containerNodeList, i );
+      
+           const char* objectID = ixmlElement_getAttribute( containerElement, "id" );
+           if ( !objectID ) continue;
+      
+           const char* childCountStr = ixmlElement_getAttribute( containerElement, "childCount" );
+           if ( !childCountStr ) continue;
+           int childCount = atoi( childCountStr );
+      
+           const char* title = xml_getChildElementValue( containerElement, "dc:title" );
+           if ( !title ) continue;
+      
+           const char* resource = xml_getChildElementValue( containerElement, "res" );
+
+           if ( resource && childCount < 1 ) 
+           { 
+               Item* item = new Item( parent, objectID, title, resource );
+               parent->addItem( item );
+           } 
+           else 
+           {
+               Container* container = new Container( parent, objectID, title );
+               parent->addContainer( container );
+
+               if ( childCount > 0 ) _fetchContents( container );
+           }
+       }
+
+       ixmlNodeList_free( containerNodeList );
+    }
+
+    IXML_NodeList* itemNodeList = ixmlDocument_getElementsByTagName( result, "item" );
+    if ( itemNodeList ) 
+    {
+       for ( unsigned int i = 0; i < ixmlNodeList_length( itemNodeList ); i++ ) 
+       {
+           IXML_Element* itemElement = ( IXML_Element* )ixmlNodeList_item( itemNodeList, i );
+       
+           const char* objectID = ixmlElement_getAttribute( itemElement, "id" );
+           if ( !objectID ) continue;
+      
+           const char* title = xml_getChildElementValue( itemElement, "dc:title" );
+           if ( !title ) continue;
+      
+           const char* resource = xml_getChildElementValue( itemElement, "res" );
+           if ( !resource ) continue;
+      
+           Item* item = new Item( parent, objectID, title, resource );
+           parent->addItem( item );
+       }
+    
+       ixmlNodeList_free( itemNodeList );
+    }
+  
+    ixmlDocument_free( result );
+  
+    return true;
+}
+
+void MediaServer::_buildPlaylist( Container* parent ) 
+{ 
+    for ( unsigned int i = 0; i < parent->getNumContainers(); i++ ) 
+    {
+       Container* container = parent->getContainer( i );
+       playlist_item_t* parentNode = parent->getPlaylistNode();
+
+       char* title = strdup( container->getTitle() );
+       playlist_item_t* node = playlist_NodeCreate( _cookie->serviceDiscovery->p_sys->p_playlist,
+                                                    VIEW_CATEGORY,
+                                                    title,
+                                                    parentNode );
+       free( title );
+       
+       container->setPlaylistNode( node );
+       _buildPlaylist( container );
+    }
+
+    for ( unsigned int i = 0; i < parent->getNumItems(); i++ ) 
+    {
+       Item* item = parent->getItem( i );
+       playlist_item_t* parentNode = parent->getPlaylistNode();
+
+       playlist_item_t* node = playlist_ItemNew( _cookie->serviceDiscovery, 
+                                                item->getResource(), 
+                                                item->getTitle() );
+    
+       playlist_NodeAddItem( _cookie->serviceDiscovery->p_sys->p_playlist, 
+                            node, 
+                            VIEW_CATEGORY,
+                            parentNode, PLAYLIST_APPEND, PLAYLIST_END );
+
+       item->setPlaylistNode( node );
+    }
+}
+
+void MediaServer::setPlaylistNode( playlist_item_t* playlistNode ) 
+{
+    _playlistNode = playlistNode;
+}
+
+bool MediaServer::compareSID( const char* sid ) 
+{
+    return ( strncmp( _subscriptionID, sid, sizeof( Upnp_SID ) ) == 0 );
+}
+
+
+// MediaServerList...
+
+MediaServerList::MediaServerList( Cookie* cookie ) 
+{
+    _cookie = cookie;
+}
+
+MediaServerList::~MediaServerList() 
+{
+    for ( unsigned int i = 0; i < _list.size(); i++ )
+    {
+       delete _list[i];
+    }
+}
+
+bool MediaServerList::addServer( MediaServer* s ) 
+{
+    if ( getServer( s->getUDN() ) != 0 ) return false;
+
+    msg_Dbg( _cookie->serviceDiscovery, "Adding server '%s'", s->getFriendlyName() );
+
+    _list.push_back( s );
+
+    char* name = strdup( s->getFriendlyName() );
+    playlist_item_t* node = playlist_NodeCreate( _cookie->serviceDiscovery->p_sys->p_playlist,
+                                               VIEW_CATEGORY,
+                                               name,
+                                               _cookie->serviceDiscovery->p_sys->p_node );
+    free( name );
+    s->setPlaylistNode( node );
+
+    return true;
+}
+
+MediaServer* MediaServerList::getServer( const char* UDN ) 
+{
+    MediaServer* result = 0;
+    
+    for ( unsigned int i = 0; i < _list.size(); i++ ) 
+    {
+       if( strcmp( UDN, _list[i]->getUDN() ) == 0 ) 
+       { 
+           result = _list[i]; 
+           break;
+       }
+    }
+
+    return result;
+}
+
+MediaServer* MediaServerList::getServerBySID( const char* sid ) 
+{ 
+    MediaServer* server = 0;
+
+    for ( unsigned int i = 0; i < _list.size(); i++ ) 
+    {
+       if ( _list[i]->compareSID( sid ) ) 
+       { 
+           server = _list[i];
+           break;
+       }
+    }
+  
+    return server;
+}
+
+void MediaServerList::removeServer( const char* UDN ) 
+{
+    MediaServer* server = getServer( UDN );
+    if ( !server ) return;
+
+    msg_Dbg( _cookie->serviceDiscovery, "Removing server '%s'", server->getFriendlyName() );  
+
+    std::vector<MediaServer*>::iterator it;
+    for ( it = _list.begin(); it != _list.end(); it++ ) 
+    {
+       if ( *it == server ) 
+       {
+           _list.erase( it );
+           delete server;
+           break;
+       }
+    }
+}
+
+
+// Item...
+
+Item::Item( Container* parent, const char* objectID, const char* title, const char* resource ) 
+{
+    _parent = parent;
+  
+    _objectID = objectID;
+    _title = title;
+    _resource = resource;
+
+    _playlistNode = 0;
+}
+
+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();
+}
+
+void Item::setPlaylistNode( playlist_item_t* node ) 
+{ 
+    _playlistNode = node; 
+}
+
+playlist_item_t* Item::getPlaylistNode() const 
+{ 
+    return _playlistNode; 
+}
+
+
+// Container...
+
+Container::Container( Container* parent, const char* objectID, const char* title ) 
+{
+    _parent = parent;
+    
+    _objectID = objectID;
+    _title = title;
+
+    _playlistNode = 0;
+}
+
+Container::~Container() 
+{
+    for ( unsigned int i = 0; i < _containers.size(); i++ ) 
+    {
+       delete _containers[i];
+    }
+
+    for ( unsigned int i = 0; i < _items.size(); i++ ) 
+    {
+       delete _items[i];
+    }
+}
+
+void Container::addItem( Item* item ) 
+{
+    _items.push_back( item );
+}
+
+void Container::addContainer( Container* container ) 
+{
+    _containers.push_back( container );
+}
+
+const char* Container::getObjectID() const 
+{ 
+    return _objectID.c_str(); 
+}
+
+const char* Container::getTitle() const 
+{ 
+    return _title.c_str(); 
+}
+
+unsigned int Container::getNumItems() const 
+{ 
+    return _items.size(); 
+}
+
+unsigned int Container::getNumContainers() const 
+{ 
+    return _containers.size(); 
+}
+
+Item* Container::getItem( unsigned int i ) const 
+{
+    if ( i < _items.size() ) return _items[i];
+    return 0;
+}
+
+Container* Container::getContainer( unsigned int i ) const 
+{
+    if ( i < _containers.size() ) return _containers[i];
+    return 0;
+}
+
+void Container::setPlaylistNode( playlist_item_t* node ) 
+{ 
+    _playlistNode = node; 
+}
+
+playlist_item_t* Container::getPlaylistNode() const 
+{ 
+    return _playlistNode; 
+}