/whsUpnp360

UPnP MediaServer using Intels SDK for Windows Home Server and Xbox 360

http://www.brains-N-brawn.com/whsUpnp360 1/23/2008 casey chesnut

comment(s) 

Introduction

'owning the living room' is all about content. my main usage scenario for media is an Xbox 360 in the living room and a Windows Home Server (WHS) in the computer room. this is accomplished using UPnP (Universal Plug & Play), more specifically UPnP AV (Audio and Video). the 360 is a UPnP MediaRenderer and WHS is a MediaServer. as a customer this works pretty good; but as a developer i wanted to know what was going on and try to customize it for my own usage scenarios. this article will step through writing a custom UPnP MediaServer to run on WHS. NOTE the result isnt really meant for end users

UPnP AV

UPnP is a set of standards that specify protocols for devices to communicate with each other. for UPnP AV, it includes the specs for MediaRenderers and Media Server. examples of MediaRenderers are the Xbox 360, PS3, and Roku SoundBridge. these devices can connect to MediaServers to play content anywhere in your house. WMP11 is an example of a software MediaRenderer. to get content to these devices, there must be a MediaServer. the MediaServers in my house include WMP11, Zune software, WHS, Infrant (NetGear) ReadyNas, and TVersity.  WMP11 is unique in that it can be both. it can serve content to other devices, as well as play content from other devices.

currently, the Xbox 360 is the only UPnP MediaRenderer that i use. i use it for music, pictures, and video. although it still needs more codec support, it allows me to access most of my media. it has mostly eliminated my usage of Media Center Edition (MCE). of course there are some things about it that i want to change, which will be discussed throughout the article.

i've tried a # of different MediaServers to get content to the 360.

out of those, i mainly use WHS for supported formats and TVersity for internet feeds and unsupported formats. i want both to support more features ... the problem is there aren't any developer hooks. so its time to write my own MediaServer

Intel UPnP SDK

since UPnP is a standard, there is a bunch of plumbing that is already implemented. the obvious choices i saw were to use Intels or Microsofts stack. Intel provides a really nice set of tools and they provide code generation for developers. although they seem to favor native developers, it can generate C# code too. NOTE the Intels stack is unsupported. Microsofts implementation is all COM based. for my first look at UPnP, the Intel stack seemed more approachable, so that is what i chose. now that i've got some footing, i would probably use Microsofts stack if i were to revisit UPnP in the future.

the first step is to get Intels UPnP tools and just become familiar with what they offer. i played around with this to browse all my UPnP devices and see what services they offered. mainly i used them to compare/contrast different MediaServers. NOTE TVersity will not be visible to the tools because its Discovery packet isnt recognized as SSDP (Simple Service Discovery Protocol).

1) UPnP Service

now its time to gen some code. install Intels Digital Home Device Code Wizard. launch the Device Builder. click Edit - Add Device. you will only have 1 device and that will be your MediaServer. click on the Device node and edit its settings. change its Root Device Type to urn:schemas-upnp-org:device:MediaServer:1 which marks it as a MediaServer. specifically for the Xbox 360, you need to make its ModelName begin with 'Windows Media Connect'. if you don't do this, then the Xbox 360 will not discover your MediaServer. yep ... that's a bunch of crap, the 360 really needs to change that. my ReadyNAS actually had UPnP support a long time ago, and should have worked with the 360 since then, but it did not because of the 360s naming rules. also make the FriendlyName to be something like 'YourName : 1' ... or the 360 wont recognize it. everything else can be set as you see fit.

now Right-Click on the Device node and 'Add Service from Network'. the dialog will popup and list all UPnP devices. WHS and WMP11 both expose 3 identical services ... so that is probably a good start. select each service, add it to your device, and rename it to something other than ImportedService. NOTE for tips about 360 UPnP compatibility check here : jems - XBox360NotesBox360Notes

now select File-Export Stack. change the Target Platform to C#. set an Output Path. and click the big button to Generate code. this creates a Console application and stub code for the 3 services that you added to your device. it also references the UPnP.dll which is Intels core code for UPnP plumbing. NOTE the UPnP.dll included with Intels Tools is a bit older than the UPnP.dll included with Device Builder ... make sure to use the latest.

