The VDR Plugin System

VDR provides an easy to use plugin interface that allows additional functionality to be added to the program by implementing a dynamically loadable library file. This interface allows programmers to develop additional functionality for VDR completely separate from the core VDR source, without the need of patching the original VDR code (and all the problems of correlating various patches).

This document describes the "outside" interface of the plugin system. It handles everything necessary for a plugin to get hooked into the core VDR program and present itself to the user.

  Important modifications introduced in version 1.1.1 are marked like this.


Quick start

Can't wait, can't wait!

Actually you should read this entire document before starting to work with VDR plugins, but you probably want to see something happening right away ;-)

So, for a quick demonstration of the plugin system, there is a sample plugin called "hello" that comes with the VDR source. To test drive this one, do the following:

If you enjoyed this brief glimpse into VDR plugin handling, read through the rest of this document and eventually write your own VDR plugin.

The name of the plugin

Give me some I.D.!

One of the first things to consider when writing a VDR plugin is giving the thing a proper name. This name will be used in the VDR command line in order to load the plugin, and will also be the name of the plugin's source directory, as well as part of the final library name.

The plugin's name should typically be as short as possible. Three letter abbreviations like dvd (for a DVD player) or mp3 (for an MP3 player) would be good choices. It is also recommended that the name consists of only lowercase letters and digits. No other characters should be used here.

A plugin can access its name through the (non virtual) member function


const char *Name(void);

The actual name is derived from the plugin's library file name, as defined in the next chapter.


The plugin directory structure

Where is everybody?

By default plugins are located in a directory named PLUGINS below the VDR source directory. Inside this directory the following subdirectory structure is used:


VDR/PLUGINS/src VDR/PLUGINS/src/hello VDR/PLUGINS/lib VDR/PLUGINS/lib/libvdr-hello.so.1.1.0

The src directory contains one subdirectory for each plugin, which carries the name of that plugin (in the above example that would be hello). What's inside the individual source directory of a plugin is entirely up to the author of that plugin. The only prerequisites are that there is a Makefile that provides the targets all and clean, and that a call to make all actually produces a dynamically loadable library file for that plugin (we'll get to the details later).

The lib directory contains the dynamically loadable libraries of all available plugins. Note that the names of these files are created by concatenating

libvdr-hello.so.1.1.0
VDR plugin
library prefix
name of
the plugin
shared object
indicator
VDR version number
this plugin was
compiled for

The plugin library files can be stored in any directory. If the default organization is not used, the path to the plugin directory has be be given to VDR through the -L option.

The VDR Makefile contains the target plugins, which calls make all in every directory found under VDR/PLUGINS/src, plus the target plugins-clean, which calls make clean in each of these directories.

If you download a plugin package from the web, it will typically have a name like

vdr-hello-0.0.1.tgz

and will unpack into a directory named

vdr-hello-0.0.1

To use the plugins and plugins-clean targets from the VDR Makefile you need to unpack such an archive into the VDR/PLUGINS/src directory and create a symbolic link with the basic plugin name, as in


ln -s vdr-hello-0.0.1 hello

Since the VDR Makefile only searches for directories with names consisting of only lowercase characters and digits, it will only follow the symbolic links, which should lead to the current version of the plugin you want to use. This way you can have several different versions of a plugin source (like vdr-hello-0.0.1 and vdr-hello-0.0.2) and define which one to actually use through the symbolic link.


Initializing a new plugin directory

A room with a view

Call the Perl script newplugin from the VDR source directory to create a new plugin directory with a Makefile and a main source file implementing the basic derived plugin class. You will also find a README file there with some inital text, where you should fill in actual information about your project. A HISTORY file is set up with an "Initial revision" entry. As your project evolves, you should add the changes here with date and version number.

newplugin also creates a copy of the GPL license file COPYING, assuming that you will release your work under that license. Change this if you have other plans.

Add further files and maybe subdirectories to your plugin source directory as necessary. Don't forget to adapt the Makefile appropriately.


The actual implementation

Use the source, Luke!

A newly initialized plugin doesn't really do very much yet. If you load it into VDR you will find a new entry in the main menu, with the same name as your plugin (where the first character has been converted to uppercase). There will also be a new entry named "Plugins" in the "Setup" menu, which will bring up a list of all loaded plugins, through which you can access each plugin's own setup parameters (if it provides any).

To implement actual functionality into your plugin you need to edit the source file that was generated as PLUGINS/src/name.c. Read the comments in that file to see where you can bring in your own code. The following sections of this document will walk you through the individual member functions of the plugin class.

