pUPnP – A PHP UPnP media controller

At the moment I’m working on an opensource UPnP client which allows to see media sources and renderer in two columns next to each other and makes it simply to specify which media should be streamed to which device.

The idea was born because I’ve recognized that I’ve a few UPnP compatible devices at home and I wanted to be able to control them from my tablet PC.

I know there are many UPnP clients for iOS and Android but none of them made me happy. In most cases you have to choose source and renderer as first step before you can start to browse your media.
But I wanted to switch the renderer without navigating to my desired location again.

At the moment I’ve got following devices:

  • 1 Raspberry PI with RaspBMC plugged to my TV in the (living room)
  • 1 Raspberry PI with Raspbian as mediarenderer using „gmediarender“ (bedroom)
  • 1 Hama IR200 Internet Radio (kitchen)
  • 1 PogoPlug as Media Source for XBMC

I’ve manually installed an FTP-Server on my pogoplug to be able to connect XBMC to it – to lean how to do this you can check out this guide.
The application is hosted at the Raspberry PI in my bedroom.

The backend for pUPnP media controller is written in PHP5 and requires the PHP GD-Library to resize images and Curl to communicate with the devices.

At least PHP5.3 is required regarding to the use of namespaces.

The frontend is plain HTML, CSS and JavaScript.
External libraries are jQuery for easier handling with JavaScript (Ajax) and Lightbox2 for image preview directly in the page.
Additional I’ve used some JavaScript functions from http://phpjs.org/.

There’s no use for any database because the devices gets identified by the standardized UPnP multicast message.

You can browse the current source code at Github.

Next time I’ll explain something about the technical arguments and preparations for this project.

24 Gedanken zu „pUPnP – A PHP UPnP media controller

  1. Da Ro

    php cronjob.php
    DISCOVER:Array
    (
    [0] => stdClass Object

    But, if I open the website, I get only:

    Source Loading devices
    Destination Loading devices

    forever.

    Any Idea?

    Antworten
    1. Mario Klug Artikelautor

      Hi,
      can you please send me the whole output of „php cronjob.php“?

      If the printed object is empty it seems that there’s no UPnP compatible devices in your network answering the discover message, but I’d like to get sure about.

      Antworten
  2. Miam Miam

    I tried to get pupnp running with MiniDLNA and I got the same error than Ra Do: Source and Destination loading forever.

    Firefox sais:
    [22:49:34.992] GET http://my.ip/backend.php?device=uuid:my-uuid&action=getChilds&ObjectID=0 [HTTP/1.1 200 OK 11ms]
    [22:49:34.954] SyntaxError: missing ) in parenthetical @ http://my.ip/res/js/pupnp-backend.js:72

    The output of cronjob.php is:
    ...
    [uuid:my-uuid] => Array
    (
    [name] => MyMiniDLNAOnUbuntu
    [services] => Array
    (
    [0] => ContentDirectory
    [1] => ConnectionManager
    [2] => X_MS_MediaReceiverRegistrar
    )

    [icons] => Array
    (
    [0] => stdClass Object
    (
    [mimetype] => image/png
    [width] => 48
    [height] => 48
    [depth] => 24
    [url] => http://nn.nn.nn.nn:8200/icons/sm.png
    )

    [1] => stdClass Object
    (
    [mimetype] => image/png
    [width] => 120
    [height] => 120
    [depth] => 24
    [url] => http://nn.nn.nn.nn:8200/icons/lrg.png
    )

    [2] => stdClass Object
    (
    [mimetype] => image/jpeg
    [width] => 48
    [height] => 48
    [depth] => 24
    [url] => http://nn.nn.nn.nn:8200/icons/sm.jpg
    )

    [3] => stdClass Object
    (
    [mimetype] => image/jpeg
    [width] => 120
    [height] => 120
    [depth] => 24
    [url] => http://nn.nn.nn.nn:8200/icons/lrg.jpg
    )

    )

    [protocols] => Array
    (
    )

    )
    ...

    Any idea what’s going wrong?

    Antworten
    1. Mario Klug Artikelautor

      Hi,
      thanks for your feedback.

      It seems that your issue is different than Ra Do’s.
      Your cronjob.php is finding your MiniDLNA instance and you should see this device in your source dropdown (on the left side).

      The Ajax-call for the directory listing (&action=getChilds) fails, but unfortunately I can’t tell what’s going wrong without the output.
      I’ve added a new Logline in revision a412a7b806455d3d561e26b45db2118ee48369d9. Can you please do me a favor and check out the new version, try it again and send me the Log?

      You can find it at [webroot]/pupnp/[year]/[month]/[day]/AjaxHandler.log.

      Also the output of http://my.ip/backend.php?device=uuid:my-uuid&action=getChilds&ObjectID=0 would be very interresting.

      Sorry in general for the problems, but this is a private project and has never been tested outside my environment – but with your help I’m shure we’ll get the problems sorted out.

      Antworten
  3. Miam Miam

    At First: Thank you for your quick response. I’m aware of the private and young state of the project. But also happy you did what you did, because none of the other projects I tested suits my needs. And if I get yours running it may be what I’m searching for.

    You are right. My devices are listed and after selecting one nothing more happens.

    Output of http://my.ip/backend.php?device=uuid:my-uuid&action=getChilds&ObjectID=0:
    {"error":"Unknown service: ContentDirectory"}

    Content of [webroot]/pupnp/[year]/[month]/[day]/AjaxHandler.log:
    ...
    2013-05-27 12:44:26 - nn.nn.nn.nn - [DEBUG] construct()
    2013-05-27 12:44:26 - nn.nn.nn.nn - [DEBUG] at\mkweb\upnp\backend\AjaxHandler::call
    2013-05-27 12:44:26 - nn.nn.nn.nn - [DEBUG] at\mkweb\upnp\backend\AjaxHandler::getDevices
    2013-05-27 12:44:26 - nn.nn.nn.nn - [DEBUG] at\mkweb\upnp\backend\AjaxHandler::respond
    2013-05-27 12:44:26 - nn.nn.nn.nn - [INFO] Response: {"uuid:my-uuid":{"name":"MiniDLNAonUbuntu","services":["ContentDirectory","ConnectionManager","X_MS_MediaReceiverRegistrar"],"icons":[{"mimetype":"image\/png","width":"48","height":"48","depth":"24","url":"http:\/\/nn.nn.nn.nn:8200\/icons\/sm.png"},{"mimetype":"image\/png","width":"120","height":"120","depth":"24","url":"http:\/\/nn.nn.nn.nn:8200\/icons\/lrg.png"},{"mimetype":"image\/jpeg","width":"48","height":"48","depth":"24","url":"http:\/\/nn.nn.nn.nn:8200\/icons\/sm.jpg"},{"mimetype":"image\/jpeg","width":"120","height":"120","depth":"24","url":"http:\/\/nn.nn.nn.nn:8200\/icons\/lrg.jpg"}],"protocols":[]},"uuid:my-uuid":{"name":"Fritz!Box","services":["urn:schemas-any-com:service:aura:1"],"icons":[],"protocols":[]},"uuid:my-uuid":{"name":"Fritz!Box","services":["urn:schemas-any-com:service:Any:1"],"icons":[{"mimetype":"image\/gif","width":"118","height":"119","depth":"8","url":"http:\/\/nn.nn.nn.nn:49000\/ligd.gif"}],"protocols":[]},"uuid:my-uuid":{"name":"TwonkyOnQnap","services":["ConnectionManager","ContentDirectory","X_MS_MediaReceiverRegistrar"],"icons":[{"mimetype":"image\/jpeg","height":"48","width":"48","depth":"24","url":"http:\/\/nn.nn.nn.nn:9000\/images\/twonkyicon-48x48.jpg"},{"mimetype":"image\/jpeg","height":"120","width":"120","depth":"24","url":"http:\/\/nn.nn.nn.nn:9000\/images\/twonkyicon-120x120.jpg"},{"mimetype":"image\/png","height":"48","width":"48","depth":"24","url":"http:\/\/nn.nn.nn.nn:9000\/images\/twonkyicon-48x48.png"},{"mimetype":"image\/png","height":"120","width":"120","depth":"24","url":"http:\/\/nn.nn.nn.nn:9000\/images\/twonkyicon-120x120.png"}],"protocols":[]}}
    2013-05-27 12:44:26 - nn.nn.nn.nn - [DEBUG] construct()
    2013-05-27 12:44:26 - nn.nn.nn.nn - [DEBUG] at\mkweb\upnp\backend\AjaxHandler::call
    2013-05-27 12:44:26 - nn.nn.nn.nn - [DEBUG] at\mkweb\upnp\backend\AjaxHandler::getFavorites
    2013-05-27 12:44:26 - nn.nn.nn.nn - [DEBUG] at\mkweb\upnp\backend\AjaxHandler::respond
    ...

    If there is more I can do, just let me know.

    Antworten
    1. Mario Klug Artikelautor

      Sorry for the late response this time.

      I’ve found the issue, it was the missing directory cache/devices in my repository.
      You can create it by your own and it should work or you can pull the new revision fec18a28bc735a4726a8a42fcbfc63dad1fe86fb.

      Antworten
  4. Mark

    Dear Mario,

    I am impressed by the work you published, a very well organized library, neatly commented and very general from nature.
    I could not get it to work directly, my startscreen kept on searching. However, I modified things step by step in order to implement in the application I am working on.
    I stumbled on two issues, one of which I think is a bug and the other which may be considered as a bug too.
    1.
    In backend there is Client.php. In function „call“ there is the header definition. One of the entries here is:
    ‚SOAPACTION: „‚ . $this->service->getId() . ‚:1#‘ . $method . ‚“‚,
    To my opinion this should be:
    ‚SOAPACTION: „‚ . $this->service->getType() . ‚#‘ . $method . ‚“‚

    It returns a slightly different value but the original does not work.

    2.
    Same file, „getRequest“ function.
    There are two lines like:
    if(array_key_exists(‚RequestedCount‘, $data) && $data[‚RequestedCount‘] == null) $data[‚RequestedCount‘] = 0;
    Basically putting default zero value on two items. In my opinion we can add a similar line to also put a default zero for „ObjectID“. At least the „Browse“ service does not work without an entry. Well, you can argue that the user has to provide a few required values anyway, so this is not truly a bug. But it would make life easier I think.

    Succes with the good work!

    Mark

    Antworten
    1. Mario Klug Artikelautor

      Hi Mark,
      congratulations to get it running and thank you very much for telling me your changes.

      I’ll take a closer look tomorrow and apply them if everything is fine.

      Best regards
      Mario

      Antworten
  5. Mark

    Dear Mario,

    I am slowly progressing with your library and am still surprised about the quality of it. I have a number of remarks / questions however.

    1. upnp.php, function getDevices. This function looks in the cache if the file ‚devices.serialized‘ exists. If so, it reads the devices from the file and continues. As far as I understand this file is made the first time you start the code and all devices which were present at that time stay there for ever. If new devices apear, they will never end up in the list, unless you first manually remove the file. Am I right or do I overlook something? If I am correct, I think there needs to be some routine to keep the list updated, e.g. everytime a new instance of upnp is created. If you agree that this is so, I will try to find a solution for this.

    2. same function, getDevices. I have a device which upon discovery returns a „dummy.xml“ file (I have no idea for what reason). However the programs crash on this, on an error thrown by ServiceXMLParser, function parse($path), line 219. I found a workaround by putting the call to this function, which is in __construct, in a try-catch with an empty catch routine:
    try{
    $this->parse($this->scpdUrl);
    }
    catch (Exception $e){ }
    For me this works, however it is maybe not the optimal solution. I wanted to share with you however that apparantly it is possible to come accross empty xml files.

    3.
    Just a question: eventing. I want to play a number of songs one after the other (an album e.g.). I am able to play the first song and subscribe to the service. And if all works well the service (AVTransport) of the mediarenderer will call event.php at state change. But from here I get lost. How to continue with the next song? Song data which is known in the „original program“ (basically a website with javascript) is unknown to event.php. You have a good idea here? Sending a „get“ request to the javascript?

    Brgds,

    Mark

    Antworten
    1. Mario Klug Artikelautor

      Hi Mark,
      I’m very happy about your interrest and contribution. Thanks for that.

      About your Questions.
      1) It’s correct, all found devices gets stored in devices.serialized and read from it. But this file is overriden every time findDevices is called. This is because of the ‚w‘ as second parameter for fopen in https://github.com/mkweb/pupnp/blob/master/src/at/mkweb/upnp/backend/UPnP.php#L179.

      2) Indeed, there should much more error handling be included 🙁 I’ve planned this since a while but haven’t found enought free time. Ignoring all parsing error is not the optimal solution but better than everything crashes. Just to get shure I’ll test it with my devices within the next days and implement it if everything runs well.

      3) I’ve asked myself the same question about 1 year ago. My best Idea was the actual implemented but I was never very happy with it and it’s also not working very well with every device.
      The idea is the playlist you can create in the frontend. If everything works like expected everything you describe will happen. The event.php file calls Device::receivedEvent(). If STOPPED is received Playlist::next() gets called here:
      https://github.com/mkweb/pupnp/blob/master/src/at/mkweb/upnp/backend/Device.php#L447

      This is the point where Playlist::next() fakes a ajax to send the next file to the device.
      https://github.com/mkweb/pupnp/blob/master/src/at/mkweb/upnp/backend/Playlist.php#L245

      Thanks againd and I hope I was able to answer you questions.

      Cheers,
      Mario

      Antworten
  6. Mark

    Dear Mario,

    1). Ok, during startup of application (website) I now explicitely call „findDevices“ before „getDevices“ to refresh the list.
    2). –
    3). Yeah, it must work in some way like you sketch. I tried to work without eventing, using javascript „setTimeOut“ to run through a playlist, but this is very nasty and does not work at all if you want to play more than 2 songs. So I have to get this eventing to work. For no I am able to play one song and suscribe for eventing, I get a proper response header. However, upon completion of a song „event.php“ is never called. I am not so well into http requests etc but can’t it be that event.php does not „listen“? I am wondering if it is possible to let the callback be to my „website“ (client side javascript). If that works, there is no need to store a playlist on the server’s disk, saving some time for opening and saving and possibly also some time due to reduced overhead. However, I have no idea if I talk rubish here.

    I will kepp you updated if I have something useful.

    Best regards,

    Mark

    Antworten
    1. Mario Klug Artikelautor

      Hi Mark,

      1). Ok, during startup of application (website) I now explicitely call „findDevices“ before „getDevices“ to refresh the list.

      Indeed, that’s an idea but it causes very long loading times. Is there really an offline device in devices.serialized after findDevices() in your setup?

      2). –

      Sorry, I’ve forgotten to apply the changes – will try to do it today, if not then tomorrow.

      3). Yeah, it must work in some way like you sketch. I tried to work without eventing, using javascript „setTimeOut“ to run through a playlist, but this is very nasty and does not work at all if you want to play more than 2 songs. So I have to get this eventing to work. For no I am able to play one song and suscribe for eventing, I get a proper response header. However, upon completion of a song „event.php“ is never called. I am not so well into http requests etc but can’t it be that event.php does not „listen“? I am wondering if it is possible to let the callback be to my „website“ (client side javascript). If that works, there is no need to store a playlist on the server’s disk, saving some time for opening and saving and possibly also some time due to reduced overhead. However, I have no idea if I talk rubish here.

      Processing the playlist with javascript on client site was also my first thought. If I remember correct the code should be anywhere within pupnp.js or pupnp-gui.js.
      But I see a huge problem because when you close the browser tab the playlist will end. Therefor I’ve added the server side way.
      The event.php file is not really listening. The listening job is in general done by your webserver and this is forwarding requestes for event.php to the file. So it’s neccessary that the renderer (the devices playing the audio or video) knows where your event.php is located. Within the UPnP protocol this is done using a „CALLBACK“- Header in the subscription request.

      https://github.com/mkweb/pupnp/blob/master/src/at/mkweb/upnp/backend/Client.php#L214

      So in your case my next step would be to check which URL the tool is sending to your renderer and then you can check if it’s correct and reachable for the renderer.

      I am wondering if it is possible to let the callback be to my „website“ (client side javascript).

      In general it’s possible using websockets. To use them you have to get a server side script running (as daemon) which listens to a random port (not 80, there’s the webserver listening). When it’s running you can connect to this port using javascript and, different to ajax, the connection stays open as long as you need it. The webserver (the daemonized server file) then is able to inform the javascript about any changes instantly without the need of any polling.

      I had my thoughts in websockets during development but I haven’t used them for some reasons:

      • It’s a HTML5 Feature and therefor not supported by older browsers
      • You have to daemonize a file – This is not possible on a shared webspace without shell access.
      • The file needs a free port where it can listen to. So if you want to use the tool and allready have anysoftware listening to this port you’d have to change the port on server and on client site. I simply don’t wanted to force potential users to do so.
      • Most restictive firewalls only allow a handfull of ports. If you want to use the tool in such network you have to change your firewall and I think that’s ugly

      Hope this helps you a bit out for your next steps.

      Regards
      Mario

      Antworten
  7. Mark

    Dear Mario,

    Thanks for your reply again. Regarding eventing: I came to the same conclusion that eventing and playlist handling should be server side. So I gorget about javascript etc. Your solution with event.php should be just perfect. The CALLBACK seems correct to me. I get a valid return from the renderer upon subscription. Also, if I hit the event.php address in a browser directly it gets called, telling me that the CALLBACK address is correct. But I never get an event message from the renderer at all (at least not that I am aware of). But I have to say, I have limited knowledge on this topic, I would like to be able to monitor network traffic between renderer and server e.g. but I did not succeed so far.

    Best regards again!

    Mark

    Antworten
    1. Mario Klug Artikelautor

      Hm… that’s bad. Have you tried a second renderer? The UPnP implementation of one of mine (a Hama IR2000 Internet Radio) is very crap. It can play audio and nothing else. At least Volume control is not working. This one is also ignoring my subscriptions. When I ask using the debug tool it tells me that there is one but I’ve never seen a real request from this device.

      Monitoring the outgoing traffic from your renderer is really a bit tricky, I’ve realised this using my PC as router to monitor traffic over the interface. But that’s very hard to explain.
      But it’s not that difficult to watch the traffic on server side (in this case your server pUPnP is running on) if the renderer really sends requests.
      On Linux with apache you can check your /var/log/apache2/access.log (if not changed by you) for incoming requests. If you can find any request you can log the send data with e.g.
      < ?php file_put_contents('/tmp/content.log', file_get_contents('php://input'));
      If you want it more detailed (but more complex) give WireShark a try.

      Good Luck and if I may ask, please share the result ;)

      Antworten
  8. Mark Couwenberg

    Dear Mario,

    The log file of Apache was a good hint, thanks! A quick test shows me that at least there is some traffic. Below a snippet which is logged upon subscription and “play” (slightly modified by me to enhance readability:

    Renderer: “NOTIFY /Onkyo/event%2ephp HTTP/1.1″ 200 176 “-” “Mediabolic-……” (subscription?)
    Renderer: “NOTIFY /Onkyo/event%2ephp HTTP/1.1″ 200 73 “-” “Mediabolic-…” (subscription?)
    Renderer: “NOTIFY /Onkyo/event%2ephp HTTP/1.1″ 200 73 “-” “Mediabolic-…” (subscription?)
    Server: “GET /Onkyo/UPnP.php?cmd=Play&arg1= etc etc” (the play command is send to the renderer, which starts playing)
    Renderer: “NOTIFY /Onkyo/event%2ephp HTTP/1.1″ 200 73 “-” “Mediabolic…” (immediately after play command, returns correct play event.
    Renderer: “NOTIFY /Onkyo/event%2ephp HTTP/1.1″ 200 73 “-” “Mediabolic-…” (same as previous)

    Summary: I get a proper NOTIFY upon subscription and at start of playing. However at the end of a song I do not receive anything.

    Anyhow, this was just a short test. Later I will be able to better investigate. I don’t blame the renderer for a poor implementation. It’s an Onkyo device which comes with a good Android app which streams perfectly well and also general-purpose UPnP apps stream well (after some hassle with connection and setting the right input source though). I assume it must be me doing something wrong here.

    Keep you posted!

    Best regards,

    Mark

    Antworten
  9. Mark

    Dear Mario,

    I proceeded one step again. I found that in fact eventing was working well but in Client.php, function „subscribe“ the timeout was set to 180 seconds, which is too short for playing songs. I now put it like this:

    ‚TIMEOUT: Second-7200‘,

    Best regards,

    Mark

    Antworten
      1. azfar

        Hi,
        Thanks for sharing this script you have done really a wonderful job appreciate your work. I am able to run your application but under devices i am getting only 2 devices listed pupnp online radio & pupnp online radio. But I was expecting other sharing devices listed here. Is something wrong ? do i have to change something?

        Antworten
        1. Mario Klug Artikelautor

          Thanks for the nice words and apoligies, this is a new feature I’m planning but it’s far from working correct. I’ve accidently pushed it to github.
          Removed this commit and it should work if you download it again or do a „git pull origin master“.

          Antworten
  10. Albert

    I was wondering if you still was working on your program.
    Just found it a few day’s ago and i was amazed about your code.
    It’s really good and not working.
    But wit a lot of debugging it’s partly working can play music.
    I have it running on windows 2008 iis server can you believe it?
    I use Twonky as server and Philips NP1100 as player i have to change a lot in your soap calls but the core was there and very good working.
    The java code for the player that is not ok you stop the player when there is no song title that is something i changed completely.
    I will focus on the play list ans a volume button.
    But wanted to thank you for the code and sharing it .
    Best regards Albert.

    Antworten
    1. Mario Klug

      Hi Albert,
      big apologize for overlooking your comment, for some reasons I must have missed the mail and there was no other comment since today.

      Sound really good what you’ve done and I’m very happy that someone can use my code for his project. May I ask if there’s a way to see what you’ve made out of it?

      Antworten
  11. Patrick

    Hi,

    i wonder if someone is reading this… last comment is quite some time ago.

    I was searching for a way to send Audio to my Busch-Jäger in-wall radio and control it from any device over a web frontend.
    A minidlna server has been setup that is confirmed working and is found by your pUPnP frontend. Also the in-wall radio appears in the list of selectable destinations.
    Problem is: as soon as i select the mindlna as source a running clock appears and thats it.

    The log says:

    2015-02-24 11:29:33 – 192.168.1.103 – [INFO] Response: {„error“:“Unknown service: ContentDirectory“}
    2015-02-24 11:29:33 – 192.168.1.103 – [INFO] Response: {„error“:“Unknown service: ContentDirectory“}
    2015-02-24 11:29:35 – 192.168.1.103 – [INFO] Response: {„current“:null,“items“:[]}
    2015-02-24 11:29:36 – 192.168.1.103 – [INFO] Response: {„error“:“Unknown service: AVTransport“}
    2015-02-24 11:29:38 – 192.168.1.103 – [INFO] Response: {„error“:“Unknown service: AVTransport“}

    Antworten
    1. Mario Klug

      Hi Patrick,
      thanks for your comment.

      I’m reading them but due to I have a fulltime job, (fortunately) enought client jobs and also a whole server/application-infrastructure to adminstrate I’d really like to but can’t find free time to support my open source projects 🙁 It’s planned to work over this library for much more then a year and I will do it – but I can’t give any promise when this will be done.

      Nevermind, if you send me your logs I’ll try to find out what’s going wrong and fix the issue.

      Antworten

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.