the code generation creates a class for each service. each class has the properties and actions (aka methods) defined that need to be implemented. there is also a SampleDevice.cs class that has the metadata for the device. if samples were generated then the code needs to be changed to not call the dummy events in SampleDevice.cs. search for ServiceName_ActionName and change the underscore (_) to a dot (.). e.g. change ConnectionManager_GetCurrentConnectionIDs to ConnectionManager.GetCurrentConnectionIDs. now the code will call the Actions in the specified service class.

build the code (i used VS 2008), and you should be able to run the console. it will startup, and start a thread listening for incoming UPnP requests. it will also send out some UDP notifications that it is an available device. IIRC, at this point, you can fire up the Xbox 360 and if you go into Media blade - select Pictures/Videos/Music - and then hit X to 'change source' your MediaServers name should show up in the list, so long as its FriendlyName and ModelName are correct. it wont work beyond that, because we have to implement the code to serve media first. NOTE you will have to open your firewall so the MediaServer can get incoming requests.

1.0 Discovery

on the 360, when you select to 'change source' the 360 sends out a UPnP broadcast on your networks subnet to a specific UPnP port. all the MediaServers are listening on this port and will send a series of messages back to the 360 that look like the following.

HTTP/1.1 200 OK
ST:urn:schemas-upnp-org:service:ContentDirectory:1
USN:uuid:5cf6dde1-9e3b-476f-8e81-622bbe556645::urn:schemas-upnp-org:service:ContentDirectory:1
Location:http://192.168.0.199:2869/upnphost/udhisapi.dll?content=uuid:5cf6dde1-9e3b-476f-8e81-622bbe556645
OPT:"http://schemas.upnp.org/upnp/1/0/"; ns=01
01-NLS:61e19ba739c777be7796cbd67b08c224
Cache-Control:max-age=900
Server:Microsoft-Windows-NT/5.1 UPnP/1.0 UPnP-Device-Host/1.0
Ext:

HTTP/1.1 200 OK
ST:urn:schemas-upnp-org:service:ContentDirectory:1
Location:http://192.168.0.199:41955/device.xml
USN:uuid:8b75c4e0-ce46-462c-a4bd-b0185902b2a1::urn:schemas-upnp-org:service:ContentDirectory:1
Server:Microsoft-Windows-NT/5.1 UPnP/1.0 UPnP-Device-Host/1.0
Cache-Control:max-age=1800
Ext:

this tells the 360 that the device is a MediaServer and its network location. NOTE i changed the Intel UPnP device stack to make it look more like WHS and WMP11. the 360 can then make an HTTP GET request for the Devices metadata (/devices.xml in this case), and the MediaServer will return its metadata and the Services it implements.

<root xmlns="urn:schemas-upnp-org:device-1-0">
   <specVersion>
      <major>1</major>
      <minor>0</minor>
   </specVersion>
   <device>
      <UDN>uuid:8b75c4e0-ce46-462c-a4bd-b0185902b2a1</UDN>
      <friendlyName>whsUpnp360 (TOSHIBA-CORE2) : 1</friendlyName>
      <deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>
      <manufacturer>brains-N-brawn.com</manufacturer>
      <manufacturerURL>http://www.brains-N-brawn.com/</manufacturerURL>
      <modelName>Windows Media Connect BLAH</modelName>
      <modelNumber>2.0</modelNumber>
      <modelURL>http://www.brains-n-brawn.com/</modelURL>
      <serialNumber>BNB_UPNP_01</serialNumber>
      <serviceList>
         <service>
            <serviceType>urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1</serviceType>
            <serviceId>urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar</serviceId>
            <SCPDURL>_urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar_scpd.xml</SCPDURL>
            <controlURL>_urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar_control</controlURL>
            <eventSubURL>_urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar_event</eventSubURL>
         </service>
         <service>
            <serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType>
            <serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>
            <SCPDURL>_urn:upnp-org:serviceId:ConnectionManager_scpd.xml</SCPDURL>
            <controlURL>_urn:upnp-org:serviceId:ConnectionManager_control</controlURL>
            <eventSubURL>_urn:upnp-org:serviceId:ConnectionManager_event</eventSubURL>
         </service>
         <service>
            <serviceType>urn:schemas-upnp-org:service:ContentDirectory:1</serviceType>
            <serviceId>urn:upnp-org:serviceId:ContentDirectory</serviceId>
            <SCPDURL>_urn:upnp-org:serviceId:ContentDirectory_scpd.xml</SCPDURL>
            <controlURL>_urn:upnp-org:serviceId:ContentDirectory_control</controlURL>
            <eventSubURL>_urn:upnp-org:serviceId:ContentDirectory_event</eventSubURL>
         </service>
      </serviceList>
   </device>
