]> git.sesse.net Git - vlc/blob - modules/services_discovery/upnp.cpp
upnp: Support UPnP A/V MediaServer:2 devices
[vlc] / modules / services_discovery / upnp.cpp
1 /*****************************************************************************
2  * upnp.cpp :  UPnP discovery module (libupnp)
3  *****************************************************************************
4  * Copyright (C) 2004-2011 the VideoLAN team
5  * $Id$
6  *
7  * Authors: RĂ©mi Denis-Courmont <rem # videolan.org> (original plugin)
8  *          Christian Henz <henz # c-lab.de>
9  *          Mirsal Ennaime <mirsal dot ennaime at gmail dot com>
10  *
11  * UPnP Plugin using the Intel SDK (libupnp) instead of CyberLink
12  *
13  * This program is free software; you can redistribute it and/or modify
14  * it under the terms of the GNU General Public License as published by
15  * the Free Software Foundation; either version 2 of the License, or
16  * (at your option) any later version.
17  *
18  * This program is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * You should have received a copy of the GNU General Public License
24  * along with this program; if not, write to the Free Software
25  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
26  *****************************************************************************/
27
28 #define __STDC_CONSTANT_MACROS 1
29
30 #undef PACKAGE_NAME
31 #ifdef HAVE_CONFIG_H
32 # include "config.h"
33 #endif
34
35 #include "upnp.hpp"
36
37 #include <vlc_plugin.h>
38 #include <vlc_services_discovery.h>
39
40 #include <assert.h>
41
42 /*
43  * Constants
44 */
45 const char* MEDIA_SERVER_DEVICE_TYPE = "urn:schemas-upnp-org:device:MediaServer:1";
46 const char* CONTENT_DIRECTORY_SERVICE_TYPE = "urn:schemas-upnp-org:service:ContentDirectory:1";
47
48 /*
49  * VLC handle
50  */
51 struct services_discovery_sys_t
52 {
53     UpnpClient_Handle client_handle;
54     MediaServerList* p_server_list;
55     vlc_mutex_t callback_lock;
56 };
57
58 /*
59  * VLC callback prototypes
60  */
61 static int Open( vlc_object_t* );
62 static void Close( vlc_object_t* );
63 VLC_SD_PROBE_HELPER( "upnp", "Universal Plug'n'Play", SD_CAT_LAN )
64
65 /*
66  * Module descriptor
67  */
68 vlc_module_begin();
69     set_shortname( "UPnP" );
70     set_description( N_( "Universal Plug'n'Play" ) );
71     set_category( CAT_PLAYLIST );
72     set_subcategory( SUBCAT_PLAYLIST_SD );
73     set_capability( "services_discovery", 0 );
74     set_callbacks( Open, Close );
75
76     VLC_SD_PROBE_SUBMODULE
77 vlc_module_end();
78
79 /*
80  * Local prototypes
81  */
82 static int Callback( Upnp_EventType event_type, void* p_event, void* p_user_data );
83
84 const char* xml_getChildElementValue( IXML_Element* p_parent,
85                                       const char*   psz_tag_name );
86
87 const char* xml_getChildElementAttributeValue( IXML_Element* p_parent,
88                                         const char* psz_tag_name_,
89                                         const char* psz_attribute_ );
90
91 IXML_Document* parseBrowseResult( IXML_Document* p_doc );
92
93 /*
94  * Initializes UPNP instance.
95  */
96 static int Open( vlc_object_t *p_this )
97 {
98     int i_res;
99     services_discovery_t *p_sd = ( services_discovery_t* )p_this;
100     services_discovery_sys_t *p_sys  = ( services_discovery_sys_t * )
101             calloc( 1, sizeof( services_discovery_sys_t ) );
102
103     if( !( p_sd->p_sys = p_sys ) )
104         return VLC_ENOMEM;
105
106     /* Initialize on first IPv4-capable adapter and first open port
107      * TODO: use UpnpInit2() to utilize IPv6.
108      */
109     i_res = UpnpInit( 0, 0 );
110     if( i_res != UPNP_E_SUCCESS )
111     {
112         msg_Err( p_sd, "Initialization failed: %s", UpnpGetErrorMessage( i_res ) );
113         free( p_sys );
114         return VLC_EGENERIC;
115     }
116
117     p_sys->p_server_list = new MediaServerList( p_sd );
118     vlc_mutex_init( &p_sys->callback_lock );
119
120     /* Register a control point */
121     i_res = UpnpRegisterClient( Callback, p_sd, &p_sys->client_handle );
122     if( i_res != UPNP_E_SUCCESS )
123     {
124         msg_Err( p_sd, "Client registration failed: %s", UpnpGetErrorMessage( i_res ) );
125         Close( (vlc_object_t*) p_sd );
126         return VLC_EGENERIC;
127     }
128
129     /* Search for media servers */
130     i_res = UpnpSearchAsync( p_sys->client_handle, 5,
131             MEDIA_SERVER_DEVICE_TYPE, p_sd );
132     if( i_res != UPNP_E_SUCCESS )
133     {
134         msg_Err( p_sd, "Error sending search request: %s", UpnpGetErrorMessage( i_res ) );
135         Close( (vlc_object_t*) p_sd );
136         return VLC_EGENERIC;
137     }
138
139     i_res = UpnpSetMaxContentLength( 0 );
140     if( i_res != UPNP_E_SUCCESS )
141     {
142         msg_Err( p_sd, "Failed to set maximum content length: %s", UpnpGetErrorMessage( i_res ) );
143         Close( (vlc_object_t*) p_sd );
144         return VLC_EGENERIC;
145     }
146
147     return VLC_SUCCESS;
148 }
149
150 /*
151  * Releases resources.
152  */
153 static void Close( vlc_object_t *p_this )
154 {
155     services_discovery_t *p_sd = ( services_discovery_t* )p_this;
156
157     UpnpUnRegisterClient( p_sd->p_sys->client_handle );
158     UpnpFinish();
159
160     delete p_sd->p_sys->p_server_list;
161     vlc_mutex_destroy( &p_sd->p_sys->callback_lock );
162
163     free( p_sd->p_sys );
164 }
165
166 /* XML utility functions */
167
168 /*
169  * Returns the value of a child element, or NULL on error
170  */
171 const char* xml_getChildElementValue( IXML_Element* p_parent,
172                                       const char*   psz_tag_name_ )
173 {
174     if ( !p_parent ) return NULL;
175     if ( !psz_tag_name_ ) return NULL;
176
177     IXML_NodeList* p_node_list = ixmlElement_getElementsByTagName( p_parent, psz_tag_name_ );
178     if ( !p_node_list ) return NULL;
179
180     IXML_Node* p_element = ixmlNodeList_item( p_node_list, 0 );
181     ixmlNodeList_free( p_node_list );
182     if ( !p_element ) return NULL;
183
184     IXML_Node* p_text_node = ixmlNode_getFirstChild( p_element );
185     if ( !p_text_node ) return NULL;
186
187     return ixmlNode_getNodeValue( p_text_node );
188 }
189
190 /*
191  * Returns the value of a child element's attribute, or NULL on error
192  */
193 const char* xml_getChildElementAttributeValue( IXML_Element* p_parent,
194                                         const char* psz_tag_name_,
195                                         const char* psz_attribute_ )
196 {
197     if ( !p_parent ) return NULL;
198     if ( !psz_tag_name_ ) return NULL;
199     if ( !psz_attribute_ ) return NULL;
200
201     IXML_NodeList* p_node_list = ixmlElement_getElementsByTagName( p_parent, psz_tag_name_ );
202     if ( !p_node_list ) return NULL;
203
204     IXML_Node* p_element = ixmlNodeList_item( p_node_list, 0 );
205     ixmlNodeList_free( p_node_list );
206     if ( !p_element ) return NULL;
207
208     return ixmlElement_getAttribute( (IXML_Element*) p_element, psz_attribute_ );
209 }
210
211 /*
212  * Extracts the result document from a SOAP response
213  */
214 IXML_Document* parseBrowseResult( IXML_Document* p_doc )
215 {
216     ixmlRelaxParser( 1 );
217
218     if ( !p_doc ) return 0;
219
220     IXML_NodeList* p_result_list = ixmlDocument_getElementsByTagName( p_doc,
221                                                                    "Result" );
222
223     if ( !p_result_list ) return 0;
224
225     IXML_Node* p_result_node = ixmlNodeList_item( p_result_list, 0 );
226
227     ixmlNodeList_free( p_result_list );
228
229     if ( !p_result_node ) return 0;
230
231     IXML_Node* p_text_node = ixmlNode_getFirstChild( p_result_node );
232     if ( !p_text_node ) return 0;
233
234     const char* psz_result_string = ixmlNode_getNodeValue( p_text_node );
235
236     IXML_Document* p_browse_doc = ixmlParseBuffer( psz_result_string );
237
238     return p_browse_doc;
239 }
240
241
242 /*
243  * Handles all UPnP events
244  */
245 static int Callback( Upnp_EventType event_type, void* p_event, void* p_user_data )
246 {
247     services_discovery_t* p_sd = ( services_discovery_t* ) p_user_data;
248     services_discovery_sys_t* p_sys = p_sd->p_sys;
249     vlc_mutex_locker locker( &p_sys->callback_lock );
250
251     switch( event_type )
252     {
253     case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
254     case UPNP_DISCOVERY_SEARCH_RESULT:
255     {
256         struct Upnp_Discovery* p_discovery = ( struct Upnp_Discovery* )p_event;
257
258         IXML_Document *p_description_doc = 0;
259
260         int i_res;
261         i_res = UpnpDownloadXmlDoc( p_discovery->Location, &p_description_doc );
262         if ( i_res != UPNP_E_SUCCESS )
263         {
264             msg_Warn( p_sd, "Could not download device description! "
265                             "Fetching data from %s failed: %s",
266                             p_discovery->Location, UpnpGetErrorMessage( i_res ) );
267             return i_res;
268         }
269
270         MediaServer::parseDeviceDescription( p_description_doc,
271                 p_discovery->Location, p_sd );
272
273         ixmlDocument_free( p_description_doc );
274     }
275     break;
276
277     case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE:
278     {
279         struct Upnp_Discovery* p_discovery = ( struct Upnp_Discovery* )p_event;
280
281         p_sys->p_server_list->removeServer( p_discovery->DeviceId );
282
283     }
284     break;
285
286     case UPNP_EVENT_RECEIVED:
287     {
288         Upnp_Event* p_e = ( Upnp_Event* )p_event;
289
290         MediaServer* p_server = p_sys->p_server_list->getServerBySID( p_e->Sid );
291         if ( p_server ) p_server->fetchContents();
292     }
293     break;
294
295     case UPNP_EVENT_AUTORENEWAL_FAILED:
296     case UPNP_EVENT_SUBSCRIPTION_EXPIRED:
297     {
298         /* Re-subscribe. */
299
300         Upnp_Event_Subscribe* p_s = ( Upnp_Event_Subscribe* )p_event;
301
302         MediaServer* p_server = p_sys->p_server_list->getServerBySID( p_s->Sid );
303         if ( p_server ) p_server->subscribeToContentDirectory();
304     }
305     break;
306
307     case UPNP_EVENT_SUBSCRIBE_COMPLETE:
308         msg_Warn( p_sd, "subscription complete" );
309         break;
310
311     case UPNP_DISCOVERY_SEARCH_TIMEOUT:
312         msg_Warn( p_sd, "search timeout" );
313         break;
314
315     default:
316         msg_Err( p_sd, "Unhandled event, please report ( type=%d )", event_type );
317         break;
318     }
319
320     return UPNP_E_SUCCESS;
321 }
322
323
324 /*
325  * Local class implementations.
326  */
327
328 /*
329  * MediaServer
330  */
331
332 void MediaServer::parseDeviceDescription( IXML_Document* p_doc,
333                                           const char*    p_location,
334                                           services_discovery_t* p_sd )
335 {
336     if ( !p_doc )
337     {
338         msg_Err( p_sd, "Null IXML_Document" );
339         return;
340     }
341
342     if ( !p_location )
343     {
344         msg_Err( p_sd, "Null location" );
345         return;
346     }
347
348     const char* psz_base_url = p_location;
349
350     /* Try to extract baseURL */
351     IXML_NodeList* p_url_list = ixmlDocument_getElementsByTagName( p_doc, "URLBase" );
352     if ( p_url_list )
353     {
354
355         if ( IXML_Node* p_url_node = ixmlNodeList_item( p_url_list, 0 ) )
356         {
357             IXML_Node* p_text_node = ixmlNode_getFirstChild( p_url_node );
358             if ( p_text_node ) psz_base_url = ixmlNode_getNodeValue( p_text_node );
359         }
360
361         ixmlNodeList_free( p_url_list );
362     }
363
364     /* Get devices */
365     IXML_NodeList* p_device_list =
366                 ixmlDocument_getElementsByTagName( p_doc, "device" );
367
368     if ( p_device_list )
369     {
370         for ( unsigned int i = 0; i < ixmlNodeList_length( p_device_list ); i++ )
371         {
372             IXML_Element* p_device_element =
373                    ( IXML_Element* ) ixmlNodeList_item( p_device_list, i );
374
375             const char* psz_device_type = xml_getChildElementValue( p_device_element,
376                                                                "deviceType" );
377             if ( !psz_device_type )
378             {
379                 msg_Warn( p_sd, "No deviceType found!" );
380                 continue;
381             }
382
383             if ( strncmp( MEDIA_SERVER_DEVICE_TYPE, psz_device_type,
384                     strlen( MEDIA_SERVER_DEVICE_TYPE ) - 1 ) != 0 )
385                 continue;
386
387             const char* psz_udn = xml_getChildElementValue( p_device_element, "UDN" );
388             if ( !psz_udn )
389             {
390                 msg_Warn( p_sd, "No UDN!" );
391                 continue;
392             }
393
394             /* Check if server is already added */
395             if ( p_sd->p_sys->p_server_list->getServer( psz_udn ) != 0 )
396             {
397                 msg_Warn( p_sd, "Server with uuid '%s' already exists.", psz_udn );
398                 continue;
399             }
400
401             const char* psz_friendly_name =
402                        xml_getChildElementValue( p_device_element,
403                                                  "friendlyName" );
404
405             if ( !psz_friendly_name )
406             {
407                 msg_Dbg( p_sd, "No friendlyName!" );
408                 continue;
409             }
410
411             MediaServer* p_server = new MediaServer( psz_udn, psz_friendly_name, p_sd );
412
413             if ( !p_sd->p_sys->p_server_list->addServer( p_server ) )
414             {
415                 delete p_server;
416                 p_server = 0;
417                 continue;
418             }
419
420             /* Check for ContentDirectory service. */
421             IXML_NodeList* p_service_list =
422                        ixmlElement_getElementsByTagName( p_device_element,
423                                                          "service" );
424             if ( p_service_list )
425             {
426                 for ( unsigned int j = 0;
427                       j < ixmlNodeList_length( p_service_list ); j++ )
428                 {
429                     IXML_Element* p_service_element =
430                         ( IXML_Element* ) ixmlNodeList_item( p_service_list, j );
431
432                     const char* psz_service_type =
433                         xml_getChildElementValue( p_service_element,
434                                                   "serviceType" );
435                     if ( !psz_service_type )
436                     {
437                         msg_Warn( p_sd, "No service type found." );
438                         continue;
439                     }
440
441                     int k = strlen( CONTENT_DIRECTORY_SERVICE_TYPE ) - 1;
442                     if ( strncmp( CONTENT_DIRECTORY_SERVICE_TYPE,
443                                 psz_service_type, k ) != 0 )
444                         continue;
445
446                     p_server->_i_content_directory_service_version =
447                         psz_service_type[k];
448
449                     const char* psz_event_sub_url =
450                         xml_getChildElementValue( p_service_element,
451                                                   "eventSubURL" );
452                     if ( !psz_event_sub_url )
453                     {
454                         msg_Warn( p_sd, "No event subscription url found." );
455                         continue;
456                     }
457
458                     const char* psz_control_url =
459                         xml_getChildElementValue( p_service_element,
460                                                   "controlURL" );
461                     if ( !psz_control_url )
462                     {
463                         msg_Warn( p_sd, "No control url found." );
464                         continue;
465                     }
466
467                     /* Try to subscribe to ContentDirectory service */
468
469                     char* psz_url = ( char* ) malloc( strlen( psz_base_url ) +
470                             strlen( psz_event_sub_url ) + 1 );
471                     if ( psz_url )
472                     {
473                         if ( UpnpResolveURL( psz_base_url, psz_event_sub_url, psz_url ) ==
474                                 UPNP_E_SUCCESS )
475                         {
476                             p_server->setContentDirectoryEventURL( psz_url );
477                             p_server->subscribeToContentDirectory();
478                         }
479
480                         free( psz_url );
481                     }
482
483                     /* Try to browse content directory. */
484
485                     psz_url = ( char* ) malloc( strlen( psz_base_url ) +
486                             strlen( psz_control_url ) + 1 );
487                     if ( psz_url )
488                     {
489                         if ( UpnpResolveURL( psz_base_url, psz_control_url, psz_url ) ==
490                                 UPNP_E_SUCCESS )
491                         {
492                             p_server->setContentDirectoryControlURL( psz_url );
493                             p_server->fetchContents();
494                         }
495
496                         free( psz_url );
497                     }
498                }
499                ixmlNodeList_free( p_service_list );
500            }
501        }
502        ixmlNodeList_free( p_device_list );
503     }
504 }
505
506 MediaServer::MediaServer( const char* psz_udn,
507                           const char* psz_friendly_name,
508                           services_discovery_t* p_sd )
509 {
510     _p_sd = p_sd;
511
512     _UDN = psz_udn;
513     _friendly_name = psz_friendly_name;
514
515     _p_contents = NULL;
516     _p_input_item = NULL;
517     _i_content_directory_service_version = 1;
518 }
519
520 MediaServer::~MediaServer()
521 {
522     delete _p_contents;
523 }
524
525 const char* MediaServer::getUDN() const
526 {
527     return _UDN.c_str();
528 }
529
530 const char* MediaServer::getFriendlyName() const
531 {
532     return _friendly_name.c_str();
533 }
534
535 void MediaServer::setContentDirectoryEventURL( const char* psz_url )
536 {
537     _content_directory_event_url = psz_url;
538 }
539
540 const char* MediaServer::getContentDirectoryEventURL() const
541 {
542     return _content_directory_event_url.c_str();
543 }
544
545 void MediaServer::setContentDirectoryControlURL( const char* psz_url )
546 {
547     _content_directory_control_url = psz_url;
548 }
549
550 const char* MediaServer::getContentDirectoryControlURL() const
551 {
552     return _content_directory_control_url.c_str();
553 }
554
555 /**
556  * Subscribes current client handle to Content Directory Service.
557  * CDS exports the server shares to clients.
558  */
559 void MediaServer::subscribeToContentDirectory()
560 {
561     const char* psz_url = getContentDirectoryEventURL();
562     if ( !psz_url )
563     {
564         msg_Dbg( _p_sd, "No subscription url set!" );
565         return;
566     }
567
568     int i_timeout = 1810;
569     Upnp_SID sid;
570
571     int i_res = UpnpSubscribe( _p_sd->p_sys->client_handle, psz_url, &i_timeout, sid );
572
573     if ( i_res == UPNP_E_SUCCESS )
574     {
575         _i_subscription_timeout = i_timeout;
576         memcpy( _subscription_id, sid, sizeof( Upnp_SID ) );
577     }
578     else
579     {
580         msg_Dbg( _p_sd, "Subscribe failed: '%s': %s",
581                 getFriendlyName(), UpnpGetErrorMessage( i_res ) );
582     }
583 }
584 /*
585  * Constructs UpnpAction to browse available content.
586  */
587 IXML_Document* MediaServer::_browseAction( const char* psz_object_id_,
588                                            const char* psz_browser_flag_,
589                                            const char* psz_filter_,
590                                            const char* psz_starting_index_,
591                                            const char* psz_requested_count_,
592                                            const char* psz_sort_criteria_ )
593 {
594     IXML_Document* p_action = 0;
595     IXML_Document* p_response = 0;
596     const char* psz_url = getContentDirectoryControlURL();
597
598     if ( !psz_url )
599     {
600         msg_Dbg( _p_sd, "No subscription url set!" );
601         return 0;
602     }
603
604     char* psz_service_type = strdup( CONTENT_DIRECTORY_SERVICE_TYPE );
605
606     psz_service_type[strlen( psz_service_type ) - 1] =
607         _i_content_directory_service_version;
608
609     int i_res;
610
611     i_res = UpnpAddToAction( &p_action, "Browse",
612             psz_service_type, "ObjectID", psz_object_id_ );
613
614     if ( i_res != UPNP_E_SUCCESS )
615     {
616         msg_Dbg( _p_sd, "AddToAction 'ObjectID' failed: %s",
617                 UpnpGetErrorMessage( i_res ) );
618         goto browseActionCleanup;
619     }
620
621     i_res = UpnpAddToAction( &p_action, "Browse",
622             psz_service_type, "BrowseFlag", psz_browser_flag_ );
623
624     if ( i_res != UPNP_E_SUCCESS )
625     {
626         msg_Dbg( _p_sd, "AddToAction 'BrowseFlag' failed: %s", 
627                 UpnpGetErrorMessage( i_res ) );
628         goto browseActionCleanup;
629     }
630
631     i_res = UpnpAddToAction( &p_action, "Browse",
632             psz_service_type, "Filter", psz_filter_ );
633
634     if ( i_res != UPNP_E_SUCCESS )
635     {
636         msg_Dbg( _p_sd, "AddToAction 'Filter' failed: %s",
637                 UpnpGetErrorMessage( i_res ) );
638         goto browseActionCleanup;
639     }
640
641     i_res = UpnpAddToAction( &p_action, "Browse",
642             psz_service_type, "StartingIndex", psz_starting_index_ );
643
644     if ( i_res != UPNP_E_SUCCESS )
645     {
646         msg_Dbg( _p_sd, "AddToAction 'StartingIndex' failed: %s",
647                 UpnpGetErrorMessage( i_res ) );
648         goto browseActionCleanup;
649     }
650
651     i_res = UpnpAddToAction( &p_action, "Browse",
652             psz_service_type, "RequestedCount", psz_requested_count_ );
653
654     if ( i_res != UPNP_E_SUCCESS )
655     {
656         msg_Dbg( _p_sd, "AddToAction 'RequestedCount' failed: %s",
657                 UpnpGetErrorMessage( i_res ) );
658         goto browseActionCleanup;
659     }
660
661     i_res = UpnpAddToAction( &p_action, "Browse",
662             psz_service_type, "SortCriteria", psz_sort_criteria_ );
663
664     if ( i_res != UPNP_E_SUCCESS )
665     {
666         msg_Dbg( _p_sd, "AddToAction 'SortCriteria' failed: %s",
667                 UpnpGetErrorMessage( i_res ) );
668         goto browseActionCleanup;
669     }
670
671     i_res = UpnpSendAction( _p_sd->p_sys->client_handle,
672               psz_url,
673               psz_service_type,
674               0, /* ignored in SDK, must be NULL */
675               p_action,
676               &p_response );
677
678     if ( i_res != UPNP_E_SUCCESS )
679     {
680         msg_Err( _p_sd, "%s when trying the send() action with URL: %s",
681                 UpnpGetErrorMessage( i_res ), psz_url );
682
683         ixmlDocument_free( p_response );
684         p_response = 0;
685     }
686
687 browseActionCleanup:
688
689     free( psz_service_type );
690
691     ixmlDocument_free( p_action );
692     return p_response;
693 }
694
695 void MediaServer::fetchContents()
696 {
697     /* Delete previous contents to prevent duplicate entries */
698     if ( _p_contents )
699     {
700         delete _p_contents;
701         services_discovery_RemoveItem( _p_sd, _p_input_item );
702         services_discovery_AddItem( _p_sd, _p_input_item, NULL );
703     }
704
705     Container* root = new Container( 0, "0", getFriendlyName() );
706
707     _fetchContents( root );
708
709     _p_contents = root;
710     _p_contents->setInputItem( _p_input_item );
711
712     _buildPlaylist( _p_contents, NULL );
713 }
714
715 /*
716  * Fetches and parses the UPNP response
717  */
718 bool MediaServer::_fetchContents( Container* p_parent )
719 {
720     if (!p_parent)
721     {
722         msg_Err( _p_sd, "No parent" );
723         return false;
724     }
725
726     IXML_Document* p_response = _browseAction( p_parent->getObjectID(),
727                                       "BrowseDirectChildren",
728                                       "*", "0", "0", "" );
729     if ( !p_response )
730     {
731         msg_Err( _p_sd, "No response from browse() action" );
732         return false;
733     }
734
735     IXML_Document* p_result = parseBrowseResult( p_response );
736     ixmlDocument_free( p_response );
737
738     if ( !p_result )
739     {
740         msg_Err( _p_sd, "browse() response parsing failed" );
741         return false;
742     }
743 #ifndef NDEBUG
744     else
745     {
746         msg_Dbg( _p_sd, "Got DIDL document: %s",
747                 ixmlPrintDocument( p_result ) );
748     }
749 #endif
750
751     IXML_NodeList* containerNodeList =
752                 ixmlDocument_getElementsByTagName( p_result, "container" );
753
754     if ( containerNodeList )
755     {
756         for ( unsigned int i = 0;
757                 i < ixmlNodeList_length( containerNodeList ); i++ )
758         {
759             IXML_Element* containerElement =
760                   ( IXML_Element* )ixmlNodeList_item( containerNodeList, i );
761
762             const char* objectID = ixmlElement_getAttribute( containerElement,
763                                                              "id" );
764             if ( !objectID )
765                 continue;
766
767             const char* title = xml_getChildElementValue( containerElement,
768                                                           "dc:title" );
769
770             if ( !title )
771                 continue;
772
773             Container* container = new Container( p_parent, objectID, title );
774             p_parent->addContainer( container );
775             _fetchContents( container );
776         }
777         ixmlNodeList_free( containerNodeList );
778     }
779
780     IXML_NodeList* itemNodeList = ixmlDocument_getElementsByTagName( p_result,
781                                                                      "item" );
782     if ( itemNodeList )
783     {
784         for ( unsigned int i = 0; i < ixmlNodeList_length( itemNodeList ); i++ )
785         {
786             IXML_Element* itemElement =
787                         ( IXML_Element* )ixmlNodeList_item( itemNodeList, i );
788
789             const char* objectID =
790                         ixmlElement_getAttribute( itemElement, "id" );
791
792             if ( !objectID )
793                 continue;
794
795             const char* title =
796                         xml_getChildElementValue( itemElement, "dc:title" );
797
798             if ( !title )
799                 continue;
800
801             const char* resource =
802                         xml_getChildElementValue( itemElement, "res" );
803
804             if ( !resource )
805                 continue;
806
807             const char* psz_duration = xml_getChildElementAttributeValue( itemElement,
808                                                                     "res",
809                                                                     "duration" );
810
811             mtime_t i_duration = -1;
812             int i_hours, i_minutes, i_seconds, i_decis;
813
814             if ( psz_duration )
815             {
816                 if( sscanf( psz_duration, "%02d:%02d:%02d.%d",
817                         &i_hours, &i_minutes, &i_seconds, &i_decis ))
818                     i_duration = INT64_C(1000000) * ( i_hours*3600 +
819                                                       i_minutes*60 +
820                                                       i_seconds ) +
821                                  INT64_C(100000) * i_decis;
822             }
823
824             Item* item = new Item( p_parent, objectID, title, resource, i_duration );
825             p_parent->addItem( item );
826         }
827         ixmlNodeList_free( itemNodeList );
828     }
829
830     ixmlDocument_free( p_result );
831     return true;
832 }
833
834 // TODO: Create a permanent fix for the item duplication bug. The current fix
835 // is essentially only a small hack. Although it fixes the problem, it introduces
836 // annoying cosmetic issues with the playlist. For example, when the UPnP Server
837 // rebroadcasts it's directory structure, the VLC Client deletes the old directory
838 // structure, causing the user to go back to the root node of the directory. The
839 // directory is then rebuilt, and the user is forced to traverse through the directory
840 // to find the item they were looking for. Some servers may not push the directory
841 // structure too often, but we cannot rely on this fix.
842 //
843 // I have thought up another fix, but this would require certain features to
844 // be present within the VLC services discovery. Currently, services_discovery_AddItem
845 // does not allow the programmer to nest items. It only allows a "2 deep" scope.
846 // An example of the limitation is below:
847 //
848 // Root Directory
849 // + Item 1
850 // + Item 2
851 //
852 // services_discovery_AddItem will not let the programmer specify a child-node to
853 // insert items into, so we would not be able to do the following:
854 //
855 // Root Directory
856 // + Item 1
857 //   + Sub Item 1
858 // + Item 2
859 //   + Sub Item 1 of Item 2
860 //     + Sub-Sub Item 1 of Sub Item 1
861 //
862 // This creates a HUGE limitation on what we are able to do. If we were able to do
863 // the above, we could simply preserve the old directory listing, and compare what items
864 // do not exist in the new directory listing, then remove them from the shown listing using
865 // services_discovery_RemoveItem. If new files were introduced within an already existing
866 // container, we could simply do so with services_discovery_AddItem.
867
868 /*
869  * Builds playlist based on available input items.
870  */
871 void MediaServer::_buildPlaylist( Container* p_parent, input_item_node_t *p_input_node )
872 {
873     bool b_send = p_input_node == NULL;
874     if( b_send )
875         p_input_node = input_item_node_Create( p_parent->getInputItem() );
876
877     for ( unsigned int i = 0; i < p_parent->getNumContainers(); i++ )
878     {
879         Container* p_container = p_parent->getContainer( i );
880
881         input_item_t* p_input_item = input_item_New( "vlc://nop",
882                                                     p_container->getTitle() );
883         input_item_node_t *p_new_node =
884             input_item_node_AppendItem( p_input_node, p_input_item );
885
886         p_container->setInputItem( p_input_item );
887         _buildPlaylist( p_container, p_new_node );
888     }
889
890     for ( unsigned int i = 0; i < p_parent->getNumItems(); i++ )
891     {
892         Item* p_item = p_parent->getItem( i );
893
894         input_item_t* p_input_item = input_item_NewExt( p_item->getResource(),
895                                                p_item->getTitle(),
896                                                0,
897                                                NULL,
898                                                0,
899                                                p_item->getDuration() );
900
901         assert( p_input_item );
902         input_item_node_AppendItem( p_input_node, p_input_item );
903         p_item->setInputItem( p_input_item );
904     }
905
906     if( b_send )
907         input_item_node_PostAndDelete( p_input_node );
908 }
909
910 void MediaServer::setInputItem( input_item_t* p_input_item )
911 {
912     if( _p_input_item == p_input_item )
913         return;
914
915     if( _p_input_item )
916         vlc_gc_decref( _p_input_item );
917
918     vlc_gc_incref( p_input_item );
919     _p_input_item = p_input_item;
920 }
921
922 input_item_t* MediaServer::getInputItem() const
923 {
924     return _p_input_item;
925 }
926
927 bool MediaServer::compareSID( const char* psz_sid )
928 {
929     return ( strncmp( _subscription_id, psz_sid, sizeof( Upnp_SID ) ) == 0 );
930 }
931
932
933 /*
934  * MediaServerList class
935  */
936 MediaServerList::MediaServerList( services_discovery_t* p_sd )
937 {
938     _p_sd = p_sd;
939 }
940
941 MediaServerList::~MediaServerList()
942 {
943     for ( unsigned int i = 0; i < _list.size(); i++ )
944     {
945         delete _list[i];
946     }
947 }
948
949 bool MediaServerList::addServer( MediaServer* p_server )
950 {
951     input_item_t* p_input_item = NULL;
952     if ( getServer( p_server->getUDN() ) != 0 ) return false;
953
954     msg_Dbg( _p_sd, "Adding server '%s' with uuid '%s'", p_server->getFriendlyName(), p_server->getUDN() );
955
956     p_input_item = input_item_New( "vlc://nop", p_server->getFriendlyName() );
957
958     input_item_SetDescription( p_input_item, p_server->getUDN() );
959
960     p_server->setInputItem( p_input_item );
961
962     services_discovery_AddItem( _p_sd, p_input_item, NULL );
963
964     _list.push_back( p_server );
965
966     return true;
967 }
968
969 MediaServer* MediaServerList::getServer( const char* psz_udn )
970 {
971     MediaServer* p_result = 0;
972
973     for ( unsigned int i = 0; i < _list.size(); i++ )
974     {
975         if( strcmp( psz_udn, _list[i]->getUDN() ) == 0 )
976         {
977             p_result = _list[i];
978             break;
979         }
980     }
981
982     return p_result;
983 }
984
985 MediaServer* MediaServerList::getServerBySID( const char* psz_sid )
986 {
987     MediaServer* p_server = 0;
988
989     for ( unsigned int i = 0; i < _list.size(); i++ )
990     {
991         if ( _list[i]->compareSID( psz_sid ) )
992         {
993             p_server = _list[i];
994             break;
995         }
996     }
997
998     return p_server;
999 }
1000
1001 void MediaServerList::removeServer( const char* psz_udn )
1002 {
1003     MediaServer* p_server = getServer( psz_udn );
1004     if ( !p_server ) return;
1005
1006     msg_Dbg( _p_sd, "Removing server '%s'", p_server->getFriendlyName() );
1007
1008     services_discovery_RemoveItem( _p_sd, p_server->getInputItem() );
1009
1010     std::vector<MediaServer*>::iterator it;
1011     for ( it = _list.begin(); it != _list.end(); ++it )
1012     {
1013         if ( *it == p_server )
1014         {
1015             _list.erase( it );
1016             delete p_server;
1017             break;
1018         }
1019     }
1020 }
1021
1022
1023 /*
1024  * Item class
1025  */
1026 Item::Item( Container* p_parent, const char* psz_object_id, const char* psz_title,
1027            const char* psz_resource, mtime_t i_duration )
1028 {
1029     _parent = p_parent;
1030
1031     _objectID = psz_object_id;
1032     _title = psz_title;
1033     _resource = psz_resource;
1034     _duration = i_duration;
1035
1036     _p_input_item = NULL;
1037 }
1038
1039 Item::~Item()
1040 {
1041     if( _p_input_item )
1042         vlc_gc_decref( _p_input_item );
1043 }
1044
1045 const char* Item::getObjectID() const
1046 {
1047     return _objectID.c_str();
1048 }
1049
1050 const char* Item::getTitle() const
1051 {
1052     return _title.c_str();
1053 }
1054
1055 const char* Item::getResource() const
1056 {
1057     return _resource.c_str();
1058 }
1059
1060 mtime_t Item::getDuration() const
1061 {
1062     return _duration;
1063 }
1064
1065 void Item::setInputItem( input_item_t* p_input_item )
1066 {
1067     if( _p_input_item == p_input_item )
1068         return;
1069
1070     if( _p_input_item )
1071         vlc_gc_decref( _p_input_item );
1072
1073     vlc_gc_incref( p_input_item );
1074     _p_input_item = p_input_item;
1075 }
1076
1077 /*
1078  * Container class
1079  */
1080 Container::Container( Container*  p_parent,
1081                       const char* psz_object_id,
1082                       const char* psz_title )
1083 {
1084     _parent = p_parent;
1085
1086     _objectID = psz_object_id;
1087     _title = psz_title;
1088
1089     _p_input_item = NULL;
1090 }
1091
1092 Container::~Container()
1093 {
1094     for ( unsigned int i = 0; i < _containers.size(); i++ )
1095     {
1096         delete _containers[i];
1097     }
1098
1099     for ( unsigned int i = 0; i < _items.size(); i++ )
1100     {
1101         delete _items[i];
1102     }
1103
1104     if( _p_input_item )
1105         vlc_gc_decref( _p_input_item );
1106 }
1107
1108 void Container::addItem( Item* item )
1109 {
1110     _items.push_back( item );
1111 }
1112
1113 void Container::addContainer( Container* p_container )
1114 {
1115     _containers.push_back( p_container );
1116 }
1117
1118 const char* Container::getObjectID() const
1119 {
1120     return _objectID.c_str();
1121 }
1122
1123 const char* Container::getTitle() const
1124 {
1125     return _title.c_str();
1126 }
1127
1128 unsigned int Container::getNumItems() const
1129 {
1130     return _items.size();
1131 }
1132
1133 unsigned int Container::getNumContainers() const
1134 {
1135     return _containers.size();
1136 }
1137
1138 Item* Container::getItem( unsigned int i_index ) const
1139 {
1140     if ( i_index < _items.size() ) return _items[i_index];
1141     return 0;
1142 }
1143
1144 Container* Container::getContainer( unsigned int i_index ) const
1145 {
1146     if ( i_index < _containers.size() ) return _containers[i_index];
1147     return 0;
1148 }
1149
1150 Container* Container::getParent()
1151 {
1152     return _parent;
1153 }
1154
1155 void Container::setInputItem( input_item_t* p_input_item )
1156 {
1157     if( _p_input_item == p_input_item )
1158         return;
1159
1160     if( _p_input_item )
1161         vlc_gc_decref( _p_input_item );
1162
1163     vlc_gc_incref( p_input_item );
1164     _p_input_item = p_input_item;
1165 }
1166
1167 input_item_t* Container::getInputItem() const
1168 {
1169     return _p_input_item;
1170 }