UMSP plugin development

From WikiDLXTV
Revision as of 09:42, 7 January 2011 by Shunte (Talk | contribs)

Jump to: navigation, search

This page is under development and may not be very useful --Mad ady 12:51, 16 December 2010 (UTC)

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);

"UPnP AV MediaServer" whose sole purpose is to share content, "UPnP AV MediaRenderer" which renders content and exposes an interface to control the playback, and a "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 WD 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.

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

For a more detailed explanation on how this is achieved refer to the UMSP originating thread presented by Zoster http://wdtvforum.com/main/index.php?topic=4459.0

UMSP has been developed as a web service with all coding in PHP. UMSP is fully integrated into the WDLXTV firmware and the details found on the originating article you'll find regarding configuration of the server on your WD no longer apply.

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

Plugin architecture

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

The UMSP server looks for compliant plugins on your WD, 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.

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

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

tar -cvzf umsp-plugins.tgz snapple.php

mv -f umsp-plugins.tgz /tmp/conf

Copy or move the archive to the /tmp/conf folder, this is a persistent folder in the WD firmware and your archive will be maintained across a reboot. When the WD boots the archive will be automatically unzipped to the /tmp/umsp-plugins folder where UMSP looks for plugin scripts

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

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 WD box.

In this example we register the snapple plugin, we'll develop this plugin throughout this article

<?php

   global $myMediaItems; # server side global containing registered plugins
   # --s-n-a-p-p-l-e-------->
   $myMediaItems[] = array (	
                         'id'             => 'umsp://plugins/snapple',
                         'parentID'       => '0',
                         'dc:title'       => 'SN-Apple - 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 SN-Apple plug-in some badge art
                         'upnp:album_art' => 'http://lh6.ggpht.com/_xJcSFBlLg_Y/TRKJLJ-3xkI/AAAAAAAAAGY/zCaQYm_CgmQ/s200/apple-logo.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 snapple.php, the web server sees this path via the umnsp://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 SN-Apple - 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.

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.




Plugin API

Here is a list of functions that can be used by your plugins, provided by the UMSP server:

  • TODO

Sample plugin

  • TODO - when I learn a bit more about the process :)

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]

Logs

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. More information: http://forum.wdlxtv.com/viewtopic.php?f=3&t=2756&start=0

Troubleshooting

  • 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