</root>

if that passes the 360s naming conventions, then your device will be listed as an available media source.

1.1 X_MS_MediaReceiverRegistrar

after initial Discovery, when the 360 requests to Browse or Search content from the MediaServer, it will also call this service. it will call the IsAuthorized and IsValidated actions. my implementation returns 1 (true) for both. the 3rd Action throws an Exception, but that hasnt come up yet

POST /_urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar_control HTTP/1.1
User-Agent: Xbox/2.0.6683.0 UPnP/1.0 Xbox/2.0.6683.0
Connection: Keep-alive
Host:192.168.0.199
SOAPACTION: "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1#IsAuthorized"
CONTENT-TYPE: text/xml; charset="utf-8"
Content-Length: 304

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
   <s:Body>
      <u:IsAuthorized xmlns:u="urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1">
         <DeviceID></DeviceID>      
      </u:IsAuthorized>
   </s:Body>
</s:Envelope>

HTTP/1.1 200 OK
EXT:
Server:Windows NT/5.0, UPnP/1.0, Intel CLR SDK/1.0
Content-Type:text/xml ; charset="utf-8"
Content-Length: 366

<?xml version="1.0" encoding="utf-8"?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
   <s:Body>
      <u:IsAuthorizedResponse xmlns:u="urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1">
         <Result>1</Result>
      </u:IsAuthorizedResponse>
   </s:Body>
</s:Envelope>
POST /_urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar_control HTTP/1.1
User-Agent: Xbox/2.0.6683.0 UPnP/1.0 Xbox/2.0.6683.0
Connection: Keep-alive
Host:192.168.0.199
SOAPACTION: "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1#IsValidated"
CONTENT-TYPE: text/xml; charset="utf-8"
Content-Length: 302

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
   <s:Body>
      <u:IsValidated xmlns:u="urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1">
         <DeviceID></DeviceID>      
      </u:IsValidated>
   </s:Body>
</s:Envelope>

HTTP/1.1 200 OK
EXT:
Server:Windows NT/5.0, UPnP/1.0, Intel CLR SDK/1.0
Content-Type:text/xml ; charset="utf-8"
Content-Length: 364

<?xml version="1.0" encoding="utf-8"?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
   <s:Body>
      <u:IsValidatedResponse xmlns:u="urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1">
         <Result>1</Result>
      </u:IsValidatedResponse>
   </s:Body>
</s:Envelope>

1.2 ConnectionManager

this service also has 3 Actions. GetCurrentConnectionIDs always returns 0. GetCurrentConnectionInfo throws an Exception. GetProtocolInfo returns a comma separated list of Mime types ... which makes it interesting. the 360 also exposes the ConnectionManager service with GetProtocolInfo. in theory, the WHS could query the 360 (or any other MediaRenderer) to know what files to return when being Browsed or Searched. i.e. WHS could automatically find out that the 360 supports MP4 and return those files. suppose the 360 adds MPEG-2 support through the next update, then WHS could discover this change, and instantly start serving those files without requiring a server update. it would probably be updated later on for metadata support, but it could start serving the files immediately without accurate metadata. NOTE i dont have any traces of these Actions being called by the 360 ... it might not be calling them at all?

1.3 ContentDirectory

the final service has 5 Actions. for GetSearchCapabilities and GetSortCapabilities i'm just returning an empty string. GetSystemUpdateID always returns 0. Browse and Search are where my code is. Browse is called when traversing Folders for Pictures and Videos. Search is called for Music and when generating playlists for Pictures. the response from these queries includes a DIDL (Digital Item Declaration Language) formatted string. this is a simple XML format for returning folder info, and media info, including metadata.

