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