package Music::Player::Manager; use base qw(Net::DBus); sub new { my $class = shift; my $service = shift; my $self = $class->SUPER::new($service, "/music/player/manager"); bless $self, $class; return $self; } 1;
Now, as mentioned, the manager with handle a number of different player backends. So we need to provide methods for registering new backends, and querying for backends capable of playing a particular file type. So modifying the above code we add a hash table in the constructor, to store the backends:
sub new { my $class = shift; my $service = shift; my $self = $class->SUPER::new($service, "/music/player/manager"); $self->{backends} = {}; bless $self, $class; return $self; }
And now a method to register a new backend. This takes a Perl module name and uses it to instantiate a backend. Since the backends are also going to be DBus objects, we need to pass in a reference to the service we are attached to, along with a path under which to register the backend. We use the "get_service" method to retreieve a reference to the service the manager is attached to, and attach the player backend to this same service: When a method on DBus object is invoked, the first parameter is the object reference ($self), and the remainder are the parameters provided to the method call. Thus writing a method implementation on a DBUs is really no different to normal object oriented Perl (cf perltoot):
sub register_backend { my $self = shift; my $name = shift; my $module = shift; eval "use $module"; if ($@) { die "cannot load backend $module: $@" ; } $self->{backends} = $module->new($self->get_service, "/music/player/backend/$name"); }
Looking at this one might wonder what happens if the "die" method is triggered. In such a scenario, rather than terminating the service process, the error will be caught and propagated back to the remote caller to deal with.
The player backends provide a method "get_track_types" which returns an array reference of the music track types they support. We can use this method to provide an API to allow easy retrieval of a backend for a particular track type. This method will return a path with which the backend object can be accessed
sub find_backend { my $self = shift; my $extension = shift; foreach my $name (keys %{$self->{backends}}) { my $backend = $self->{backends}->{$name}; foreach my $type (@{$backend->get_track_types}) { if ($type eq $extension) { return $backend->get_object_path; } } } die "no backend for type $extension"; }
Lets take a quick moment to consider how this method would be used to play a music track. If you've not already done so, refresh your memory from Net::DBus::Tutorial::UsingObjects. Now, we have an MP3 file which we wish to play, so we search for the path to a backend, then retrieve the object for it, and play the track:
...get the music player service... # Ask for a path to a player for mp3 files my $path = $service->find_backend("mp3"); # $path now contains '/music/player/backend/mpg123' # and we can get the backend object my $backend = $service->get_object($path); # and finally play the track $backend->play("/vol/music/beck/guero/09-scarecrow.mp3");
The first step in providing introspection data for a DBus object in Perl, is to specify the name of the interface provided by the object. This is typically a period separated string, by convention containing the domain name of the application as its first component. Since most Perl modules end up living on CPAN, one might use "org.cpan" as the first component, followed by the package name of the module (replacing :: with .), eg "org.cpan.music.player.manager". If it is not planned to host the module on CPAN, a personal/project domain might be used eg "com.berrange.music.player.manager". The interface for an object is defined by loading the Net::DBus::Exporter module, providing the interface as its first parameter. So the earlier code example would be modified to look like:
package Music::Player::Manager; use base qw(Net::DBus); use Net::DBus::Exporter qw(com.berrange.music.player.manager)
Next up, it is necessary to provide data types for the parameters and return values of the methods. The Net::DBus::Exporter module provides a method "dbus_method" for this purpose, which takes three parameter, the name of the method being exported, an array reference of parameter types, and an array reference of return types (the latter can be omitted if there are no return values). This can be called at any point in the module's code, but by convention it is preferrable to associate calls to "dbus_method" with the actual method implementation, thus:
dbus_method("register_backend", ["string", "string"]); sub register_backend { my $self = shift; my $name = shift; my $module = shift; .. snipped rest of method body ... }
And, thus:
dbus_method("find_backend", ["string"], ["string"]) sub find_backend { my $self = shift; my $extension = shift; ... snip method body... }
The Net::DBus::Service module provides the means to register a service. Its constructor expects a reference to the bus object (an instance of Net::DBus), along with the name of the service. As with interface names, the first component of a service name is usually derived from a domain name, and then suffixed with the name of the application, in our example forming "org.cpan.Music.Player". While some objects will be created on the fly during execution of the application, others are created upon initial startup. The music player manager object created earlier in this tutorial is an example of the latter. It is typical to instantiate and register these objects in the constructor for the service. Thus a service object for the music player application would look like:
package Music::Player; use base qw(Net::DBus::Service); sub new { my $class = shift; my $bus = shift; my $self = $class->SUPER::new($bus, "org.cpan.music.player"); bless $self, $class; $self->{manager} = Music::Player::Manager->new($self); return $self; }
The Net::DBus::Service automatically provides one special object to all services, under the path "/org/freedesktop/DBus/Exporter". This object implements the "org.freedesktop.DBus.Exporter" interface which has a method "ListObject". This enables clients to determine a list of all objects exported within a service. While not functionally necessary for most applications, it is none-the-less a useful tool for developers debugging applications, or wondering what a service provides.
use Net::DBus; my $bus = Net::DBus->find; my $player = Music::Player->new($bus);
With the service attached to the bus, it is merely necessary to run the main event processing loop to listen out for & handle incoming DBus messages. So the above code is modified to start a simple reactor:
use Net::DBus; use Net::DBus::Reactor; my $bus = Net::DBus->find; my $player = Music::Player->new($bus); Net::DBus::Reactor->main->run; exit 0;
Saving this code into a script "/usr/bin/music-player.pl", coding is complete and the service ready for use by clients on the bus.
[D-BUS Service] Name=org.cpan.music.player Exec=/usr/bin/music-player.pl