Libupnpp is a C++ wrapper for libupnp a.k.a Portable UPnP (up to branch 0.17), or its C++ descendant, libnpupnp (versions 0.18 and later).

Libupnpp can be used to implement UPnP devices and services, or Control Points.

The Control Point side of libupnpp, which is documented here, allows a C++ program to discover UPnP devices, and exchange commands and status with them.

The library has a number of predefined classes for controlling specific AVTransport or OpenHome audio services, and it is easy to add classes for other services externally (the internal classes have no more access to library internals than an external module would).

Limitations:

  • The old libupnp underlying library only supports a single IP interface. The newer libnpupnp supports multiple interfaces for IPV4, but a single one for IPV6 at the moment.

  • Most libupnpp methods are blocking. Multithreading will be needed to achieve parallelism in your program (in any case libupnpp and the underlying library use threads, so multithreading support is a requirement).

The Library object

The library is represented by a global singleton with a number of utility methods, mostly related to setting parameters for the underlying library.

The instanciation call takes a number of arguments to specify non-default values, e.g. for the IP address or interface to use.

If you have no need for the non-default parameter values, or the singleton utility methods, there is no necessity to explicitly instanciate the library object, this will be done internally as needed.

Discovery

The UPnP Discovery framework allows searching for devices on a network and maintaining a directory of active devices.

Libupnpp implements this function with the UPnPDeviceDirectory class, which is instanciated as a single object.

Example code for accessing a device for which you know the 'friendly name':

    auto *superdir = UPnPClient::UPnPDeviceDirectory::getTheDir();

    UPnPClient::UPnPDeviceDesc devicedesc;
    bool ok = superdir->getDevByFName(name, devicedesc);

This call may delay for a couple seconds if the library was just activated, and an UPnP search is being waited for. During program execution, the call should return immediately.

Other methods of the device directory objects allow enumerating the devices which advertised themselves on the network.

Description

UPnP devices describe their capabilities in XML documents which can be retrieved from them through HTTP.

In libupnpp, the content of the main description document for a given device is provided by a UPnPClient::UPnPDeviceDesc object.

When using the devices and services predefined by the library, this may be managed as an opaque structure, which you will get through the discovery interface, and pass to a Device or Service constructor. However its internals are accessible and contain the full parsed description of the device, its services and possible sub-devices, and a copy of the raw data.

Devices

UPnP devices are entities which can contain other, embedded devices, and services.

Embedded devices are quite rare, and in my experience badly supported by common control points.

In general, the service is the interesting entity, and the wise approach is the Pythonic "quacks like a duck" one: if a device has the service you need, you can use it.

For example the predefined 'MediaRenderer' class in libupnpp does not even bother to verify its own type when built from a description: it just provides methods to query and retrieve handles to interesting services usually found in a Media Renderer (both OpenHome and UPnP AV). In most cases, not all services will be available, and the caller will compose an "a la carte" object to serve its needs (e.g. using either UPnP AV Rendering Control or OpenHome Volume for controlling volume, depending on availability).

As another example, the 'myrdcvolume' program from the libupnpp samples shows how to implement a service interface without using the predefined ones. It does not bother with the device type either (actually it does not bother at all with a device object), but just checks for the existence of the appropriate service (i.e. 'urn:schemas-upnp-org:service:RenderingControl'), and action ('Volume').

The library predefines two device classes, for Media Server devices and Media Renderer ones. Both are convenience classes with utility code to build the underlying service objects and retrieve handles for them.

Services

Most UPnP functionality is provided through services, which are end points implemented inside a device, callable through HTTP/SOAP.

libupnpp provides a set of easy C++ interfaces for most UPnP audio services (some of which also work with video). You will usually not need to bother constructing the service interface objects: rather you will let the device classes do it and return a handle (but there is nothing to prevent you from building the service objects directly).

There are two possible approaches for accessing a service which does not have a predefined interface class in the library:

  • Use the library helper functions for implementing a specific service interface, by deriving from the base Service class.

  • Use the TypedService generic string-based interface class.

Predefined Service classes

The role of most methods in these classes is to marshal the input data into SOAP format, perform the SOAP call, then marshal and return the output data. They are all synchronous.

String based interface

The TypedService class provides access to the actions in a service through a simple string-based interface.

The user only needs specify an action name and a list of arguments to call an action. The class uses the service description retrieved from the device to generate the actual SOAP call. Any returned data is provided through a C++ name-value map.

The class also has an associated helper function which provides a simplified interface to discovery.

While the class is less convenient to use than one customized to a single service, which can provide full encoding/decoding between SOAP data and a natural C++ interface, it avoids having to write the corresponding code. It was mostly implemented as a convenient interface for the SWIG module, but it can probably have other uses.

Eventing

UPnP services can report changes in their state to Control Points through events. In practise, the Control Point implements an internal HTTP server to which the services connect to report events.

Event reporting is not active by default and needs to be activated by the Control Point by 'subscribing' to the service.