Depending on what your plugin shall do, you may or may not need all of the given member functions. Except for the MainMenuEntry() function they all by default return values that will result in no actual functionality. You can either completely delete unused functions from your source file, or just leave them as they are. If your plugin shall not be accessible through VDR's main menu, simply remove (or comment out) the line implementing the MainMenuEntry() function.

At the end of the plugin's source file you will find a line that looks like this:


VDRPLUGINCREATOR(cPluginHello);

This is the "magic" hook that allows VDR to actually load the plugin into its memory. You don't need to worry about the details behind all this.

If your plugin requires additional source files, simply add them to your plugin's source directory and adjust the Makefile accordingly.


Construction and Destruction

What goes up, must come down...

The constructor and destructor of a plugin are defined as


cPlugin(void); virtual ~cPlugin();

The constructor shall initialize any member variables the plugin defines, but must not access any global structures of VDR. It also must not create any threads or other large data structures. These things are done in the Start() function later. Constructing a plugin object shall not have any side effects or produce any output, since VDR, for instance, has to create the plugin objects in order to get their command line help - and after that immediately destroys them again.

The destructor has to clean up any data created by the plugin, and has to take care that any threads the plugin may have created will be stopped.

Of course, if your plugin doesn't define any member variables that need to be initialized (and deleted), you don't need to implement either of these functions.


Version number

Which incarnation is this?

Every plugin must have a version number of its own, which does not necessarily have to be in any way related to the VDR version number. VDR requests a plugin's version number through a call to the function


virtual const char *Version(void) = 0;

Since this is a "pure" virtual function, any derived plugin class must implement it. The returned string should identify this version of the plugin. Typically this would be something like "0.0.1", but it may also contain other information, like for instance "0.0.1pre2" or the like. The string should only be as long as really necessary, and shall not contain the plugin's name itself. Here's an example:


static const char *VERSION = "0.0.1"; const char *cPluginHello::Version(void) { return VERSION; }

Note that the definition of the version number is expected to be located in the main source file, and must be written as

static const char *VERSION = ...
just like shown in the above example. This is a convention that allows the Makefile to extract the version number when generating the file name for the distribution archive.

A new plugin project should start with version number 0.0.1 and should reach version 1.0.0 once it is completely operative and well tested. Following the Linux kernel version numbering scheme, versions with even release numbers (like 1.0.x, 1.2.x, 1.4.x...) should be stable releases, while those with odd release numbers (like 1.1.x, 1.3.x, 1.5.x...) are usually considered "under development". The three parts of a version number are not limited to single digits, so a version number of 1.2.15 would be acceptable.


Description

What is it that you do?

In order to tell the user what exactly a plugin does, it must implement the function


virtual const char *Description(void) = 0;

which returns a short, one line description of the plugin's purpose:


static const char *DESCRIPTION = "A friendly greeting"; virtual const char *Description(void) { return tr(DESCRIPTION); }

Note the tr() around the DESCRIPTION, which allows the description to be internationalized.


Command line arguments

Taking orders

A VDR plugin can have command line arguments just like any normal program. If a plugin wants to react on command line arguments, it needs to implement the function


virtual bool ProcessArgs(int argc, char *argv[]);

The parameters argc and argv have exactly the same meaning as in a normal C program's main() function. argv[0] contains the name of the plugin (as given in the -P option of the vdr call).

Each plugin has its own set of command line options, which are totally independent from those of any other plugin or VDR itself.

You can use the getopt() or getopt_long() function to process these arguments. As with any normal C program, the strings pointed to by argv will survive the entire lifetime of the plugin, so it is safe to store pointers to these values inside the plugin. Here's an example:


bool cPluginHello::ProcessArgs(int argc, char *argv[]) { // Implement command line argument processing here if applicable. static struct option long_options[] = { { "aaa", required_argument, NULL, 'a' }, { "bbb", no_argument, NULL, 'b' }, { NULL } }; int c; while ((c = getopt_long(argc, argv, "a:b", long_options, NULL)) != -1) { switch (c) { case 'a': option_a = optarg; break; case 'b': option_b = true; break; default: return false; } } return true; }

The return value must be true if all options have been processed correctly, or false in case of an error. The first plugin that returns false from a call to its ProcessArgs() function will cause VDR to exit.


Command line help

Tell me about it...

If a plugin accepts command line options, it should implement the function


virtual const char *CommandLineHelp(void);

which will be called if the user enters the -h option when starting VDR. The returned string should contain the command line help for this plugin, formatted in the same way as done by VDR itself:


const char *cPluginHello::CommandLineHelp(void) { // Return a string that describes all known command line options. return " -a ABC, --aaa=ABC do something nice with ABC\n" " -b, --bbb activate 'plan B'\n"; }

This command line help will be printed directly below VDR's help texts (separated by a line indicating the plugin's name, version and description), so if you use the same formatting as shown here it will line up nicely. Note that all lines should be terminated with a newline character, and should be shorter than 80 characters.


Getting started

Let's get ready to rumble!

If a plugin implements a function that runs in the background (presumably in a thread of its own), or wants to make use of internationalization, it needs to implement the function


virtual void Start(void);

which is called once for each plugin at program startup. Inside this function the plugin must set up everything necessary to perform its task. This may, for instance, be a thread that collects data from the DVB stream, which is later presented to the user via a function that is available from the main menu.

If the plugin doesn't implement any background functionality or internationalized texts, it doesn't need to implement this function.


Main menu entry

Today's special is...

If the plugin implements a feature that the user shall be able to access from VDR's main menu, it needs to implement the function


virtual const char *MainMenuEntry(void);

The default implementation returns a NULL pointer, which means that this plugin will not have an item in the main menu. Here's an example of a plugin that will have a main menu item:


static const char *MAINMENUENTRY = "Hello"; const char *cPluginHello::MainMenuEntry(void) { return tr(MAINMENUENTRY); }

The menu entries of all plugins will be inserted into VDR's main menu right after the Recordings item, in the same sequence as they were given in the call to VDR.


User interaction

It's showtime!

If the user selects the main menu entry of a plugin, VDR calls the function


virtual cOsdMenu *MainMenuAction(void);

which can do one of two things:

It is very important that a call to MainMenuAction() returns as soon as possible! As long as the program stays inside this function, no other user interaction is possible. If a specific action takes longer than a few seconds, the plugin should launch a separate thread to do this.

Setup parameters

Remember me...

If a plugin requires its own setup parameters, it needs to implement the following functions to handle these parameters:


virtual cMenuSetupPage *SetupMenu(void); virtual bool SetupParse(const char *Name, const char *Value);

The SetupMenu() function shall return the plugin's Setup menu page, where the user can adjust all the parameters known to this plugin.

SetupParse() will be called for each parameter the plugin has previously stored in the global setup data (see below). It shall return true if the parameter was parsed correctly, false in case of an error. If false is returned, an error message will be written to the log file (and program execution will continue).
  A possible implementation of SetupParse() could look like this:


bool cPluginHello::SetupParse(const char *Name, const char *Value) { // Parse your own setup parameters and store their values. if (!strcasecmp(Name, "GreetingTime")) GreetingTime = atoi(Value); else if (!strcasecmp(Name, "UseAlternateGreeting")) UseAlternateGreeting = atoi(Value); else return false; return true;

It is important to make sure that the parameter names are exactly the same as used in the Setup menu's Store() function.

The plugin's setup parameters are stored in the same file as VDR's parameters. In order to allow each plugin (and VDR itself) to have its own set of parameters, the Name of each parameter will be preceeded with the plugin's name, as in

hello.GreetingTime = 3

The prefix will be handled by the core VDR setup code, so the individual plugins need not worry about this.

To store its values in the global setup, a plugin has to call the function


void SetupStore(const char *Name, type Value);

where Name is the name of the parameter ("GreetingTime" in the above example, without the prefix "hello.") and Value is a simple data type (like char *, int etc). Note that this is not a function that the individual plugin class needs to implement! SetupStore() is a non-virtual member function of the cPlugin class.

To remove a parameter from the setup data, call SetupStore() with the appropriate name and without any value, as in

SetupStore("GreetingTime");

The VDR menu "Setup/Plugins" will list all loaded plugins with their name, version number and description. Selecting an item in this list will bring up the plugin's "Setup" menu if that plugin has implemented the SetupMenu() function.

Finally, a plugin doesn't have to implement the SetupMenu() if it only needs setup parameters that are not directly user adjustable. It can use SetupStore() and SetupParse() without presenting these parameters to the user.
 

The Setup menu

Have it your way!

To implement a Setup menu, a plugin needs to derive a class from cMenuSetupPage and implement its constructor and the pure virtual Store() member function:


int GreetingTime = 3; int UseAlternateGreeting = false; class cMenuSetupHello : public cMenuSetupPage { private: int newGreetingTime; int newUseAlternateGreeting; protected: virtual void Store(void); public: cMenuSetupHello(void); }; cMenuSetupHello::cMenuSetupHello(void) { newGreetingTime = GreetingTime; newUseAlternateGreeting = UseAlternateGreeting; Add(new cMenuEditIntItem( tr("Greeting time (s)"), &newGreetingTime)); Add(new cMenuEditBoolItem(tr("Use alternate greeting"), &newUseAlternateGreeting)); } void cMenuSetupHello::Store(void) { SetupStore("GreetingTime", GreetingTime = newGreetingTime); SetupStore("UseAlternateGreeting", UseAlternateGreeting = newUseAlternateGreeting); }

In this example we have two global setup parameters (GreetingTime and UseAlternateGreeting). The constructor initializes two private members with the values of these parameters, so that the Setup menu can work with temporary copies (in order to discard any changes if the user doesn't confirm them by pressing the "Ok" button). After this the constructor adds the appropriate menu items, using internationalized texts and the addresses of the temporary variables. That's all there is to inizialize a Setup menu - the rest will be done by the core VDR code.

Once the user has pressed the "Ok" button to confirm the changes, the Store() function will be called, in which all setup parameters must be actually stored in VDR's global setup data. This is done by calling the SetupStore() function for each of the parameters. The Name string given here will be used to identify the parameter in VDR's setup.conf file, and will be automatically prepended with the plugin's name.

Note that in this small example the new values of the parameters are copied into the global variables within each SetupStore() call. This is not mandatory, however. You can first assign the temporary values to the global variables and then do the SetupStore() calls, or you can define a class or struct that contains all your setup parameters and use that one to copy all parameters with one single statement (like VDR does with its cSetup class).


Internationalization

Welcome to Babylon!

If a plugin displays texts to the user, it should implement internationalized versions of these texts and call the function


void RegisterI18n(const tI18nPhrase * const Phrases);

to register them with VDR's internationalization mechanism.

The call to this function must be done in the Start() function of the plugin:


const tI18nPhrase Phrases[] = { { "Hello world!", "Hallo Welt!", "",// TODO "",// TODO "",// TODO "",// TODO "",// TODO "",// TODO "",// TODO "",// TODO "",// TODO "",// TODO }, { NULL } }; void cPluginHello::Start(void) { RegisterI18n(Phrases); }

Each entry of type tI18nPhrase must have exactly as many members as defined by the constant I18nNumLanguages in the file VDR/i18n.h, and the sequence of the various languages must be the same as defined in VDR/i18n.c.
It is very important that the array is terminated with a { NULL } entry!.

Usually you won't be able to fill in all the different translations by yourself, so you may want to contact the maintainers of these languages (listed in the file VDR/i18n.c) and ask them to provide the additional translations.

The actual runtime selection of the texts corresponding to the selected language is done by wrapping each internationalized text with the tr() macro:


const char *s = tr("Hello world!");

The text given here must be the first one defined in the related Phrases entry (which is the English version), and the returned pointer is either a translated version (if available) or the original string. In the latter case a message will be written to the log file, indicating that a translation is missing. Texts are first searched for in the Phrases registered for this plugin (if any) and then in the global VDR texts. So a plugin can make use of texts defined by the core VDR code.


Loading plugins into VDR

Saddling up!

Plugins are loaded into VDR using the command line option -P, as in


vdr -Phello

If the plugin accepts command line options, they are given as part of the argument to the -P option, which then has to be enclosed in quotes:


vdr -P"hello -a abc -b"

Any number of plugins can be loaded this way, each with its own -P option:


vdr -P"hello -a abc -b" -Pdvd -Pmp3

If you are not starting VDR from the VDR source directory (and thus your plugins cannot be found at their default location) you need to tell VDR the location of the plugins through the -L option:


vdr -L/usr/lib/vdr -Phello

There can be any number of -L options, and each of them will apply to the -P options following it.

When started with the -h or -V option (for help or version information, respectively), VDR will automatically load all plugins in the default or given directory that match the VDR plugin naming convention, and display their help and/or version information in addition to its own output.


Building the distribution package

Let's get this show on the road!

If you want to make your plugin available to other VDR users, you'll need to make a package that can be easily distributed. The Makefile that has been created by the call to newplugin provides the target package, which does this for you.

Simply change into your source directory and execute make package:


cd VDR/PLUGINS/src/hello make package

After this you should find a file named like


vdr-hello-0.0.1.tgz

in your source directory, where hello will be replaced with your actual plugin's name, and 0.0.1 will be your plugin's current version number.