]> git.sesse.net Git - vlc/blobdiff - modules/services_discovery/upnp.cpp
Kate: unused arguments
[vlc] / modules / services_discovery / upnp.cpp
index 569ab389e0eaf0279fbdf70d3dcbabfea61bfa81..3a9b174c6dc8789211d995bab51ceb191649b191 100644 (file)
@@ -1,14 +1,14 @@
 /*****************************************************************************
- * upnp.cpp :  UPnP discovery module
+ * Upnp.cpp :  UPnP discovery module (libupnp)
  *****************************************************************************
- * Copyright (C) 2004-2005 the VideoLAN team
- * $Id: sap.c 11664 2005-07-09 06:17:09Z courmisch $
+ * Copyright (C) 2004-2011 the VideoLAN team
+ * $Id$
  *
- * Authors: Rémi Denis-Courmont <rem # videolan.org>
- * 
- * Based on original wxWindows patch for VLC, and dependant on CyberLink
- * UPnP library from :
- *          Satoshi Konno <skonno@cybergarage.org>
+ * 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>
+ *
+ * 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
  *
  * 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., 59 Temple Place - Suite 330, Boston, MA  02111, USA.
- *****************************************************************************/
-
-/*****************************************************************************
- * Includes
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
  *****************************************************************************/
-#include <stdlib.h>                                      /* malloc(), free() */
 
-#include <cybergarage/upnp/media/player/MediaPlayer.h>
+#define __STDC_CONSTANT_MACROS 1
 
 #undef PACKAGE_NAME
-#include <vlc/vlc.h>
-#include <vlc/intf.h>
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
 
-/* FIXME: thread-safety ?? */
-/* FIXME: playlist locking */
+#include "upnp.hpp"
 
-/************************************************************************
- * Macros and definitions
- ************************************************************************/
-using namespace std;
-using namespace CyberLink;
+#include <vlc_plugin.h>
+#include <vlc_services_discovery.h>
 
-/*****************************************************************************
- * Module descriptor
- *****************************************************************************/
+#include <assert.h>
+
+/*
+ * 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";
+
+/*
+ * VLC handle
+ */
+struct services_discovery_sys_t
+{
+    UpnpClient_Handle client_handle;
+    MediaServerList* p_server_list;
+    vlc_mutex_t callback_lock;
+};
 
-/* Callbacks */
-    static int  Open ( vlc_object_t * );
-    static void Close( vlc_object_t * );
+/*
+ * VLC callback prototypes
+ */
+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();
-    set_shortname( _("UPnP"));
-    set_description( _("Universal Plug'n'Play discovery") );
+    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 );
 
+    VLC_SD_PROBE_SUBMODULE
 vlc_module_end();
 
+/*
+ * Local prototypes
+ */
+static int Callback( Upnp_EventType event_type, void* p_event, void* p_user_data );
 
-/*****************************************************************************
- * Local structures
- *****************************************************************************/
+const char* xml_getChildElementValue( IXML_Element* p_parent,
+                                      const char*   psz_tag_name );
 
-struct services_discovery_sys_t
-{
-    /* playlist node */
-    playlist_item_t *p_node;
-    playlist_t *p_playlist;
-};
+const char* xml_getChildElementAttributeValue( IXML_Element* p_parent,
+                                        const char* psz_tag_name_,
+                                        const char* psz_attribute_ );
 
-/*****************************************************************************
- * Local prototypes
- *****************************************************************************/
-
-/* Main functions */
-    static void Run    ( services_discovery_t *p_sd );
+IXML_Document* parseBrowseResult( IXML_Document* p_doc );
 
-/*****************************************************************************
- * Open: initialize and create stuff
- *****************************************************************************/
+/*
+ * 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 *)
-                                malloc( sizeof( services_discovery_sys_t ) );
+    services_discovery_sys_t *p_sys  = ( services_discovery_sys_t * )
+            calloc( 1, sizeof( services_discovery_sys_t ) );
 
-    playlist_view_t     *p_view;
-    vlc_value_t         val;
+    if( !( p_sd->p_sys = p_sys ) )
+        return VLC_ENOMEM;
 
-    p_sd->pf_run = Run;
-    p_sd->p_sys  = p_sys;
+    /* Initialize on first IPv4-capable adapter and first open port
+     * TODO: use UpnpInit2() to utilize IPv6.
+     */
+    i_res = UpnpInit( 0, 0 );
+    if( i_res != UPNP_E_SUCCESS )
+    {
+        msg_Err( p_sd, "Initialization failed: %s", UpnpGetErrorMessage( i_res ) );
+        free( p_sys );
+        return VLC_EGENERIC;
+    }
+
+    p_sys->p_server_list = new MediaServerList( p_sd );
+    vlc_mutex_init( &p_sys->callback_lock );
 
