Experiments with GUPnP

Now that through black magic, voodoo, and a working network driver I have Avahi working on my NAS and UPnP working on my SoundBridge, I can get back to the task of learning a bit about GUPnP, a GObject-based library for UPnP. My first hack is a simple tool that pops up notification bubbles when the currently playing track changes, and it was surprisingly easy.

First, wecreate a Control Point for the service we want to control. This object will emit signals when devices on the network are discovered which provide this service, so by connecting to that signal all of the discovery is handled for us.

GUPnPContext *context = gupnp_context_new (NULL, NULL, 0, &error);
GUPnPControlPoint *cp = gupnp_control_point_new (context, "urn:schemas-upnp-org:service:AVTransport:1");
g_signal_connect (cp, "service-proxy-available",
                  G_CALLBACK (service_proxy_available_cb), NULL);
gssdp_resource_browser_set_active (GSSDP_RESOURCE_BROWSER (cp), TRUE);

Now in service_proxy_available_cb we are passed a Service Proxy object to which represents a device providing the service we asked for. Service proxies can have actions invoked on them, or we can be notified when a State Variable change. There is a state variable in AVTransport called CurrentTrackMetaData but don't let that fool you: for reasons too boring to detail that doesn't cause notifications. The only interesting state variable in AVTransport is LastChange, which is basically a meta-variable which contains the name and value of the last variable which changed. Madness, I agree. This is where we come to the problem with GUPnP: it makes the protocol seem so clean and simple that when warts like this become obvious, they stand out.

Anyway, we want to listen for changes to the LastChange state variable. This involves adding a notify and then subscribing to the proxy. Luckily state variables emit their current value when we subscribe to them, so we never need to fetch the current value.

static void
service_proxy_available_cb (GUPnPControlPoint *cp, GUPnPServiceProxy *proxy)
{
    gupnp_service_proxy_add_notify (proxy, "LastChange",
                                    G_TYPE_STRING, notify_cb, NULL);
    gupnp_service_proxy_set_subscribed (proxy, TRUE);
}

Believe it or not, we're actually nearly finished. Now to implement notify_cb. This is passed the variable name and a GValue containing the new value. A basic implementation is pretty simple.

static void
notify_cb (GUPnPServiceProxy *proxy,
           const char        *variable,
           GValue            *value,
           gpointer           user_data)
{
    g_print ("%s changed to %s\n", variable, g_value_get_string (value));
}

And we're done! When the program starts it discovers any devices on the network, creates proxies and watches the variables, printing them as they change. Devices can come and go, GUPnP handles that automatically. Easy as pie.

Well, sort of. The string is an XML document which could contain any variable, so we need to parse it looking for a CurrentTrackMetaData node. On that node the val attribute contains an escaped XML document describing the metadata in the (and I kid you not) DIDL-Lite format. The saving grace here is that at least its partially Dublin Core, but that needs to be parsed for the title and artist information. This is all standard XML mojo, and quite boring. The end result is that my first hack application is 150 lines long, the first 100 of which are entirely devoted to XML parsing. The next step is to write a convenience library around DIDL to avoid having to parse it manually. It's not exactly a complicated task, but quite tiresome.

For the curious, the full source is in this Bazaar branch.

NP: Directions EP, Variou (from Acroplane I think)

17:50 Tuesday, 09 Oct 2007 [#] [computers] (0 comments)


Name:


E-mail:


URL:


Add 10 and 7 (required):


Comment: