Upnp/satip: default to auto and rename options
[vlc.git] / modules / services_discovery / upnp.cpp
bloba2a66b9ed4f9bf70a836dbda3c5d8f5c2d220976
1 /*****************************************************************************
2 * upnp.cpp : UPnP discovery module (libupnp)
3 *****************************************************************************
4 * Copyright (C) 2004-2018 VLC authors and VideoLAN
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>
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.
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.
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 *****************************************************************************/
27 #ifdef HAVE_CONFIG_H
28 # include "config.h"
29 #endif
31 #include <vlc_common.h>
33 #include "upnp.hpp"
35 #include <vlc_access.h>
36 #include <vlc_plugin.h>
37 #include <vlc_interrupt.h>
38 #include <vlc_services_discovery.h>
40 #include <assert.h>
41 #include <limits.h>
42 #include <algorithm>
43 #include <set>
44 #include <string>
47 * Constants
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";
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 "Auto", "ASTRA_19_2E", "ASTRA_28_2E", "ASTRA_23_5E", "MasterList", "ServerList", "CustomList"
58 static const char *const ppsz_readible_satip_channel_lists[] = {
59 N_("Auto"), "Astra 19.2°E", "Astra 28.2°E", "Astra 23.5°E", N_("SAT>IP Main List"), N_("Device List"), N_("Custom List")
62 namespace {
65 * VLC handle
67 struct services_discovery_sys_t
69 UpnpInstanceWrapper* p_upnp;
70 std::shared_ptr<SD::MediaServerList> p_server_list;
71 vlc_thread_t thread;
74 struct access_sys_t
76 UpnpInstanceWrapper* p_upnp;
79 } // namespace
82 * VLC callback prototypes
84 namespace SD
86 static int OpenSD( vlc_object_t* );
87 static void CloseSD( vlc_object_t* );
90 namespace Access
92 static int OpenAccess( vlc_object_t* );
93 static void CloseAccess( vlc_object_t* );
96 VLC_SD_PROBE_HELPER( "upnp", N_("Universal Plug'n'Play"), SD_CAT_LAN )
99 * Module descriptor
101 vlc_module_begin()
102 set_shortname( "UPnP" );
103 set_description( N_( "Universal Plug'n'Play" ) );
104 set_category( CAT_PLAYLIST );
105 set_subcategory( SUBCAT_PLAYLIST_SD );
106 set_capability( "services_discovery", 0 );
107 set_callbacks( SD::OpenSD, SD::CloseSD );
109 add_string( "satip-channelist", "auto", SATIP_CHANNEL_LIST,
110 SATIP_CHANNEL_LIST, false )
111 change_string_list( ppsz_satip_channel_lists, ppsz_readible_satip_channel_lists )
112 add_string( "satip-channellist-url", NULL, SATIP_CHANNEL_LIST_URL,
113 SATIP_CHANNEL_LIST_URL, false )
115 add_submodule()
116 set_category( CAT_INPUT )
117 set_subcategory( SUBCAT_INPUT_ACCESS )
118 set_callbacks( Access::OpenAccess, Access::CloseAccess )
119 set_capability( "access", 0 )
121 VLC_SD_PROBE_SUBMODULE
122 vlc_module_end()
125 * Extracts the result document from a SOAP response
127 IXML_Document* parseBrowseResult( IXML_Document* p_doc )
129 assert( p_doc );
131 // ixml*_getElementsByTagName will ultimately only case the pointer to a Node
132 // pointer, and pass it to a private function. Don't bother have a IXML_Document
133 // version of getChildElementValue
134 const char* psz_raw_didl = xml_getChildElementValue( (IXML_Element*)p_doc, "Result" );
136 if( !psz_raw_didl )
137 return NULL;
139 /* First, try parsing the buffer as is */
140 IXML_Document* p_result_doc = ixmlParseBuffer( psz_raw_didl );
141 if( !p_result_doc ) {
142 /* Missing namespaces confuse the ixml parser. This is a very ugly
143 * hack but it is needeed until devices start sending valid XML.
145 * It works that way:
147 * The DIDL document is extracted from the Result tag, then wrapped into
148 * a valid XML header and a new root tag which contains missing namespace
149 * definitions so the ixml parser understands it.
151 * If you know of a better workaround, please oh please fix it */
152 const char* psz_xml_result_fmt = "<?xml version=\"1.0\" ?>"
153 "<Result xmlns:sec=\"urn:samsung:metadata:2009\">%s</Result>";
155 char* psz_xml_result_string = NULL;
156 if( -1 == asprintf( &psz_xml_result_string,
157 psz_xml_result_fmt,
158 psz_raw_didl) )
159 return NULL;
161 p_result_doc = ixmlParseBuffer( psz_xml_result_string );
162 free( psz_xml_result_string );
165 if( !p_result_doc )
166 return NULL;
168 IXML_NodeList *p_elems = ixmlDocument_getElementsByTagName( p_result_doc,
169 "DIDL-Lite" );
171 IXML_Node *p_node = ixmlNodeList_item( p_elems, 0 );
172 ixmlNodeList_free( p_elems );
174 return (IXML_Document*)p_node;
177 namespace SD
180 static void *
181 SearchThread( void *p_data )
183 services_discovery_t *p_sd = ( services_discovery_t* )p_data;
184 services_discovery_sys_t *p_sys = reinterpret_cast<services_discovery_sys_t *>( p_sd->p_sys );
186 /* Search for media servers */
187 int i_res = UpnpSearchAsync( p_sys->p_upnp->handle(), 5,
188 MEDIA_SERVER_DEVICE_TYPE, MEDIA_SERVER_DEVICE_TYPE );
189 if( i_res != UPNP_E_SUCCESS )
191 msg_Err( p_sd, "Error sending search request: %s", UpnpGetErrorMessage( i_res ) );
192 return NULL;
195 /* Search for Sat Ip servers*/
196 i_res = UpnpSearchAsync( p_sys->p_upnp->handle(), 5,
197 SATIP_SERVER_DEVICE_TYPE, MEDIA_SERVER_DEVICE_TYPE );
198 if( i_res != UPNP_E_SUCCESS )
199 msg_Err( p_sd, "Error sending search request: %s", UpnpGetErrorMessage( i_res ) );
200 return NULL;
204 * Initializes UPNP instance.
206 static int OpenSD( vlc_object_t *p_this )
208 services_discovery_t *p_sd = ( services_discovery_t* )p_this;
209 services_discovery_sys_t *p_sys = new (std::nothrow) services_discovery_sys_t();
211 if( !( p_sd->p_sys = p_sys ) )
212 return VLC_ENOMEM;
214 p_sd->description = _("Universal Plug'n'Play");
216 p_sys->p_upnp = UpnpInstanceWrapper::get( p_this );
217 if ( !p_sys->p_upnp )
219 delete p_sys;
220 return VLC_EGENERIC;
225 p_sys->p_server_list = std::make_shared<SD::MediaServerList>( p_sd );
227 catch ( const std::bad_alloc& )
229 msg_Err( p_sd, "Failed to create a MediaServerList");
230 p_sys->p_upnp->release();
231 delete p_sys;
232 return VLC_EGENERIC;
234 p_sys->p_upnp->addListener( p_sys->p_server_list );
236 /* XXX: Contrary to what the libupnp doc states, UpnpSearchAsync is
237 * blocking (select() and send() are called). Therefore, Call
238 * UpnpSearchAsync from an other thread. */
239 if ( vlc_clone( &p_sys->thread, SearchThread, p_this,
240 VLC_THREAD_PRIORITY_LOW ) )
242 p_sys->p_upnp->removeListener( p_sys->p_server_list );
243 p_sys->p_upnp->release();
244 delete p_sys;
245 return VLC_EGENERIC;
248 return VLC_SUCCESS;
252 * Releases resources.
254 static void CloseSD( vlc_object_t *p_this )
256 services_discovery_t *p_sd = ( services_discovery_t* )p_this;
257 services_discovery_sys_t *p_sys = reinterpret_cast<services_discovery_sys_t *>( p_sd->p_sys );
259 vlc_join( p_sys->thread, NULL );
260 p_sys->p_upnp->removeListener( p_sys->p_server_list );
261 p_sys->p_upnp->release();
262 delete p_sys;
265 MediaServerDesc::MediaServerDesc( const std::string& udn, const std::string& fName,
266 const std::string& loc, const std::string& iconUrl )
267 : UDN( udn )
268 , friendlyName( fName )
269 , location( loc )
270 , iconUrl( iconUrl )
271 , inputItem( NULL )
272 , isSatIp( false )
276 MediaServerDesc::~MediaServerDesc()
278 if (inputItem)
279 input_item_Release( inputItem );
283 * MediaServerList class
285 MediaServerList::MediaServerList( services_discovery_t* p_sd )
286 : m_sd( p_sd )
290 MediaServerList::~MediaServerList()
292 vlc_delete_all(m_list);
295 bool MediaServerList::addServer( MediaServerDesc* desc )
297 input_item_t* p_input_item = NULL;
298 if ( getServer( desc->UDN ) )
299 return false;
301 msg_Dbg( m_sd, "Adding server '%s' with uuid '%s'", desc->friendlyName.c_str(), desc->UDN.c_str() );
303 if ( desc->isSatIp )
305 p_input_item = input_item_NewDirectory( desc->location.c_str(),
306 desc->friendlyName.c_str(),
307 ITEM_NET );
308 if ( !p_input_item )
309 return false;
311 input_item_SetSetting( p_input_item, SATIP_SERVER_DEVICE_TYPE );
313 char *psz_playlist_option;
315 if (asprintf( &psz_playlist_option, "satip-host=%s",
316 desc->satIpHost.c_str() ) >= 0 ) {
317 input_item_AddOption( p_input_item, psz_playlist_option, 0 );
318 free( psz_playlist_option );
320 } else {
321 char* psz_mrl;
322 // We might already have some options specified in the location.
323 char opt_delim = desc->location.find( '?' ) == 0 ? '?' : '&';
324 if( asprintf( &psz_mrl, "upnp://%s%cObjectID=0", desc->location.c_str(), opt_delim ) < 0 )
325 return false;
327 p_input_item = input_item_NewDirectory( psz_mrl,
328 desc->friendlyName.c_str(),
329 ITEM_NET );
330 free( psz_mrl );
332 if ( !p_input_item )
333 return false;
335 input_item_SetSetting( p_input_item, MEDIA_SERVER_DEVICE_TYPE );
338 if ( desc->iconUrl.empty() == false )
339 input_item_SetArtworkURL( p_input_item, desc->iconUrl.c_str() );
340 desc->inputItem = p_input_item;
341 input_item_SetDescription( p_input_item, desc->UDN.c_str() );
342 services_discovery_AddItem( m_sd, p_input_item );
343 m_list.push_back( desc );
345 return true;
348 MediaServerDesc* MediaServerList::getServer( const std::string& udn )
350 std::vector<MediaServerDesc*>::const_iterator it = m_list.begin();
351 std::vector<MediaServerDesc*>::const_iterator ite = m_list.end();
353 for ( ; it != ite; ++it )
355 if( udn == (*it)->UDN )
357 return *it;
360 return NULL;
363 void MediaServerList::parseNewServer( IXML_Document *doc, const std::string &location )
365 if ( !doc )
367 msg_Err( m_sd, "Null IXML_Document" );
368 return;
371 if ( location.empty() )
373 msg_Err( m_sd, "Empty location" );
374 return;
377 const char* psz_base_url = location.c_str();
379 /* Try to extract baseURL */
380 IXML_NodeList* p_url_list = ixmlDocument_getElementsByTagName( doc, "URLBase" );
381 if ( p_url_list )
383 if ( IXML_Node* p_url_node = ixmlNodeList_item( p_url_list, 0 ) )
385 IXML_Node* p_text_node = ixmlNode_getFirstChild( p_url_node );
386 if ( p_text_node )
387 psz_base_url = ixmlNode_getNodeValue( p_text_node );
389 ixmlNodeList_free( p_url_list );
392 /* Get devices */
393 IXML_NodeList* p_device_list = ixmlDocument_getElementsByTagName( doc, "device" );
395 if ( !p_device_list )
396 return;
398 for ( unsigned int i = 0; i < ixmlNodeList_length( p_device_list ); i++ )
400 IXML_Element* p_device_element = ( IXML_Element* ) ixmlNodeList_item( p_device_list, i );
402 if( !p_device_element )
403 continue;
405 const char* psz_device_type = xml_getChildElementValue( p_device_element, "deviceType" );
407 if ( !psz_device_type )
409 msg_Warn( m_sd, "No deviceType found!" );
410 continue;
413 if ( strncmp( MEDIA_SERVER_DEVICE_TYPE, psz_device_type,
414 strlen( MEDIA_SERVER_DEVICE_TYPE ) - 1 )
415 && strncmp( SATIP_SERVER_DEVICE_TYPE, psz_device_type,
416 strlen( SATIP_SERVER_DEVICE_TYPE ) - 1 ) )
417 continue;
419 const char* psz_udn = xml_getChildElementValue( p_device_element,
420 "UDN" );
421 if ( !psz_udn )
423 msg_Warn( m_sd, "No UDN!" );
424 continue;
427 /* Check if server is already added */
428 if ( getServer( psz_udn ) )
430 msg_Warn( m_sd, "Server with uuid '%s' already exists.", psz_udn );
431 continue;
434 const char* psz_friendly_name =
435 xml_getChildElementValue( p_device_element,
436 "friendlyName" );
438 if ( !psz_friendly_name )
440 msg_Dbg( m_sd, "No friendlyName!" );
441 continue;
444 std::string iconUrl = getIconURL( p_device_element, psz_base_url );
446 // We now have basic info, we need to get the content browsing url
447 // so the access module can browse without fetching the manifest again
448 if ( !strncmp( SATIP_SERVER_DEVICE_TYPE, psz_device_type,
449 strlen( SATIP_SERVER_DEVICE_TYPE ) - 1 ) ) {
450 parseSatipServer( p_device_element, psz_base_url, psz_udn, psz_friendly_name, iconUrl );
453 /* Check for ContentDirectory service. */
454 IXML_NodeList* p_service_list = ixmlElement_getElementsByTagName( p_device_element, "service" );
455 if ( !p_service_list )
456 continue;
457 for ( unsigned int j = 0; j < ixmlNodeList_length( p_service_list ); j++ )
459 IXML_Element* p_service_element = (IXML_Element*)ixmlNodeList_item( p_service_list, j );
461 const char* psz_service_type = xml_getChildElementValue( p_service_element, "serviceType" );
462 if ( !psz_service_type )
464 msg_Warn( m_sd, "No service type found." );
465 continue;
468 int k = strlen( CONTENT_DIRECTORY_SERVICE_TYPE ) - 1;
469 if ( strncmp( CONTENT_DIRECTORY_SERVICE_TYPE,
470 psz_service_type, k ) )
471 continue;
473 const char* psz_control_url = xml_getChildElementValue( p_service_element,
474 "controlURL" );
475 if ( !psz_control_url )
477 msg_Warn( m_sd, "No control url found." );
478 continue;
481 /* Try to browse content directory. */
482 char* psz_url = ( char* ) malloc( strlen( psz_base_url ) + strlen( psz_control_url ) + 1 );
483 if ( psz_url )
485 if ( UpnpResolveURL( psz_base_url, psz_control_url, psz_url ) == UPNP_E_SUCCESS )
487 SD::MediaServerDesc* p_server = new(std::nothrow) SD::MediaServerDesc( psz_udn,
488 psz_friendly_name, psz_url, iconUrl );
489 free( psz_url );
490 if ( unlikely( !p_server ) )
491 break;
493 if ( !addServer( p_server ) )
495 delete p_server;
496 continue;
499 else
500 free( psz_url );
503 ixmlNodeList_free( p_service_list );
505 ixmlNodeList_free( p_device_list );
508 std::string MediaServerList::getIconURL( IXML_Element* p_device_elem, const char* psz_base_url )
510 std::string res;
511 IXML_NodeList* p_icon_lists = ixmlElement_getElementsByTagName( p_device_elem, "iconList" );
512 if ( p_icon_lists == NULL )
513 return res;
514 IXML_Element* p_icon_list = (IXML_Element*)ixmlNodeList_item( p_icon_lists, 0 );
515 if ( p_icon_list != NULL )
517 IXML_NodeList* p_icons = ixmlElement_getElementsByTagName( p_icon_list, "icon" );
518 if ( p_icons != NULL )
520 unsigned int maxWidth = 0;
521 unsigned int maxHeight = 0;
522 for ( unsigned int i = 0; i < ixmlNodeList_length( p_icons ); ++i )
524 IXML_Element* p_icon = (IXML_Element*)ixmlNodeList_item( p_icons, i );
525 const char* widthStr = xml_getChildElementValue( p_icon, "width" );
526 const char* heightStr = xml_getChildElementValue( p_icon, "height" );
527 if ( widthStr == NULL || heightStr == NULL )
528 continue;
529 unsigned int width = atoi( widthStr );
530 unsigned int height = atoi( heightStr );
531 if ( width <= maxWidth || height <= maxHeight )
532 continue;
533 const char* iconUrl = xml_getChildElementValue( p_icon, "url" );
534 if ( iconUrl == NULL )
535 continue;
536 maxWidth = width;
537 maxHeight = height;
538 res = iconUrl;
540 ixmlNodeList_free( p_icons );
543 ixmlNodeList_free( p_icon_lists );
545 if ( res.empty() == false )
547 vlc_url_t url;
548 vlc_UrlParse( &url, psz_base_url );
549 char* psz_url;
550 if ( asprintf( &psz_url, "%s://%s:%u%s", url.psz_protocol, url.psz_host, url.i_port, res.c_str() ) < 0 )
551 res.clear();
552 else
554 res = psz_url;
555 free( psz_url );
557 vlc_UrlClean( &url );
559 return res;
562 void
563 MediaServerList::parseSatipServer( IXML_Element* p_device_element, const char *psz_base_url, const char *psz_udn, const char *psz_friendly_name, std::string iconUrl )
565 SD::MediaServerDesc* p_server = NULL;
567 vlc_url_t url;
568 vlc_UrlParse( &url, psz_base_url );
570 char *psz_satip_channellist = config_GetPsz("satip-channelist");
571 if( !psz_satip_channellist ) {
572 psz_satip_channellist = strdup("Auto");
575 /* Part 1: a user may have provided a custom playlist url */
576 if (strncmp(psz_satip_channellist, "CustomList", 10) == 0) {
577 char *psz_satip_playlist_url = config_GetPsz( "satip-channellist-url" );
578 if ( psz_satip_playlist_url ) {
579 p_server = new(std::nothrow) SD::MediaServerDesc( psz_udn, psz_friendly_name, psz_satip_playlist_url, iconUrl );
581 if( likely( p_server ) ) {
582 p_server->satIpHost = url.psz_host;
583 p_server->isSatIp = true;
584 if( !addServer( p_server ) ) {
585 delete p_server;
589 /* to comply with the SAT>IP specification, we don't fall back on another channel list if this path failed */
590 free( psz_satip_playlist_url );
591 vlc_UrlClean( &url );
592 return;
596 /* Part 2: device playlist
597 * In Automatic mode, or if requested by the user, check for a SAT>IP m3u list on the device */
598 if (strncmp(psz_satip_channellist, "ServerList", 10) == 0 ||
599 strncmp(psz_satip_channellist, "Auto", strlen ("Auto") == 0 )) {
600 const char* psz_m3u_url = xml_getChildElementValue( p_device_element, "satip:X_SATIPM3U" );
601 if ( psz_m3u_url ) {
602 if ( strncmp( "http", psz_m3u_url, 4) )
604 char* psz_url = NULL;
605 if ( UpnpResolveURL2( psz_base_url, psz_m3u_url, &psz_url ) == UPNP_E_SUCCESS )
607 p_server = new(std::nothrow) SD::MediaServerDesc( psz_udn, psz_friendly_name, psz_url, iconUrl );
608 free(psz_url);
610 } else {
611 p_server = new(std::nothrow) SD::MediaServerDesc( psz_udn, psz_friendly_name, psz_m3u_url, iconUrl );
614 if ( unlikely( !p_server ) )
616 free( psz_satip_channellist );
617 vlc_UrlClean( &url );
618 return;
621 p_server->satIpHost = url.psz_host;
622 p_server->isSatIp = true;
623 if ( !addServer( p_server ) )
624 delete p_server;
625 } else {
626 msg_Dbg( m_sd, "SAT>IP server '%s' did not provide a playlist", url.psz_host);
629 if(strncmp(psz_satip_channellist, "ServerList", 10) == 0) {
630 /* to comply with the SAT>IP specifications, we don't fallback on another channel list if this path failed,
631 * but in Automatic mode, we continue */
632 free(psz_satip_channellist);
633 vlc_UrlClean( &url );
634 return;
638 /* Part 3: satip.info playlist
639 * In the normal case, fetch a playlist from the satip website,
640 * which will be processed by a lua script a bit later, to make it work sanely
641 * MasterList is a list of usual Satellites */
643 /* In Auto mode, default to MasterList list from satip.info */
644 if( strncmp(psz_satip_channellist, "Auto", strlen ("Auto") == 0 ) ) {
645 psz_satip_channellist = strdup( "MasterList" );
648 char *psz_url;
649 if (asprintf( &psz_url, "http://www.satip.info/Playlists/%s.m3u",
650 psz_satip_channellist ) < 0 ) {
651 vlc_UrlClean( &url );
652 free( psz_satip_channellist );
653 return;
656 p_server = new(std::nothrow) SD::MediaServerDesc( psz_udn,
657 psz_friendly_name, psz_url, iconUrl );
659 if( likely( p_server ) ) {
660 p_server->satIpHost = url.psz_host;
661 p_server->isSatIp = true;
662 if( !addServer( p_server ) ) {
663 delete p_server;
666 free( psz_url );
667 free( psz_satip_channellist );
668 vlc_UrlClean( &url );
671 void MediaServerList::removeServer( const std::string& udn )
673 MediaServerDesc* p_server = getServer( udn );
674 if ( !p_server )
675 return;
677 msg_Dbg( m_sd, "Removing server '%s'", p_server->friendlyName.c_str() );
679 assert(p_server->inputItem);
680 services_discovery_RemoveItem( m_sd, p_server->inputItem );
682 std::vector<MediaServerDesc*>::iterator it = std::find(m_list.begin(), m_list.end(), p_server);
683 if (it != m_list.end())
685 m_list.erase( it );
687 delete p_server;
691 * Handles servers listing UPnP events
693 int MediaServerList::onEvent( Upnp_EventType event_type, UpnpEventPtr p_event, void* p_user_data )
695 if (p_user_data != MEDIA_SERVER_DEVICE_TYPE)
696 return 0;
698 switch( event_type )
700 case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
701 case UPNP_DISCOVERY_SEARCH_RESULT:
703 const UpnpDiscovery* p_discovery = ( const UpnpDiscovery* )p_event;
705 IXML_Document *p_description_doc = NULL;
707 int i_res;
708 i_res = UpnpDownloadXmlDoc( UpnpDiscovery_get_Location_cstr( p_discovery ), &p_description_doc );
710 if ( i_res != UPNP_E_SUCCESS )
712 msg_Warn( m_sd, "Could not download device description! "
713 "Fetching data from %s failed: %s",
714 UpnpDiscovery_get_Location_cstr( p_discovery ), UpnpGetErrorMessage( i_res ) );
715 return i_res;
717 parseNewServer( p_description_doc, UpnpDiscovery_get_Location_cstr( p_discovery ) );
718 ixmlDocument_free( p_description_doc );
720 break;
722 case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE:
724 const UpnpDiscovery* p_discovery = ( const UpnpDiscovery* )p_event;
725 removeServer( UpnpDiscovery_get_DeviceID_cstr( p_discovery ) );
727 break;
729 case UPNP_EVENT_SUBSCRIBE_COMPLETE:
731 msg_Warn( m_sd, "subscription complete" );
733 break;
735 case UPNP_DISCOVERY_SEARCH_TIMEOUT:
737 msg_Warn( m_sd, "search timeout" );
739 break;
741 case UPNP_EVENT_RECEIVED:
742 case UPNP_EVENT_AUTORENEWAL_FAILED:
743 case UPNP_EVENT_SUBSCRIPTION_EXPIRED:
744 // Those are for the access part
745 break;
747 default:
749 msg_Err( m_sd, "Unhandled event, please report ( type=%d )", event_type );
751 break;
754 return UPNP_E_SUCCESS;
759 namespace Access
762 namespace
764 class ItemDescriptionHolder
766 private:
767 struct Slave : std::string
769 slave_type type;
771 Slave(std::string const &url, slave_type type) :
772 std::string(url), type(type)
777 std::set<Slave> slaves;
779 const char* objectID,
780 * title,
781 * psz_artist,
782 * psz_genre,
783 * psz_album,
784 * psz_date,
785 * psz_orig_track_nb,
786 * psz_album_artist,
787 * psz_albumArt;
789 public:
790 enum MEDIA_TYPE
792 VIDEO = 0,
793 AUDIO,
794 IMAGE,
795 CONTAINER
798 MEDIA_TYPE media_type;
800 ItemDescriptionHolder()
804 bool init(IXML_Element *itemElement)
806 objectID = ixmlElement_getAttribute( itemElement, "id" );
807 if ( !objectID )
808 return false;
809 title = xml_getChildElementValue( itemElement, "dc:title" );
810 if ( !title )
811 return false;
812 const char *psz_subtitles = xml_getChildElementValue( itemElement, "sec:CaptionInfo" );
813 if ( !psz_subtitles &&
814 !(psz_subtitles = xml_getChildElementValue( itemElement, "sec:CaptionInfoEx" )) )
815 psz_subtitles = xml_getChildElementValue( itemElement, "pv:subtitlefile" );
816 addSlave(psz_subtitles, SLAVE_TYPE_SPU);
817 psz_artist = xml_getChildElementValue( itemElement, "upnp:artist" );
818 psz_genre = xml_getChildElementValue( itemElement, "upnp:genre" );
819 psz_album = xml_getChildElementValue( itemElement, "upnp:album" );
820 psz_date = xml_getChildElementValue( itemElement, "dc:date" );
821 psz_orig_track_nb = xml_getChildElementValue( itemElement, "upnp:originalTrackNumber" );
822 psz_album_artist = xml_getChildElementValue( itemElement, "upnp:albumArtist" );
823 psz_albumArt = xml_getChildElementValue( itemElement, "upnp:albumArtURI" );
824 const char *psz_media_type = xml_getChildElementValue( itemElement, "upnp:class" );
825 if (strncmp(psz_media_type, "object.item.videoItem", 21) == 0)
826 media_type = VIDEO;
827 else if (strncmp(psz_media_type, "object.item.audioItem", 21) == 0)
828 media_type = AUDIO;
829 else if (strncmp(psz_media_type, "object.item.imageItem", 21) == 0)
830 media_type = IMAGE;
831 else if (strncmp(psz_media_type, "object.container", 16 ) == 0)
832 media_type = CONTAINER;
833 else
834 return false;
835 return true;
838 void addSlave(const char *psz_slave, slave_type type)
840 if (psz_slave)
841 slaves.insert(Slave(psz_slave, type));
844 void addSubtitleSlave(IXML_Element* p_resource)
846 if (slaves.empty())
847 addSlave(ixmlElement_getAttribute( p_resource, "pv:subtitleFileUri" ),
848 SLAVE_TYPE_SPU);
851 void setArtworkURL(IXML_Element* p_resource)
853 psz_albumArt = xml_getChildElementValue( p_resource, "res" );
856 void apply(input_item_t *p_item)
858 if ( psz_artist != NULL )
859 input_item_SetArtist( p_item, psz_artist );
860 if ( psz_genre != NULL )
861 input_item_SetGenre( p_item, psz_genre );
862 if ( psz_album != NULL )
863 input_item_SetAlbum( p_item, psz_album );
864 if ( psz_date != NULL )
865 input_item_SetDate( p_item, psz_date );
866 if ( psz_orig_track_nb != NULL )
867 input_item_SetTrackNumber( p_item, psz_orig_track_nb );
868 if ( psz_album_artist != NULL )
869 input_item_SetAlbumArtist( p_item, psz_album_artist );
870 if ( psz_albumArt != NULL )
871 input_item_SetArtworkURL( p_item, psz_albumArt );
872 for (std::set<Slave>::iterator it = slaves.begin(); it != slaves.end(); ++it)
874 input_item_slave *p_slave = input_item_slave_New( it->c_str(), it->type,
875 SLAVE_PRIORITY_MATCH_ALL );
876 if ( p_slave )
877 input_item_AddSlave( p_item, p_slave );
881 input_item_t *createNewItem(IXML_Element *p_resource)
883 vlc_tick_t i_duration = INPUT_DURATION_UNKNOWN;
884 const char* psz_resource_url = xml_getChildElementValue( p_resource, "res" );
885 if( !psz_resource_url )
886 return NULL;
887 const char* psz_duration = ixmlElement_getAttribute( p_resource, "duration" );
888 if ( psz_duration )
890 int i_hours, i_minutes, i_seconds;
891 if( sscanf( psz_duration, "%d:%02d:%02d", &i_hours, &i_minutes, &i_seconds ) )
892 i_duration = vlc_tick_from_sec( i_hours * 3600 + i_minutes * 60 +
893 i_seconds );
895 return input_item_NewExt( psz_resource_url, title, i_duration,
896 ITEM_TYPE_FILE, ITEM_NET );
899 input_item_t *createNewContainerItem( const char* psz_root )
901 if ( objectID == NULL || title == NULL )
902 return NULL;
904 char* psz_url;
905 if( asprintf( &psz_url, "upnp://%s?ObjectID=%s", psz_root, objectID ) < 0 )
906 return NULL;
908 input_item_t* p_item = input_item_NewDirectory( psz_url, title, ITEM_NET );
909 free( psz_url);
910 return p_item;
915 Upnp_i11e_cb::Upnp_i11e_cb( Upnp_FunPtr callback, void *cookie )
916 : m_refCount( 2 ) /* 2: owned by the caller, and the Upnp Async function */
917 , m_callback( callback )
918 , m_cookie( cookie )
921 vlc_mutex_init( &m_lock );
922 vlc_sem_init( &m_sem, 0 );
925 Upnp_i11e_cb::~Upnp_i11e_cb()
927 vlc_mutex_destroy( &m_lock );
928 vlc_sem_destroy( &m_sem );
931 void Upnp_i11e_cb::waitAndRelease( void )
933 vlc_sem_wait_i11e( &m_sem );
935 vlc_mutex_lock( &m_lock );
936 if ( --m_refCount == 0 )
938 /* The run callback is processed, we can destroy this object */
939 vlc_mutex_unlock( &m_lock );
940 delete this;
941 } else
943 /* Interrupted, let the run callback destroy this object */
944 vlc_mutex_unlock( &m_lock );
948 int Upnp_i11e_cb::run( Upnp_EventType eventType, UpnpEventPtr p_event, void *p_cookie )
950 Upnp_i11e_cb *self = static_cast<Upnp_i11e_cb*>( p_cookie );
952 vlc_mutex_lock( &self->m_lock );
953 if ( --self->m_refCount == 0 )
955 /* Interrupted, we can destroy self */
956 vlc_mutex_unlock( &self->m_lock );
957 delete self;
958 return 0;
960 /* Process the user callback_ */
961 self->m_callback( eventType, p_event, self->m_cookie);
962 vlc_mutex_unlock( &self->m_lock );
964 /* Signal that the callback is processed */
965 vlc_sem_post( &self->m_sem );
966 return 0;
969 MediaServer::MediaServer( stream_t *p_access, input_item_node_t *node )
970 : m_psz_objectId( NULL )
971 , m_access( p_access )
972 , m_node( node )
975 m_psz_root = strdup( p_access->psz_location );
976 char* psz_objectid = strstr( m_psz_root, "ObjectID=" );
977 if ( psz_objectid != NULL )
979 // Remove this parameter from the URL, since it might cause some servers to fail
980 // Keep in mind that we added a '&' or a '?' to the URL, so remove it as well
981 *( psz_objectid - 1) = 0;
982 m_psz_objectId = &psz_objectid[strlen( "ObjectID=" )];
986 MediaServer::~MediaServer()
988 free( m_psz_root );
991 bool MediaServer::addContainer( IXML_Element* containerElement )
993 ItemDescriptionHolder holder;
995 if ( holder.init( containerElement ) == false )
996 return false;
998 input_item_t* p_item = holder.createNewContainerItem( m_psz_root );
999 if ( !p_item )
1000 return false;
1001 holder.apply( p_item );
1002 input_item_CopyOptions( p_item, m_node->p_item );
1003 input_item_node_AppendItem( m_node, p_item );
1004 input_item_Release( p_item );
1005 return true;
1008 bool MediaServer::addItem( IXML_Element* itemElement )
1010 ItemDescriptionHolder holder;
1012 if (!holder.init(itemElement))
1013 return false;
1014 /* Try to extract all resources in DIDL */
1015 IXML_NodeList* p_resource_list = ixmlDocument_getElementsByTagName( (IXML_Document*) itemElement, "res" );
1016 if ( !p_resource_list)
1017 return false;
1018 int list_lenght = ixmlNodeList_length( p_resource_list );
1019 if (list_lenght <= 0 ) {
1020 ixmlNodeList_free( p_resource_list );
1021 return false;
1023 input_item_t *p_item = NULL;
1025 for (int index = 0; index < list_lenght; index++)
1027 IXML_Element* p_resource = ( IXML_Element* ) ixmlNodeList_item( p_resource_list, index );
1028 const char* rez_type = ixmlElement_getAttribute( p_resource, "protocolInfo" );
1030 if (strncmp(rez_type, "http-get:*:video/", 17) == 0 && holder.media_type == ItemDescriptionHolder::VIDEO)
1032 if (!p_item)
1033 p_item = holder.createNewItem(p_resource);
1034 holder.addSubtitleSlave(p_resource);
1036 else if (strncmp(rez_type, "http-get:*:image/", 17) == 0)
1037 switch (holder.media_type)
1039 case ItemDescriptionHolder::IMAGE:
1040 if (!p_item) {
1041 p_item = holder.createNewItem(p_resource);
1042 break;
1044 case ItemDescriptionHolder::VIDEO:
1045 case ItemDescriptionHolder::AUDIO:
1046 holder.setArtworkURL(p_resource);
1047 break;
1048 case ItemDescriptionHolder::CONTAINER:
1049 msg_Warn( m_access, "Unexpected object.container in item enumeration" );
1050 continue;
1052 else if (strncmp(rez_type, "http-get:*:text/", 16) == 0)
1053 holder.addSlave(xml_getChildElementValue( p_resource, "res" ), SLAVE_TYPE_SPU);
1054 else if (strncmp(rez_type, "http-get:*:audio/", 17) == 0)
1056 if (holder.media_type == ItemDescriptionHolder::AUDIO)
1058 if (!p_item)
1059 p_item = holder.createNewItem(p_resource);
1061 else
1062 holder.addSlave(xml_getChildElementValue( p_resource, "res" ),
1063 SLAVE_TYPE_AUDIO);
1066 ixmlNodeList_free( p_resource_list );
1067 if (!p_item)
1068 return false;
1069 holder.apply(p_item);
1070 input_item_CopyOptions( p_item, m_node->p_item );
1071 input_item_node_AppendItem( m_node, p_item );
1072 input_item_Release( p_item );
1073 return true;
1076 int MediaServer::sendActionCb( Upnp_EventType eventType,
1077 UpnpEventPtr p_event, void *p_cookie )
1079 if( eventType != UPNP_CONTROL_ACTION_COMPLETE )
1080 return 0;
1081 IXML_Document** pp_sendActionResult = (IXML_Document** )p_cookie;
1082 const UpnpActionComplete *p_result = (const UpnpActionComplete *)p_event;
1084 /* The only way to dup the result is to print it and parse it again */
1085 DOMString tmpStr = ixmlPrintNode( ( IXML_Node * ) UpnpActionComplete_get_ActionResult( p_result ) );
1086 if (tmpStr == NULL)
1087 return 0;
1089 *pp_sendActionResult = ixmlParseBuffer( tmpStr );
1090 ixmlFreeDOMString( tmpStr );
1091 return 0;
1094 /* Access part */
1095 IXML_Document* MediaServer::_browseAction( const char* psz_object_id_,
1096 const char* psz_browser_flag_,
1097 const char* psz_filter_,
1098 const char* psz_requested_count_,
1099 const char* psz_sort_criteria_ )
1101 IXML_Document* p_action = NULL;
1102 IXML_Document* p_response = NULL;
1103 Upnp_i11e_cb *i11eCb = NULL;
1104 access_sys_t *sys = (access_sys_t *)m_access->p_sys;
1106 int i_res;
1108 if ( vlc_killed() )
1109 return NULL;
1111 i_res = UpnpAddToAction( &p_action, "Browse",
1112 CONTENT_DIRECTORY_SERVICE_TYPE, "ObjectID", psz_object_id_ ? psz_object_id_ : "0" );
1114 if ( i_res != UPNP_E_SUCCESS )
1116 msg_Dbg( m_access, "AddToAction 'ObjectID' failed: %s",
1117 UpnpGetErrorMessage( i_res ) );
1118 goto browseActionCleanup;
1121 i_res = UpnpAddToAction( &p_action, "Browse",
1122 CONTENT_DIRECTORY_SERVICE_TYPE, "BrowseFlag", psz_browser_flag_ );
1124 if ( i_res != UPNP_E_SUCCESS )
1126 msg_Dbg( m_access, "AddToAction 'BrowseFlag' failed: %s",
1127 UpnpGetErrorMessage( i_res ) );
1128 goto browseActionCleanup;
1131 i_res = UpnpAddToAction( &p_action, "Browse",
1132 CONTENT_DIRECTORY_SERVICE_TYPE, "Filter", psz_filter_ );
1134 if ( i_res != UPNP_E_SUCCESS )
1136 msg_Dbg( m_access, "AddToAction 'Filter' failed: %s",
1137 UpnpGetErrorMessage( i_res ) );
1138 goto browseActionCleanup;
1141 i_res = UpnpAddToAction( &p_action, "Browse",
1142 CONTENT_DIRECTORY_SERVICE_TYPE, "StartingIndex", "0" );
1143 if ( i_res != UPNP_E_SUCCESS )
1145 msg_Dbg( m_access, "AddToAction 'StartingIndex' failed: %s",
1146 UpnpGetErrorMessage( i_res ) );
1147 goto browseActionCleanup;
1150 i_res = UpnpAddToAction( &p_action, "Browse",
1151 CONTENT_DIRECTORY_SERVICE_TYPE, "RequestedCount", psz_requested_count_ );
1153 if ( i_res != UPNP_E_SUCCESS )
1155 msg_Dbg( m_access, "AddToAction 'RequestedCount' failed: %s",
1156 UpnpGetErrorMessage( i_res ) );
1157 goto browseActionCleanup;
1160 i_res = UpnpAddToAction( &p_action, "Browse",
1161 CONTENT_DIRECTORY_SERVICE_TYPE, "SortCriteria", psz_sort_criteria_ );
1163 if ( i_res != UPNP_E_SUCCESS )
1165 msg_Dbg( m_access, "AddToAction 'SortCriteria' failed: %s",
1166 UpnpGetErrorMessage( i_res ) );
1167 goto browseActionCleanup;
1170 /* Setup an interruptible callback that will call sendActionCb if not
1171 * interrupted by vlc_interrupt_kill */
1172 i11eCb = new Upnp_i11e_cb( sendActionCb, &p_response );
1173 i_res = UpnpSendActionAsync( sys->p_upnp->handle(),
1174 m_psz_root,
1175 CONTENT_DIRECTORY_SERVICE_TYPE,
1176 NULL, /* ignored in SDK, must be NULL */
1177 p_action,
1178 Upnp_i11e_cb::run, i11eCb );
1180 if ( i_res != UPNP_E_SUCCESS )
1182 msg_Err( m_access, "%s when trying the send() action with URL: %s",
1183 UpnpGetErrorMessage( i_res ), m_access->psz_location );
1185 /* Wait for the callback to fill p_response or wait for an interrupt */
1186 i11eCb->waitAndRelease();
1188 browseActionCleanup:
1189 ixmlDocument_free( p_action );
1190 return p_response;
1194 * Fetches and parses the UPNP response
1196 bool MediaServer::fetchContents()
1198 IXML_Document* p_response = _browseAction( m_psz_objectId,
1199 "BrowseDirectChildren",
1200 "*",
1201 // Some servers don't understand "0" as "no-limit"
1202 "5000", /* RequestedCount */
1203 "" /* SortCriteria */
1205 if ( !p_response )
1207 msg_Err( m_access, "No response from browse() action" );
1208 return false;
1211 IXML_Document* p_result = parseBrowseResult( p_response );
1213 ixmlDocument_free( p_response );
1215 if ( !p_result )
1217 msg_Err( m_access, "browse() response parsing failed" );
1218 return false;
1221 #ifndef NDEBUG
1222 msg_Dbg( m_access, "Got DIDL document: %s", ixmlPrintDocument( p_result ) );
1223 #endif
1225 IXML_NodeList* containerNodeList =
1226 ixmlDocument_getElementsByTagName( p_result, "container" );
1228 if ( containerNodeList )
1230 for ( unsigned int i = 0; i < ixmlNodeList_length( containerNodeList ); i++ )
1231 addContainer( (IXML_Element*)ixmlNodeList_item( containerNodeList, i ) );
1232 ixmlNodeList_free( containerNodeList );
1235 IXML_NodeList* itemNodeList = ixmlDocument_getElementsByTagName( p_result,
1236 "item" );
1237 if ( itemNodeList )
1239 for ( unsigned int i = 0; i < ixmlNodeList_length( itemNodeList ); i++ )
1240 addItem( (IXML_Element*)ixmlNodeList_item( itemNodeList, i ) );
1241 ixmlNodeList_free( itemNodeList );
1244 ixmlDocument_free( p_result );
1245 return true;
1248 static int ReadDirectory( stream_t *p_access, input_item_node_t* p_node )
1250 MediaServer server( p_access, p_node );
1252 if ( !server.fetchContents() )
1253 return VLC_EGENERIC;
1254 return VLC_SUCCESS;
1257 static int OpenAccess( vlc_object_t *p_this )
1259 stream_t* p_access = (stream_t*)p_this;
1260 access_sys_t* p_sys = new(std::nothrow) access_sys_t;
1261 if ( unlikely( !p_sys ) )
1262 return VLC_ENOMEM;
1264 p_access->p_sys = p_sys;
1265 p_sys->p_upnp = UpnpInstanceWrapper::get( p_this );
1266 if ( !p_sys->p_upnp )
1268 delete p_sys;
1269 return VLC_EGENERIC;
1272 p_access->pf_readdir = ReadDirectory;
1273 p_access->pf_control = access_vaDirectoryControlHelper;
1275 return VLC_SUCCESS;
1278 static void CloseAccess( vlc_object_t* p_this )
1280 stream_t* p_access = (stream_t*)p_this;
1281 access_sys_t *sys = (access_sys_t *)p_access->p_sys;
1283 sys->p_upnp->release();
1284 delete sys;