-    /* 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 )
+    /* Register a control point */
+    i_res = UpnpRegisterClient( Callback, p_sd, &p_sys->client_handle );
+    if( i_res != UPNP_E_SUCCESS )
     {
-        msg_Warn( p_sd, "unable to find playlist, cancelling UPnP listening");
+        msg_Err( p_sd, "Client registration failed: %s", UpnpGetErrorMessage( i_res ) );
+        Close( (vlc_object_t*) p_sd );
         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 );
+    /* 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;
+    }
+
+    i_res = UpnpSetMaxContentLength( 0 );
+    if( i_res != 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;
 }
 
-
-/*****************************************************************************
- * Close:
- *****************************************************************************/
+/*
+ * Releases resources.
+ */
 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 )
+    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
+ */
+const char* xml_getChildElementValue( IXML_Element* p_parent,
+                                      const char*   psz_tag_name_ )
+{
+    if ( !p_parent ) return NULL;
+    if ( !psz_tag_name_ ) return NULL;
+
+    IXML_NodeList* 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;
+
+    IXML_Node* p_text_node = ixmlNode_getFirstChild( p_element );
+    if ( !p_text_node ) return NULL;
+
+    return ixmlNode_getNodeValue( p_text_node );
+}
+
+/*
+ * 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_ )
+{
+    if ( !p_parent ) return NULL;
+    if ( !psz_tag_name_ ) return NULL;
+    if ( !psz_attribute_ ) return NULL;
+
+    IXML_NodeList* 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_ );
+}
+
+/*
+ * Extracts the result document from a SOAP response
+ */
+IXML_Document* parseBrowseResult( IXML_Document* p_doc )
+{
+    ixmlRelaxParser( 1 );
+
+    if ( !p_doc ) return 0;
+
+    IXML_NodeList* p_result_list = ixmlDocument_getElementsByTagName( p_doc,
+                                                                   "Result" );
+
+    if ( !p_result_list ) return 0;
+
+    IXML_Node* p_result_node = ixmlNodeList_item( p_result_list, 0 );
+
+    ixmlNodeList_free( p_result_list );
+
+    if ( !p_result_node ) return 0;
+
+    IXML_Node* p_text_node = ixmlNode_getFirstChild( p_result_node );
+    if ( !p_text_node ) return 0;
+
+    const char* psz_result_string = ixmlNode_getNodeValue( p_text_node );
+
+    IXML_Document* p_browse_doc = ixmlParseBuffer( psz_result_string );
+
+    return p_browse_doc;
+}
+
+
+/*
+ * Handles all UPnP events
+ */
+static int Callback( Upnp_EventType event_type, void* p_event, void* p_user_data )
+{
+    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;
+
+        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;
+        }
+
+        MediaServer::parseDeviceDescription( p_description_doc,
+                p_discovery->Location, p_sd );
+
+        ixmlDocument_free( p_description_doc );
+    }
+    break;
+
+    case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE:
+    {
+        struct Upnp_Discovery* p_discovery = ( struct Upnp_Discovery* )p_event;
+
+        p_sys->p_server_list->removeServer( p_discovery->DeviceId );
+
+    }
+    break;
+
+    case UPNP_EVENT_RECEIVED:
     {
-        playlist_NodeDelete( p_sys->p_playlist, p_sys->p_node, VLC_TRUE,
-                             VLC_TRUE );
-        vlc_object_release( p_sys->p_playlist );
+        Upnp_Event* p_e = ( Upnp_Event* )p_event;
+
+        MediaServer* p_server = p_sys->p_server_list->getServerBySID( p_e->Sid );
+        if ( p_server ) p_server->fetchContents();
+    }
+    break;
+
+    case UPNP_EVENT_AUTORENEWAL_FAILED:
+    case UPNP_EVENT_SUBSCRIPTION_EXPIRED:
+    {
+        /* 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;
     }
 
-    free( p_sys );
+    return UPNP_E_SUCCESS;
 }
 
-/*****************************************************************************
- * Run: main UPnP thread
- *****************************************************************************
- * Processes UPnP events
- *****************************************************************************/
-class UPnPHandler : public MediaPlayer, public DeviceChangeListener,
-                    /*public EventListener,*/ public SearchResponseListener
+
+/*
+ * Local class implementations.
+ */
+
+/*
+ * MediaServer
+ */
+
+void MediaServer::parseDeviceDescription( IXML_Document* p_doc,
+                                          const char*    p_location,
+                                          services_discovery_t* p_sd )
 {
-    private:
-        services_discovery_t *p_sd;
-        services_discovery_sys_t *p_sys;
+    if ( !p_doc )
+    {
+        msg_Err( p_sd, "Null IXML_Document" );
+        return;
+    }
+
+    if ( !p_location )
+    {
+        msg_Err( p_sd, "Null location" );
+        return;
+    }
+
+    const char* psz_base_url = p_location;
 
-        Device *GetDeviceFromUSN( const string& usn )
+    /* Try to extract baseURL */
+    IXML_NodeList* p_url_list = ixmlDocument_getElementsByTagName( p_doc, "baseURL" );
+    if ( p_url_list )
+    {
+
+        if ( IXML_Node* p_url_node = ixmlNodeList_item( p_url_list, 0 ) )
         {
-            return getDevice( usn.substr( 0, usn.find( "::" ) ).c_str() );
+            IXML_Node* p_text_node = ixmlNode_getFirstChild( p_url_node );
+            if ( p_text_node ) psz_base_url = ixmlNode_getNodeValue( p_text_node );
         }
 
-        playlist_item_t *FindDeviceNode( Device *dev )
+        ixmlNodeList_free( p_url_list );
+    }
+
+    /* Get devices */
+    IXML_NodeList* p_device_list =
+                ixmlDocument_getElementsByTagName( p_doc, "device" );
+
+    if ( p_device_list )
+    {
+        for ( unsigned int i = 0; i < ixmlNodeList_length( p_device_list ); i++ )
         {
-            return playlist_ChildSearchName( p_sys->p_node, dev->getFriendlyName() );
-        }
+            IXML_Element* p_device_element =
+                   ( IXML_Element* ) ixmlNodeList_item( p_device_list, i );
+
+            const char* psz_device_type = xml_getChildElementValue( p_device_element,
+                                                               "deviceType" );
+            if ( !psz_device_type )
+            {
+                msg_Warn( p_sd, "No deviceType found!" );
+                continue;
+            }
+
+            if ( strcmp( MEDIA_SERVER_DEVICE_TYPE, psz_device_type ) != 0 )
+                continue;
+
+            const char* psz_udn = xml_getChildElementValue( p_device_element, "UDN" );
+            if ( !psz_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 )
+            {
+                msg_Warn( p_sd, "Server with uuid '%s' already exists.", psz_udn );
+                continue;
+            }
+
+            const char* psz_friendly_name =
+                       xml_getChildElementValue( p_device_element,
+                                                 "friendlyName" );
+
+            if ( !psz_friendly_name )
+            {
+                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;
+            }
+
+            /* Check for ContentDirectory 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++ )
+                {
+                    IXML_Element* p_service_element =
+                        ( IXML_Element* ) ixmlNodeList_item( p_service_list, j );
+
+                    const char* psz_service_type =
+                        xml_getChildElementValue( p_service_element,
+                                                  "serviceType" );
+                    if ( !psz_service_type )
+                    {
+                        msg_Warn( p_sd, "No service type found." );
+                        continue;
+                    }
+
+                    if ( strcmp( CONTENT_DIRECTORY_SERVICE_TYPE,
+                                psz_service_type ) != 0 )
+                        continue;
+
+                    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,
+                                                  "controlURL" );
+                    if ( !psz_control_url )
+                    {
+                        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 ) +
+                            strlen( psz_control_url ) + 1 );
+                    if ( psz_url )
+                    {
+                        if ( UpnpResolveURL( psz_base_url, psz_control_url, psz_url ) ==
+                                UPNP_E_SUCCESS )
+                        {
+                            p_server->setContentDirectoryControlURL( psz_url );
+                            p_server->fetchContents();
+                        }
+
+                        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 )
+{
+    _p_sd = p_sd;
+
+    _UDN = psz_udn;
+    _friendly_name = psz_friendly_name;
+
+    _p_contents = NULL;
+    _p_input_item = NULL;
+}
+
+MediaServer::~MediaServer()
+{
+    delete _p_contents;
+}
+
+const char* MediaServer::getUDN() const
+{
+    return _UDN.c_str();
+}
+
+const char* MediaServer::getFriendlyName() const
+{
+    return _friendly_name.c_str();
+}
+
+void MediaServer::setContentDirectoryEventURL( const char* psz_url )
+{
+    _content_directory_event_url = psz_url;
+}
+
+const char* MediaServer::getContentDirectoryEventURL() const
+{
+    return _content_directory_event_url.c_str();
+}
+
+void MediaServer::setContentDirectoryControlURL( const char* psz_url )
+{
+    _content_directory_control_url = psz_url;
+}
+
+const char* MediaServer::getContentDirectoryControlURL() const
+{
+    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()
+{
+    const char* psz_url = getContentDirectoryEventURL();
+    if ( !psz_url )
+    {
+        msg_Dbg( _p_sd, "No subscription url set!" );
+        return;
+    }
+
+    int i_timeout = 1810;
+    Upnp_SID sid;
+
+    int i_res = UpnpSubscribe( _p_sd->p_sys->client_handle, psz_url, &i_timeout, sid );
+
+    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 ) );
+    }
+}
+/*
+ * Constructs UpnpAction to browse available content.
+ */
+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();
+
+    if ( !psz_url )
+    {
+        msg_Dbg( _p_sd, "No subscription url set!" );
+        return 0;
+    }
+
+    char* psz_service_type = strdup( CONTENT_DIRECTORY_SERVICE_TYPE );
+
+    int i_res;
+
+    i_res = UpnpAddToAction( &p_action, "Browse",
+            psz_service_type, "ObjectID", psz_object_id_ );
+
+    if ( i_res != UPNP_E_SUCCESS )
+    {
+        msg_Dbg( _p_sd, "AddToAction 'ObjectID' failed: %s",
+                UpnpGetErrorMessage( i_res ) );
+        goto browseActionCleanup;
+    }
+
+    i_res = UpnpAddToAction( &p_action, "Browse",
+            psz_service_type, "BrowseFlag", psz_browser_flag_ );
+
+    if ( i_res != UPNP_E_SUCCESS )
+    {
+        msg_Dbg( _p_sd, "AddToAction 'BrowseFlag' failed: %s", 
+                UpnpGetErrorMessage( i_res ) );
+        goto browseActionCleanup;
+    }
+
+    i_res = UpnpAddToAction( &p_action, "Browse",
+            psz_service_type, "Filter", psz_filter_ );
+
+    if ( i_res != UPNP_E_SUCCESS )
+    {
+        msg_Dbg( _p_sd, "AddToAction 'Filter' failed: %s",
+                UpnpGetErrorMessage( i_res ) );
+        goto browseActionCleanup;
+    }
+
+    i_res = UpnpAddToAction( &p_action, "Browse",
+            psz_service_type, "StartingIndex", psz_starting_index_ );
+
+    if ( i_res != UPNP_E_SUCCESS )
+    {
+        msg_Dbg( _p_sd, "AddToAction 'StartingIndex' failed: %s",
+                UpnpGetErrorMessage( i_res ) );
+        goto browseActionCleanup;
+    }
+
+    i_res = UpnpAddToAction( &p_action, "Browse",
+            psz_service_type, "RequestedCount", psz_requested_count_ );
+
+    if ( i_res != UPNP_E_SUCCESS )
+    {
+        msg_Dbg( _p_sd, "AddToAction 'RequestedCount' failed: %s",
+                UpnpGetErrorMessage( i_res ) );
+        goto browseActionCleanup;
+    }
+
+    i_res = UpnpAddToAction( &p_action, "Browse",
+            psz_service_type, "SortCriteria", psz_sort_criteria_ );
+
+    if ( i_res != UPNP_E_SUCCESS )
+    {
+        msg_Dbg( _p_sd, "AddToAction 'SortCriteria' failed: %s",
+                UpnpGetErrorMessage( i_res ) );
+        goto browseActionCleanup;
+    }
+
+    i_res = UpnpSendAction( _p_sd->p_sys->client_handle,
+              psz_url,
+              psz_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",
+                UpnpGetErrorMessage( i_res ), psz_url );
+
+        ixmlDocument_free( p_response );
+        p_response = 0;
+    }
+
+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 );
+
+    _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 )
+{
+    if (!p_parent)
+    {
+        msg_Err( _p_sd, "No parent" );
+        return false;
+    }
 
-        playlist_item_t *FindDeviceNode( const string &usn )
+    IXML_Document* p_response = _browseAction( p_parent->getObjectID(),
+                                      "BrowseDirectChildren",
+                                      "*", "0", "0", "" );
+    if ( !p_response )
+    {
+        msg_Err( _p_sd, "No response from browse() action" );
+        return false;
+    }
+
+    IXML_Document* p_result = parseBrowseResult( p_response );
+    ixmlDocument_free( p_response );
+
+    if ( !p_result )
+    {
+        msg_Err( _p_sd, "browse() response parsing failed" );
+        return false;
+    }
+#ifndef NDEBUG
+    else
+    {
+        msg_Dbg( _p_sd, "Got DIDL document: %s",
+                ixmlPrintDocument( p_result ) );
+    }
+#endif
+
+    IXML_NodeList* containerNodeList =
+                ixmlDocument_getElementsByTagName( p_result, "container" );
+
+    if ( containerNodeList )
+    {
+        for ( unsigned int i = 0;
+                i < ixmlNodeList_length( containerNodeList ); i++ )
         {
-            return FindDeviceNode( GetDeviceFromUSN( usn ) );
-        }
+            IXML_Element* containerElement =
+                  ( IXML_Element* )ixmlNodeList_item( containerNodeList, i );
+
+            const char* objectID = ixmlElement_getAttribute( containerElement,
+                                                             "id" );
+            if ( !objectID )
+                continue;
 
-        playlist_item_t *AddDevice( Device *dev );
-        void AddDeviceContent( Device *dev );
-        void AddContent( playlist_item_t *p_parent, ContentNode *node );
-        void RemoveDevice( Device *dev );
+            const char* title = xml_getChildElementValue( containerElement,
+                                                          "dc:title" );
 
-        /* CyberLink callbacks */
-        virtual void deviceAdded( Device *dev );
-        virtual void deviceRemoved( Device *dev );
+            if ( !title )
+                continue;
 
-        virtual void deviceSearchResponseReceived( SSDPPacket *packet );
-        /*virtual void eventNotifyReceived( const char *uuid, long seq,
-                                          const char *name,
-                                          const char *value );*/
+            Container* container = new Container( p_parent, objectID, title );
+            p_parent->addContainer( container );
+            _fetchContents( container );
+        }
+        ixmlNodeList_free( containerNodeList );
+    }
 
