Commit 18f84613 authored by Rémi Denis-Courmont's avatar Rémi Denis-Courmont

Add libupnp-based UPnP discovery from Christian Henz

with proper configure checks
parent 9efe16d1
......@@ -30,7 +30,7 @@ Bruno Vella <allevb at tin.it> - Italian localization
Carlo Calabrò <murray at via.ecp.fr> - Italian localization
Carsten Gottbehüt <gottbehuet at active-elements dot de> - v4l hotplug fix
Chris Clepper - OpenGL fix
Christian Henz - UPnP service discovery fixes
Christian Henz - libupnp service discovery plugin, CyberLink UPnP fixes
Christof Baumgaertner - dbox web intf
Christophe Mutricy <xtophe at nxtelevision dot com> - many fixes (preferences, M3U, ...)
Christopher Johnson <cjohnson at mint.net> - Qt fix in vlc.spec
......
......@@ -4092,8 +4092,8 @@ AS_IF([test "${CXX}" != "" -a "${enable_cyberlink}" = "yes" || (test "${enable_c
CPPFLAGS_cyberlink="-I${real_cyberlink_tree}/include"
CPPFLAGS="${CPPFLAGS} ${CPPFLAGS_cyberlink}"
AC_CHECK_HEADERS([cybergarage/upnp/MediaServer.h],
[ VLC_ADD_CXXFLAGS([upnp], [${CPPFLAGS_cyberlink}])
VLC_ADD_PLUGINS([upnp])
[ VLC_ADD_CXXFLAGS([upnp_cc], [${CPPFLAGS_cyberlink}])
VLC_ADD_PLUGINS([upnp_cc])
],[
AC_MSG_ERROR([cannot find CyberLink for C++ headers])
])
......@@ -4130,7 +4130,7 @@ class testclass : public SearchResponseListener, public MediaPlayer
AS_IF([test "${LIBS_cclink}" == "no"],
[AC_MSG_FAILURE([cannot find XML parser for CyberLink])])
AC_MSG_RESULT([${LIBS_cclink}])
VLC_ADD_LDFLAGS([upnp], [${real_cyberlink_tree}/lib/unix/libclink.a -lpthread ${LIBS_cclink}])
VLC_ADD_LDFLAGS([upnp_cc], [${real_cyberlink_tree}/lib/unix/libclink.a -lpthread ${LIBS_cclink}])
], [
AC_MSG_RESULT(no)
AC_MSG_ERROR([cannot find ${real_cyberlink_tree}/lib/unix/libclink.a, make sure you compiled CyberLink for C++ in ${with_cyberlink_tree}])
......@@ -4140,6 +4140,30 @@ class testclass : public SearchResponseListener, public MediaPlayer
])
])
dnl
dnl UPnP Plugin (Intel SDK)
dnl
AC_ARG_ENABLE(upnp,
[ --enable-upnp Intel UPnP SDK (default auto)])
VLC_ADD_CXXFLAGS([upnp_intel], [ ])
AS_IF([test "x${enable_upnp}" != "xno"], [
AC_CHECK_LIB([upnp], [UpnpInit], [has_upnp="yes"], [has_upnp="no"], [-lpthread])
AS_IF([test "x${enable_upnp}" != "x" && test "${has_upnp}" == "no"], [
AC_MSG_ERROR([cannot find Intel UPnP SDK (libupnp)])
])
AS_IF([test "${has_upnp}" == "yes"], [
VLC_ADD_LDFLAGS([upnp_intel], [-lupnp])
])
], [
has_upnp="no"
])
AS_IF([test "${has_upnp}" == "yes"], [
VLC_ADD_PLUGINS([upnp_intel])
])
dnl
dnl Interface plugins
dnl
......
......@@ -2,6 +2,7 @@ SOURCES_sap = sap.c
SOURCES_hal = hal.c
SOURCES_daap = daap.c
SOURCES_shout = shout.c
SOURCES_upnp = upnp.cpp
SOURCES_upnp_cc = upnp_cc.cpp
SOURCES_upnp_intel = upnp_intel.cpp
SOURCES_bonjour = bonjour.c
SOURCES_podcast = podcast.c
/*****************************************************************************
* upnp.cpp : UPnP discovery module
* upnp_cc.cpp : UPnP discovery module
*****************************************************************************
* Copyright (C) 2004-2005 the VideoLAN team
* $Id$
......
/*****************************************************************************
* Upnp_intell.cpp : UPnP discovery module (Intel SDK)
*****************************************************************************
* Copyright (C) 2004-2006 the VideoLAN team
* $Id$
*
* Authors: Rémi Denis-Courmont <rem # videolan.org> (original plugin)
* Christian Henz <henz # c-lab.de>
*
* UPnP Plugin using the Intel SDK (libupnp) instead of CyberLink
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
/*
\TODO: Debug messages: "__FILE__, __LINE__" ok ???, Wrn/Err ???
\TODO: Change names to VLC standard ???
*/
#include <stdlib.h>
#include <vector>
#include <string>
#include <upnp/upnp.h>
#include <upnp/upnptools.h>
#undef PACKAGE_NAME
#include <vlc/vlc.h>
#include <vlc/intf.h>
// VLC handle
struct services_discovery_sys_t
{
playlist_item_t *p_node;
playlist_t *p_playlist;
};
// Constants
const char* MEDIA_SERVER_DEVICE_TYPE = "urn:schemas-upnp-org:device:MediaServer:1";
const char* CONTENT_DIRECTORY_SERVICE_TYPE = "urn:schemas-upnp-org:service:ContentDirectory:1";
// Classes
class MediaServer;
class MediaServerList;
class Item;
class Container;
// Cookie that is passed to the callback
typedef struct
{
services_discovery_t* serviceDiscovery;
UpnpClient_Handle clientHandle;
MediaServerList* serverList;
} Cookie;
// Class definitions...
class Lockable
{
public:
Lockable( Cookie* c )
{
vlc_mutex_init( c->serviceDiscovery, &_mutex );
}
~Lockable()
{
vlc_mutex_destroy( &_mutex );
}
void lock() { vlc_mutex_lock( &_mutex ); }
void unlock() { vlc_mutex_unlock( &_mutex ); }
private:
vlc_mutex_t _mutex;
};
class Locker
{
public:
Locker( Lockable* l )
{
_lockable = l;
_lockable->lock();
}
~Locker()
{
_lockable->unlock();
}
private:
Lockable* _lockable;
};
class MediaServer
{
public:
static void parseDeviceDescription( IXML_Document* doc, const char* location, Cookie* cookie );
MediaServer( const char* UDN, const char* friendlyName, Cookie* cookie );
~MediaServer();
const char* getUDN() const;
const char* getFriendlyName() const;
void setContentDirectoryEventURL( const char* url );
const char* getContentDirectoryEventURL() const;
void setContentDirectoryControlURL( const char* url );
const char* getContentDirectoryControlURL() const;
void subscribeToContentDirectory();
void fetchContents();
void setPlaylistNode( playlist_item_t* node );
bool compareSID( const char* sid );
private:
bool _fetchContents( Container* parent );
void _buildPlaylist( Container* container );
IXML_Document* _browseAction( const char*, const char*, const char*, const char*, const char*, const char* );
Cookie* _cookie;
Container* _contents;
playlist_item_t* _playlistNode;
std::string _UDN;
std::string _friendlyName;
std::string _contentDirectoryEventURL;
std::string _contentDirectoryControlURL;
int _subscriptionTimeOut;
Upnp_SID _subscriptionID;
};
class MediaServerList
{
public:
MediaServerList( Cookie* cookie );
~MediaServerList();
bool addServer( MediaServer* s );
void removeServer( const char* UDN );
MediaServer* getServer( const char* UDN );
MediaServer* getServerBySID( const char* );
private:
Cookie* _cookie;
std::vector<MediaServer*> _list;
};
class Item
{
public:
Item( Container* parent, const char* objectID, const char* title, const char* resource );
const char* getObjectID() const;
const char* getTitle() const;
const char* getResource() const;
void setPlaylistNode( playlist_item_t* node );
playlist_item_t* getPlaylistNode() const ;
private:
playlist_item_t* _playlistNode;
Container* _parent;
std::string _objectID;
std::string _title;
std::string _resource;
};
class Container
{
public:
Container( Container* parent, const char* objectID, const char* title );
~Container();
void addItem( Item* item );
void addContainer( Container* container );
const char* getObjectID() const;
const char* getTitle() const;
unsigned int getNumItems() const;
unsigned int getNumContainers() const;
Item* getItem( unsigned int i ) const;
Container* getContainer( unsigned int i ) const;
void setPlaylistNode( playlist_item_t* node );
playlist_item_t* getPlaylistNode() const;
private:
playlist_item_t* _playlistNode;
Container* _parent;
std::string _objectID;
std::string _title;
std::vector<Item*> _items;
std::vector<Container*> _containers;
};
// VLC callback prototypes
static int Open( vlc_object_t* );
static void Close( vlc_object_t* );
static void Run( services_discovery_t *p_sd );
// Module descriptor
vlc_module_begin();
set_shortname( "UPnP" );
set_description( _( "Universal Plug'n'Play discovery ( Intel SDK )" ) );
set_category( CAT_PLAYLIST );
set_subcategory( SUBCAT_PLAYLIST_SD );
set_capability( "services_discovery", 0 );
set_callbacks( Open, Close );
vlc_module_end();
// More prototypes...
static Lockable* CallbackLock;
static int Callback( Upnp_EventType eventType, void* event, void* pCookie );
char* xml_makeSpecialChars( const char* in );
const char* xml_getChildElementValue( IXML_Element* parent, const char* tagName );
IXML_Document* parseBrowseResult( IXML_Document* doc );
// VLC callbacks...
static int Open( vlc_object_t *p_this )
{
services_discovery_t *p_sd = ( services_discovery_t* )p_this;
services_discovery_sys_t *p_sys = ( services_discovery_sys_t * )
malloc( sizeof( services_discovery_sys_t ) );
playlist_view_t *p_view;
vlc_value_t val;
p_sd->pf_run = Run;
p_sd->p_sys = p_sys;
/* Create our playlist node */
p_sys->p_playlist = ( playlist_t * )vlc_object_find( p_sd,
VLC_OBJECT_PLAYLIST,
FIND_ANYWHERE );
if( !p_sys->p_playlist )
{
msg_Warn( p_sd, "unable to find playlist, cancelling UPnP listening" );
return VLC_EGENERIC;
}
p_view = playlist_ViewFind( p_sys->p_playlist, VIEW_CATEGORY );
p_sys->p_node = playlist_NodeCreate( p_sys->p_playlist, VIEW_CATEGORY,
"UPnP", p_view->p_root );
p_sys->p_node->i_flags |= PLAYLIST_RO_FLAG;
p_sys->p_node->i_flags &= ~PLAYLIST_SKIP_FLAG;
val.b_bool = VLC_TRUE;
var_Set( p_sys->p_playlist, "intf-change", val );
return VLC_SUCCESS;
}
static void Close( vlc_object_t *p_this )
{
services_discovery_t *p_sd = ( services_discovery_t* )p_this;
services_discovery_sys_t *p_sys = p_sd->p_sys;
if( p_sys->p_playlist )
{
playlist_NodeDelete( p_sys->p_playlist, p_sys->p_node, VLC_TRUE,
VLC_TRUE );
vlc_object_release( p_sys->p_playlist );
}
free( p_sys );
}
static void Run( services_discovery_t* p_sd )
{
int res;
res = UpnpInit( 0, 0 );
if( res != UPNP_E_SUCCESS )
{
msg_Err( p_sd, "%s", UpnpGetErrorMessage( res ) );
return;
}
Cookie cookie;
cookie.serviceDiscovery = p_sd;
cookie.serverList = new MediaServerList( &cookie );
CallbackLock = new Lockable( &cookie );
res = UpnpRegisterClient( Callback, &cookie, &cookie.clientHandle );
if( res != UPNP_E_SUCCESS )
{
msg_Err( p_sd, "%s", UpnpGetErrorMessage( res ) );
goto shutDown;
}
res = UpnpSearchAsync( cookie.clientHandle, 5, MEDIA_SERVER_DEVICE_TYPE, &cookie );
if( res != UPNP_E_SUCCESS )
{
msg_Err( p_sd, "%s", UpnpGetErrorMessage( res ) );
goto shutDown;
}
msg_Dbg( p_sd, "UPnP discovery started" );
while( !p_sd->b_die )
{
msleep( 500 );
}
msg_Dbg( p_sd, "UPnP discovery stopped" );
shutDown:
UpnpFinish();
delete cookie.serverList;
delete CallbackLock;
}
// XML utility functions:
// Returns the value of a child element, or 0 on error
const char* xml_getChildElementValue( IXML_Element* parent, const char* tagName )
{
if ( !parent ) return 0;
if ( !tagName ) return 0;
char* s = strdup( tagName );
IXML_NodeList* nodeList = ixmlElement_getElementsByTagName( parent, s );
free( s );
if ( !nodeList ) return 0;
IXML_Node* element = ixmlNodeList_item( nodeList, 0 );
ixmlNodeList_free( nodeList );
if ( !element ) return 0;
IXML_Node* textNode = ixmlNode_getFirstChild( element );
if ( !textNode ) return 0;
return ixmlNode_getNodeValue( textNode );
}
// Replaces "&lt;" with "<" etc.
// Returns a newly created string that has to be freed by the caller.
// Returns 0 on error ( out of mem )
// \TODO: Probably not very robust!!!
char* xml_makeSpecialChars( const char* in )
{
if ( !in ) return 0;
char* result = ( char* )malloc( strlen( in ) + 1 );
if ( !result ) return 0;
char* out = result;
while( *in )
{
if ( strncmp( "&amp;", in, 5 ) == 0 )
{
*out = '&';
in += 5;
out++;
}
else if ( strncmp( "&quot;", in, 6 ) == 0 )
{
*out = '"';
in += 6;
out++;
}
else if ( strncmp( "&gt;", in, 4 ) == 0 )
{
*out = '>';
in += 4;
out++;
}
else if ( strncmp( "&lt;", in, 4 ) == 0 )
{
*out = '<';
in += 4;
out++;
}
else
{
*out = *in;
in++;
out++;
}
}
*out = '\0';
return result;
}
// Extracts the result document from a SOAP response
IXML_Document* parseBrowseResult( IXML_Document* doc )
{
if ( !doc ) return 0;
IXML_NodeList* resultList = ixmlDocument_getElementsByTagName( doc, "Result" );
if ( !resultList ) return 0;
IXML_Node* resultNode = ixmlNodeList_item( resultList, 0 );
ixmlNodeList_free( resultList );
if ( !resultNode ) return 0;
IXML_Node* textNode = ixmlNode_getFirstChild( resultNode );
if ( !textNode ) return 0;
const char* resultString = ixmlNode_getNodeValue( textNode );
char* resultXML = xml_makeSpecialChars( resultString );
IXML_Document* browseDoc = ixmlParseBuffer( resultXML );
free( resultXML );
return browseDoc;
}
// Handles all UPnP events
static int Callback( Upnp_EventType eventType, void* event, void* pCookie )
{
Locker locker( CallbackLock );
Cookie* cookie = ( Cookie* )pCookie;
switch( eventType ) {
case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE:
case UPNP_DISCOVERY_SEARCH_RESULT:
{
struct Upnp_Discovery* discovery = ( struct Upnp_Discovery* )event;
IXML_Document *descriptionDoc = 0;
int res;
res = UpnpDownloadXmlDoc( discovery->Location, &descriptionDoc );
if ( res != UPNP_E_SUCCESS )
{
msg_Dbg( cookie->serviceDiscovery, "%s:%d: Could not download device description!", __FILE__, __LINE__ );
return res;
}
MediaServer::parseDeviceDescription( descriptionDoc, discovery->Location, cookie );
ixmlDocument_free( descriptionDoc );
}
break;
case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE:
{
struct Upnp_Discovery* discovery = ( struct Upnp_Discovery* )event;
cookie->serverList->removeServer( discovery->DeviceId );
}
break;
case UPNP_EVENT_RECEIVED:
{
Upnp_Event* e = ( Upnp_Event* )event;
MediaServer* server = cookie->serverList->getServerBySID( e->Sid );
if ( server ) server->fetchContents();
}
break;
case UPNP_EVENT_AUTORENEWAL_FAILED:
case UPNP_EVENT_SUBSCRIPTION_EXPIRED:
{
// Re-subscribe...
Upnp_Event_Subscribe* s = ( Upnp_Event_Subscribe* )event;
MediaServer* server = cookie->serverList->getServerBySID( s->Sid );
if ( server ) server->subscribeToContentDirectory();
}
break;
default:
msg_Dbg( cookie->serviceDiscovery, "%s:%d: DEBUG: UNHANDLED EVENT ( TYPE=%d )", __FILE__, __LINE__, eventType );
break;
}
return UPNP_E_SUCCESS;
}
// Class implementations...
// MediaServer...
void MediaServer::parseDeviceDescription( IXML_Document* doc, const char* location, Cookie* cookie )
{
if ( !doc ) { msg_Dbg( cookie->serviceDiscovery, "%s:%d: NULL", __FILE__, __LINE__ ); return; }
if ( !location ) { msg_Dbg( cookie->serviceDiscovery, "%s:%d: NULL", __FILE__, __LINE__ ); return; }
const char* baseURL = location;
// Try to extract baseURL
IXML_NodeList* urlList = ixmlDocument_getElementsByTagName( doc, "baseURL" );
if ( urlList )
{
if ( IXML_Node* urlNode = ixmlNodeList_item( urlList, 0 ) )
{
IXML_Node* textNode = ixmlNode_getFirstChild( urlNode );
if ( textNode ) baseURL = ixmlNode_getNodeValue( textNode );
}
ixmlNodeList_free( urlList );
}
// Get devices
IXML_NodeList* deviceList = ixmlDocument_getElementsByTagName( doc, "device" );
if ( deviceList )
{
for ( unsigned int i = 0; i < ixmlNodeList_length( deviceList ); i++ )
{
IXML_Element* deviceElement = ( IXML_Element* )ixmlNodeList_item( deviceList, i );
const char* deviceType = xml_getChildElementValue( deviceElement, "deviceType" );
if ( !deviceType ) { msg_Dbg( cookie->serviceDiscovery, "%s:%d: no deviceType!", __FILE__, __LINE__ ); continue; }
if ( strcmp( MEDIA_SERVER_DEVICE_TYPE, deviceType ) != 0 ) continue;
const char* UDN = xml_getChildElementValue( deviceElement, "UDN" );
if ( !UDN ) { msg_Dbg( cookie->serviceDiscovery, "%s:%d: no UDN!", __FILE__, __LINE__ ); continue; }
if ( cookie->serverList->getServer( UDN ) != 0 ) continue;
const char* friendlyName = xml_getChildElementValue( deviceElement, "friendlyName" );
if ( !friendlyName ) { msg_Dbg( cookie->serviceDiscovery, "%s:%d: no friendlyName!", __FILE__, __LINE__ ); continue; }
MediaServer* server = new MediaServer( UDN, friendlyName, cookie );
if ( !cookie->serverList->addServer( server ) ) {
delete server;
server = 0;
continue;
}
// Check for ContentDirectory service...
IXML_NodeList* serviceList = ixmlElement_getElementsByTagName( deviceElement, "service" );
if ( serviceList )
{
for ( unsigned int j = 0; j < ixmlNodeList_length( serviceList ); j++ )
{
IXML_Element* serviceElement = ( IXML_Element* )ixmlNodeList_item( serviceList, j );
const char* serviceType = xml_getChildElementValue( serviceElement, "serviceType" );
if ( !serviceType ) continue;
if ( strcmp( CONTENT_DIRECTORY_SERVICE_TYPE, serviceType ) != 0 ) continue;
const char* eventSubURL = xml_getChildElementValue( serviceElement, "eventSubURL" );
if ( !eventSubURL ) continue;
const char* controlURL = xml_getChildElementValue( serviceElement, "controlURL" );
if ( !controlURL ) continue;
// Try to subscribe to ContentDirectory service
char* url = ( char* )malloc( strlen( baseURL ) + strlen( eventSubURL ) + 1 );
if ( url )
{
char* s1 = strdup( baseURL );
char* s2 = strdup( eventSubURL );
if ( UpnpResolveURL( s1, s2, url ) == UPNP_E_SUCCESS )
{
// msg_Dbg( cookie->serviceDiscovery, "CDS EVENT URL: %s", url );
server->setContentDirectoryEventURL( url );
server->subscribeToContentDirectory();
}
free( s1 );
free( s2 );
free( url );
}
// Try to browse content directory...
url = ( char* )malloc( strlen( baseURL ) + strlen( controlURL ) + 1 );
if ( url )
{
char* s1 = strdup( baseURL );
char* s2 = strdup( controlURL );
if ( UpnpResolveURL( s1, s2, url ) == UPNP_E_SUCCESS )
{
// msg_Dbg( cookie->serviceDiscovery, "CDS CTRL URL: %s", url );
server->setContentDirectoryControlURL( url );
server->fetchContents();