the Browse request from the 360 has a bug. its request passes a ContainerID parameter instead of ObjectID. this was causing an exception to be thrown in the Intel SDK during reflection. to get around this, i used Reflector to export the UPnP.dll so i could modify its code. even though the Intel SDK is unsupported and they generate code for you, they do not provide the code for the core libraries ... at least its not obfuscated. ended up modifying it in a number of places (marked by //MOD). some of the modifications are quick hacks to get it to play nice with the 360.

1.3.1 Pictures

started out trying to get Pictures functionality to work. when you select Pictures, the 360 sends a Browse request. the ContainerID (16) designates that its requesting Pictures.

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
   <s:Body>
      <u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
<ContainerID>16</ContainerID>
<BrowseFlag>BrowseDirectChildren</BrowseFlag>
<Filter>dc:title,res,res@resolution</Filter>
<StartingIndex>0</StartingIndex>
<RequestedCount>1000</RequestedCount>
<SortCriteria>-upnp:class,+dc:title</SortCriteria>
      </u:Browse>
   </s:Body>
</s:Envelope>

the MediaServer response follows.

<?xml version="1.0" encoding="utf-8"?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
   <s:Body>
      <u:BrowseResponse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
         <Result>___ENCODED_DIDL_LITE___</Result>
         <NumberReturned>2</NumberReturned>
         <TotalMatches>2</TotalMatches>
         <UpdateID>0</UpdateID>
      </u:BrowseResponse>
   </s:Body>
</s:Envelope>

the most important part is the Result element which contains the DIDL. it is escaped XML, that is unescaped below. the surrounding Soap envelope isnt interesting, so we'll mainly just look at the DIDL-Lite from now on. the DIDL below shows that 2 folders were returned (Pictures and Pictures2). they are folders because of their upnp:class of storageFolder. also they have their own unique container ID.

<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">
   <container id="16.0" restricted="1" parentID="16">
      <dc:title>Pictures</dc:title>
      <upnp:class>object.container.storageFolder</upnp:class>
   </container>
   <container id="16.1" restricted="1" parentID="16">
      <dc:title>Pictures2</dc:title>
      <upnp:class>object.container.storageFolder</upnp:class
   ></container>
</DIDL-Lite>

if the user of the 360 were to select the Pictures folder, then it would send the following request. its ContainerID has changed to request the content of the Pictures directory.

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
   <s:Body>
      <u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
<ContainerID>16.0</ContainerID>
<BrowseFlag>BrowseDirectChildren</BrowseFlag>
<Filter>dc:title,res,res@resolution</Filter>
<StartingIndex>0</StartingIndex>
<RequestedCount>1000</RequestedCount>
<SortCriteria>-upnp:class,+dc:title</SortCriteria>
      </u:Browse>
   </s:Body>
</s:Envelope>

the folder contained 2 images, so 2 <item> elements are returned. each item has its own unique identifier. its upnp:class designates it as a photo, and its <res> element specifies its path and metadata info. NOTE the resolution for my app is hardcoded to always return 500x500 (the 360 doesnt care). NOTE if the Pictures directory had contained a subdirectory, then it would have returned a combination of <container> and <item> elements. 

<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">
   <item id="16.0_0" parentID="16.0" restricted="1">
      <dc:title>100_0564.JPG</dc:title>
      <res protocolInfo="http-get:*:image/jpeg:*" resolution="500x500">http://192.168.0.199:41955/pictures/16.0_0.JPG</res>
      <upnp:class>object.item.imageItem.photo</upnp:class>
   </item>
   <item id="16.0_1" parentID="16.0" restricted="1">
      <dc:title>100_0565.JPG</dc:title>
      <res protocolInfo="http-get:*:image/jpeg:*" resolution="500x500">http://192.168.0.199:41955/pictures/16.0_1.JPG</res>
      <upnp:class>object.item.imageItem.photo</upnp:class>
   </item>
</DIDL-Lite>

since the response was only <item> elements, the 360 will then follow up and make a GET request for each image item using its <res> value and display a thumbnail on screen. if you select a thumbnail, it will display the full sized image. it does not need to re-request the image, since it already requested the full sized image and scaled it down.

if the response had contained both <container> and <item> elements, then the 360 would only display the folders but not the images. i'm not sure why it doesnt display folders first and then images below the folders?

that is Browsing images. if you select to 'Play Slideshow' then the 360 will make a Search request. since the ContainerID is 16, we know that this search is for all available images (instead of constrained to a child folder). the RequestedCount is set to 500, meaning it only wants a max of 500 <item> elements returned ... but you can ignore that. i've returned up to 5000 images without any problems, although trying to return 10000+ images resulted in a network timeout.

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
   <s:Body>
      <u:Search xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
<ContainerID>16</ContainerID>
<SearchCriteria>(upnp:class = "object.item.imageItem.photo")</SearchCriteria>
<Filter>dc:title,res,res@resolution</Filter>
<StartingIndex>0</StartingIndex>
<RequestedCount>500</RequestedCount>
<SortCriteria>+@parentID,+dc:title</SortCriteria>
      </u:Search>
   </s:Body>
</s:Envelope>

the response DIDL will be just like the DIDL response for Browse. except for Search, you should only return <item> elements. the 360 will then make a GET request for each of those images and play a slideshow.

A. Multiple folders

this is slightly better than the WHS MediaServer because it allows you to select more than one folder that contains images.

B. Zipped Image Archives

this is the reason i wrote the app in the first place. i dont store loose image files. all my images are zipped. this makes it easier to archive and copy over the network. plus i try to reduce the # of files that i keep on my network storage. the problem is none of the MediaServers handle zipped images. so i extended the picture functionality to handle zips. when it encounters a zip file, it returns a <container> item for the zip file. if the 360 user selects that folder, then the MediaServer uses SharpZipLib to read the file contents of that zip file and returns a list of <items> for each image that it archives. when the 360 requests an image resource, then the image stream is extracted from the zip and returned as if it were just a loose file that existed on the file system.

C. Other Ideas

could also be extended to work with Image feeds and Image Urls (like TVersity). or other apps that take feeds and generate images for display on the 360. e.g. it could retrieve your local weather info from a service, and then dynamically generate an image so you could check the weather forecast on your 360. yep ... the 360 really needs a web browser

1.3.2 Videos

the next step was to get Video playback to work. its slightly easier than Pictures in that you only need to implement Browse, but serving large files is more difficult. first, the 360 will make a request for Video content defined as ContainerID 15.

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
   <s:Body>
      <u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
<ContainerID>15</ContainerID>
<BrowseFlag>BrowseDirectChildren</BrowseFlag>
<Filter>dc:title,res,res@protection,res@duration,res@bitrate,upnp:genre,upnp:actor,res@microsoft:codec</Filter>
<StartingIndex>0</StartingIndex>
<RequestedCount>1000</RequestedCount>
<SortCriteria>+upnp:class,+dc:title</SortCriteria>
      </u:Browse>
   </s:Body>
</s:Envelope>

the MediaServer response will return the initial video containers. NOTE remember that i'm only showing the DIDL from now on. this would have been escaped and contained within a Soap envelope.

<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">
   <container id="15.0" restricted="1" parentID="15">
      <dc:title>Videos</dc:title>
      <upnp:class>object.container.storageFolder</upnp:class>
   </container>
</DIDL-Lite>

when the 360 user selects the Video container, it will send a Browse request for container 15.0. the MediaServer will then return any <container> or <item> elements. the following returns a single folder and 3 videos. just like Pictures, the Videos are returned as <item> elements. the only difference is that the <res> metadata is different and there is an additional upnp:genre element. NOTE that i'm just sending dummy metadata back, except for protocolInfo.

<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">
   <container id="15.0.0" restricted="1" parentID="15.0">
      <dc:title>Sample Videos</dc:title>
      <upnp:class>object.container.storageFolder</upnp:class>
   </container>
   <item id="15.0_0" parentID="15.0" restricted="1">
      <dc:title>Alex Guadino - Destination Calabria.avi</dc:title>
      <res duration="0:00:00.000" bitrate="0" protocolInfo="http-get:*:video/avi:*">http://192.168.0.199:41955/videos/15.0_0.avi</res>
      <upnp:class>object.item.videoItem</upnp:class>
      <upnp:genre>[Unknown Genre]</upnp:genre>
   </item>
   <item id="15.0_1" parentID="15.0" restricted="1">
      <dc:title>Gorillaz - Clint Eastwood.MP4</dc:title>
      <res duration="0:00:00.000" bitrate="0" protocolInfo="http-get:*:video/mp4:*">http://192.168.0.199:41955/videos/15.0_1.MP4</res>
      <upnp:class>object.item.videoItem</upnp:class>
      <upnp:genre>[Unknown Genre]</upnp:genre>
   </item>
   <item id="15.0_2" parentID="15.0" restricted="1">
      <dc:title>Pussycat Dolls ft. SnoopDogg - Buttons.wmv</dc:title>
      <res duration="0:00:00.000" bitrate="0" protocolInfo="http-get:*:video/x-ms-wmv:*">http://192.168.0.199:41955/videos/15.0_2.wmv</res>
      <upnp:class>object.item.videoItem</upnp:class>
      <upnp:genre>[Unknown Genre]</upnp:genre>
   </item>
</DIDL-Lite>

when the user selects a Video, the 360 will make a GET request. the Intel SDK has a method WebSession.SendStreamObject() that can be used to stream large items like video files. so all the code does is get a FileStream reference to the video and then calls SendStreamObject ... and the video starts to play. at this point, on the 360, the user can fast forward. if the user selects to skip forward or back, then it will send a GET request with an additional HTTP Header for RANGE.

GET /videos/15.0_2.wmv HTTP/1.1
User-Agent: Xenon
Connection: Keep-alive
Host:192.168.0.199
RANGE: bytes=31981568-

the MediaServer will detect the RANGE header and stream the object from that point in the stream (specified in bytes). that is how the 360 can resume playback and skip within video.

A. Serve MP4s

a small improvement over WHS is that it allows you to specify multiple video folders. i also wrote this to return MP4 files ... which it does. but the big problem is i can only get WMV files to play. for some reason, i cant get the 360 to play MP4s or AVIs from my MediaServer. so that makes this Video implementation unusable.

B. Video Playlists

also tried to get multiple videos to play. specifically, i wanted to get a directory of music videos to play as a video playlist. sadly, the 360 client only supports playlists for music and pictures. the other option would be to transcode the videos on the fly into one large video file, but that would be too CPU intensive on a WHS box.

C. Internet TV

the next step was to try and get MCEs Internet TV to the 360 (without MCE). so i wrote some code to retrieve the MCML for Internet TV and parse out the links for Top Picks, ultimately getting to the mms: feeds. although it is WMV content, the 360 would not play the mms: stream. it seems that the 360 will only play mms: feeds through MCE. so i'd like for the 360 to be able to work with ASX files and play MMS streams through UPnP in the future.

D. Other Ideas

this could also be extended to work with Video URLs, Video Feeds, and to Transcode (although TVersity already supports those features very well).

1.3.3 Music1.3.3 Music

2 down ... that leaves Music, which only uses the Search action. when you select Music, it will initially search for Albums (ContainerID 7). it can also search by Artists (6), Saved Playlists (F), Songs (4), and Genres (5). unlike Pictures and Videos, the 360 client does not search by Folders (Container ID 14).

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
   <s:Body>
      <u:Search xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
<ContainerID>7</ContainerID>
<SearchCriteria>(upnp:class = "object.container.album.musicAlbum")</SearchCriteria>
<Filter>dc:title,upnp:artist</Filter>
<StartingIndex>0</StartingIndex>
<RequestedCount>1000</RequestedCount>
<SortCriteria>+dc:title</SortCriteria>
      </u:Search>
   </s:Body>
</s:Envelope>

as with Pictures and Videos, the MediaServer will respond with a list of <container> elements. this result returns 2 containers. my implementation ignores the specific search, and always returns Folders. so if you search for Albums, Artists, or Genres ... it always returns Folders. i like files and folders ... i hate media libraries.

<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">
   <container id="7.1" restricted="1" parentID="7">
      <dc:title>Music</dc:title>
      <upnp:class>object.container.album.musicAlbum</upnp:class>
   </container>
   <container id="7.2" restricted="1" parentID="7">
      <dc:title>Carnavas</dc:title>
      <upnp:class>object.container.album.musicAlbum</upnp:class>
   </container>
</DIDL-Lite>

if the 360 user were to drill into one of the containers (or if they were to select Songs), another Search request would be made.

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
   <s:Body>
      <u:Search xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
<ContainerID>7.2</ContainerID>
<SearchCriteria>(upnp:class derivedfrom "object.item.audioItem")</SearchCriteria>
<Filter>dc:title,res,res@protection,res@duration,res@sampleFrequency,res@bitsPerSample,res@bitrate,res@nrAudioChannels,upnp:artist,upnp:artist@role,upnp:genre,upnp:album</Filter>
<StartingIndex>0</StartingIndex>
<RequestedCount>1000</RequestedCount>
<SortCriteria>+upnp:originalTrackNumber</SortCriteria>
      </u:Search>
   </s:Body>
</s:Envelope>

then the MediaServer would return an array of <item> elements representing music files. it requests a lot of metadata, all of which i return dummy data for. protocolInfo is about the only value the app sets based on file extension.

<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">
   <item id="7.2_1" parentID="7.2" restricted="1">
      <dc:title>01 - Melatonin.mp3</dc:title>
      <res bitsPerSample="0" protocolInfo="http-get:*:audio/mp3:*" duration="0:00:00.000" nrAudioChannels="0" bitrate="0" sampleFrequency="0">http://192.168.0.199:41955/music/7.2_1.mp3</res>
      <upnp:artist>[Unknown Artist]</upnp:artist>
      <upnp:album>[Unknown Album]</upnp:album>
      <upnp:genre>[Unknown Genre]</upnp:genre>
      <upnp:class>object.item.audioItem.musicTrack</upnp:class>
   </item>
   <item id="7.2_2" parentID="7.2" restricted="1">
      <dc:title>02 - Well Thought Out Twinkles.mp3</dc:title>
      <res bitsPerSample="0" protocolInfo="http-get:*:audio/mp3:*" duration="0:00:00.000" nrAudioChannels="0" bitrate="0" sampleFrequency="0">http://192.168.0.199:41955/music/7.2_2.mp3</res>
      <upnp:artist>[Unknown Artist]</upnp:artist>
      <upnp:album>[Unknown Album]</upnp:album>
      <upnp:genre>[Unknown Genre]</upnp:genre>
      <upnp:class>object.item.audioItem.musicTrack</upnp:class>
   </item>
</DIDL-Lite>

with a list of songs, the 360 user can select to 'Play All' or to play a specific song, and then the MediaServer will begin to send that item to play. if there is more than one song, then the 360 will request the next song when its needed.

A. Folder based

my implementation is entirely file based, so there is no metadata library to get out of sync. i really wish the 360 would just add another pivot to let us Browse music files and folders.

B. Skip and Resume

unlike Video, Music does not support the RANGE header, so you cannot skip and resume playback. this really sucks for 60 minute music mixes and audio books.

C. Other Ideas

this could also be extended to support Internet Feeds and Internet URLS (already supported by TVersity)

that wraps up the MediaServer portion. it extends Pictures functionality to include zip files, provides folder-based Music browsing, and basic Video support to the 360.

2) WHS AddIn