-    public:
-        UPnPHandler( services_discovery_t *p_this )
-            : p_sd( p_this ), p_sys( p_this->p_sys )
+    IXML_NodeList* itemNodeList = ixmlDocument_getElementsByTagName( p_result,
+                                                                     "item" );
+    if ( itemNodeList )
+    {
+        for ( unsigned int i = 0; i < ixmlNodeList_length( itemNodeList ); i++ )
         {
-            addDeviceChangeListener( this );
-            addSearchResponseListener( this );
-            //addEventListener( this );
+            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;
+
+            const char* psz_duration = xml_getChildElementAttributeValue( itemElement,
+                                                                    "res",
+                                                                    "duration" );
+
+            mtime_t i_duration = -1;
+            int i_hours, i_minutes, i_seconds, i_decis;
+
+            if ( psz_duration )
+            {
+                if( sscanf( psz_duration, "%02d:%02d:%02d.%d",
+                        &i_hours, &i_minutes, &i_seconds, &i_decis ))
+                    i_duration = INT64_C(1000000) * ( i_hours*3600 +
+                                                      i_minutes*60 +
+                                                      i_seconds ) +
+                                 INT64_C(100000) * i_decis;
+            }
+
+            Item* item = new Item( p_parent, objectID, title, resource, i_duration );
+            p_parent->addItem( item );
         }
+        ixmlNodeList_free( itemNodeList );
+    }
 
