Difference between revisions of "UMSP plugin development"

From WikiDLXTV
Jump to: navigation, search
(Added UML class diagram for plugins)
Line 165: Line 165:
  
 
===Plugin Interfaces _pluginMain===
 
===Plugin Interfaces _pluginMain===
 +
 +
[[File:Umsp-interface-uml.png]]
  
 
Once registered the plugin must pass a second level of checks to assess it is compliant with the UMSP service.
 
Once registered the plugin must pass a second level of checks to assess it is compliant with the UMSP service.

Revision as of 00:36, 13 January 2011

So What Is This UMSP Thing Anyway?

UMSP is a service on the WDLXTV firmware that lets you view various types of media on your TV screen, this includes streaming audio and video content from any Internet source.

Each media item or stream requires a different flavor of plugin designed specifically to interface the media source with the WDTV.

You get a number of plugins out of the box, inclusive of SHOUTcast, BBC Audio broadcasts, and even a plugin that enables you to play DVD and CD disks connected to the WDTV.

Additional plugins can be obtained from SVN - the WD source repository, added by developers or even yourself if you care to work a little code.

UMSP Forum

How UMSP works

Universal Plug and Play is a set of computer network protocols from the UPnP Forum. The goals of UPnP are to allow UPnP devices on a network to detect each other and connect seamlessly, and to simplify the implementation of networks in the home (data sharing, communications, and entertainment) and corporate environments.

The UPnP architecture supports zero-configuration, which implies no need for manual configuration on the user's end.

There are typically three types of UPnP AV (Audio/Video) device control protocols (also called profiles);

  1. "UPnP AV MediaServer" whose sole purpose is to share content,
  2. "UPnP AV MediaRenderer" which renders content and exposes an interface to control the playback, and a
  3. "UPnP AV MediaServer ControlPoint" which can detect/find "UPnP AV MediaServers" and browse them to read media from them. A DMP (Digital Media Player) typically only implements a UPnP AV MediaServer ControlPoint, to be able to play files from UPnP AV MediaServers.

The WDTV can act as a UPnP MediaRenderer as you may have observed if you have a UPnP/DLNA compliant device on your network; for example a Windows 7 machine, or you're running a specific UPnP software package like TVersity, Twonky and many others.

These are the items you see listed in the Media Servers section of your WDTV menus.

So how does this come into play with UMSP? Simply put the UMSP is a background process that acts as a UpnP MediaServer self publishing to your WDTV box.

For a more detailed explanation on how this is achieved refer to the UMSP originating thread presented by Zoster

UMSP has been developed as a web service with all coding in PHP. UMSP is fully integrated into the WDLXTV firmware; the details found on the originating article regarding configuration of the server on your WDTV no longer apply - it's all auto-magic.

Given the implementation both the Apache web-server and the UMSP daemon must be active on your WDTV to allow the process to publish content.

And, as you would expect the UMSP server shows up under the Media Servers section of your WDTV menus.

Plugin architecture

Content can be published via one or more complaint UMSP plug-ins, we'll discuss compliance shortly.

The UMSP server looks for compliant plugins on your WDTV, a plugin makes its presence known by initially registering with the UMSP server.

Registration is achieved by adding details to the $myMediaItems server side global.

There are a handful of plugins that do this automatically in the firmware but you can add others in a number of ways - under the hood they're all adding themselves to the $myMediaItems global.

Adding Plugins with umsp.php

You can add your own plugins to the server by creating an archive file umsp-plugins.tgz in the /tmp/conf folder on the WDTV

The archive contains the scripts that implement your plugin, in this example we're adding a script called revolution.php, your plugin may have additional files and/or folder structures - these would be contained within the archive too

 tar -cvzf umsp-plugins.tgz revolution.php
 mv -f umsp-plugins.tgz /tmp/conf


If you've prepared or obtained the archive from another location copy or move the archive to the /tmp/conf folder, this is a persistent folder in the WDTV firmware and your archive will be maintained across a reboot. When the WDTV boots the archive will be automatically unzipped to the /tmp/umsp-plugins folder where UMSP looks for plugin scripts

Along with the archive a script called umsp.php must be provided, this too is located in the /tmp/conf folder.

Note that there is limited space allocated for /tmp/conf so its best not to go too crazy with storing files to this location, the archive and umsp.php file are pretty small so it's doubtful they'll cause issues. There are potential changes in the works that will change where the UMSP plugins can be located and the use of /tmp/conf will no longer be a bottleneck.

The umsp.php script contains one or more entries that add to the server global in effect registering the presence of the plugin. The details provided are attributes that will be published by the UMSP server - they define the attributes of the UPnP entity that may be consumed by any UPnP compliant device capable of rendering said entity - in our case that would be the WDTV box.

In this example we register the revolution plugin we added to the archive, we'll develop this plugin throughout this article

 <?php
   global $myMediaItems; # server side global containing registered plugins
   # --r-e-v-o-l-u-t-i-o-n---b-a-b-y------>
   $myMediaItems[] = array (	
                         'id'             => 'umsp://plugins/revolution',
                         'parentID'       => '0',
                         'dc:title'       => 'REVO-lution - an example UMSP plug-in',
                         'upnp:class'     => 'object.container',
                         # many other attributes are supported that comply to UPnP protocol, these are all optional
                         # here we give our REVO-lution plug-in some badge art
                         'upnp:album_art' => 'http://lh6.ggpht.com/_xJcSFBlLg_Y/TRq3jGGDncI/AAAAAAAAAJ8/A1TLqM9trAI/s200/upnp-items.png',
                     );
 ?>

At a minimum you'll need to provide the plugin with an id, a title and a upnp class.

The id tells the UMSP server where to find your plugin script, in this example it'll look for a script file located under /tmp/umsp-plugins called revolution.php, the web server sees this path via the umsp://plugins/ naming scheme - if you dig you'll see that the plugins folder under the UMSP server implementation is a symlink to the actual umsp-plugins folder

The title is what will be displayed on the UMSP menu, in this case the REVO-lution - an example UMSP plug-in will show up on the initial UMSP menu along with the built in plugins; for those wanting a Hello World example substitute that string now.

Do Not! include any HTMl formatting in the title, this can cause the UMSP menu to freeze - you will see the "blue ring of death" and the only resolution is to reboot.

