Libupnpp is a C++ wrapper for libupnp, a.k.a Portable UPnP, which is a direct descendant of the Open Source SDK released by Intel in support of UPnP development.

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 modules for controlling specific AVTransport or OpenHome audio services, and it is relatively easy to add modules for other services externally (the internal modules have no more access to library internals than an external module would).

Limitations:

  • The underlying libupnp only supports a single IP interface.

  • The libupnpp methods are blocking. Multithreading will be needed to achieve parallelism in your program (in any case the underlying libupnp uses 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 libupnp 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 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);

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 is largely an opaque structure, which you will get through the discovery interface, and pass to a Device or Service constructor.

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.

Some details of event handling in libupnpp have changed as of version 0.16.

Event handling in version 0.15 and before

Services implemented by the library always subscribe to events. This happens in the object constructor, by a call to the class derived registerCallback() method. Example:

avtransport.hxx:

class AVTransport : public Service {
   ...
    AVTransport(const UPnPDeviceDesc& device,
                const UPnPServiceDesc& service)
        : Service(device, service) {
        registerCallback();
    }
...

avtransport.cxx:

void AVTransport::registerCallback()
{
    Service::registerCallback(bind(&AVTransport::evtCallback, this, _1));
}

Service::registerCallback() performs the UPnP subscription and takes note of the function to call when an event arrive.

This means that a subscription (needing a network exchange) is performed each time a Service object is built, even if events are not actually needed by the user.

The Service base class destructor erases the callback hook and cancels the event subscription.

Event handling for version 0.16 and later

As of version 0.16 of the library, the event subscription is only performed if and when installReporter() is called. If you don’t need events, you will not incur their overhead.

Logging

Both libupnp and libupnpp have configurable error and debug logging.

libupnp logging

The log from libupnp is very detailed and mostly useful for low level debugging of UPnP issues. The logging functions in libupnp are conditionnally 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. 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 only needed for library version 0.16 and later. It 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.