-};
+    ixmlDocument_free( p_result );
+    return true;
+}
 
-static void Run( services_discovery_t *p_sd )
+// 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 )
 {
-    UPnPHandler u( p_sd );
+    bool b_send = p_input_node == NULL;
+    if( b_send )
+        p_input_node = input_item_node_Create( p_parent->getInputItem() );
 
-    u.start();
+    for ( unsigned int i = 0; i < p_parent->getNumContainers(); i++ )
+    {
+        Container* p_container = p_parent->getContainer( i );
 
-    msg_Dbg( p_sd, "UPnP discovery started" );
-    /* read SAP packets */
-    while( !p_sd->b_die )
+        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 );
+
+        p_container->setInputItem( p_input_item );
+        _buildPlaylist( p_container, p_new_node );
+    }
+
+    for ( unsigned int i = 0; i < p_parent->getNumItems(); i++ )
     {
-        msleep( 500 );
+        Item* p_item = p_parent->getItem( i );
+
+        input_item_t* p_input_item = input_item_NewExt( p_item->getResource(),
+                                               p_item->getTitle(),
+                                               0,
+                                               NULL,
+                                               0,
+                                               p_item->getDuration() );
+
+        assert( p_input_item );
+        input_item_node_AppendItem( p_input_node, p_input_item );
+        p_item->setInputItem( p_input_item );
     }
 