Users of Service classes can receive asynchronous events by calling the installReporter() method, to specify what functions should be called when an event arrives.

Note
The event functions are called from a separate thread and some synchronization will usually be required.

The event subscription is only performed by libupnpp if and when the service installReporter() method is called.

Logging

Both 'libupnp/libnpupnp' and 'libupnpp' have configurable error and debug logging.

libupnp logging

The log from the base library is very detailed and mostly useful for low level debugging of UPnP issues. The logging functions in are conditionally compiled, and may not be enabled for your distribution (you can check UPNP_HAVE_DEBUG in 'include/upnp/upnpconfig.h').

If the 'libupnp' logging functions are enabled, you can control them through the LibUPnP::setLogFileName() and 'LibUPnP::setLogLevel()' methods.

libupnpp logging

'libupnpp' logging is distinct from the 'libupnp' functions, and always enabled, at a configurable level of verbosity.

The log is initialized by a call to Logger::getTheLog(). The verbosity level can be adjusted through 'setLogLevel()', and macros are used to emit the actual messages. The printing is based on the C++ iostreams.

See libupnpp/log.h for more details.

Exemple:

    if (Logger::getTheLog(logfilename) == 0) {
        cerr << "Can't initialize log" << endl;
        return 1;
    }
    Logger::getTheLog("")->setLogLevel(Logger::LLINF);

    ...

    LOGINF("Message at level INFO, it outputs some value: " << value);

Of course you can use the 'LOGXX' macros in your own code.

Implementing a Service class

If you want to access a service for which no predefined class exists in libupnpp, you need to define its interface yourself, by deriving the 'libupnpp' Service class.

The methods in the base class and in the helper modules make it very easy to write the derived class.

Note
libupnpp has no provision to use the service description XML document to define the service client methods, except, partially, when using the TypedService class. However, the device side has a script to turn a service description into an implementation device-side skeleton.

The derived class main constructor will take Device and Service description structures are arguments and will need to call the base class constructor. Example:

class OHPlaylist : public Service {
public:

    OHPlaylist(const UPnPDeviceDesc& device, const UPnPServiceDesc& service)
        : Service(device, service) {
    }
    ...

If there are initialization steps which are specific to the service type, they should be done inside the 'serviceInit()' method, which should be called from the constructor (see below for a more detailed description). Most services don’t need 'serviceInit()', so an empty implementation is provided by the base class.

Action methods are then just ordinary methods, which will call the base class methods to perform the networky things.

Example of an action setting a value:

int RenderingControl::setMute(bool mute, const string& channel)
{
    SoapOutgoing args(getServiceType(), "SetMute");
    args("InstanceID", "0")("Channel", channel)
    ("DesiredMute", SoapHelp::i2s(mute?1:0));
    SoapIncoming data;
    return runAction(args, data);
}

The SoapOutgoing constructor takes a service type and action name arguments. Its 'operator ()' accepts additional named string arguments.

The Service::runAction() method performs the SOAP call and provides any resulting data in a SoapIncoming object, from which named values can be easily extracted. There is no return data in the above example, see below for an action using 'SoapIncoming::get()' to extract data.

Example of an action retrieving a value (note there is nothing to prevent an action to both set and retrieve data).

bool RenderingControl::getMute(const string& channel)
{
    SoapOutgoing args(getServiceType(), "GetMute");
    args("InstanceID", "0")("Channel", channel);
    SoapIncoming data;
    int ret = runAction(args, data);
    if (ret != UPNP_E_SUCCESS) {
        return false;
    }
    bool mute;
    if (!data.get("CurrentMute", &mute)) {
        LOGERR("RenderingControl:getMute: missing CurrentMute in response"
               << endl);
        return false;
    }
    return mute;
}

There are utility methods in the base class for actions which either transport no data, or send or receive a single value: runTrivialAction(), runSimpleAction(), runSimpleGet()

Methods which may need reimplementation

virtual bool UPnPClient::Service::serviceTypeMatch(const std::string& tp)=0;

This is used by the base class to find a matching service description in the device description service list, which is done when calling initFromDescription().

The latter method is useful for building an object in two phases, first an empty constructor, then initialization from the device description.

Derived classes compare the service types in the list with their own type string.

virtual bool UPnPClient::Service::serviceInit(const UPnPDeviceDesc& device,
                                          const UPnPServiceDesc& service);

This is the part of the initialization specific to the service type. When it is called, the Service class is fully initialized. Most services don’t need to do anything in there. An example of use would be the Rendering Control service calling the device to retrieve the range of volume control values.

An empty implementation is provided by the base class.

The method is called from initFromDescription(), and, if it does anything, it should be called from the non-empty derived class constructor too.

Eventing

Derived service classes can define a registerCallBack() method to register a function to be called when an event arrives. 'registerCallBack()' will be called by 'Service::installReporter()' when the user register their interest in events. In turn, 'registerCallback()' should call Service::registerCallBack(evtCBFunc) to register their actual callback routine.

Callback functions can be any 'std::function' taking appropriate arguments, usually a member function of the service object.