ultimately the MediaServer will be a Windows Service that runs on WHS. the Windows Service will read user-defined settings (e.g. media directories) from the registry. and WHS users will be able to update the registry settings using a WHS AddIn. this section will detail the steps for creating that WHS AddIn.

WHS supports 3rd party AddIns that exist as Tabs and/or Settings. these are just WinForms controls that implement a certain interface, and follow a naming convention, for WHS to host.

2.1) Dev Environment

the WHS (quote) SDK (unquote) is just documentation on MSDN. luckily, Brendan Grant has put together some great WHS Dev Tips to get us started. the following steps are from Brendan's dev tips. first, you need to copy assemblies from WHS (c:\program files\windows home server\*) to your local dev machine (c:\WHS_SDK). second, run a *.reg file to register some of those assemblies to the GAC, which will simplify 'Adding a Reference' to those .NET Assemblies. third, copy the generated intellisense XML files to c:\WHS_SDK. finally, copy Brendan's VS templates to c:\_USER_NAME_\Documents\Visual Studio 2008\Templates\Visual C#\*.zip (do not unzip the template).

it's awesome that Brendan has put all this together, but lets hope the WHS team provides a proper SDK in the future : local assemblies, local docs, project templates, intellisense, code samples, setup project (with WHSLogo set) ... everything we expect an SDK to be.