-    u.stop();
-    msg_Dbg( p_sd, "UPnP discovery stopped" );
+    if( b_send )
+        input_item_node_PostAndDelete( p_input_node );
 }
 
-
-playlist_item_t *UPnPHandler::AddDevice( Device *dev )
+void MediaServer::setInputItem( input_item_t* p_input_item )
 {
-    if( dev == NULL )
-        return NULL;
+    if( _p_input_item == p_input_item )
+        return;
 
-    /* We are not interested in IGD devices or whatever (at the moment) */
-    if ( !dev->isDeviceType( MediaServer::DEVICE_TYPE ) )
-        return NULL;
+    if( _p_input_item )
+        vlc_gc_decref( _p_input_item );
 
-    playlist_item_t *p_item = FindDeviceNode( dev );
-    if ( p_item != NULL )
-        return p_item;
+    vlc_gc_incref( p_input_item );
+    _p_input_item = p_input_item;
+}
 
-    /* FIXME:
-     * Maybe one day, VLC API will make sensible use of the const keyword;
-     * That day, you will no longer need this strdup().
-     */
-    char *str = strdup( dev->getFriendlyName( ) );
+input_item_t* MediaServer::getInputItem() const
+{
+    return _p_input_item;
+}
+
+bool MediaServer::compareSID( const char* psz_sid )
+{
+    return ( strncmp( _subscription_id, psz_sid, sizeof( Upnp_SID ) ) == 0 );
+}
 
