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