]> git.sesse.net Git - vlc/blob - modules/services_discovery/upnp.cpp
macosx: correctly toggle enabled state of record menu item
[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 "services_discovery/upnp.hpp"
36
37 #include <vlc_plugin.h>
38 #include <vlc_services_discovery.h>
39
40 #include <assert.h>
41 #include <limits.h>
42
43 /*
44  * Constants
45 */
46 const char* MEDIA_SERVER_DEVICE_TYPE = "urn:schemas-upnp-org:device:MediaServer:1";
47 const char* CONTENT_DIRECTORY_SERVICE_TYPE = "urn:schemas-upnp-org:service:ContentDirectory:1";
48
49 /*
50  * VLC handle
51  */
52 struct services_discovery_sys_t
53 {
54     UpnpClient_Handle client_handle;
55     MediaServerList* p_server_list;
56     vlc_mutex_t callback_lock;
57 };
58
59 /*
60  * VLC callback prototypes
61  */
62 static int Open( vlc_object_t* );
63 static void Close( vlc_object_t* );
64 VLC_SD_PROBE_HELPER( "upnp", "Universal Plug'n'Play", SD_CAT_LAN )
65
66 /*
67  * Module descriptor
68  */
69 vlc_module_begin();
70     set_shortname( "UPnP" );
71     set_description( N_( "Universal Plug'n'Play" ) );
72     set_category( CAT_PLAYLIST );
73     set_subcategory( SUBCAT_PLAYLIST_SD );
74     set_capability( "services_discovery", 0 );
75     set_callbacks( Open, Close );
76
77     VLC_SD_PROBE_SUBMODULE
78 vlc_module_end();
79
80 /*
81  * Local prototypes
82  */
83 static int Callback( Upnp_EventType event_type, void* p_event, void* p_user_data );
84
85 const char* xml_getChildElementValue( IXML_Element* p_parent,
86                                       const char*   psz_tag_name );
87
88 const char* xml_getChildElementValue( IXML_Document* p_doc,
89                                       const char*    psz_tag_name );
90
91 const char* xml_getChildElementAttributeValue( IXML_Element* p_parent,
92                                         const char* psz_tag_name,
93                                         const char* psz_attribute );
94
95 int xml_getNumber( IXML_Document* p_doc,
96                    const char*    psz_tag_name );
97
98 IXML_Document* parseBrowseResult( IXML_Document* p_doc );
99
100 /*
101  * Initializes UPNP instance.
102  */
103 static int Open( vlc_object_t *p_this )
104 {
105     int i_res;
106     services_discovery_t *p_sd = ( services_discovery_t* )p_this;
107     services_discovery_sys_t *p_sys  = ( services_discovery_sys_t * )
108             calloc( 1, sizeof( services_discovery_sys_t ) );
109
110     if( !( p_sd->p_sys = p_sys ) )
111         return VLC_ENOMEM;
112
113 #ifdef UPNP_ENABLE_IPV6
114     char* psz_miface;
115     psz_miface = var_InheritString( p_sd, "miface" );
116     msg_Info( p_sd, "Initializing libupnp on '%s' interface", psz_miface );
117     i_res = UpnpInit2( psz_miface, 0 );
118     free( psz_miface );
119 #else
120     /* If UpnpInit2 isnt available, initialize on first IPv4-capable interface */
121     i_res = UpnpInit( 0, 0 );
122 #endif
123     if( i_res != UPNP_E_SUCCESS )
124     {
125         msg_Err( p_sd, "Initialization failed: %s", UpnpGetErrorMessage( i_res ) );
126         free( p_sys );
127         return VLC_EGENERIC;
128     }
129
130     ixmlRelaxParser( 1 );
131
132     p_sys->p_server_list = new MediaServerList( p_sd );
133     vlc_mutex_init( &p_sys->callback_lock );
134
135     /* Register a control point */
136     i_res = UpnpRegisterClient( Callback, p_sd, &p_sys->client_handle );
137     if( i_res != UPNP_E_SUCCESS )
138     {
139         msg_Err( p_sd, "Client registration failed: %s", UpnpGetErrorMessage( i_res ) );
140         Close( (vlc_object_t*) p_sd );
141         return VLC_EGENERIC;
142     }
143
144     /* Search for media servers */
145     i_res = UpnpSearchAsync( p_sys->client_handle, 5,
146             MEDIA_SERVER_DEVICE_TYPE, p_sd );
147     if( i_res != UPNP_E_SUCCESS )
148     {
149         msg_Err( p_sd, "Error sending search request: %s", UpnpGetErrorMessage( i_res ) );
150         Close( (vlc_object_t*) p_sd );
151         return VLC_EGENERIC;
152     }
153
154     /* libupnp does not treat a maximum content length of 0 as unlimited
155      * until 64dedf (~ pupnp v1.6.7) and provides no sane way to discriminate
156      * between versions */
157     if( (i_res = UpnpSetMaxContentLength( INT_MAX )) != UPNP_E_SUCCESS )
158     {
159         msg_Err( p_sd, "Failed to set maximum content length: %s",
160                 UpnpGetErrorMessage( i_res ));
161
162         Close( (vlc_object_t*) p_sd );
163         return VLC_EGENERIC;
164     }
165
166     return VLC_SUCCESS;
167 }
168
169 /*
170  * Releases resources.
171  */
172 static void Close( vlc_object_t *p_this )
173 {
174     services_discovery_t *p_sd = ( services_discovery_t* )p_this;
175
176     UpnpUnRegisterClient( p_sd->p_sys->client_handle );
177     UpnpFinish();
178
179     delete p_sd->p_sys->p_server_list;
180     vlc_mutex_destroy( &p_sd->p_sys->callback_lock );
181
182     free( p_sd->p_sys );
183 }
184
185 /* XML utility functions */
186
187 /*
188  * Returns the value of a child element, or NULL on error
189  */
190 const char* xml_getChildElementValue( IXML_Element* p_parent,
191                                       const char*   psz_tag_name )
192 {
193     assert( p_parent );
194     assert( psz_tag_name );
195
196     IXML_NodeList* p_node_list;
197     p_node_list = ixmlElement_getElementsByTagName( p_parent, psz_tag_name );
198     if ( !p_node_list ) return NULL;
199
200     IXML_Node* p_element = ixmlNodeList_item( p_node_list, 0 );
201     ixmlNodeList_free( p_node_list );
202     if ( !p_element )   return NULL;
203
204     IXML_Node* p_text_node = ixmlNode_getFirstChild( p_element );
205     if ( !p_text_node ) return NULL;
206
207     return ixmlNode_getNodeValue( p_text_node );
208 }
209
210 /*
211  * Returns the value of a child element's attribute, or NULL on error
212  */
213 const char* xml_getChildElementAttributeValue( IXML_Element* p_parent,
214                                         const char* psz_tag_name,
215                                         const char* psz_attribute )
216 {
217     assert( p_parent );
218     assert( psz_tag_name );
219     assert( psz_attribute );
220
221     IXML_NodeList* p_node_list;
222     p_node_list = ixmlElement_getElementsByTagName( p_parent, psz_tag_name );
223     if ( !p_node_list )   return NULL;
224
225     IXML_Node* p_element = ixmlNodeList_item( p_node_list, 0 );
226     ixmlNodeList_free( p_node_list );
227     if ( !p_element )     return NULL;
228
229     return ixmlElement_getAttribute( (IXML_Element*) p_element, psz_attribute );
230 }
231
232 /*
233  * Returns the value of a child element, or NULL on error
234  */
235 const char* xml_getChildElementValue( IXML_Document*  p_doc,
236                                       const char*     psz_tag_name )
237 {
238     assert( p_doc );
239     assert( psz_tag_name );
240
241     IXML_NodeList* p_node_list;
242     p_node_list = ixmlDocument_getElementsByTagName( p_doc, psz_tag_name );
243     if ( !p_node_list )  return NULL;
244
245     IXML_Node* p_element = ixmlNodeList_item( p_node_list, 0 );
246     ixmlNodeList_free( p_node_list );
247     if ( !p_element )    return NULL;
248
249     IXML_Node* p_text_node = ixmlNode_getFirstChild( p_element );
250     if ( !p_text_node )  return NULL;
251
252     return ixmlNode_getNodeValue( p_text_node );
253 }
254
255 /*
256  * Extracts the result document from a SOAP response
257  */
258 IXML_Document* parseBrowseResult( IXML_Document* p_doc )
259 {
260     assert( p_doc );
261
262     /* Missing namespaces confuse the ixml parser. This is a very ugly
263      * hack but it is needeed until devices start sending valid XML.
264      *
265      * It works that way:
266      *
267      * The DIDL document is extracted from the Result tag, then wrapped into
268      * a valid XML header and a new root tag which contains missing namespace
269      * definitions so the ixml parser understands it.
270      *
271      * If you know of a better workaround, please oh please fix it */
272     const char* psz_xml_result_fmt = "<?xml version=\"1.0\" ?>"
273         "<Result xmlns:sec=\"urn:samsung:metadata:2009\">%s</Result>";
274
275     char* psz_xml_result_string = NULL;
276     const char* psz_raw_didl = xml_getChildElementValue( p_doc, "Result" );
277
278     if( !psz_raw_didl )
279         return NULL;
280
281     if( -1 == asprintf( &psz_xml_result_string,
282                          psz_xml_result_fmt,
283                          psz_raw_didl) )
284         return NULL;
285
286
287     IXML_Document* p_result_doc = ixmlParseBuffer( psz_xml_result_string );
288     free( psz_xml_result_string );
289
290     if( !p_result_doc )
291         return NULL;
292
293     IXML_NodeList *p_elems = ixmlDocument_getElementsByTagName( p_result_doc,
294                                                                 "DIDL-Lite" );
295
296     IXML_Node *p_node = ixmlNodeList_item( p_elems, 0 );
297     ixmlNodeList_free( p_elems );
298
299     return (IXML_Document*)p_node;
300 }
301
302 /*
303  * Get the number value from a SOAP response
304  */
305 int xml_getNumber( IXML_Document* p_doc,
306                    const char* psz_tag_name )
307 {
308     assert( p_doc );
309     assert( psz_tag_name );
310
311     const char* psz = xml_getChildElementValue( p_doc, psz_tag_name );
312
313     if( !psz )
314         return 0;
315
316     char *psz_end;
317     long l = strtol( psz, &psz_end, 10 );
318
319     if( *psz_end || l < 0 || l > INT_MAX )
320         return 0;
321
322     return (int)l;
323 }
324
325 /*
326  * Handles all UPnP events
327  */
328 static int Callback( Upnp_EventType event_type, void* p_event, void* p_user_data )
329 {
330     services_discovery_t* p_sd = ( services_discovery_t* ) p_user_data;
331     services_discovery_sys_t* p_sys = p_sd->p_sys;
332     vlc_mutex_locker locker( &p_sys->callback_lock );
333
334     switch( event_type )
335     {
336     case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
337     case UPNP_DISCOVERY_SEARCH_RESULT:
338     {
339         struct Upnp_Discovery* p_discovery = ( struct Upnp_Discovery* )p_event;
340
341         IXML_Document *p_description_doc = 0;
342
343         int i_res;
344         i_res = UpnpDownloadXmlDoc( p_discovery->Location, &p_description_doc );
345         if ( i_res != UPNP_E_SUCCESS )
346         {
347             msg_Warn( p_sd, "Could not download device description! "
348                             "Fetching data from %s failed: %s",
349                             p_discovery->Location, UpnpGetErrorMessage( i_res ) );
350             return i_res;
351         }
352
353         MediaServer::parseDeviceDescription( p_description_doc,
354                 p_discovery->Location, p_sd );
355
356         ixmlDocument_free( p_description_doc );
357     }
358     break;
359
360     case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE:
361     {
362         struct Upnp_Discovery* p_discovery = ( struct Upnp_Discovery* )p_event;
363
364         p_sys->p_server_list->removeServer( p_discovery->DeviceId );
365
366     }
367     break;
368
369     case UPNP_EVENT_RECEIVED:
370     {
371         Upnp_Event* p_e = ( Upnp_Event* )p_event;
372
373         MediaServer* p_server = p_sys->p_server_list->getServerBySID( p_e->Sid );
374         if ( p_server ) p_server->fetchContents();
375     }
376     break;
377
378     case UPNP_EVENT_AUTORENEWAL_FAILED:
379     case UPNP_EVENT_SUBSCRIPTION_EXPIRED:
380     {
381         /* Re-subscribe. */
382
383         Upnp_Event_Subscribe* p_s = ( Upnp_Event_Subscribe* )p_event;
384
385         MediaServer* p_server = p_sys->p_server_list->getServerBySID( p_s->Sid );
386         if ( p_server ) p_server->subscribeToContentDirectory();
387     }
388     break;
389
390     case UPNP_EVENT_SUBSCRIBE_COMPLETE:
391         msg_Warn( p_sd, "subscription complete" );
392         break;
393
394     case UPNP_DISCOVERY_SEARCH_TIMEOUT:
395         msg_Warn( p_sd, "search timeout" );
396         break;
397
398     default:
399         msg_Err( p_sd, "Unhandled event, please report ( type=%d )", event_type );
400         break;
401     }
402
403     return UPNP_E_SUCCESS;
404 }
405
406
407 /*
408  * Local class implementations.
409  */
410
411 /*
412  * MediaServer
413  */
414
415 void MediaServer::parseDeviceDescription( IXML_Document* p_doc,
416                                           const char*    p_location,
417                                           services_discovery_t* p_sd )
418 {
419     if ( !p_doc )
420     {
421         msg_Err( p_sd, "Null IXML_Document" );
422         return;
423     }
424
425     if ( !p_location )
426     {
427         msg_Err( p_sd, "Null location" );
428         return;
429     }
430
431     const char* psz_base_url = p_location;
432
433     /* Try to extract baseURL */
434     IXML_NodeList* p_url_list = ixmlDocument_getElementsByTagName( p_doc, "URLBase" );
435     if ( p_url_list )
436     {
437
438         if ( IXML_Node* p_url_node = ixmlNodeList_item( p_url_list, 0 ) )
439         {
440             IXML_Node* p_text_node = ixmlNode_getFirstChild( p_url_node );
441             if ( p_text_node ) psz_base_url = ixmlNode_getNodeValue( p_text_node );
442         }
443
444         ixmlNodeList_free( p_url_list );
445     }
446
447     /* Get devices */
448     IXML_NodeList* p_device_list =
449                 ixmlDocument_getElementsByTagName( p_doc, "device" );
450
451     if ( p_device_list )
452     {
453         for ( unsigned int i = 0; i < ixmlNodeList_length( p_device_list ); i++ )
454         {
455             IXML_Element* p_device_element =
456                    ( IXML_Element* ) ixmlNodeList_item( p_device_list, i );
457
458             if( !p_device_element )
459                 continue;
460
461             const char* psz_device_type =
462                 xml_getChildElementValue( p_device_element, "deviceType" );
463
464             if ( !psz_device_type )
465             {
466                 msg_Warn( p_sd, "No deviceType found!" );
467                 continue;
468             }
469
470             if ( strncmp( MEDIA_SERVER_DEVICE_TYPE, psz_device_type,
471                     strlen( MEDIA_SERVER_DEVICE_TYPE ) - 1 ) != 0 )
472                 continue;
473
474             const char* psz_udn = xml_getChildElementValue( p_device_element,
475                                                             "UDN" );
476             if ( !psz_udn )
477             {
478                 msg_Warn( p_sd, "No UDN!" );
479                 continue;
480             }
481
482             /* Check if server is already added */
483             if ( p_sd->p_sys->p_server_list->getServer( psz_udn ) != 0 )
484             {
485                 msg_Warn( p_sd, "Server with uuid '%s' already exists.", psz_udn );
486                 continue;
487             }
488
489             const char* psz_friendly_name =
490                        xml_getChildElementValue( p_device_element,
491                                                  "friendlyName" );
492
493             if ( !psz_friendly_name )
494             {
495                 msg_Dbg( p_sd, "No friendlyName!" );
496                 continue;
497             }
498
499             MediaServer* p_server = new MediaServer( psz_udn,
500                     psz_friendly_name, p_sd );
501
502             if ( !p_sd->p_sys->p_server_list->addServer( p_server ) )
503             {
504                 delete p_server;
505                 p_server = 0;
506                 continue;
507             }
508
509             /* Check for ContentDirectory service. */
510             IXML_NodeList* p_service_list =
511                        ixmlElement_getElementsByTagName( p_device_element,
512                                                          "service" );
513             if ( p_service_list )
514             {
515                 for ( unsigned int j = 0;
516                       j < ixmlNodeList_length( p_service_list ); j++ )
517                 {
518                     IXML_Element* p_service_element =
519                        ( IXML_Element* ) ixmlNodeList_item( p_service_list, j );
520
521                     const char* psz_service_type =
522                         xml_getChildElementValue( p_service_element,
523                                                   "serviceType" );
524                     if ( !psz_service_type )
525                     {
526                         msg_Warn( p_sd, "No service type found." );
527                         continue;
528                     }
529
530                     int k = strlen( CONTENT_DIRECTORY_SERVICE_TYPE ) - 1;
531                     if ( strncmp( CONTENT_DIRECTORY_SERVICE_TYPE,
532                                 psz_service_type, k ) != 0 )
533                         continue;
534
535                     p_server->_i_content_directory_service_version =
536                         psz_service_type[k];
537
538                     const char* psz_event_sub_url =
539                         xml_getChildElementValue( p_service_element,
540                                                   "eventSubURL" );
541                     if ( !psz_event_sub_url )
542                     {
543                         msg_Warn( p_sd, "No event subscription url found." );
544                         continue;
545                     }
546
547                     const char* psz_control_url =
548                         xml_getChildElementValue( p_service_element,
549                                                   "controlURL" );
550                     if ( !psz_control_url )
551                     {
552                         msg_Warn( p_sd, "No control url found." );
553                         continue;
554                     }
555
556                     /* Try to subscribe to ContentDirectory service */
557
558                     char* psz_url = ( char* ) malloc( strlen( psz_base_url ) +
559                             strlen( psz_event_sub_url ) + 1 );
560                     if ( psz_url )
561                     {
562                         if ( UpnpResolveURL( psz_base_url, psz_event_sub_url, psz_url ) ==
563                                 UPNP_E_SUCCESS )
564                         {
565                             p_server->setContentDirectoryEventURL( psz_url );
566                             p_server->subscribeToContentDirectory();
567                         }
568
569                         free( psz_url );
570                     }
571
572                     /* Try to browse content directory. */
573
574                     psz_url = ( char* ) malloc( strlen( psz_base_url ) +
575                             strlen( psz_control_url ) + 1 );
576                     if ( psz_url )
577                     {
578                         if ( UpnpResolveURL( psz_base_url, psz_control_url, psz_url ) ==
579                                 UPNP_E_SUCCESS )
580                         {
581                             p_server->setContentDirectoryControlURL( psz_url );
582                             p_server->fetchContents();
583                         }
584
585                         free( psz_url );
586                     }
587                }
588                ixmlNodeList_free( p_service_list );
589            }
590        }
591        ixmlNodeList_free( p_device_list );
592     }
593 }
594
595 MediaServer::MediaServer( const char* psz_udn,
596                           const char* psz_friendly_name,
597                           services_discovery_t* p_sd )
598 {
599     _p_sd = p_sd;
600
601     _UDN = psz_udn;
602     _friendly_name = psz_friendly_name;
603
604     _p_contents = NULL;
605     _p_input_item = NULL;
606     _i_content_directory_service_version = 1;
607 }
608
609 MediaServer::~MediaServer()
610 {
611     delete _p_contents;
612 }
613
614 const char* MediaServer::getUDN() const
615 {
616     return _UDN.c_str();
617 }
618
619 const char* MediaServer::getFriendlyName() const
620 {
621     return _friendly_name.c_str();
622 }
623
624 void MediaServer::setContentDirectoryEventURL( const char* psz_url )
625 {
626     _content_directory_event_url = psz_url;
627 }
628
629 const char* MediaServer::getContentDirectoryEventURL() const
630 {
631     return _content_directory_event_url.c_str();
632 }
633
634 void MediaServer::setContentDirectoryControlURL( const char* psz_url )
635 {
636     _content_directory_control_url = psz_url;
637 }
638
639 const char* MediaServer::getContentDirectoryControlURL() const
640 {
641     return _content_directory_control_url.c_str();
642 }
643
644 /**
645  * Subscribes current client handle to Content Directory Service.
646  * CDS exports the server shares to clients.
647  */
648 void MediaServer::subscribeToContentDirectory()
649 {
650     const char* psz_url = getContentDirectoryEventURL();
651     if ( !psz_url )
652     {
653         msg_Dbg( _p_sd, "No subscription url set!" );
654         return;
655     }
656
657     int i_timeout = 1810;
658     Upnp_SID sid;
659
660     int i_res = UpnpSubscribe( _p_sd->p_sys->client_handle, psz_url, &i_timeout, sid );
661
662     if ( i_res == UPNP_E_SUCCESS )
663     {
664         _i_subscription_timeout = i_timeout;
665         memcpy( _subscription_id, sid, sizeof( Upnp_SID ) );
666     }
667     else
668     {
669         msg_Dbg( _p_sd, "Subscribe failed: '%s': %s",
670                 getFriendlyName(), UpnpGetErrorMessage( i_res ) );
671     }
672 }
673 /*
674  * Constructs UpnpAction to browse available content.
675  */
676 IXML_Document* MediaServer::_browseAction( const char* psz_object_id_,
677                                            const char* psz_browser_flag_,
678                                            const char* psz_filter_,
679                                            const char* psz_starting_index_,
680                                            const char* psz_requested_count_,
681                                            const char* psz_sort_criteria_ )
682 {
683     IXML_Document* p_action = 0;
684     IXML_Document* p_response = 0;
685     const char* psz_url = getContentDirectoryControlURL();
686
687     if ( !psz_url )
688     {
689         msg_Dbg( _p_sd, "No subscription url set!" );
690         return 0;
691     }
692
693     char* psz_service_type = strdup( CONTENT_DIRECTORY_SERVICE_TYPE );
694
695     psz_service_type[strlen( psz_service_type ) - 1] =
696         _i_content_directory_service_version;
697
698     int i_res;
699
700     i_res = UpnpAddToAction( &p_action, "Browse",
701             psz_service_type, "ObjectID", psz_object_id_ );
702
703     if ( i_res != UPNP_E_SUCCESS )
704     {
705         msg_Dbg( _p_sd, "AddToAction 'ObjectID' failed: %s",
706                 UpnpGetErrorMessage( i_res ) );
707         goto browseActionCleanup;
708     }
709
710     i_res = UpnpAddToAction( &p_action, "Browse",
711             psz_service_type, "BrowseFlag", psz_browser_flag_ );
712
713     if ( i_res != UPNP_E_SUCCESS )
714     {
715         msg_Dbg( _p_sd, "AddToAction 'BrowseFlag' failed: %s", 
716                 UpnpGetErrorMessage( i_res ) );
717         goto browseActionCleanup;
718     }
719
720     i_res = UpnpAddToAction( &p_action, "Browse",
721             psz_service_type, "Filter", psz_filter_ );
722
723     if ( i_res != UPNP_E_SUCCESS )
724     {
725         msg_Dbg( _p_sd, "AddToAction 'Filter' failed: %s",
726                 UpnpGetErrorMessage( i_res ) );
727         goto browseActionCleanup;
728     }
729
730     i_res = UpnpAddToAction( &p_action, "Browse",
731             psz_service_type, "StartingIndex", psz_starting_index_ );
732
733     if ( i_res != UPNP_E_SUCCESS )
734     {
735         msg_Dbg( _p_sd, "AddToAction 'StartingIndex' failed: %s",
736                 UpnpGetErrorMessage( i_res ) );
737         goto browseActionCleanup;
738     }
739
740     i_res = UpnpAddToAction( &p_action, "Browse",
741             psz_service_type, "RequestedCount", psz_requested_count_ );
742
743     if ( i_res != UPNP_E_SUCCESS )
744     {
745         msg_Dbg( _p_sd, "AddToAction 'RequestedCount' failed: %s",
746                 UpnpGetErrorMessage( i_res ) );
747         goto browseActionCleanup;
748     }
749
750     i_res = UpnpAddToAction( &p_action, "Browse",
751             psz_service_type, "SortCriteria", psz_sort_criteria_ );
752
753     if ( i_res != UPNP_E_SUCCESS )
754     {
755         msg_Dbg( _p_sd, "AddToAction 'SortCriteria' failed: %s",
756                 UpnpGetErrorMessage( i_res ) );
757         goto browseActionCleanup;
758     }
759
760     i_res = UpnpSendAction( _p_sd->p_sys->client_handle,
761               psz_url,
762               psz_service_type,
763               0, /* ignored in SDK, must be NULL */
764               p_action,
765               &p_response );
766
767     if ( i_res != UPNP_E_SUCCESS )
768     {
769         msg_Err( _p_sd, "%s when trying the send() action with URL: %s",
770                 UpnpGetErrorMessage( i_res ), psz_url );
771
772         ixmlDocument_free( p_response );
773         p_response = 0;
774     }
775
776 browseActionCleanup:
777
778     free( psz_service_type );
779
780     ixmlDocument_free( p_action );
781     return p_response;
782 }
783
784 void MediaServer::fetchContents()
785 {
786     /* Delete previous contents to prevent duplicate entries */
787     if ( _p_contents )
788     {
789         delete _p_contents;
790         services_discovery_RemoveItem( _p_sd, _p_input_item );
791         services_discovery_AddItem( _p_sd, _p_input_item, NULL );
792     }
793
794     Container* root = new Container( 0, "0", getFriendlyName() );
795
796     _fetchContents( root, 0 );
797
798     _p_contents = root;
799     _p_contents->setInputItem( _p_input_item );
800
801     _buildPlaylist( _p_contents, NULL );
802 }
803
804 /*
805  * Fetches and parses the UPNP response
806  */
807 bool MediaServer::_fetchContents( Container* p_parent, int i_offset )
808 {
809     if (!p_parent)
810     {
811         msg_Err( _p_sd, "No parent" );
812         return false;
813     }
814
815     char* psz_starting_index;
816     if( asprintf( &psz_starting_index, "%d", i_offset ) < 0 )
817     {
818         msg_Err( _p_sd, "asprintf error:%d", i_offset );
819         return false;
820     }
821
822     IXML_Document* p_response = _browseAction( p_parent->getObjectID(),
823                                       "BrowseDirectChildren",
824                                       "id,dc:title,res," /* Filter */
825                                       "sec:CaptionInfo,sec:CaptionInfoEx,"
826                                       "pv:subtitlefile",
827                                       psz_starting_index, /* StartingIndex */
828                                       "0", /* RequestedCount */
829                                       "" /* SortCriteria */
830                                       );
831     free( psz_starting_index );
832     if ( !p_response )
833     {
834         msg_Err( _p_sd, "No response from browse() action" );
835         return false;
836     }
837
838     IXML_Document* p_result = parseBrowseResult( p_response );
839     int i_number_returned = xml_getNumber( p_response, "NumberReturned" );
840     int i_total_matches   = xml_getNumber( p_response , "TotalMatches" );
841
842 #ifndef NDEBUG
843     msg_Dbg( _p_sd, "i_offset[%d]i_number_returned[%d]_total_matches[%d]\n",
844              i_offset, i_number_returned, i_total_matches );
845 #endif
846
847     ixmlDocument_free( p_response );
848
849     if ( !p_result )
850     {
851         msg_Err( _p_sd, "browse() response parsing failed" );
852         return false;
853     }
854
855 #ifndef NDEBUG
856     msg_Dbg( _p_sd, "Got DIDL document: %s", ixmlPrintDocument( p_result ) );
857 #endif
858
859     IXML_NodeList* containerNodeList =
860                 ixmlDocument_getElementsByTagName( p_result, "container" );
861
862     if ( containerNodeList )
863     {
864         for ( unsigned int i = 0;
865                 i < ixmlNodeList_length( containerNodeList ); i++ )
866         {
867             IXML_Element* containerElement =
868                   ( IXML_Element* )ixmlNodeList_item( containerNodeList, i );
869
870             const char* objectID = ixmlElement_getAttribute( containerElement,
871                                                              "id" );
872             if ( !objectID )
873                 continue;
874
875             const char* title = xml_getChildElementValue( containerElement,
876                                                           "dc:title" );
877
878             if ( !title )
879                 continue;
880
881             Container* container = new Container( p_parent, objectID, title );
882             p_parent->addContainer( container );
883             _fetchContents( container, 0 );
884         }
885         ixmlNodeList_free( containerNodeList );
886     }
887
888     IXML_NodeList* itemNodeList = ixmlDocument_getElementsByTagName( p_result,
889                                                                      "item" );
890     if ( itemNodeList )
891     {
892         for ( unsigned int i = 0; i < ixmlNodeList_length( itemNodeList ); i++ )
893         {
894             IXML_Element* itemElement =
895                         ( IXML_Element* )ixmlNodeList_item( itemNodeList, i );
896
897             const char* objectID =
898                         ixmlElement_getAttribute( itemElement, "id" );
899
900             if ( !objectID )
901                 continue;
902
903             const char* title =
904                         xml_getChildElementValue( itemElement, "dc:title" );
905
906             if ( !title )
907                 continue;
908
909             const char* psz_subtitles = xml_getChildElementValue( itemElement,
910                     "sec:CaptionInfo" );
911
912             if ( !psz_subtitles )
913                 psz_subtitles = xml_getChildElementValue( itemElement,
914                         "sec:CaptionInfoEx" );
915
916             if ( !psz_subtitles )
917                 psz_subtitles = xml_getChildElementValue( itemElement,
918                         "pv:subtitlefile" );
919
920             /* Try to extract all resources in DIDL */
921             IXML_NodeList* p_resource_list = ixmlDocument_getElementsByTagName( (IXML_Document*) itemElement, "res" );
922             if ( p_resource_list )
923             {
924                 int i_length = ixmlNodeList_length( p_resource_list );
925                 for ( int i = 0; i < i_length; i++ )
926                 {
927                     mtime_t i_duration = -1;
928                     int i_hours, i_minutes, i_seconds;
929                     IXML_Element* p_resource = ( IXML_Element* ) ixmlNodeList_item( p_resource_list, i );
930                     const char* psz_resource_url = xml_getChildElementValue( p_resource, "res" );
931                     if( !psz_resource_url )
932                         continue;
933                     const char* psz_duration = ixmlElement_getAttribute( p_resource, "duration" );
934
935                     if ( psz_duration )
936                     {
937                         if( sscanf( psz_duration, "%d:%02d:%02d",
938                             &i_hours, &i_minutes, &i_seconds ) )
939                             i_duration = INT64_C(1000000) * ( i_hours*3600 +
940                                                               i_minutes*60 +
941                                                               i_seconds );
942                     }
943
944                     Item* item = new Item( p_parent, objectID, title, psz_resource_url, psz_subtitles, i_duration );
945                     p_parent->addItem( item );
946                 }
947                 ixmlNodeList_free( p_resource_list );
948             }
949             else continue;
950         }
951         ixmlNodeList_free( itemNodeList );
952     }
953
954     ixmlDocument_free( p_result );
955
956     if( i_offset + i_number_returned < i_total_matches )
957         return _fetchContents( p_parent, i_offset + i_number_returned );
958
959     return true;
960 }
961
962 // TODO: Create a permanent fix for the item duplication bug. The current fix
963 // is essentially only a small hack. Although it fixes the problem, it introduces
964 // annoying cosmetic issues with the playlist. For example, when the UPnP Server
965 // rebroadcasts it's directory structure, the VLC Client deletes the old directory
966 // structure, causing the user to go back to the root node of the directory. The
967 // directory is then rebuilt, and the user is forced to traverse through the directory
968 // to find the item they were looking for. Some servers may not push the directory
969 // structure too often, but we cannot rely on this fix.
970 //
971 // I have thought up another fix, but this would require certain features to
972 // be present within the VLC services discovery. Currently, services_discovery_AddItem
973 // does not allow the programmer to nest items. It only allows a "2 deep" scope.
974 // An example of the limitation is below:
975 //
976 // Root Directory
977 // + Item 1
978 // + Item 2
979 //
980 // services_discovery_AddItem will not let the programmer specify a child-node to
981 // insert items into, so we would not be able to do the following:
982 //
983 // Root Directory
984 // + Item 1
985 //   + Sub Item 1
986 // + Item 2
987 //   + Sub Item 1 of Item 2
988 //     + Sub-Sub Item 1 of Sub Item 1
989 //
990 // This creates a HUGE limitation on what we are able to do. If we were able to do
991 // the above, we could simply preserve the old directory listing, and compare what items
992 // do not exist in the new directory listing, then remove them from the shown listing using
993 // services_discovery_RemoveItem. If new files were introduced within an already existing
994 // container, we could simply do so with services_discovery_AddItem.
995
996 /*
997  * Builds playlist based on available input items.
998  */
999 void MediaServer::_buildPlaylist( Container* p_parent, input_item_node_t *p_input_node )
1000 {
1001     bool b_send = p_input_node == NULL;
1002     if( b_send )
1003         p_input_node = input_item_node_Create( p_parent->getInputItem() );
1004
1005     for ( unsigned int i = 0; i < p_parent->getNumContainers(); i++ )
1006     {
1007         Container* p_container = p_parent->getContainer( i );
1008
1009         input_item_t* p_input_item = input_item_New( "vlc://nop",
1010                                                     p_container->getTitle() );
1011         input_item_node_t *p_new_node =
1012             input_item_node_AppendItem( p_input_node, p_input_item );
1013
1014         p_container->setInputItem( p_input_item );
1015         _buildPlaylist( p_container, p_new_node );
1016     }
1017
1018     for ( unsigned int i = 0; i < p_parent->getNumItems(); i++ )
1019     {
1020         Item* p_item = p_parent->getItem( i );
1021
1022         char **ppsz_opts = NULL;
1023         char *psz_input_slave = p_item->buildInputSlaveOption();
1024         if( psz_input_slave )
1025         {
1026             ppsz_opts = (char**)malloc( 2 * sizeof( char* ) );
1027             ppsz_opts[0] = psz_input_slave;
1028             ppsz_opts[1] = p_item->buildSubTrackIdOption();
1029         }
1030
1031         input_item_t* p_input_item = input_item_NewExt( p_item->getResource(),
1032                                            p_item->getTitle(),
1033                                            psz_input_slave ? 2 : 0,
1034                                            psz_input_slave ? ppsz_opts : NULL,
1035                                            VLC_INPUT_OPTION_TRUSTED, /* XXX */
1036                                            p_item->getDuration() );
1037
1038         assert( p_input_item );
1039         if( ppsz_opts )
1040         {
1041             free( ppsz_opts[0] );
1042             free( ppsz_opts[1] );
1043             free( ppsz_opts );
1044
1045             psz_input_slave = NULL;
1046         }
1047
1048         input_item_node_AppendItem( p_input_node, p_input_item );
1049         p_item->setInputItem( p_input_item );
1050     }
1051
1052     if( b_send )
1053         input_item_node_PostAndDelete( p_input_node );
1054 }
1055
1056 void MediaServer::setInputItem( input_item_t* p_input_item )
1057 {
1058     if( _p_input_item == p_input_item )
1059         return;
1060
1061     if( _p_input_item )
1062         vlc_gc_decref( _p_input_item );
1063
1064     vlc_gc_incref( p_input_item );
1065     _p_input_item = p_input_item;
1066 }
1067
1068 input_item_t* MediaServer::getInputItem() const
1069 {
1070     return _p_input_item;
1071 }
1072
1073 bool MediaServer::compareSID( const char* psz_sid )
1074 {
1075     return ( strncmp( _subscription_id, psz_sid, sizeof( Upnp_SID ) ) == 0 );
1076 }
1077
1078
1079 /*
1080  * MediaServerList class
1081  */
1082 MediaServerList::MediaServerList( services_discovery_t* p_sd )
1083 {
1084     _p_sd = p_sd;
1085 }
1086
1087 MediaServerList::~MediaServerList()
1088 {
1089     for ( unsigned int i = 0; i < _list.size(); i++ )
1090     {
1091         delete _list[i];
1092     }
1093 }
1094
1095 bool MediaServerList::addServer( MediaServer* p_server )
1096 {
1097     input_item_t* p_input_item = NULL;
1098     if ( getServer( p_server->getUDN() ) != 0 ) return false;
1099
1100     msg_Dbg( _p_sd, "Adding server '%s' with uuid '%s'", p_server->getFriendlyName(), p_server->getUDN() );
1101
1102     p_input_item = input_item_New( "vlc://nop", p_server->getFriendlyName() );
1103
1104     input_item_SetDescription( p_input_item, p_server->getUDN() );
1105
1106     p_server->setInputItem( p_input_item );
1107
1108     services_discovery_AddItem( _p_sd, p_input_item, NULL );
1109
1110     _list.push_back( p_server );
1111
1112     return true;
1113 }
1114
1115 MediaServer* MediaServerList::getServer( const char* psz_udn )
1116 {
1117     MediaServer* p_result = 0;
1118
1119     for ( unsigned int i = 0; i < _list.size(); i++ )
1120     {
1121         if( strcmp( psz_udn, _list[i]->getUDN() ) == 0 )
1122         {
1123             p_result = _list[i];
1124             break;
1125         }
1126     }
1127
1128     return p_result;
1129 }
1130
1131 MediaServer* MediaServerList::getServerBySID( const char* psz_sid )
1132 {
1133     MediaServer* p_server = 0;
1134
1135     for ( unsigned int i = 0; i < _list.size(); i++ )
1136     {
1137         if ( _list[i]->compareSID( psz_sid ) )
1138         {
1139             p_server = _list[i];
1140             break;
1141         }
1142     }
1143
1144     return p_server;
1145 }
1146
1147 void MediaServerList::removeServer( const char* psz_udn )
1148 {
1149     MediaServer* p_server = getServer( psz_udn );
1150     if ( !p_server ) return;
1151
1152     msg_Dbg( _p_sd, "Removing server '%s'", p_server->getFriendlyName() );
1153
1154     services_discovery_RemoveItem( _p_sd, p_server->getInputItem() );
1155
1156     std::vector<MediaServer*>::iterator it;
1157     for ( it = _list.begin(); it != _list.end(); ++it )
1158     {
1159         if ( *it == p_server )
1160         {
1161             _list.erase( it );
1162             delete p_server;
1163             break;
1164         }
1165     }
1166 }
1167
1168
1169 /*
1170  * Item class
1171  */
1172 Item::Item( Container* p_parent,
1173         const char* psz_object_id, const char* psz_title,
1174         const char* psz_resource, const char* psz_subtitles,
1175         mtime_t i_duration )
1176 {
1177     _parent = p_parent;
1178
1179     _objectID = psz_object_id;
1180     _title = psz_title;
1181     _resource = psz_resource;
1182     _subtitles = psz_subtitles ? psz_subtitles : "";
1183     _duration = i_duration;
1184
1185     _p_input_item = NULL;
1186 }
1187
1188 Item::~Item()
1189 {
1190     if( _p_input_item )
1191         vlc_gc_decref( _p_input_item );
1192 }
1193
1194 const char* Item::getObjectID() const
1195 {
1196     return _objectID.c_str();
1197 }
1198
1199 const char* Item::getTitle() const
1200 {
1201     return _title.c_str();
1202 }
1203
1204 const char* Item::getResource() const
1205 {
1206     return _resource.c_str();
1207 }
1208
1209 const char* Item::getSubtitles() const
1210 {
1211     if( !_subtitles.size() )
1212         return NULL;
1213
1214     return _subtitles.c_str();
1215 }
1216
1217 mtime_t Item::getDuration() const
1218 {
1219     return _duration;
1220 }
1221
1222 char* Item::buildInputSlaveOption() const
1223 {
1224     const char *psz_subtitles    = getSubtitles();
1225
1226     const char *psz_scheme_delim = "://";
1227     const char *psz_sub_opt_fmt  = ":input-slave=%s/%s://%s";
1228     const char *psz_demux        = "subtitle";
1229
1230     char       *psz_uri_scheme   = NULL;
1231     const char *psz_scheme_end   = NULL;
1232     const char *psz_uri_location = NULL;
1233     char       *psz_input_slave  = NULL;
1234
1235     size_t i_scheme_len;
1236
1237     if( !psz_subtitles )
1238         return NULL;
1239
1240     psz_scheme_end = strstr( psz_subtitles, psz_scheme_delim );
1241
1242     /* subtitles not being an URI would make no sense */
1243     if( !psz_scheme_end )
1244         return NULL;
1245
1246     i_scheme_len   = psz_scheme_end - psz_subtitles;
1247     psz_uri_scheme = (char*)malloc( i_scheme_len + 1 );
1248
1249     if( !psz_uri_scheme )
1250         return NULL;
1251
1252     memcpy( psz_uri_scheme, psz_subtitles, i_scheme_len );
1253     psz_uri_scheme[i_scheme_len] = '\0';
1254
1255     /* If the subtitles try to force a vlc demux,
1256      * then something is very wrong */
1257     if( strchr( psz_uri_scheme, '/' ) )
1258     {
1259         free( psz_uri_scheme );
1260         return NULL;
1261     }
1262
1263     psz_uri_location = psz_scheme_end + strlen( psz_scheme_delim );
1264
1265     if( -1 == asprintf( &psz_input_slave, psz_sub_opt_fmt,
1266             psz_uri_scheme, psz_demux, psz_uri_location ) )
1267         psz_input_slave = NULL;
1268
1269     free( psz_uri_scheme );
1270     return psz_input_slave;
1271 }
1272
1273 char* Item::buildSubTrackIdOption() const
1274 {
1275     return strdup( ":sub-track-id=2" );
1276 }
1277
1278 void Item::setInputItem( input_item_t* p_input_item )
1279 {
1280     if( _p_input_item == p_input_item )
1281         return;
1282
1283     if( _p_input_item )
1284         vlc_gc_decref( _p_input_item );
1285
1286     vlc_gc_incref( p_input_item );
1287     _p_input_item = p_input_item;
1288 }
1289
1290 /*
1291  * Container class
1292  */
1293 Container::Container( Container*  p_parent,
1294                       const char* psz_object_id,
1295                       const char* psz_title )
1296 {
1297     _parent = p_parent;
1298
1299     _objectID = psz_object_id;
1300     _title = psz_title;
1301
1302     _p_input_item = NULL;
1303 }
1304
1305 Container::~Container()
1306 {
1307     for ( unsigned int i = 0; i < _containers.size(); i++ )
1308     {
1309         delete _containers[i];
1310     }
1311
1312     for ( unsigned int i = 0; i < _items.size(); i++ )
1313     {
1314         delete _items[i];
1315     }
1316
1317     if( _p_input_item )
1318         vlc_gc_decref( _p_input_item );
1319 }
1320
1321 void Container::addItem( Item* item )
1322 {
1323     _items.push_back( item );
1324 }
1325
1326 void Container::addContainer( Container* p_container )
1327 {
1328     _containers.push_back( p_container );
1329 }
1330
1331 const char* Container::getObjectID() const
1332 {
1333     return _objectID.c_str();
1334 }
1335
1336 const char* Container::getTitle() const
1337 {
1338     return _title.c_str();
1339 }
1340
1341 unsigned int Container::getNumItems() const
1342 {
1343     return _items.size();
1344 }
1345
1346 unsigned int Container::getNumContainers() const
1347 {
1348     return _containers.size();
1349 }
1350
1351 Item* Container::getItem( unsigned int i_index ) const
1352 {
1353     if ( i_index < _items.size() ) return _items[i_index];
1354     return 0;
1355 }
1356
1357 Container* Container::getContainer( unsigned int i_index ) const
1358 {
1359     if ( i_index < _containers.size() ) return _containers[i_index];
1360     return 0;
1361 }
1362
1363 Container* Container::getParent()
1364 {
1365     return _parent;
1366 }
1367
1368 void Container::setInputItem( input_item_t* p_input_item )
1369 {
1370     if( _p_input_item == p_input_item )
1371         return;
1372
1373     if( _p_input_item )
1374         vlc_gc_decref( _p_input_item );
1375
1376     vlc_gc_incref( p_input_item );
1377     _p_input_item = p_input_item;
1378 }
1379
1380 input_item_t* Container::getInputItem() const
1381 {
1382     return _p_input_item;
1383 }