-    p_item = playlist_NodeCreate( p_sys->p_playlist, VIEW_CATEGORY,
-                                  str, p_sys->p_node );
-    p_item->i_flags &= ~PLAYLIST_SKIP_FLAG;
-    msg_Dbg( p_sd, "device %s added", str );
-    free( str );
 
-    return p_item;
+/*
+ * MediaServerList class
+ */
+MediaServerList::MediaServerList( services_discovery_t* p_sd )
+{
+    _p_sd = p_sd;
 }
 
-void UPnPHandler::AddDeviceContent( Device *dev )
+MediaServerList::~MediaServerList()
 {
-    playlist_item_t *p_devnode = AddDevice( dev );
+    for ( unsigned int i = 0; i < _list.size(); i++ )
+    {
+        delete _list[i];
+    }
+}
 
-    if( p_devnode == NULL )
-        return;
+bool MediaServerList::addServer( MediaServer* p_server )
+{
+    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 );
 
-    AddContent( p_devnode, getContentDirectory( dev ) );
+    _list.push_back( p_server );
+
+    return true;
 }
 
-void UPnPHandler::AddContent( playlist_item_t *p_parent, ContentNode *node )
+MediaServer* MediaServerList::getServer( const char* psz_udn )
 {
-    if( node == NULL )
-        return;
+    MediaServer* p_result = 0;
 
-    const char *title = node->getTitle();
-    if( title == NULL )
-        return;
+    for ( unsigned int i = 0; i < _list.size(); i++ )
+    {
+        if( strcmp( psz_udn, _list[i]->getUDN() ) == 0 )
+        {
+            p_result = _list[i];
+            break;
+        }
+    }
+
+    return p_result;
+}
+
+MediaServer* MediaServerList::getServerBySID( const char* psz_sid )
+{
+    MediaServer* p_server = 0;
 
-    msg_Dbg( p_sd, "title = %s", title );
-    
-    if( !node->isContainerNode() )
+    for ( unsigned int i = 0; i < _list.size(); i++ )
     {
-        msg_Dbg( p_sd, "not a container" );
-        return;
+        if ( _list[i]->compareSID( psz_sid ) )
+        {
+            p_server = _list[i];
+            break;
+        }
     }
 
-    ContainerNode *conNode = (ContainerNode *)node;
-    ItemNode *iNode = (ItemNode *)node;
+    return p_server;
+}
 
