DCP: fixed include-guards
[vlc.git] / 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  *          Hugo Beauzée-Luyssen <hugo@beauzee.fr>
11  *
12  * UPnP Plugin using the Intel SDK (libupnp) instead of CyberLink
13  *
14  * This program is free software; you can redistribute it and/or modify
15  * it under the terms of the GNU General Public License as published by
16  * the Free Software Foundation; either version 2 of the License, or
17  * (at your option) any later version.
18  *
19  * This program is distributed in the hope that it will be useful,
20  * but WITHOUT ANY WARRANTY; without even the implied warranty of
21  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22  * GNU General Public License for more details.
23  *
24  * You should have received a copy of the GNU General Public License
25  * along with this program; if not, write to the Free Software
26  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
27  *****************************************************************************/
28
29 #include "upnp.hpp"
30
31 #include <vlc_access.h>
32 #include <vlc_plugin.h>
33 #include <vlc_interrupt.h>
34 #include <vlc_services_discovery.h>
35
36 #include <assert.h>
37 #include <limits.h>
38 #include <algorithm>
39
40 /*
41  * Constants
42 */
43 const char* MEDIA_SERVER_DEVICE_TYPE = "urn:schemas-upnp-org:device:MediaServer:1";
44 const char* CONTENT_DIRECTORY_SERVICE_TYPE = "urn:schemas-upnp-org:service:ContentDirectory:1";
45 const char* SATIP_SERVER_DEVICE_TYPE = "urn:ses-com:device:SatIPServer:1";
46
47 #define SATIP_SATELLITE N_("SAT>IP satellite")
48 #define SATIP_SATELLITE_LONG N_( "VLC will download the channel list for SAT>IP " \
49 "playback based on the chosen satellite.")
50 static const char *const ppsz_satip_satellites[] = {
51     "ASTRA_19_2E", "ASTRA_28_2E", "ASTRA_23_5E", "eutelsat_13_0E", "eutelsat_09_0E",
52     "eutelsat_05_0W", "hispasat_30_0W"
53 };
54 static const char *const ppsz_readible_satip_satellites[] = {
55     "Astra 19.2°E", "Astra 28.2°E", "Astra 23.5°E", "Eutelsat 13.0°E", "Eutelsat 09.0°E",
56     "Eutelsat 05.0°W", "Hispasat 30.0°W"
57 };
58
59 /*
60  * VLC handle
61  */
62 struct services_discovery_sys_t
63 {
64     UpnpInstanceWrapper* p_upnp;
65     SD::MediaServerList* p_server_list;
66     vlc_thread_t         thread;
67 };
68
69 struct access_sys_t
70 {
71     UpnpInstanceWrapper* p_upnp;
72     Access::MediaServer* p_server;
73 };
74
75 UpnpInstanceWrapper* UpnpInstanceWrapper::s_instance;
76 vlc_mutex_t UpnpInstanceWrapper::s_lock = VLC_STATIC_MUTEX;
77
78 /*
79  * VLC callback prototypes
80  */
81 namespace SD
82 {
83     static int Open( vlc_object_t* );
84     static void Close( vlc_object_t* );
85 }
86
87 namespace Access
88 {
89     static int Open( vlc_object_t* );
90     static void Close( vlc_object_t* );
91 }
92
93 VLC_SD_PROBE_HELPER( "upnp", "Universal Plug'n'Play", SD_CAT_LAN )
94
95 /*
96  * Module descriptor
97  */
98 vlc_module_begin()
99     set_shortname( "UPnP" );
100     set_description( N_( "Universal Plug'n'Play" ) );
101     set_category( CAT_PLAYLIST );
102     set_subcategory( SUBCAT_PLAYLIST_SD );
103     set_capability( "services_discovery", 0 );
104     set_callbacks( SD::Open, SD::Close );
105
106     set_description( N_("SAT>IP") )
107     add_string( "satip-satellite", "ASTRA_19_2E", SATIP_SATELLITE,
108                 SATIP_SATELLITE_LONG, false )
109     change_string_list( ppsz_satip_satellites, ppsz_readible_satip_satellites )
110     change_safe ()
111
112     add_submodule()
113         set_category( CAT_INPUT )
114         set_subcategory( SUBCAT_INPUT_ACCESS )
115         set_callbacks( Access::Open, Access::Close )
116         set_capability( "access", 0 )
117
118     VLC_SD_PROBE_SUBMODULE
119 vlc_module_end()
120
121
122 /*
123  * Returns the value of a child element, or NULL on error
124  */
125 const char* xml_getChildElementValue( IXML_Element* p_parent,
126                                       const char*   psz_tag_name )
127 {
128     assert( p_parent );
129     assert( psz_tag_name );
130
131     IXML_NodeList* p_node_list;
132     p_node_list = ixmlElement_getElementsByTagName( p_parent, psz_tag_name );
133     if ( !p_node_list ) return NULL;
134
135     IXML_Node* p_element = ixmlNodeList_item( p_node_list, 0 );
136     ixmlNodeList_free( p_node_list );
137     if ( !p_element )   return NULL;
138
139     IXML_Node* p_text_node = ixmlNode_getFirstChild( p_element );
140     if ( !p_text_node ) return NULL;
141
142     return ixmlNode_getNodeValue( p_text_node );
143 }
144
145 /*
146  * Extracts the result document from a SOAP response
147  */
148 IXML_Document* parseBrowseResult( IXML_Document* p_doc )
149 {
150     assert( p_doc );
151
152     // ixml*_getElementsByTagName will ultimately only case the pointer to a Node
153     // pointer, and pass it to a private function. Don't bother have a IXML_Document
154     // version of getChildElementValue
155     const char* psz_raw_didl = xml_getChildElementValue( (IXML_Element*)p_doc, "Result" );
156
157     if( !psz_raw_didl )
158         return NULL;
159
160     /* First, try parsing the buffer as is */
161     IXML_Document* p_result_doc = ixmlParseBuffer( psz_raw_didl );
162     if( !p_result_doc ) {
163         /* Missing namespaces confuse the ixml parser. This is a very ugly
164          * hack but it is needeed until devices start sending valid XML.
165          *
166          * It works that way:
167          *
168          * The DIDL document is extracted from the Result tag, then wrapped into
169          * a valid XML header and a new root tag which contains missing namespace
170          * definitions so the ixml parser understands it.
171          *
172          * If you know of a better workaround, please oh please fix it */
173         const char* psz_xml_result_fmt = "<?xml version=\"1.0\" ?>"
174             "<Result xmlns:sec=\"urn:samsung:metadata:2009\">%s</Result>";
175
176         char* psz_xml_result_string = NULL;
177         if( -1 == asprintf( &psz_xml_result_string,
178                              psz_xml_result_fmt,
179                              psz_raw_didl) )
180             return NULL;
181
182         p_result_doc = ixmlParseBuffer( psz_xml_result_string );
183         free( psz_xml_result_string );
184     }
185
186     if( !p_result_doc )
187         return NULL;
188
189     IXML_NodeList *p_elems = ixmlDocument_getElementsByTagName( p_result_doc,
190                                                                 "DIDL-Lite" );
191
192     IXML_Node *p_node = ixmlNodeList_item( p_elems, 0 );
193     ixmlNodeList_free( p_elems );
194
195     return (IXML_Document*)p_node;
196 }
197
198 namespace SD
199 {
200
201 static void *
202 SearchThread( void *p_data )
203 {
204     services_discovery_t *p_sd = ( services_discovery_t* )p_data;
205     services_discovery_sys_t *p_sys  = p_sd->p_sys;
206
207     /* Search for media servers */
208     int i_res = UpnpSearchAsync( p_sys->p_upnp->handle(), 5,
209             MEDIA_SERVER_DEVICE_TYPE, p_sys->p_upnp );
210     if( i_res != UPNP_E_SUCCESS )
211     {
212         msg_Err( p_sd, "Error sending search request: %s", UpnpGetErrorMessage( i_res ) );
213         return NULL;
214     }
215
216     /* Search for Sat Ip servers*/
217     i_res = UpnpSearchAsync( p_sys->p_upnp->handle(), 5,
218             SATIP_SERVER_DEVICE_TYPE, p_sys->p_upnp );
219     if( i_res != UPNP_E_SUCCESS )
220         msg_Err( p_sd, "Error sending search request: %s", UpnpGetErrorMessage( i_res ) );
221     return NULL;
222 }
223
224 /*
225  * Initializes UPNP instance.
226  */
227 static int Open( vlc_object_t *p_this )
228 {
229     services_discovery_t *p_sd = ( services_discovery_t* )p_this;
230     services_discovery_sys_t *p_sys  = ( services_discovery_sys_t * )
231             calloc( 1, sizeof( services_discovery_sys_t ) );
232
233     if( !( p_sd->p_sys = p_sys ) )
234         return VLC_ENOMEM;
235
236     p_sys->p_server_list = new(std::nothrow) SD::MediaServerList( p_sd );
237     if ( unlikely( p_sys->p_server_list == NULL ) )
238     {
239         free(p_sys);
240         return VLC_ENOMEM;
241     }
242
243     p_sys->p_upnp = UpnpInstanceWrapper::get( p_this, SD::MediaServerList::Callback, p_sys->p_server_list );
244     if ( !p_sys->p_upnp )
245     {
246         delete p_sys->p_server_list;
247         free(p_sys);
248         return VLC_EGENERIC;
249     }
250
251     /* XXX: Contrary to what the libupnp doc states, UpnpSearchAsync is
252      * blocking (select() and send() are called). Therefore, Call
253      * UpnpSearchAsync from an other thread. */
254     if ( vlc_clone( &p_sys->thread, SearchThread, p_this,
255                     VLC_THREAD_PRIORITY_LOW ) )
256     {
257         p_sys->p_upnp->release( true );
258         delete p_sys->p_server_list;
259         free(p_sys);
260         return VLC_EGENERIC;
261     }
262
263     return VLC_SUCCESS;
264 }
265
266 /*
267  * Releases resources.
268  */
269 static void Close( vlc_object_t *p_this )
270 {
271     services_discovery_t *p_sd = ( services_discovery_t* )p_this;
272     services_discovery_sys_t *p_sys = p_sd->p_sys;
273
274     vlc_join( p_sys->thread, NULL );
275     p_sys->p_upnp->release( true );
276     delete p_sys->p_server_list;
277     free( p_sys );
278 }
279
280 MediaServerDesc::MediaServerDesc( const std::string& udn, const std::string& fName,
281                                   const std::string& loc, const std::string& iconUrl )
282     : UDN( udn )
283     , friendlyName( fName )
284     , location( loc )
285     , iconUrl( iconUrl )
286     , inputItem( NULL )
287     , isSatIp( false )
288 {
289 }
290
291 MediaServerDesc::~MediaServerDesc()
292 {
293     if (inputItem)
294         vlc_gc_decref( inputItem );
295 }
296
297 /*
298  * MediaServerList class
299  */
300 MediaServerList::MediaServerList( services_discovery_t* p_sd )
301     : p_sd_( p_sd )
302 {
303     vlc_mutex_init( &lock_ );
304 }
305
306 MediaServerList::~MediaServerList()
307 {
308     vlc_delete_all(list_);
309     vlc_mutex_destroy( &lock_ );
310 }
311
312 bool MediaServerList::addServer( MediaServerDesc* desc )
313 {
314     vlc_mutex_locker lock( &lock_ );
315     input_item_t* p_input_item = NULL;
316     if ( getServer( desc->UDN ) )
317         return false;
318
319     msg_Dbg( p_sd_, "Adding server '%s' with uuid '%s'", desc->friendlyName.c_str(), desc->UDN.c_str() );
320
321     if ( desc->isSatIp )
322     {
323         p_input_item = input_item_NewWithTypeExt( desc->location.c_str(), desc->friendlyName.c_str(), 0,
324                                                   NULL, 0, -1, ITEM_TYPE_NODE, 1);
325     } else {
326         char* psz_mrl;
327         if( asprintf(&psz_mrl, "upnp://%s?ObjectID=0", desc->location.c_str() ) < 0 )
328             return false;
329
330         p_input_item = input_item_NewWithTypeExt( psz_mrl, desc->friendlyName.c_str(), 0,
331                                                   NULL, 0, -1, ITEM_TYPE_NODE, 1);
332         free( psz_mrl );
333     }
334     if ( !p_input_item )
335         return false;
336
337     if ( desc->iconUrl.empty() == false )
338         input_item_SetArtworkURL( p_input_item, desc->iconUrl.c_str() );
339     desc->inputItem = p_input_item;
340     input_item_SetDescription( p_input_item, desc->UDN.c_str() );
341     services_discovery_AddItem( p_sd_, p_input_item, NULL );
342     list_.push_back( desc );
343
344     return true;
345 }
346
347 MediaServerDesc* MediaServerList::getServer( const std::string& udn )
348 {
349     std::vector<MediaServerDesc*>::const_iterator it = list_.begin();
350     std::vector<MediaServerDesc*>::const_iterator ite = list_.end();
351
352     for ( ; it != ite; ++it )
353     {
354         if( udn == (*it)->UDN )
355         {
356             return *it;
357         }
358     }
359     return NULL;
360 }
361
362 void MediaServerList::parseNewServer( IXML_Document *doc, const std::string &location )
363 {
364     if ( !doc )
365     {
366         msg_Err( p_sd_, "Null IXML_Document" );
367         return;
368     }
369
370     if ( location.empty() )
371     {
372         msg_Err( p_sd_, "Empty location" );
373         return;
374     }
375
376     const char* psz_base_url = location.c_str();
377
378     /* Try to extract baseURL */
379     IXML_NodeList* p_url_list = ixmlDocument_getElementsByTagName( doc, "URLBase" );
380     if ( p_url_list )
381     {
382         if ( IXML_Node* p_url_node = ixmlNodeList_item( p_url_list, 0 ) )
383         {
384             IXML_Node* p_text_node = ixmlNode_getFirstChild( p_url_node );
385             if ( p_text_node )
386                 psz_base_url = ixmlNode_getNodeValue( p_text_node );
387         }
388         ixmlNodeList_free( p_url_list );
389     }
390
391     /* Get devices */
392     IXML_NodeList* p_device_list = ixmlDocument_getElementsByTagName( doc, "device" );
393
394     if ( !p_device_list )
395         return;
396     for ( unsigned int i = 0; i < ixmlNodeList_length( p_device_list ); i++ )
397     {
398         IXML_Element* p_device_element = ( IXML_Element* ) ixmlNodeList_item( p_device_list, i );
399
400         if( !p_device_element )
401             continue;
402
403         const char* psz_device_type = xml_getChildElementValue( p_device_element, "deviceType" );
404
405         if ( !psz_device_type )
406         {
407             msg_Warn( p_sd_, "No deviceType found!" );
408             continue;
409         }
410
411         if ( strncmp( MEDIA_SERVER_DEVICE_TYPE, psz_device_type,
412                 strlen( MEDIA_SERVER_DEVICE_TYPE ) - 1 )
413                 && strncmp( SATIP_SERVER_DEVICE_TYPE, psz_device_type,
414                         strlen( SATIP_SERVER_DEVICE_TYPE ) - 1 ) )
415             continue;
416
417         const char* psz_udn = xml_getChildElementValue( p_device_element,
418                                                         "UDN" );
419         if ( !psz_udn )
420         {
421             msg_Warn( p_sd_, "No UDN!" );
422             continue;
423         }
424
425         /* Check if server is already added */
426         if ( p_sd_->p_sys->p_server_list->getServer( psz_udn ) )
427         {
428             msg_Warn( p_sd_, "Server with uuid '%s' already exists.", psz_udn );
429             continue;
430         }
431
432         const char* psz_friendly_name =
433                    xml_getChildElementValue( p_device_element,
434                                              "friendlyName" );
435
436         if ( !psz_friendly_name )
437         {
438             msg_Dbg( p_sd_, "No friendlyName!" );
439             continue;
440         }
441
442         std::string iconUrl = getIconURL( p_device_element, psz_base_url );
443
444         // We now have basic info, we need to get the content browsing url
445         // so the access module can browse without fetching the manifest again
446
447         if ( !strncmp( SATIP_SERVER_DEVICE_TYPE, psz_device_type,
448                 strlen( SATIP_SERVER_DEVICE_TYPE ) - 1 ) )
449         {
450             /* Check for SAT>IP m3u list, which is provided by some off-standard devices */
451             const char* psz_m3u_url = xml_getChildElementValue( p_device_element, "satip:X_SATIPM3U" );
452             SD::MediaServerDesc* p_server = NULL;
453             if ( psz_m3u_url ) {
454
455                 if ( strncmp( "http://", psz_m3u_url, 7) && strncmp( "https://", psz_m3u_url, 8) )
456                 {
457                     char* psz_url = NULL;
458                     if ( UpnpResolveURL2( psz_base_url, psz_m3u_url, &psz_url ) == UPNP_E_SUCCESS )
459                     {
460                         p_server = new(std::nothrow) SD::MediaServerDesc( psz_udn, psz_friendly_name, psz_url, iconUrl );
461                         free(psz_url);
462                     }
463                 } else
464                     p_server = new(std::nothrow) SD::MediaServerDesc( psz_udn, psz_friendly_name, psz_m3u_url, iconUrl );
465
466                 if ( unlikely( !p_server ) )
467                     break;
468
469                 p_server->isSatIp = true;
470                 if ( !addServer( p_server ) )
471                     delete p_server;
472             } else {
473                 /* if no playlist is found, add a playlist from the web based on the chosen
474                  * satellite, which will be processed by a lua script a bit later */
475                 char *psz_satellite = config_GetPsz(p_sd_, "satip-satellite");
476                 if( !psz_satellite ) {
477                     break;
478                 }
479                 char *psz_url;
480                 vlc_url_t url;
481                 vlc_UrlParse( &url, psz_base_url );
482
483                 if (asprintf( &psz_url, "http/lua://www.satip.info/Playlists/%s.m3u?device=%s",
484                              psz_satellite,
485                              url.psz_host ) < 0 ) {
486                     vlc_UrlClean( &url );
487                     free( psz_satellite );
488                     continue;
489                 }
490                 free( psz_satellite );
491                 vlc_UrlClean( &url );
492
493                 p_server = new(std::nothrow) SD::MediaServerDesc( psz_udn,
494                                                                   psz_friendly_name, psz_url, iconUrl );
495
496                 p_server->isSatIp = true;
497                 if( !addServer( p_server ) ) {
498                     delete p_server;
499                 }
500                 free( psz_url );
501             }
502
503             continue;
504         }
505
506         /* Check for ContentDirectory service. */
507         IXML_NodeList* p_service_list = ixmlElement_getElementsByTagName( p_device_element, "service" );
508         if ( !p_service_list )
509             continue;
510         for ( unsigned int j = 0; j < ixmlNodeList_length( p_service_list ); j++ )
511         {
512             IXML_Element* p_service_element = (IXML_Element*)ixmlNodeList_item( p_service_list, j );
513
514             const char* psz_service_type = xml_getChildElementValue( p_service_element, "serviceType" );
515             if ( !psz_service_type )
516             {
517                 msg_Warn( p_sd_, "No service type found." );
518                 continue;
519             }
520
521             int k = strlen( CONTENT_DIRECTORY_SERVICE_TYPE ) - 1;
522             if ( strncmp( CONTENT_DIRECTORY_SERVICE_TYPE,
523                         psz_service_type, k ) )
524                 continue;
525
526             const char* psz_control_url = xml_getChildElementValue( p_service_element,
527                                           "controlURL" );
528             if ( !psz_control_url )
529             {
530                 msg_Warn( p_sd_, "No control url found." );
531                 continue;
532             }
533
534             /* Try to browse content directory. */
535             char* psz_url = ( char* ) malloc( strlen( psz_base_url ) + strlen( psz_control_url ) + 1 );
536             if ( psz_url )
537             {
538                 if ( UpnpResolveURL( psz_base_url, psz_control_url, psz_url ) == UPNP_E_SUCCESS )
539                 {
540                     SD::MediaServerDesc* p_server = new(std::nothrow) SD::MediaServerDesc( psz_udn,
541                             psz_friendly_name, psz_url, iconUrl );
542                     free( psz_url );
543                     if ( unlikely( !p_server ) )
544                         break;
545
546                     if ( !addServer( p_server ) )
547                     {
548                         delete p_server;
549                         continue;
550                     }
551                 }
552                 else
553                     free( psz_url );
554             }
555         }
556         ixmlNodeList_free( p_service_list );
557     }
558     ixmlNodeList_free( p_device_list );
559 }
560
561 std::string MediaServerList::getIconURL( IXML_Element* p_device_elem, const char* psz_base_url )
562 {
563     std::string res;
564     IXML_NodeList* p_icon_lists = ixmlElement_getElementsByTagName( p_device_elem, "iconList" );
565     if ( p_icon_lists == NULL )
566         return res;
567     IXML_Element* p_icon_list = (IXML_Element*)ixmlNodeList_item( p_icon_lists, 0 );
568     if ( p_icon_list != NULL )
569     {
570         IXML_NodeList* p_icons = ixmlElement_getElementsByTagName( p_icon_list, "icon" );
571         if ( p_icons != NULL )
572         {
573             unsigned int maxWidth = 0;
574             unsigned int maxHeight = 0;
575             for ( unsigned int i = 0; i < ixmlNodeList_length( p_icons ); ++i )
576             {
577                 IXML_Element* p_icon = (IXML_Element*)ixmlNodeList_item( p_icons, i );
578                 const char* widthStr = xml_getChildElementValue( p_icon, "width" );
579                 const char* heightStr = xml_getChildElementValue( p_icon, "height" );
580                 if ( widthStr == NULL || heightStr == NULL )
581                     continue;
582                 unsigned int width = atoi( widthStr );
583                 unsigned int height = atoi( heightStr );
584                 if ( width <= maxWidth || height <= maxHeight )
585                     continue;
586                 const char* iconUrl = xml_getChildElementValue( p_icon, "url" );
587                 if ( iconUrl == NULL )
588                     continue;
589                 maxWidth = width;
590                 maxHeight = height;
591                 res = iconUrl;
592             }
593             ixmlNodeList_free( p_icons );
594         }
595     }
596     ixmlNodeList_free( p_icon_lists );
597
598     if ( res.empty() == false )
599     {
600         vlc_url_t url;
601         vlc_UrlParse( &url, psz_base_url );
602         char* psz_url;
603         if ( asprintf( &psz_url, "%s://%s:%u%s", url.psz_protocol, url.psz_host, url.i_port, res.c_str() ) < 0 )
604             res.clear();
605         else
606         {
607             res = psz_url;
608             free( psz_url );
609         }
610         vlc_UrlClean( &url );
611     }
612     return res;
613 }
614
615 void MediaServerList::removeServer( const std::string& udn )
616 {
617     vlc_mutex_locker lock( &lock_ );
618
619     MediaServerDesc* p_server = getServer( udn );
620     if ( !p_server )
621         return;
622
623     msg_Dbg( p_sd_, "Removing server '%s'", p_server->friendlyName.c_str() );
624
625     assert(p_server->inputItem);
626     services_discovery_RemoveItem( p_sd_, p_server->inputItem );
627
628     std::vector<MediaServerDesc*>::iterator it = std::find(list_.begin(), list_.end(), p_server);
629     if (it != list_.end())
630     {
631         list_.erase( it );
632     }
633     delete p_server;
634 }
635
636 /*
637  * Handles servers listing UPnP events
638  */
639 int MediaServerList::Callback( Upnp_EventType event_type, void* p_event, void* p_user_data )
640 {
641     MediaServerList* self = static_cast<MediaServerList*>( p_user_data );
642     services_discovery_t* p_sd = self->p_sd_;
643
644     switch( event_type )
645     {
646     case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
647     case UPNP_DISCOVERY_SEARCH_RESULT:
648     {
649         struct Upnp_Discovery* p_discovery = ( struct Upnp_Discovery* )p_event;
650
651         IXML_Document *p_description_doc = NULL;
652
653         int i_res;
654         i_res = UpnpDownloadXmlDoc( p_discovery->Location, &p_description_doc );
655         if ( i_res != UPNP_E_SUCCESS )
656         {
657             msg_Warn( p_sd, "Could not download device description! "
658                             "Fetching data from %s failed: %s",
659                             p_discovery->Location, UpnpGetErrorMessage( i_res ) );
660             return i_res;
661         }
662         self->parseNewServer( p_description_doc, p_discovery->Location );
663         ixmlDocument_free( p_description_doc );
664     }
665     break;
666
667     case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE:
668     {
669         struct Upnp_Discovery* p_discovery = ( struct Upnp_Discovery* )p_event;
670
671         self->removeServer( p_discovery->DeviceId );
672     }
673     break;
674
675     case UPNP_EVENT_SUBSCRIBE_COMPLETE:
676         msg_Warn( p_sd, "subscription complete" );
677         break;
678
679     case UPNP_DISCOVERY_SEARCH_TIMEOUT:
680         msg_Warn( p_sd, "search timeout" );
681         break;
682
683     case UPNP_EVENT_RECEIVED:
684     case UPNP_EVENT_AUTORENEWAL_FAILED:
685     case UPNP_EVENT_SUBSCRIPTION_EXPIRED:
686         // Those are for the access part
687         break;
688
689     default:
690         msg_Err( p_sd, "Unhandled event, please report ( type=%d )", event_type );
691         break;
692     }
693
694     return UPNP_E_SUCCESS;
695 }
696
697 }
698
699 namespace Access
700 {
701
702 Upnp_i11e_cb::Upnp_i11e_cb( Upnp_FunPtr callback, void *cookie )
703     : refCount_( 2 ) /* 2: owned by the caller, and the Upnp Async function */
704     , callback_( callback )
705     , cookie_( cookie )
706
707 {
708     vlc_mutex_init( &lock_ );
709     vlc_sem_init( &sem_, 0 );
710 }
711
712 Upnp_i11e_cb::~Upnp_i11e_cb()
713 {
714     vlc_mutex_destroy( &lock_ );
715     vlc_sem_destroy( &sem_ );
716 }
717
718 void Upnp_i11e_cb::waitAndRelease( void )
719 {
720     vlc_sem_wait_i11e( &sem_ );
721
722     vlc_mutex_lock( &lock_ );
723     if ( --refCount_ == 0 )
724     {
725         /* The run callback is processed, we can destroy this object */
726         vlc_mutex_unlock( &lock_ );
727         delete this;
728     } else
729     {
730         /* Interrupted, let the run callback destroy this object */
731         vlc_mutex_unlock( &lock_ );
732     }
733 }
734
735 int Upnp_i11e_cb::run( Upnp_EventType eventType, void *p_event, void *p_cookie )
736 {
737     Upnp_i11e_cb *self = static_cast<Upnp_i11e_cb*>( p_cookie );
738
739     vlc_mutex_lock( &self->lock_ );
740     if ( --self->refCount_ == 0 )
741     {
742         /* Interrupted, we can destroy self */
743         vlc_mutex_unlock( &self->lock_ );
744         delete self;
745         return 0;
746     }
747     /* Process the user callback_ */
748     self->callback_( eventType, p_event, self->cookie_);
749     vlc_mutex_unlock( &self->lock_ );
750
751     /* Signal that the callback is processed */
752     vlc_sem_post( &self->sem_ );
753     return 0;
754 }
755
756 MediaServer::MediaServer( access_t *p_access )
757     : psz_root_( NULL )
758     , psz_objectId_( NULL )
759     , access_( p_access )
760     , xmlDocument_( NULL )
761     , containerNodeList_( NULL )
762     , containerNodeIndex_( 0 )
763     , itemNodeList_( NULL )
764     , itemNodeIndex_( 0 )
765 {
766     vlc_url_t url;
767     vlc_UrlParse( &url, p_access->psz_location );
768     if ( asprintf( &psz_root_, "%s://%s:%u%s", url.psz_protocol,
769                   url.psz_host, url.i_port ? url.i_port : 80, url.psz_path ) < 0 )
770         psz_root_ = NULL;
771
772     if ( url.psz_option && !strncmp( url.psz_option, "ObjectID=", strlen( "ObjectID=" ) ) )
773         psz_objectId_ = strdup( &url.psz_option[strlen( "ObjectID=" )] );
774     vlc_UrlClean( &url );
775 }
776
777 MediaServer::~MediaServer()
778 {
779     ixmlNodeList_free( containerNodeList_ );
780     ixmlNodeList_free( itemNodeList_ );
781     ixmlDocument_free( xmlDocument_ );
782     free( psz_objectId_ );
783     free( psz_root_ );
784 }
785
786 input_item_t* MediaServer::newItem( const char *objectID, const char *title )
787 {
788     char* psz_url;
789
790     if( asprintf( &psz_url, "upnp://%s?ObjectID=%s", psz_root_, objectID ) < 0 )
791         return NULL;
792
793     input_item_t* p_item = input_item_NewWithTypeExt( psz_url, title, 0, NULL,
794                                                       0, -1, ITEM_TYPE_DIRECTORY, 1 );
795     free( psz_url);
796     return p_item;
797 }
798
799 input_item_t* MediaServer::newItem(const char* title, const char*,
800                                    mtime_t duration, const char* psz_url)
801 {
802     return input_item_NewWithTypeExt( psz_url, title, 0, NULL, 0,
803                                       duration, ITEM_TYPE_FILE, 1 );
804 }
805
806 int MediaServer::sendActionCb( Upnp_EventType eventType,
807                                void *p_event, void *p_cookie )
808 {
809     if( eventType != UPNP_CONTROL_ACTION_COMPLETE )
810         return 0;
811     IXML_Document** pp_sendActionResult = (IXML_Document** )p_cookie;
812     Upnp_Action_Complete *p_result = (Upnp_Action_Complete *)p_event;
813
814     /* The only way to dup the result is to print it and parse it again */
815     DOMString tmpStr = ixmlPrintNode( ( IXML_Node * ) p_result->ActionResult );
816     if (tmpStr == NULL)
817         return 0;
818
819     *pp_sendActionResult = ixmlParseBuffer( tmpStr );
820     ixmlFreeDOMString( tmpStr );
821     return 0;
822 }
823
824 /* Access part */
825 IXML_Document* MediaServer::_browseAction( const char* psz_object_id_,
826                                            const char* psz_browser_flag_,
827                                            const char* psz_filter_,
828                                            const char* psz_requested_count_,
829                                            const char* psz_sort_criteria_ )
830 {
831     IXML_Document* p_action = NULL;
832     IXML_Document* p_response = NULL;
833     Upnp_i11e_cb *i11eCb = NULL;
834
835     int i_res;
836
837     if ( vlc_killed() )
838         return NULL;
839
840     i_res = UpnpAddToAction( &p_action, "Browse",
841             CONTENT_DIRECTORY_SERVICE_TYPE, "ObjectID", psz_object_id_ );
842
843     if ( i_res != UPNP_E_SUCCESS )
844     {
845         msg_Dbg( access_, "AddToAction 'ObjectID' failed: %s",
846                 UpnpGetErrorMessage( i_res ) );
847         goto browseActionCleanup;
848     }
849
850     i_res = UpnpAddToAction( &p_action, "Browse",
851             CONTENT_DIRECTORY_SERVICE_TYPE, "StartingIndex", "0" );
852     if ( i_res != UPNP_E_SUCCESS )
853     {
854         msg_Dbg( access_, "AddToAction 'StartingIndex' failed: %s",
855                 UpnpGetErrorMessage( i_res ) );
856         goto browseActionCleanup;
857     }
858
859     i_res = UpnpAddToAction( &p_action, "Browse",
860             CONTENT_DIRECTORY_SERVICE_TYPE, "BrowseFlag", psz_browser_flag_ );
861
862     if ( i_res != UPNP_E_SUCCESS )
863     {
864         msg_Dbg( access_, "AddToAction 'BrowseFlag' failed: %s",
865                 UpnpGetErrorMessage( i_res ) );
866         goto browseActionCleanup;
867     }
868
869     i_res = UpnpAddToAction( &p_action, "Browse",
870             CONTENT_DIRECTORY_SERVICE_TYPE, "Filter", psz_filter_ );
871
872     if ( i_res != UPNP_E_SUCCESS )
873     {
874         msg_Dbg( access_, "AddToAction 'Filter' failed: %s",
875                 UpnpGetErrorMessage( i_res ) );
876         goto browseActionCleanup;
877     }
878
879     i_res = UpnpAddToAction( &p_action, "Browse",
880             CONTENT_DIRECTORY_SERVICE_TYPE, "RequestedCount", psz_requested_count_ );
881
882     if ( i_res != UPNP_E_SUCCESS )
883     {
884         msg_Dbg( access_, "AddToAction 'RequestedCount' failed: %s",
885                 UpnpGetErrorMessage( i_res ) );
886         goto browseActionCleanup;
887     }
888
889     i_res = UpnpAddToAction( &p_action, "Browse",
890             CONTENT_DIRECTORY_SERVICE_TYPE, "SortCriteria", psz_sort_criteria_ );
891
892     if ( i_res != UPNP_E_SUCCESS )
893     {
894         msg_Dbg( access_, "AddToAction 'SortCriteria' failed: %s",
895                 UpnpGetErrorMessage( i_res ) );
896         goto browseActionCleanup;
897     }
898
899     /* Setup an interruptible callback that will call sendActionCb if not
900      * interrupted by vlc_interrupt_kill */
901     i11eCb = new Upnp_i11e_cb( sendActionCb, &p_response );
902     i_res = UpnpSendActionAsync( access_->p_sys->p_upnp->handle(),
903               psz_root_,
904               CONTENT_DIRECTORY_SERVICE_TYPE,
905               NULL, /* ignored in SDK, must be NULL */
906               p_action,
907               Upnp_i11e_cb::run, i11eCb );
908
909     if ( i_res != UPNP_E_SUCCESS )
910     {
911         msg_Err( access_, "%s when trying the send() action with URL: %s",
912                 UpnpGetErrorMessage( i_res ), access_->psz_location );
913     }
914     /* Wait for the callback to fill p_response or wait for an interrupt */
915     i11eCb->waitAndRelease();
916
917 browseActionCleanup:
918     ixmlDocument_free( p_action );
919     return p_response;
920 }
921
922 /*
923  * Fetches and parses the UPNP response
924  */
925 void MediaServer::fetchContents()
926 {
927     IXML_Document* p_response = _browseAction( psz_objectId_,
928                                       "BrowseDirectChildren",
929                                       "*",
930                                       "0", /* RequestedCount */
931                                       "" /* SortCriteria */
932                                       );
933     if ( !p_response )
934     {
935         msg_Err( access_, "No response from browse() action" );
936         return;
937     }
938
939     xmlDocument_ = parseBrowseResult( p_response );
940
941     ixmlDocument_free( p_response );
942
943     if ( !xmlDocument_ )
944     {
945         msg_Err( access_, "browse() response parsing failed" );
946         return;
947     }
948
949 #ifndef NDEBUG
950     msg_Dbg( access_, "Got DIDL document: %s", ixmlPrintDocument( xmlDocument_ ) );
951 #endif
952
953     containerNodeList_ = ixmlDocument_getElementsByTagName( xmlDocument_, "container" );
954     itemNodeList_ = ixmlDocument_getElementsByTagName( xmlDocument_, "item" );
955 }
956
957 input_item_t* MediaServer::getNextItem()
958 {
959     input_item_t *p_item = NULL;
960
961     if( !xmlDocument_ )
962     {
963         fetchContents();
964         if( !xmlDocument_ )
965             return NULL;
966     }
967
968     if ( containerNodeList_ )
969     {
970         for ( ; !p_item && containerNodeIndex_ < ixmlNodeList_length( containerNodeList_ )
971               ; containerNodeIndex_++ )
972         {
973             IXML_Element* containerElement = (IXML_Element*)ixmlNodeList_item( containerNodeList_,
974                                                                                 containerNodeIndex_ );
975
976             const char* objectID = ixmlElement_getAttribute( containerElement,
977                                                              "id" );
978             if ( !objectID )
979                 continue;
980
981             const char* title = xml_getChildElementValue( containerElement,
982                                                           "dc:title" );
983             if ( !title )
984                 continue;
985             p_item = newItem(objectID, title);
986         }
987     }
988
989     if( itemNodeList_ )
990     {
991         for ( ; !p_item && itemNodeIndex_ < ixmlNodeList_length( itemNodeList_ ) ; itemNodeIndex_++ )
992         {
993             IXML_Element* itemElement = ( IXML_Element* )ixmlNodeList_item( itemNodeList_,
994                                                                             itemNodeIndex_ );
995
996             const char* objectID = ixmlElement_getAttribute( itemElement, "id" );
997             if ( !objectID )
998                 continue;
999
1000             const char* title = xml_getChildElementValue( itemElement, "dc:title" );
1001             if ( !title )
1002                 continue;
1003
1004             const char* psz_subtitles = xml_getChildElementValue( itemElement, "sec:CaptionInfo" );
1005
1006             if ( !psz_subtitles )
1007                 psz_subtitles = xml_getChildElementValue( itemElement, "sec:CaptionInfoEx" );
1008
1009             if ( !psz_subtitles )
1010                 psz_subtitles = xml_getChildElementValue( itemElement, "pv:subtitlefile" );
1011
1012             /* Try to extract all resources in DIDL */
1013             IXML_NodeList* p_resource_list = ixmlDocument_getElementsByTagName( (IXML_Document*) itemElement, "res" );
1014             if ( p_resource_list && ixmlNodeList_length( p_resource_list ) > 0 )
1015             {
1016                 mtime_t i_duration = -1;
1017                 IXML_Element* p_resource = ( IXML_Element* ) ixmlNodeList_item( p_resource_list, 0 );
1018                 const char* psz_resource_url = xml_getChildElementValue( p_resource, "res" );
1019                 if( !psz_resource_url )
1020                     continue;
1021                 const char* psz_duration = ixmlElement_getAttribute( p_resource, "duration" );
1022
1023                 if ( psz_duration )
1024                 {
1025                     int i_hours, i_minutes, i_seconds;
1026                     if( sscanf( psz_duration, "%d:%02d:%02d", &i_hours, &i_minutes, &i_seconds ) )
1027                         i_duration = INT64_C(1000000) * ( i_hours * 3600 +
1028                                                           i_minutes * 60 +
1029                                                           i_seconds );
1030                 }
1031
1032                 p_item = newItem( title, objectID, i_duration,
1033                                   psz_resource_url );
1034                 if ( p_item != NULL )
1035                 {
1036                     const char* psz_artist = xml_getChildElementValue( itemElement, "upnp:artist" );
1037                     if ( psz_artist != NULL )
1038                         input_item_SetArtist( p_item, psz_artist );
1039                     const char* psz_genre = xml_getChildElementValue( itemElement, "upnp:genre" );
1040                     if ( psz_genre != NULL )
1041                         input_item_SetGenre( p_item, psz_genre );
1042                     const char* psz_album = xml_getChildElementValue( itemElement, "upnp:album" );
1043                     if ( psz_album != NULL )
1044                         input_item_SetAlbum( p_item, psz_album );
1045                     const char* psz_date = xml_getChildElementValue( itemElement, "dc:date" );
1046                     if ( psz_date != NULL )
1047                         input_item_SetDate( p_item, psz_date );
1048                     const char* psz_orig_track_nb = xml_getChildElementValue( itemElement, "upnp:originalTrackNumber" );
1049                     if ( psz_orig_track_nb != NULL )
1050                         input_item_SetTrackNumber( p_item, psz_orig_track_nb );
1051                     const char* psz_album_artist = xml_getChildElementValue( itemElement, "upnp:albumArtist" );
1052                     if ( psz_album_artist != NULL )
1053                         input_item_SetAlbumArtist( p_item, psz_album_artist );
1054                     const char* psz_albumArt = xml_getChildElementValue( itemElement, "upnp:albumArtURI" );
1055                     if ( psz_albumArt != NULL )
1056                         input_item_SetArtworkURL( p_item, psz_albumArt );
1057                 }
1058             }
1059             ixmlNodeList_free( p_resource_list );
1060         }
1061     }
1062
1063     return p_item;
1064 }
1065
1066 static input_item_t* ReadDirectory( access_t *p_access )
1067 {
1068     return p_access->p_sys->p_server->getNextItem();
1069 }
1070
1071 static int ControlDirectory( access_t *p_access, int i_query, va_list args )
1072 {
1073     switch( i_query )
1074     {
1075     case ACCESS_IS_DIRECTORY:
1076         *va_arg( args, bool * ) = true; /* is sorted */
1077         *va_arg( args, bool * ) = true; /* might loop */
1078         break;
1079     default:
1080         return access_vaDirectoryControlHelper( p_access, i_query, args );
1081     }
1082
1083     return VLC_SUCCESS;
1084 }
1085
1086 static int Open( vlc_object_t *p_this )
1087 {
1088     access_t* p_access = (access_t*)p_this;
1089     access_sys_t* p_sys = new(std::nothrow) access_sys_t;
1090     if ( unlikely( !p_sys ) )
1091         return VLC_ENOMEM;
1092
1093     p_access->p_sys = p_sys;
1094     p_sys->p_server = new(std::nothrow) MediaServer( p_access );
1095     if ( !p_sys->p_server )
1096     {
1097         delete p_sys;
1098         return VLC_EGENERIC;
1099     }
1100     p_sys->p_upnp = UpnpInstanceWrapper::get( p_this, NULL, NULL );
1101     if ( !p_sys->p_upnp )
1102     {
1103         delete p_sys->p_server;
1104         delete p_sys;
1105         return VLC_EGENERIC;
1106     }
1107
1108     p_access->pf_readdir = ReadDirectory;
1109     p_access->pf_control = ControlDirectory;
1110
1111     return VLC_SUCCESS;
1112 }
1113
1114 static void Close( vlc_object_t* p_this )
1115 {
1116     access_t* p_access = (access_t*)p_this;
1117     p_access->p_sys->p_upnp->release( false );
1118     delete p_access->p_sys->p_server;
1119     delete p_access->p_sys;
1120 }
1121
1122 }
1123
1124 UpnpInstanceWrapper::UpnpInstanceWrapper()
1125     : handle_( -1 )
1126     , opaque_( NULL )
1127     , callback_( NULL )
1128     , refcount_( 0 )
1129 {
1130     vlc_mutex_init( &callback_lock_ );
1131 }
1132
1133 UpnpInstanceWrapper::~UpnpInstanceWrapper()
1134 {
1135     UpnpUnRegisterClient( handle_ );
1136     UpnpFinish();
1137     vlc_mutex_destroy( &callback_lock_ );
1138 }
1139
1140 UpnpInstanceWrapper *UpnpInstanceWrapper::get(vlc_object_t *p_obj, Upnp_FunPtr callback, SD::MediaServerList *opaque)
1141 {
1142     vlc_mutex_locker lock( &s_lock );
1143     if ( s_instance == NULL )
1144     {
1145         UpnpInstanceWrapper* instance = new(std::nothrow) UpnpInstanceWrapper;
1146         if ( unlikely( !instance ) )
1147             return NULL;
1148
1149     #ifdef UPNP_ENABLE_IPV6
1150         char* psz_miface = var_InheritString( p_obj, "miface" );
1151         msg_Info( p_obj, "Initializing libupnp on '%s' interface", psz_miface );
1152         int i_res = UpnpInit2( psz_miface, 0 );
1153         free( psz_miface );
1154     #else
1155         /* If UpnpInit2 isnt available, initialize on first IPv4-capable interface */
1156         int i_res = UpnpInit( 0, 0 );
1157     #endif
1158         if( i_res != UPNP_E_SUCCESS )
1159         {
1160             msg_Err( p_obj, "Initialization failed: %s", UpnpGetErrorMessage( i_res ) );
1161             delete instance;
1162             return NULL;
1163         }
1164
1165         ixmlRelaxParser( 1 );
1166
1167         /* Register a control point */
1168         i_res = UpnpRegisterClient( Callback, instance, &instance->handle_ );
1169         if( i_res != UPNP_E_SUCCESS )
1170         {
1171             msg_Err( p_obj, "Client registration failed: %s", UpnpGetErrorMessage( i_res ) );
1172             delete instance;
1173             return NULL;
1174         }
1175
1176         /* libupnp does not treat a maximum content length of 0 as unlimited
1177          * until 64dedf (~ pupnp v1.6.7) and provides no sane way to discriminate
1178          * between versions */
1179         if( (i_res = UpnpSetMaxContentLength( INT_MAX )) != UPNP_E_SUCCESS )
1180         {
1181             msg_Err( p_obj, "Failed to set maximum content length: %s",
1182                     UpnpGetErrorMessage( i_res ));
1183             delete instance;
1184             return NULL;
1185         }
1186         s_instance = instance;
1187     }
1188     s_instance->refcount_++;
1189     // This assumes a single UPNP SD instance
1190     if (callback && opaque)
1191     {
1192         vlc_mutex_locker lock( &s_instance->callback_lock_ );
1193         assert(!s_instance->callback_ && !s_instance->opaque_);
1194         s_instance->opaque_ = opaque;
1195         s_instance->callback_ = callback;
1196     }
1197     return s_instance;
1198 }
1199
1200 void UpnpInstanceWrapper::release(bool isSd)
1201 {
1202     vlc_mutex_locker lock( &s_lock );
1203     if ( isSd )
1204     {
1205         vlc_mutex_locker lock( &callback_lock_ );
1206         callback_ = NULL;
1207         opaque_ = NULL;
1208     }
1209     if (--s_instance->refcount_ == 0)
1210     {
1211         delete s_instance;
1212         s_instance = NULL;
1213     }
1214 }
1215
1216 UpnpClient_Handle UpnpInstanceWrapper::handle() const
1217 {
1218     return handle_;
1219 }
1220
1221 int UpnpInstanceWrapper::Callback(Upnp_EventType event_type, void *p_event, void *p_user_data)
1222 {
1223     UpnpInstanceWrapper* self = static_cast<UpnpInstanceWrapper*>( p_user_data );
1224     vlc_mutex_locker lock( &self->callback_lock_ );
1225     if ( !self->callback_ )
1226         return 0;
1227     self->callback_( event_type, p_event, self->opaque_ );
1228     return 0;
1229 }