The upnp:class tells the UPnP complaint device that this entity is a container, basically a folder, that contains additional entities, be they other containers or media items that the device can render. Again if you require lots more details see the UPnP documentation. In UMSP plugin development we normally only deal with the following flavors of upnp:class


upnp:class description
object.container folder of media items, sub-folders etc
object.item.videoItem video steam or file
object.item.audioItem audio steam or file
object.item.imageItem image, photo or other

Be careful when preparing the umsp.php script that you don't introduce any syntax errors. If the UMSP server hits an error in this script it will fail to register any of the UMSP content. If you see the "No Media In Current Folder" message from the root UMSP menu this is likely the cause.

A Quick Introduction To Error Resolution

The WDTV will write to various logs when performing UMSP server processes and it may be useful for you to prepare a script that will dump the tail end of the appropriate logs, the content of the log dumper would look something like this :

dumplog.sh

 #!/bin/sh
 tailLog() { [ -f "$1" ] && echo "----> $1" && tail "$1"; }
 clear
 tailLog /tmp/dmaosd.log
 tailLog /tmp/.root/var/log/php5/error_log
 tailLog /tmp/.root/var/log/apache2/rewrite.log
 tailLog /tmp/.root/var/log/apache2/access.log
 tailLog /tmp/.root/var/log/apache2/other_vhosts_access.log
 tailLog /tmp/.root/var/log/apache2/error.log
 tailLog /tmp/messages.txt
 tailLog /tmp/umsp-log.txt

Running this on a system with a broken umsp.php will show you the problems that the UMSP server has encountered

   ---> /tmp/.root/var/log/php5/error_log
   [09-Jan-2011 13:31:03] PHP Parse error:  syntax error, unexpected T_CONSTANT_ENCAPSED_STRING, expecting ')' in /tmp/conf/umsp.php on line 46

To resolve the problem I edited the umsp.php file finding that I was missing a comma on one $myMediaItems declarations. This was fixed and the UMSP menu tried again, this time with success.

Note that there's no need to reboot after the edit as the script is loaded "live" when the UMSP menu is requested, this is true for anything you modify under the active UMSP plugin code. But, remember that for changes to be persistant across a reboot you have to include the modified code in our archive.

Your plugin can also write to these files to add debug and status information that you or others may find useful, we'll discuss that later in this article.


Plugins From SVN

Plugins that are sourced from SVN use the the same $myMediaItems to register themselves when they are activated. Instead of being unzipped from an archive the plugin code is pulled from SVN on reboot, this is why any code changes you make to these plugins will be lost each time you reboot. Given we're working in a Linux environment there are several mechanisms to make your changes persist - we'll discuss these towards the end of the article.

You can browse the SVN repository, feel free to see whats available and browse through the code for examples if your looking to start development.

It's hoped that over time the UMSP plugin repository will grow, it needs plugin contributions from developers - thats you right!

Once you've enabled a SVN based plugin from the webend configuration, WEC , it can be accessed from your WDTV - no need to reboot to do this as again these are evaluated "live" by the UMSP server.

Alternate Plugin Addition Method

A plugin called Umsp Items can be obtained from SVN. This plugin allows you to "register" your own plugins through a configuration file.

Details on how to use Umsp Items plugin can be found here

UMSP And The Blue Circle Of Death

If you see The Blue Circle Of Death when accessing the UMSP menu it's likely that you've coded an endless loop in your configuration code, or have embedded HTML in your plugin name or description.

Reboot your WDTV from the webend, or via telnet. Correct the issue prior to accessing the UMSP menu.

Hopefully you'll never encounter the problem.


Plugin Interfaces _pluginMain

Umsp-interface-uml.png

Once registered the plugin must pass a second level of checks to assess it is compliant with the UMSP service.

In the main plugin script you must provide at minimum an entry point interface that the UMSP server will use to load the plugins content

When a user selects the plugins menu item the UMSP server loads the plugin script and makes a call to the main plugin entry API - named as we'd expect _pluginMain. The plugin must contain the _pluginMain function.

The intent of the function is to return addition UPnP complaint entities that the WDTV can render and display on your TV; that's about all you need to be complaint, registered in the server side global and have a _pluginMain interface

The _pluginMain should always return an array of items, if no items are returned you'll see the "No Media In Folder" message