2.2) Hello World AddIn

now we can create our first AddIn. first, create a new 'Home Server AddIn' project. this will generate the base code for you and add references to the WHS assemblies. go into the project properties and change the assembly name (and possibly the namespace). both need to follow this naming convention (be aware that YourTabName needs to be PascalCased) :

that's it for a HelloWorld Addin. an easy way to test it before deploying is to open the HomeServerConsoleTab.YourTabName.dll using Brendan's WhsTestLoader.exe app. NOTE that it will exception for certain WHS calls that do not exist on your dev environment. to test on your HomeServer, you can copy the DLL to c:\program files\windows home server\. you'll need to remote desktop into the WHS to make this copy. we'll make an installer later for end users

2.3) WHS UPnP AddIn

the UPnP config AddIn ends up having very little app logic. the Tab user control has some buttons for starting and stopping the custom UPnP service using .NETs ServiceController class. it also reads user-defined settings from some TextBoxes and writes those to the registry. that's it ... now we need to install it.

3) Installer

started out by creating a Setup & Deployment project. now it needs to install the UPnP Service, open a firewall port (for the service to receive incoming 360 requests), and install the WHS AddIn.

3.1) UPnP Service

installing a Windows Service is pretty well documented. the only thing i could not figure out is how to actually have the Installer start the Service. instead, it just sets it to Automatic start and then the WHS User will be able to start it initially using the WHS AddIn after they modify some settings.

