Attempting UPnP on Windows Phone 7.5 (Mango), Part 1: SSDP Discovery

[Update: SSDP is WORKING now: all code updated]

I have been on a mission for maybe six months now to get something resembling a UPnP stack working on Windows Phone Mango. This post covers Discovery.

First off some basics: read the UPnP spec (download this and read documents/UPnP-arch-DeviceArchitecture-v1.1-20081015.pdf), which I found surpisingly straight-forward for a hard-core specification. Next download the Intel UPnP tools, which includes the awesome Device Spy application. Oh and get some UPnP devices on your home network (although you probably have many already even if you didn't know). For me the target of my work has been my Sonos hardware, but on my network I also have a UPnP router and my PCs that respond as various UPnP devices.

UPnP consists of a few basic operations. Here they are, and how successful (or otherwise) I have been on the phone with them to date:

  • Discovery: the finding of devices on the network, using the SSDP protocol. I had been utterly unsuccessful but thanks to Tracey Trewin from Visual Studio it is working, see below.
  • Invocation: the calling of UPnP methods on a device, via http/SOAP. This I have been 100% successful with, which is good as without this then it doesnt matter if I can get the other basics working or not.
  • Eventing: the registering of events such that a control point can be notified of events from a device. This does not appear to be possible on Windows Phone Mango as the equivalent of bind is not available.

This post is concerned with the first item: the discovery of devices, using SSDP.

First off here is the consumer code form a standard Silverlight main page, with a button and textblock added:

         private void button1_Click(object sender, RoutedEventArgs e)
        {
            SSDPFinder finder = new SSDPFinder();
            string item;
 
            item = "urn:schemas-upnp-org:device:ZonePlayer:1";  // Sonos hardware
            item = "urn:schemas-upnp-org:device:Basic:1";       // eg my Home Server
            item = "urn:schemas-upnp-org:device:MediaServer:1"; // eg my PCs
            item = "ssdp:all"                                   // everything (NOT * as it was previously)
            
            finder.FindAsync(item, 4, (findresult) =>
                {
                    Dispatcher.BeginInvoke(() =>
                        {
                            var newservice = HandleSSDPResponse(findresult);
                            if (newservice != null)
                            {
                                textBlock1.Text += "\r\n" + newservice;
                                Debug.WriteLine(newservice);
                            }
                        });
                });
        }
 
        private List<string> RootDevicesSoFar = new List<string>();
 
        // Primitive SSDP response handler
        private string HandleSSDPResponse(string response)
        {
            StringReader reader = new StringReader(response);
            List<string> lines = new List<string>();
            string line;
            for (; ; )
            {
                line = reader.ReadLine();
                if (line == null)
                    break;
                if (line != "")
                    lines.Add(line);
            }
            // Note ToLower addition!
            string location = lines.Where(lin => lin.ToLower().StartsWith("location:")).FirstOrDefault();
 
            // Only record the first time we see each location
            if (!RootDevicesSoFar.Contains(location))
            {
                RootDevicesSoFar.Add(location);
                return location;
            }
            else
            {
                return null;
            }
        }

and here SSDP Discovery:

 
 using System;
using System.Net;
using System.Net.Sockets;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
 
namespace SSDPTest
{
    public class SSDPFinder
    {
        public void FindAsync(string whatToFind, int seconds, Action<string> FoundCallback)
        {
            const string multicastIP = "239.255.255.250";
            const int multicastPort = 1900;
            const int unicastPort = 1901;
            const int MaxResultSize = 8000;
 
            if (seconds < 1 || seconds > 4)
                throw new ArgumentOutOfRangeException();
 
            string find = "M-SEARCH * HTTP/1.1\r\n" +
               "HOST: 239.255.255.250:1900\r\n" +
               "MAN: \"ssdp:discover\"\r\n" +
               "MX: " + seconds.ToString() + "\r\n" +
               "ST: " + whatToFind + "\r\n" +
               "\r\n";
 
            Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            byte [] MulticastData = Encoding.UTF8.GetBytes(find);
            socket.SendBufferSize = MulticastData.Length;
            SocketAsyncEventArgs sendEvent = new SocketAsyncEventArgs();
            sendEvent.RemoteEndPoint = new IPEndPoint(IPAddress.Parse(multicastIP), multicastPort);
            sendEvent.SetBuffer(MulticastData, 0, MulticastData.Length);
            sendEvent.Completed += new EventHandler<SocketAsyncEventArgs>((sender, e) =>
                {
                    if (e.SocketError!=SocketError.Success)
                    {
                        Debug.WriteLine("Socket error {0}", e.SocketError);
                    }
                    else
                    {
                        if (e.LastOperation == SocketAsyncOperation.SendTo)
                        {
                            // When the initial multicast is done, get rady to receive responses
                            e.RemoteEndPoint = new IPEndPoint(IPAddress.Any, unicastPort);
                            socket.ReceiveBufferSize = MaxResultSize;
                            byte[] receiveBuffer = new byte[MaxResultSize];
                            e.SetBuffer(receiveBuffer, 0, MaxResultSize);
                            socket.ReceiveFromAsync(e);
                        }
                        else if (e.LastOperation == SocketAsyncOperation.ReceiveFrom)
                        {
                            // Got a response, so decode it
                            string result = Encoding.UTF8.GetString(e.Buffer, 0, e.BytesTransferred);
                            if (result.StartsWith("HTTP/1.1 200 OK"))
                            {
                                Debug.WriteLine(result);
                                FoundCallback(result);
                            }
                            else
                            {
                                Debug.WriteLine("INVALID SEARCH RESPONSE");
                            }
 
                            // And kick off another read
                            socket.ReceiveFromAsync(e);
                        }
                    }
                });
 
            // Set a one-shot timer for double the Search time, to be sure we are done before we stop everything
            TimerCallback cb = new TimerCallback((state) =>
                {
                    socket.Close();
                });
            Timer timer = new Timer(cb, null, TimeSpan.FromSeconds(seconds*2), new TimeSpan(-1));
 
            // Kick off the initial Send
            socket.SendToAsync(sendEvent);
        }
    }
}