Here we see a code snippet from the revolution.php script showing _pluginMain

 <?php
 # _pluginMain must be provided, it is the entry point to publish content for the plugin
 # this will automatically be called when a user select the main menu item (or others)
 # from the UMSP menu
 function _pluginMain($prmQuery)
 {
   parse_str($prmQuery, $queryData); # parse query string to name value pair arguments - this is assumes the '&' delimiter
   if (isset($queryData['menu'])&&($queryData['menu']!=""))
     $items = _pluginCreateItems($queryData['menu]); # call a plugin specific function passing the attribute, see below for details
   else
     $items = _pluginCreateMyItemsList(); # call a plugin specific function to generate a list of renderable items
     return $items; # always return items for the plugin
 } # end function
 ?>

The UMSP server automatically passes any URL query arguments it receives for the respective plugin in through the call so they can be further evaluated. From the initial UMSP menu the query string will be empty. If the plugin needs to evaluate these arguments you should code accordingly.

In the above example when the query string is empty, called from the main menu, the plugin calls a second function that returns a list of UPnP items, these could be additional containers or media items that the WDTV can play and display on your TV screen

Beyond the required naming convention used for the UMSP interface functions you can name your underlying functions whatever you wish, the original plugin authors used the same naming strategy as the required interfaces but its your call, no pun intended, as to whether you follow the same route.

In the above example the main function makes a call to another function called _pluginCreateMyItemsList, from a main menu call, lets take a look at what it provides.

Note that the following are for example purposes, the content may or may not render in your browser.

The example returns UPnP items for video, image, audio, and menu items that can be rendered and played via the WDTV

  # return a couple of media items, a hard coded list of media items for now
  function _pluginCreateMyItemsList()
  {
     $items = array(); # initialize your return array - not mandatory but good practice if your plugin has some complex branching logic
     items[] = array (
        'id'           => 'umsp://plugins/revolution?exampleid=1', # just an example
        'dc:title'     => 'UMSP Isn\'t bad - It won\'t make your head spin',
        # upnp:class - when is not a container there is a resource associated - this is the video, image or audio track
        # here we have a movie for your enjoyment stored locally 
        'res'          => 'file://path/to/video/spinning_earth.avi',
        'upnp:class'   => 'object.item.videoItem',
        'protocolInfo' => 'http-get:*:*:*', # wild-carded - let the UPnP renderer work out the details
     );
     items[] = array (
        'id'           => 'umsp://plugins/revolution?exampleid=2',
        'dc:title'     => 'Media Item is an image - click me',
        'res'          => 'http://veimages.gsfc.nasa.gov/2429/globe_east_2048.jpg',
        'upnp:class'   => 'object.item.imageItem',
        'protocolInfo' => 'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG', # some protocol detail provided this time
     );
     items[] = array (
        'id'           => 'umsp://plugins/revolution?exampleid=3', # just an example
        'dc:title'     => 'REVO-lution Baby!!!',
        'res'          => 'http://v22.nonxt2.googlevideo.com/videoplayback?id=c339b9bb5198af3a&itag=18&begin=0&len=2147483647&app=picasa&et=INVALID&el=INVALID&ip=0.0.0.0&ipbits=0&expire=2871264432&sparams=id,itag,ip,ipbits,expire&signature=317AA7B94080F0A653A84DDF6E326D480EBB42CD.6DFA60B55F3D57D47A8C7AEFCEAA9E38E8D10BDB&key=ck1',
        'upnp:class'   => 'object.item.videoItem',
        'protocolInfo' => 'http-get:*:video/mpeg4:*', # we know the format, see UPnP documentation for additional protocolInfo attributes
     );
     items[] = array (
        'id'           => 'umsp://plugins/revolution?exampleid=4',
        'dc:title'     => 'An audio stream - BBC content',
        'res'          => 'http://downloads.bbc.co.uk/podcasts/radio1/reviewshow/reviewshow_20110105-1214a.mp3',
        'upnp:class'   => 'object.item.audioItem',
        'protocolInfo' => 'http-get:*:audio/mpeg:*', # some protocol detail for an mp3 audio stream
        # items can have thumbnails too!
        'upnp:album_art'=> 'http://lh4.ggpht.com/_xJcSFBlLg_Y/TRSIeDbvcbI/AAAAAAAAAIY/5hBsQUQode0/s200/bbc200x200slate.png',
     );
     # if you're passing arguments format them accordingly, some twists and tricks that we'll discuss shortly
     $dataStr = http_build_query(array('menu' => 'Looking for an Argument'), 'pluginvar_');
     items[] = array (
        'id'           => 'umsp://plugins/revolution?'.$dataStr,
        'dc:title'     => 'Sub-menu - more items here',
        'upnp:class'   => 'object.container',
        'upnp:album_art'=> 'http://lh6.ggpht.com/_xJcSFBlLg_Y/TRq3jGGDncI/AAAAAAAAAJ8/A1TLqM9trAI/s200/upnp-items.png',
     );
     return $items;
  }

The important attributes here are the resource identifier, this is a UNC path to a local or remote resource. It can be as simple as a file or as relatively complex as a URL for a proxied stream with one or more query attributes - a fat URL parameterized to drive downstream functionality.

The protocolInfo informs UPnP how the resource is to be rendered, in most plugins you'll provide as much information as you can up front, you may be lucky and get away with wild-carding the information and let the downstream rendering device work out the details; rule of thumb - if you know what you're dealing with and can provide the information do so up front.

Note for a container that will display a sub-menu we all ourselves passing the arguments that can be evaluated in the _pluginMain call. If you know of a plug that can fulfill some of the functionality you require there's nothing stopping you calling the other plugin as resource too. We'll cover the building blocks provided in the firmware that make plugin development a little easier shortly.

more details to follow here showing the example plugin on the WDTV

example plugin will be published to the forum, code inlining on the wiki isn't too clean

Plugin Interfaces _pluginSearch

The UMSP server also provides an interface call for search functionality, much like _pluginMain it is logically named - _pluginSearch. A user can enter a search with the plugin menu up on screen, the plug needs to be active so that the server can determine who to call when you press search. Under the hood the UMSP server is keeping track of what plugin is currently active.

Much like the _pluginMain call _pluginSearch accept query string attributes, these ill contain the detailed search attributes.

Depending from what main menu the UMSP server was activated, Video, Photo, or Music, the WDTV will display various search options

Main Menu Available Search Options
Video Allows search by Title
Photo Allows search by Title
Music Allows search by Title, Album and Genre

If you don't need that level of detail you can write a generic search that parses for any of these variations using a simple regular expression evaluation; similarly modify the regex to match the level of detail you wish to attain

Once again _pluginSearch returns UPnP items

 function _pluginSearch($prmQuery)
 {
   # here we have a generic regex - just get the search term - we don't care what WDTV search menu was used
   if (preg_match('/and dc:(title|album|genre) contains "(.*?)"/', $prmQuery, $searchterm))
   {
     # do something with the search term - here we pass to Picasa for evaluation 
     $param = array (
       'albumurl'    => urlencode("http://picasaweb.google.com/data/feed/api/all?q={$searchterm[1]}"),
       'propername'  => 'User Search',
     );
     return _pluginCreateMoreItems($param);
   }
   else
     return null;
 }

Additional details on implementing search can be found in Zoster's search how-to

Plugin API

There are a number of scripts and functions within the UMSP server that may help in your development process. Several of these will be discussed.

Plugin Configuration

Include the /usr/share/plugins/funcs-config.php script in your plugin code to access some basic configuration and registration functions

For your plugin to take advantage of the web end configurator, WEC, you need to define a couple of plugin deliverables that will hook into the WEC

The WEC will manage whether the plugin is active as well as other configuration attributes you may need

Refer to the WEC documentation for more details, it can be found here

The info.php script can be included in a number of deliverables, it centralizes the version and descriptive attributes for your plugin that is displayed within various interfaces on the webend and your WDTV

This example comes from the TWiT.tv UMSP plugin

info.php

 <?php
 # meta-name="TWiT.tv Netcasting"
 # meta-desc="TWiT.tv Netcasting UMSP Plug-in"
 # meta-author="shunte"
 # meta-date="2011-01-02"
 # meta-version="0.1.2.2"
 # meta-type="umsp"
 # meta-url="http://forum.wdlxtv.com/viewtopic.php?f=49&t=2648"
 # meta-filename="twittv.php"
 # meta-id="twittv"
 $pluginInfo = array (
   'name'      => 'TWiT.tv Netcasting',
   'desc'      => 'TWiT.tv Netcasting UMSP Plug-in',
   'author'    => 'shunte',
   'date'      => '2011-01-02',
   'version'   => '0.1.2.2',
   'url'       => 'http://forum.wdlxtv.com/viewtopic.php?f=49&t=2648',
   'id'        => 'twittv',
   'thumb'     => 'http://lh5.ggpht.com/_xJcSFBlLg_Y/TQtpX9fuqqI/AAAAAAAAAC4/citAdIhoOVA/s60/twit-logo.png',
   'art'       => 'http://lh5.ggpht.com/_xJcSFBlLg_Y/TQtpX9fuqqI/AAAAAAAAAC4/citAdIhoOVA/s200/twit-logo.png',
 );
 ?>

Note: do not include HTML in any of these attributes as this can cause the UMSP server to freeze from the main menu.

The webend looks for scripts name <pluginame>_wec.php, if we wanted to have TWiT.tv web configurable we'd create a file called twittv_wec.php in the same folder as the plugin code.

This is an example of what that script would look like. You can use the following code as is if all you need is a plugin activation

Example pluginname_wec.php

 <?php
   include_once('/usr/share/umsp/funcs-config.php'); // UMSP configuration functions found here
   include('info.php'); // your plugin info found here
   // Does this WEC version support custom hooks?
   if ((defined('WECVERSION')) && (WECVERSION >= 3)) {
     // Insert badge if we have one
     if ((isset($pluginInfo['thumb']))&&($pluginInfo['thumb']!="")) 
     {

$desc = '<div style="float: left;"><img src="'.$pluginInfo['thumb'].'" width="60" height="60" alt="logo"></div>' .'<div>'.$pluginInfo['name']." v".$pluginInfo['version']." (".$pluginInfo['date'].") by " .$pluginInfo['author'].".<br>".$pluginInfo['desc']."<br>Information: <a href='".$pluginInfo['url']."'>".$pluginInfo['url']."</a>" .'</div>';

   }
   elseif ((isset($pluginInfo['art']))&&($pluginInfo['art']!="")) 
   {

$desc = '<div style="float: left;"><img src="'.$pluginInfo['art'].'" width="60" height="60" alt="logo"></div>' .'<div>'.$pluginInfo['name']." v".$pluginInfo['version']." (".$pluginInfo['date'].") by " .$pluginInfo['author'].".<br>".$pluginInfo['desc']."<br>Information: <a href='".$pluginInfo['url']."'>".$pluginInfo['url']."</a>" .'</div>';

   }
   else
   {

$desc = $pluginInfo['name'].' v'.$pluginInfo['version'].' ('.$pluginInfo['date'].') by ' .$pluginInfo['author'].'.<br>'.$pluginInfo['desc']."<br>Information: <a href='".$pluginInfo['url']."'>".$pluginInfo['url'].'</a>';

   }
   $key = strtoupper("{$pluginInfo['id']}_DESC");
   $wec_options[$key] = array(
     'configname' => $key,
     'configdesc' => $desc,
     'longdesc' => "",
     'group' => $pluginInfo['name'],
     'type' => WECT_DESC,
     'page' => WECP_UMSP,
     'displaypri' => -25,
     'availval' => array(),
     'availvalname' => array(),
     'defaultval' => "",
     'currentval' => ""
   );
   $wec_options[$pluginInfo['id']] = array(
     'configname' => $pluginInfo['id'],
     'configdesc' => 'Enable '.$pluginInfo['name'].' UMSP plugin',
     'longdesc' => "",
     'group' => $pluginInfo['name'],
     'type' => WECT_BOOL,
     'page' => WECP_UMSP,
     'displaypri' => -10,
     'availval' => array('off','on'),
     'availvalname' => array(),
     'defaultval' => "",
     'currentval' => "",
     'readhook' => wec_umspwrap_read,
     'writehook' => wec_umspwrap_write,
     'backuphook' => NULL,
     'restorehook' => NULL
   );
   }
 ?>

You can use HTML here as this is only displayed from the webend.

A config.php script should be provided for backwards compatibility, the old style webend used this mechanism

config.php

 <?php
 include('info.php');
 # _DONT_RUN_CONFIG_ gets set by external scripts that just want to get the pluginInfo array via include() without running any code. Better solution?
 if ( !defined('_DONT_RUN_CONFIG_') ) 
 {
   include_once('/usr/share/umsp/funcs-config.php');
   # Check for a form submit that changes the plugin status:
   if ( isset($_GET['pluginStatus']) )
     $writeResult = _writePluginStatus($pluginInfo['id'], $_GET['pluginStatus']);
   # Read the current status of the plugin ('on'/'off') from conf
   $pluginStatus = _readPluginStatus($pluginInfo['id']);
   # New or unknown plugins return null. Add special handling here:
   # Special handling can check for configuration attributes that the plugin requires to run
   # for example a configuration file or configuration keys used to activate a plugin
   # Example the ReleaseLog plugin code check if the authorization keys have been defined by the user
   # and activate the plugin accordingly
   if ( $pluginStatus === null )
     $pluginStatus = 'off';
   # _configMainHTML generates a standard plugin dialog based on the pluginInfo array:
   $retHTML = _configMainHTML($pluginInfo, $pluginStatus);
   echo $retHTML;
   # Add additonal HTML or code here
   # _configMainHTML doesn't return end tags so add them here:
   echo '</body>';
   echo '</html>';
 }
 ?>

Note you won't need this file if your intent is to only use the plugin for yourself, if you wish to share your plugin with the WDTV community and wish to make the plugin available via SVN it's best to provide the info.php. config.php and <pluginname>_wec.php in your deliverables.

Logging

Include the /usr/share/plugins/funcs-log.php script in your plugin code to access some basic logging functions, capture exceptions or simply instrument your code for easier debugging and improved user experience.

Logging is written to the /tmp/umsp-log.txt file by default, you can override this if you wish

include via

 include_once('/usr/share/plugins/funcs-log.php');

usage :

 // set the logging level, one of L_ALL, L_DEBUG, L_INFO, L_INFO, L_WARNING, L_ERROR, L_OFF
 // this is a server side global and can be set anywhere in your code, the default is L_ERROR
 // here we're requesting logging at all levels
 global $logLevel = L_ALL;
 // OPTIONAL :: override the default log - play nice and switch back when you're done
 global $logFile;
 $saveLogFile = $logFile; // so we can set back to default when done
 $logFile = '/tmp/mnt/somepath/mylog.txt'
 // OPTIONAL :: you can specify "who" you are by setting the ident, the log script will automatically test who called it and define this for you
 global $logIdent = 'WDPicasa';

call anyone of the following functions in your code to achieve granular levels of log reporting

 // debug level logging, if log level is L_ALL or L_DEBUG these messages will be written to the log
 function _logDebug ($someText, $someVar = null)
 // debug level logging, if log level is L_ALL, L_DEBUG or L_INFO these messages will be written to the log
 function _logInfo ($someText, $someVar = null)
 // debug level logging, if log level is L_ALL, L_DEBUG, L_INFO or L_WARNING these messages will be written to the log
 function _logWarning ($someText, $someVar = null)
 // debug level logging, if log level is L_ALL, L_DEBUG, L_INFO, L_WARNING or L_ERROR these messages will be written to the log
 function _logError ($someText, $someVar = null)

Additional functions will be added over time

Sample plugin

The sample plugin we've discussed in this article can be found in this forum thread here

Passing Multiple Arguments To A Plugin

If you're only dealing with a single argument being passed by a sub-menu UMSP item then you can use the following construct in your code

Example :

     $data = array('menu' => 'onlyonearg');
     $dataStr = http_build_query($data, 'pluginvar_');
     items[] = array (
        'id'           => 'umsp://plugins/myplugin?'.$dataStr,
        'dc:title'     => 'My Plugin with One Argument',
        'upnp:class'   => 'object.container',
     );

If have more than one argument it's been observer that the command parser in the UMSP server can run into problems and it generates bad XML used in the UPnP communication

To get around this we use a variation of the http_build_query call where we specify our own delimiter

Example :

     $data = array (
       'menu' => 'one argument',
       'movie' => $movie_url,
       'another' => 'another argument, more than one for sure',
     );
     # add the secret sauce so parser plays nice
     $dataStr = http_build_query($data,"",'&');
     items[] = array (
        'id'           => 'umsp://plugins/myplugin?'.$dataStr, # "safe" query string here
        'dc:title'     => 'My Plugin with More Arguments',
        'upnp:class'   => 'object.container',
     );

In the _pluginMain function of our plugin we should deal with this

Example :

 function _pluginMain($prmQuery)
 {
   $queryData = array();
   // deal with our own secret sauce delimiter in our query string
   if (strpos($prmQuery,'&')!==false) $prmQuery=str_replace('&','&',$prmQuery);
   // and parse as "normal"
   parse_str($prmQuery, $queryData);
   // specific plugin calls developing UPnP items go here
   // these calls can accept any number of arguments passed to the plugin
 }

That may seem a little redundant but I promise it'll save you time and many headaches developing your plugins

Code you might find useful

The following code snippets may be useful in your development, they may work as is or require some specification on your part

Using /conf/config

The following function pulls details from configurations and returns a plugin specific configuration variable Substitute and add your own configuration keys specific to your plugin

 function getConfigData()
 {
   $config = file_get_contents('/conf/config');
   if preg_match('/PLUGIN_CONFIG_KEY1=\'(.+)\'/', $config, $m);
     $plugin_config_1 = $m[1];
   if preg_match('/PLUGIN_CONFIG_KEY2=\'(.+)\'/', $config, $m);
     $plugin_config_2 = $m[1];
   if preg_match('/PLUGIN_CONFIG_KEY3=\'(.+)\'/', $config, $m);
     $plugin_config_3 = $m[1];
   # return the config details
   return array (
     'my_config1' => $plugin_config_1,
     'plugin_config12 => (($plugin_config_2=="")?'default':$plugin_config_2),
     'my_config3' => $plugin_config_3,
   );
 }

Note that calling the /usr/bin/get_config utility is rather slow, this is the sister function to set_config you may have used to add configuration data to /conf/config

In testing its been found to be ~4 to 5 times slower than using the method described above

Results of getting one configured entry 10 times via :

 <?php
 function getMicroTime()
 {
   list($usec, $sec) = explode(' ',microtime());
   return ((float)$usec + (float)$sec);
 }
 echo "config via /usr/bin/get_config\n";
 $intime1 = getMicroTime();
 for($z=1;$z<=10;$z++) 
 {
   $intime = getMicroTime();
   $out = shell_exec('/usr/bin/get_config WDTV_SN');
   print 'Value WDTV_SN is := '.trim($out)."\n";
   $time = getMicroTime() - $intime;
   print 'get_config test :'.round($time,2)." seconds.\n";
 }
 $time = getMicroTime() - $intime1;
 print 'get_config 10 reps :'.round($time,2)." seconds.\n";
 echo "config via PHP file_get_contents and pgrep_match\n";
 $intime1 = getMicroTime();
 for($z=1;$z<=10;$z++)
 {
   $intime = getMicroTime();
   $config = file_get_contents('/conf/config');
   if(preg_match('/WDTV_SN=\'(.+)\'/', $config, $out))
     print 'Value WDTV_SN is := '.trim($out[1])."\n";
   $time = getMicroTime() - $intime;
   print 'file_get_contents test :'.round($time,2)." seconds.\n";
 }
 $time = getMicroTime() - $intime1;
 print 'file_get_contents 10 reps :'.round($time,2)." seconds.\n";
 ?>

Yield the following results :

 config via /usr/bin/get_config
 Value WDTV_SN is := WNV193807ZZZ
 get_config test :0.06 seconds.
 Value WDTV_SN is := WNV193807ZZZ
 get_config test :0.06 seconds.
 Value WDTV_SN is := WNV193807ZZZ
 get_config test :0.18 seconds.
 Value WDTV_SN is := WNV193807ZZZ
 get_config test :0.06 seconds.
 Value WDTV_SN is := WNV193807ZZZ
 get_config test :0.28 seconds.
 Value WDTV_SN is := WNV193807ZZZ
 get_config test :0.1 seconds.
 Value WDTV_SN is := WNV193807ZZZ  
 get_config test :0.08 seconds.
 Value WDTV_SN is := WNV193807ZZZ
 get_config test :0.05 seconds.
 Value WDTV_SN is := WNV193807ZZZ
 get_config test :0.05 seconds.
 Value WDTV_SN is := WNV193807ZZZ
 get_config test :0.07000000000000001 seconds.
 get_config 10 reps :1 seconds.
 config via PHP file_get_contents and pgrep_match
 Value WDTV_SN is := WNV193807ZZZ
 file_get_contents test :0 seconds.
 Value WDTV_SN is := WNV193807ZZZ
 file_get_contents test :0 seconds.
 Value WDTV_SN is := WNV193807ZZZ
 file_get_contents test :0 seconds.
 Value WDTV_SN is := WNV193807ZZZ
 file_get_contents test :0 seconds.
 Value WDTV_SN is := WNV193807ZZZ
 file_get_contents test :0 seconds.
 Value WDTV_SN is := WNV193807ZZZ
 file_get_contents test :0 seconds.
 Value WDTV_SN is := WNV193807ZZZ
 file_get_contents test :0 seconds.
 Value WDTV_SN is := WNV193807ZZZ
 file_get_contents test :0 seconds.
 Value WDTV_SN is := WNV193807ZZZ
 file_get_contents test :0 seconds.
 Value WDTV_SN is := WNV193807ZZZ
 file_get_contents test :0 seconds.
 file_get_contents 10 reps :0.02 seconds.

Not a huge impact but every little helps!

Do I have a Thumbdrive?

The following function looks for an attached thumbdrive, handy if you want to make a cache for your plugin or store configuration details somewhere other than /conf If you have defined the UMSP_CONFIG_LOCATION then this will be used

 function getStick()
 {
   # check if user has supplied a config location
   $config = file_get_contents('/conf/config');
   if (preg_match('/UMSP_CONFIG_LOCATION=\'(.+)\'/', $config, $m))
   {
     if(trim($m[1])!="")
       return $m[1];
   }
   # no "manually" defined config, look for thumbdrive
   $stick = null;
   $tests =array('.wd_tv','.wdtvext-plugins','wdtvlive.bin','root.bin','umsp-user-config-here'); # last is our own magic key, device it is on must be writeable
   foreach ($tests as $test) 
   {
     $res = shell_exec("find '/tmp/mnt/' -name '$test'");
     if(trim($res)!="")
     {
       $stick = str_replace($test,"",trim($res));
       break;
     }
   }
   # check for null in caller and default as appropriate
   return $stick;
 }

Make A Cache Folder

Makes a folder that all processes can write to, create the folder where you specify

 function makeCacheFolder($folder)
 {
   if (!file_exists($folder)) 
   {
     // make cache folder to store cached images and hash table (assuming in same directory)
     $oldumask = umask(0);
     @mkdir($folder, 0777);
     umask($oldumask);
   }
 }

These and other functions to be incorporated in the UMSP codebase for use by developers

Plugins with proxies

A proxy is by definition an entity that behaves like the real target server, but it's not. Proxies are useful for when you need to do some extra steps in order to get the data you want, but you want to do this transparently for other processes.

Basically a proxy gets called like any other media file. The MediaPlayer process will query the proxy like it's the end server and will request details about the media that is to be played (e.g. Content-length), and it will also request the media file (or bits and pieces of it).

Umsp-proxy-msc.png

In the MSC ([[1]]) above a few interactions are shown between the player, the proxy and the server.

The player first makes a request to the "server" (which is the proxy plugin) in order to find more about the media it's supposed to play. The parameters of every query between the MediaPlayer and the Proxy are sent as HTTP header entries and can be accessed in the PHP variable $_SERVER. The first query usually asks for the server's header in order to get the media's Content-length and to see if streaming is supported by the server.

Here's a sample view of the $_SERVER variable in a serialized way for the first query:

a:28:{
 s:9:"UNIQUE_ID";s:24:"TSBV538AAAEAAA6qC-sAAAAB";
 s:10:"SCRIPT_URL";s:67:"/umsp/plugins/youtube-subscriptions/youtube-subscriptions-proxy.php";
 s:10:"SCRIPT_URI";s:83:"http://127.0.0.1/umsp/plugins/youtube-subscriptions/youtube-subscriptions-proxy.php";
 s:9:"HTTP_HOST";s:12:"127.0.0.1:80";
 s:32:"HTTP_GETCONTENTFEATURES_DLNA_ORG";s:1:"1";
 s:26:"HTTP_TRANSFERMODE_DLNA_ORG";s:9:"Streaming";
 s:15:"HTTP_USER_AGENT";s:49:"INTEL_NMPR/2.1 DLNADOC/1.50 dma/3.0 alphanetworks";
 s:4:"PATH";s:103:"/apps/vim/bin:/apps/mc/bin:/apps/iftop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
 s:16:"SERVER_SIGNATURE";s:115:"<address>Apache/2.2.15 (Debian) PHP/5.3.2-1 with Suhosin-Patch mod_scgi/1.13 Server at 127.0.0.1 Port 80</address>";
 s:15:"SERVER_SOFTWARE";s:67:"Apache/2.2.15 (Debian) PHP/5.3.2-1 with Suhosin-Patch mod_scgi/1.13";
 s:11:"SERVER_NAME";s:9:"127.0.0.1";
 s:11:"SERVER_ADDR";s:9:"127.0.0.1";
 s:11:"SERVER_PORT";s:2:"80";
 s:11:"REMOTE_ADDR";s:9:"127.0.0.1";
 s:13:"DOCUMENT_ROOT";s:8:"/var/www";
 s:12:"SERVER_ADMIN";s:19:"webmaster@localhost";
 s:15:"SCRIPT_FILENAME";s:75:"/var/www/umsp/plugins/youtube-subscriptions/youtube-subscriptions-proxy.php";
 s:11:"REMOTE_PORT";s:5:"40702";
 s:17:"GATEWAY_INTERFACE";s:7:"CGI/1.1";
 s:15:"SERVER_PROTOCOL";s:8:"HTTP/1.0";
 s:14:"REQUEST_METHOD";s:4:"HEAD";
 s:12:"QUERY_STRING";s:20:"video_id=FPfiY33nqgo";
 s:11:"REQUEST_URI";s:88:"/umsp/plugins/youtube-subscriptions/youtube-subscriptions-proxy.php?video_id=FPfiY33nqgo";
 s:11:"SCRIPT_NAME";s:67:"/umsp/plugins/youtube-subscriptions/youtube-subscriptions-proxy.php";
 s:8:"PHP_SELF";s:67:"/umsp/plugins/youtube-subscriptions/youtube-subscriptions-proxy.php";
 s:12:"REQUEST_TIME";i:1293964775;
 s:4:"argv";a:1:{i:0;s:20:"video_id=FPfiY33nqgo";}s:4:"argc";i:1;}

Most notable in this query is the use of REQUEST_METHOD=HEAD (to avoid asking for the whole file). The proxy script should ask for the video file (also with a HEAD query) and should return back the server's HTTP headers. The proxy should add an extra header as Content-Disposition: attachment; filename="video.flv" if none is provided (change the filename name and extension appropriately).

Based on the first query, the MediaPlayer could make several more requests in order to get more information. One such query might be "play from the start":

a:28:{
 s:9:"UNIQUE_ID";s:24:"TSBV6H8AAAEAAAjPBNEAAAAA";
 s:10:"SCRIPT_URL";s:67:"/umsp/plugins/youtube-subscriptions/youtube-subscriptions-proxy.php";
 s:10:"SCRIPT_URI";s:83:"http://127.0.0.1/umsp/plugins/youtube-subscriptions/youtube-subscriptions-proxy.php";
 s:9:"HTTP_HOST";s:12:"127.0.0.1:80";
 s:14:"CONTENT_LENGTH";s:1:"0";
 s:26:"HTTP_TRANSFERMODE_DLNA_ORG";s:9:"Streaming";
 s:15:"HTTP_USER_AGENT";s:49:"INTEL_NMPR/2.1 DLNADOC/1.50 dma/3.0 alphanetworks";
 s:4:"PATH";s:103:"/apps/vim/bin:/apps/mc/bin:/apps/iftop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
 s:16:"SERVER_SIGNATURE";s:115:"<address>Apache/2.2.15 (Debian) PHP/5.3.2-1 with Suhosin-Patch mod_scgi/1.13 Server at 127.0.0.1 Port 80</address>";
 s:15:"SERVER_SOFTWARE";s:67:"Apache/2.2.15 (Debian) PHP/5.3.2-1 with Suhosin-Patch mod_scgi/1.13";
 s:11:"SERVER_NAME";s:9:"127.0.0.1";
 s:11:"SERVER_ADDR";s:9:"127.0.0.1";
 s:11:"SERVER_PORT";s:2:"80";
 s:11:"REMOTE_ADDR";s:9:"127.0.0.1";
 s:13:"DOCUMENT_ROOT";s:8:"/var/www";
 s:12:"SERVER_ADMIN";s:19:"webmaster@localhost";
 s:15:"SCRIPT_FILENAME";s:75:"/var/www/umsp/plugins/youtube-subscriptions/youtube-subscriptions-proxy.php";
 s:11:"REMOTE_PORT";s:5:"40705";
 s:17:"GATEWAY_INTERFACE";s:7:"CGI/1.1";
 s:15:"SERVER_PROTOCOL";s:8:"HTTP/1.0";
 s:14:"REQUEST_METHOD";s:3:"GET";
 s:12:"QUERY_STRING";s:20:"video_id=FPfiY33nqgo";
 s:11:"REQUEST_URI";s:88:"/umsp/plugins/youtube-subscriptions/youtube-subscriptions-proxy.php?video_id=FPfiY33nqgo";
 s:11:"SCRIPT_NAME";s:67:"/umsp/plugins/youtube-subscriptions/youtube-subscriptions-proxy.php";
 s:8:"PHP_SELF";s:67:"/umsp/plugins/youtube-subscriptions/youtube-subscriptions-proxy.php";
 s:12:"REQUEST_TIME";i:1293964776;
 s:4:"argv";a:1:{i:0;s:20:"video_id=FPfiY33nqgo";}s:4:"argc";i:1;}

Here you can see that the Content-Length is set to zero (we're not sending anything), the Request-method is GET. The proxy should reply to the MediaPlayer with the header received from the server, adding Content-Disposition (as above) and passing the output from the proxy to the server (e.g. using fpassthru($fp))


If the user decides to navigate inside the video, the MediaPlayer process will receive DLNA commands (like MediaPlay(64 1)) telling it to jump to a specific point in time (e.g. 64 seconds into the video). Fortunately, the MediaPlayer process will convert the time it receives to a specific byte offset in the media stream and will make a request to the proxy to fetch data starting from that offset (using HTTP Ranges requests - see http://labs.apache.org/webarch/http/draft-fielding-http/p5-range.html). If the server supports ranges, it will return data from that offset, and it's the MediaPlayer's job to render it. The proxy just has to forward the request to the server.

Here is a request for such a jump, starting from offset 238192480 (238MB into the video):

a:28:{
 s:9:"UNIQUE_ID";s:24:"TSBYSX8AAAEAAAjPBNIAAAAA";
 s:10:"SCRIPT_URL";s:67:"/umsp/plugins/youtube-subscriptions/youtube-subscriptions-proxy.php";
 s:10:"SCRIPT_URI";s:83:"http://127.0.0.1/umsp/plugins/youtube-subscriptions/youtube-subscriptions-proxy.php";
 s:9:"HTTP_HOST";s:12:"127.0.0.1:80";
 s:10:"HTTP_RANGE";s:16:"bytes=238192480-";
 s:26:"HTTP_TRANSFERMODE_DLNA_ORG";s:9:"Streaming";
 s:15:"HTTP_USER_AGENT";s:49:"INTEL_NMPR/2.1 DLNADOC/1.50 dma/3.0 alphanetworks";
 s:4:"PATH";s:103:"/apps/vim/bin:/apps/mc/bin:/apps/iftop/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
 s:16:"SERVER_SIGNATURE";s:115:"<address>Apache/2.2.15 (Debian) PHP/5.3.2-1 with Suhosin-Patch mod_scgi/1.13 Server at 127.0.0.1 Port 80</address>";
 s:15:"SERVER_SOFTWARE";s:67:"Apache/2.2.15 (Debian) PHP/5.3.2-1 with Suhosin-Patch mod_scgi/1.13";
 s:11:"SERVER_NAME";s:9:"127.0.0.1";
 s:11:"SERVER_ADDR";s:9:"127.0.0.1";
 s:11:"SERVER_PORT";s:2:"80";
 s:11:"REMOTE_ADDR";s:9:"127.0.0.1";
 s:13:"DOCUMENT_ROOT";s:8:"/var/www";
 s:12:"SERVER_ADMIN";s:19:"webmaster@localhost";
 s:15:"SCRIPT_FILENAME";s:75:"/var/www/umsp/plugins/youtube-subscriptions/youtube-subscriptions-proxy.php";
 s:11:"REMOTE_PORT";s:5:"45030";
 s:17:"GATEWAY_INTERFACE";s:7:"CGI/1.1";
 s:15:"SERVER_PROTOCOL";s:8:"HTTP/1.0";
 s:14:"REQUEST_METHOD";s:3:"GET";
 s:12:"QUERY_STRING";s:20:"video_id=FPfiY33nqgo";
 s:11:"REQUEST_URI";s:88:"/umsp/plugins/youtube-subscriptions/youtube-subscriptions-proxy.php?video_id=FPfiY33nqgo";
 s:11:"SCRIPT_NAME";s:67:"/umsp/plugins/youtube-subscriptions/youtube-subscriptions-proxy.php";
 s:8:"PHP_SELF";s:67:"/umsp/plugins/youtube-subscriptions/youtube-subscriptions-proxy.php";
 s:12:"REQUEST_TIME";i:1293965385;
 s:4:"argv";a:1:{i:0;s:20:"video_id=FPfiY33nqgo";}s:4:"argc";i:1;}

The difference between this request and the request above (the one for playback) is that now "Content-length=0" is no longer set and there's a new parameter called "HTTP_RANGE" that tells the server exactly where to go. The proxy should forward these parameters to the server and should return the content to the player just like above.

Note: You can enable debugging in the youtube-subscriptions-proxy.php (switch on $want_debug) and you will get more detailed debug output in /var/log/php5/error_log. The output will also show the headers that are sent and received from communicating with the server, and it will show what gets sent back to the MediaPlayer client.

Note2: Some relevant links about this subject: [2], [3], [4]

Additional Logging Techniques

You can log your errors/warnings/etc to /var/www/php5/error_log by using

 error_log("whatever you're trying to log");

As a best practice rule you can add a script name and a sevrity to help with filtering later on:

 error_log("yt-subscriptions-proxy: INFO: whatever you're trying to log");

It is recommended (for your own troubleshooting ease) to enable logs only when you need them and disable them (except for errors) before commiting to SVN.

The same log file will hold syntax errors and other PHP errors.

Many deliverables with the WDLXTV firmware utilize logging to the /tmp/messages.txt file

This is achieved using the OS level logger utility

 Usage: logger [OPTION]... [MESSAGE]
 Write MESSAGE to the system log. If MESSAGE is omitted, log stdin.
 Options:
       -s      Log to stderr as well as the system log
       -t TAG  Log using the specified tag (defaults to user name)
       -p PRIO Priority (numeric or facility.level pair)

Add logger calls in your PHP code like so :

 exec("logger -t pluginnamehere 'This is my log message'")


More information: http://forum.wdlxtv.com/viewtopic.php?f=3&t=2756&start=0

Troubleshooting

Your plugin can be tested most readily via the WDTV menus, should you encounter problems you may wish to apply some debugging techniques to determine whats going wrong

There are a number of deliverables you can take advantage of to help with this debugging process

  • A very simple test wrapper can be used to exercise your plugin code, an example :
 <?php
 function getMicroTime()
 {
   list($usec, $sec) = explode(' ',microtime());
   return ((float)$usec + (float)$sec);
 }
 $test = 'revolution'; # plugin name here
 # make sure we have the code we wish to evaluate
 # this assumes you're working from your thumbdrive
 # I do this a lot as on reboot I know my code will be safe
 shell_exec('mkdir /tmp/umsp-plugins/'.$test);
 shell_exec('cp -fR '.__DIR__.'/'.$test.'/* /tmp/umsp-plugins/'.$test.'/');
 # path to the plugin to "debug"
 # usually one or the other of these flavors
 #$cmdtest = '/tmp/umsp-plugins/'.$test.'/'.$test.'.php';
 $cmdtest = '/tmp/umsp-plugins/'.$test.'.php';
 # include the plugin code
 include_once($cmdtest);
 echo "$cmdtest\n";
 # start
 print "TEST BEGINS>>>\n";
 $r= array();
 #
 for($z=1;$z<=3;$z++)
 {
   $i=substr($r[0]['id'],strpos($r[0]['id'],'?')+1,strlen($r[0]['id']));
   print "IN TEST>>>\n$i\n";
   $intime = getMicroTime();
   if(strpos($i,'title contains "')!==false)
     $r=_pluginSearch($i);
   else 
     $r=_pluginMain($i);
   $time = getMicroTime() - $intime;
   print_r($r);
   print "\n$i\nTest [$test] Menu Drilldown [$z] ".round($time,2)." seconds.\n<<<IN TEST\n";
 }
 print "<<<<TEST ENDS\n";
 # exercise the WEC script - checks for syntax errors
 #define('WECVERSION',4.99);
 #include ("/tmp/umsp-plugins/$test/{$test}_wec.php");
 #var_dump($wec_options);
 ?>

The rig will drill down through upto 3 levels of menus that your plugin exposes and dumps the media items that are returned

  • You can test out and troubleshoot UMSP plugins (including proxies) by using the built-in umsp-test.php script. Point your browser to http://wdtvlive-IP/umsp/umsp-test.php
  • User PaulF submitted a test script for UMSP plugins that can show output and errors. Save the file as /tmp/www-tmp/umsp2html.php:

http://forum.wdlxtv.com/viewtopic.php?f=3&t=2756#p21358. On your PC browse to: http://wdtvlive-IP/tmp/umsp2html.php?path=0

  • You can start the MediaPlayer process (which handles playback) in debug mode to observe what's going on. Details here: http://forum.wdlxtv.com/viewtopic.php?f=53&t=517#p3287. You will be getting a lot of debug output including the DLNA commands for skipping, pausing, etc:
 killall MediaLogic
 MediaLogic AV MSGL_DBG