the hard part was figuring out how to open a firewall port. see the ICFAppListInstaller class (in the upnpWinService project) for how to open the port. NOTE the CustomActionData passed in the upnpWinService_Installer.

3.2) WHS AddIn

the same Setup & Deployment project can also install the WHS AddIn. add the 'Primary Output' from the WHS AddIn Project. make sure it is directed to c:\program files\windows home server\ directory on WHS. also exclude the Detected Dependencies for assemblies that will already exist on the WHS (e.g. HomeServerExt). for the .NET dependency, WHS already has .NET 2.0, but not .NET 3.0. the installer still has to be tagged so that WHS will recognize that it is an AddIn. you can use the Orca.exe tool to open the created Installer.msi; then you need to add a row to 'Property' that is named WHSLogo and has the value of 1. now a WHS user can copy the *.msi to \\SERVER\Software\AddIns. then when they open the Windows Home Server Console, it will show up as an available AddIn to install.

install the AddIn and it will deploy the UPnP Windows Service, add an Exception to the Windows Firewall, and install the WHS AddIn for setup. re-open the WHS Console, setup what directories you want to be shared, and start the service. then start the 360, go to the Media blade, select Pictures/Music/Videos, select X to change source, and the custom WHS UPnP service should show up as whsUpnp360 (ServerName).

Video

whsUpnp360 Video

Conclusion

it ended up being relatively easy to write a custom UPnP MediaServer for the 360 and WHS. it is annoying that the 360 doesnt work with generic MediaServers out of the box, and that MediaServers have to be tweaked to work with the 360 instead ... that needs to change. the Intel UPnP SDK tools are excellent, but their SDK needs some work. i would probably try to use the MS stack next time. the WHS SDK is more of an API, and i hope it evolves into a true SDK. i only got to add a couple of the features that i wanted to. my main constraints ended up being what the 360 supported as a Media Renderer (lack of codec or feature support) and the limited CPU power of WHS (e.g. transcoding is out of the question for my HP box).

Source

here is the C# source code : whsUpnp360_Source.zip (VS 2008)

here is a pre-built WHS installer : whsUpnp360_Installer.msi

Updates

none planned, although i'll probably be writing some other UPnP devices for home use. also thinking about buying a PS3 and seeing what it takes to make it work with both

Future

couple ideas of what to write next. later