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