-    playlist_item_t *p_item;
-    p_item = playlist_ItemNew( p_sd, iNode->getResource(), title );
-    playlist_NodeAddItem( p_sys->p_playlist, p_item, VIEW_CATEGORY,
-                          p_parent, PLAYLIST_APPEND, PLAYLIST_END );
+void MediaServerList::removeServer( const char* psz_udn )
+{
+    MediaServer* p_server = getServer( psz_udn );
+    if ( !p_server ) return;
 
-    /*if( !cnode->hasContainerNodes() )
-        return;*/
+    msg_Dbg( _p_sd, "Removing server '%s'", p_server->getFriendlyName() );
 
-    unsigned nContentNodes = conNode->getNContentNodes();
+    services_discovery_RemoveItem( _p_sd, p_server->getInputItem() );
 
-    for( unsigned n = 0; n < nContentNodes; n++ )
-        AddContent( p_item, conNode->getContentNode( n ) );
+    std::vector<MediaServer*>::iterator it;
+    for ( it = _list.begin(); it != _list.end(); ++it )
+    {
+        if ( *it == p_server )
+        {
+            _list.erase( it );
+            delete p_server;
+            break;
+        }
+    }
 }
 
 
-void UPnPHandler::RemoveDevice( Device *dev )
+/*
+ * Item class
+ */
+Item::Item( Container* p_parent, const char* psz_object_id, const char* psz_title,
+           const char* psz_resource, mtime_t i_duration )
 {
-    playlist_item_t *p_item = FindDeviceNode( dev );
+    _parent = p_parent;
 
-    if( p_item != NULL )
-        playlist_NodeDelete( p_sys->p_playlist, p_item, VLC_TRUE, VLC_TRUE );
+    _objectID = psz_object_id;
+    _title = psz_title;
+    _resource = psz_resource;
+    _duration = i_duration;
+
+    _p_input_item = NULL;
 }
 
+Item::~Item()
+{
+    if( _p_input_item )
+        vlc_gc_decref( _p_input_item );
+}
 
-void UPnPHandler::deviceAdded( Device *dev )
+const char* Item::getObjectID() const
 {
-    msg_Dbg( p_sd, "adding device" );
-    AddDeviceContent( dev );
+    return _objectID.c_str();
 }
 
+const char* Item::getTitle() const
+{
+    return _title.c_str();
+}
 
-void UPnPHandler::deviceRemoved( Device *dev )
+const char* Item::getResource() const
 {
-    msg_Dbg( p_sd, "removing device" );
-    RemoveDevice( dev );
+    return _resource.c_str();
 }
 
+mtime_t Item::getDuration() const
+{
+    return _duration;
+}
 
-void UPnPHandler::deviceSearchResponseReceived( SSDPPacket *packet )
+void Item::setInputItem( input_item_t* p_input_item )
 {
-    if( !packet->isRootDevice() )
+    if( _p_input_item == p_input_item )
         return;
 
-    string usn, nts, nt, udn;
+    if( _p_input_item )
+        vlc_gc_decref( _p_input_item );
 
-    packet->getUSN( usn );
-    packet->getNT( nt );
-    packet->getNTS( nts );
-    udn = usn.substr( 0, usn.find( "::" ) );
+    vlc_gc_incref( p_input_item );
+    _p_input_item = p_input_item;
+}
 
-    Device *dev = GetDeviceFromUSN( usn );
-    if( packet->isByeBye() )
-        RemoveDevice( dev );
-    else
-        AddDeviceContent( dev );
+/*
+ * 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;
 }
 
-/*void UPnPHandler::eventNotifyReceived( const char *uuid, long seq,
-                                       const char *name, const char *value )
+Container::~Container()
 {
-    msg_Dbg( p_sd, "event notify received" );
-    msg_Dbg( p_sd, "uuid = %s, name = %s, value = %s", uuid, name, value );
-}*/
+    for ( unsigned int i = 0; i < _containers.size(); i++ )
+    {
+        delete _containers[i];
+    }
+
+    for ( unsigned int i = 0; i < _items.size(); i++ )
+    {
+        delete _items[i];
+    }
+
+    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
+{
+    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_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];
+    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;
+}