"Play To" using an Application (Part I)
This project was writen using VS2010 and come as two units with the first part consisting of two C# .cs core files (DLNADevice.cs, SSDP.cs) that are in a test application with a form that can be downloaded from DLNACore.zip where these C# files are used to send out SSDP request to Digital Living Network Alliance (DLNA) devices using a multicast message on the LAN and then waits for any replies using UDP on port 1900 to come in.
The next part needed to stream files to a media device is to then request a list of services from each DLNA device over TCP and to then process the XML response so that we know what address and ports each device is listening on so that we can stream our media to the device or TV using the correct ControlUrl.
Pat two of the project is concerned with using what we have learned about "Play to" in an application so that we can add "Play To" on a web-site so that we can watch movies on our "Smart Tv's" using nothing more than a mobile phone and a browser to control the TV.
Below is the code that sends out the SSDP request using UDP to multicast/broadcast the message on the Local Area Network (LAN) and then wait for any replies from DLNA devices on the network which can take as long as fourteen seconds to arrive so I have wrapped this up as a service using a process thread that is aborted if the stop method is called after first setting "Running" to false
private static void SendRequestNow()
{
IPEndPoint LocalEndPoint = new IPEndPoint(IPAddress.Any, 6000);
IPEndPoint MulticastEndPoint = new IPEndPoint(IPAddress.Parse("239.255.255.250"), 1900);
Socket UdpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
UdpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
UdpSocket.Bind(LocalEndPoint);
UdpSocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(MulticastEndPoint.Address, IPAddress.Any));
UdpSocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 2);
UdpSocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastLoopback, true);
string SearchString = "M-SEARCH * HTTP/1.1\r\nHOST:239.255.255.250:1900\r\nMAN:\"ssdp:discover\"\r\nST:ssdp:all\r\nMX:3\r\n\r\n";
UdpSocket.SendTo(Encoding.UTF8.GetBytes(SearchString), SocketFlags.None, MulticastEndPoint);
byte[] ReceiveBuffer = new byte[4000];
int ReceivedBytes = 0;
int Count = 0;
while (Running && Count < 100)
{
Count++;
if (UdpSocket.Available > 0)
{
ReceivedBytes = UdpSocket.Receive(ReceiveBuffer, SocketFlags.None);
if (ReceivedBytes > 0)
{
string Data = Encoding.UTF8.GetString(ReceiveBuffer, 0, ReceivedBytes);
if (Data.ToUpper().IndexOf("LOCATION: ") > -1)
{
Data = Data.ChopOffBefore("LOCATION: ").ChopOffAfter(Environment.NewLine);
if (NewServer.ToLower().IndexOf(Data.ToLower()) == -1)
NewServer += " " + Data;
}
}
}
else
Thread.Sleep(100);
}
if (NewServer.Length > 0) Servers = NewServer.Trim();
UdpSocket.Close();
THSend = null;
UdpSocket = null;
}
}
The above SSDP function will return a space seperated string from all the DLNA clients that replied to our UDP broadcast and includes the port and address that cleints are listening on with the returned string looking something like this:
http://192.168.0.40:7676/smp_24_ http://192.168.0.40:7676/smp_14_ http://192.168.0.40:7676/smp_6_ http://192.168.0.40:7676/smp_2_ http://192.168.0.60:2869/upnphost/udhisapi.dll?content=uuid:c0694c13-85a7-4ebc-b02d-49b0c63489a9 http://192.168.0.60:2869/upnphost/udhisapi.dll?content=uuid:cxxxxxx-xxxxx-45ae-a49c-xxxxxxx5 http://192.168.0.42:52323/dmr.xml
This string should be persisted because we don't need or want to keep having to poll for DLNA clients on the network and later we can just talk directly to clients like "http://192.168.0.40:7676/smp_24_" to know if the device is connected or not so the next step in the process is to split the above string to an array and create our DLNADevice objects so they are ready to do some talking.
foreach (string Server in DLNA.SSDP.Servers.Split(' '))
{
DLNA.DLNADevice D = new DLNA.DLNADevice(Server);
if (D.IsConnected())
{
Output += "<tr><td>" + D.FriendlyName + "</td><td>" + D.IP + ":" + D.Port + "/" + D.SMP + "</td></tr>" + Environment.NewLine;
DLNAGood +=D.FriendlyName.Replace(" "," ") + "#URL#" + Server + " ";
}
}
Helper.SaveSetting("DLNAGood", DLNAGood.Trim());
Output += "<tr><td colspan='2'><a href='" + this.Request.Url.AbsoluteUri + "?Refresh=true'>Refresh DLNA</a></td></tr>" + Environment.NewLine;
The constructor for each DNLADevice is shown below
public DLNADevice(string url)
{ this.IP = url.ChopOffBefore("http://").ChopOffAfter(":");
this.SMP = url.ChopOffBefore(this.IP).ChopOffBefore("/");
string StrPort = url.ChopOffBefore(this.IP).ChopOffBefore(":").ChopOffAfter("/");
int.TryParse(StrPort, out this.Port);
}
Now we need to call the device using TCP (Not UDP Like SSDP) on the correct address to test if the device is connected and what type of services has the device got to offer. The service we are looking for and is the most common used is "avtransport" which we can read by parsing the XML that is returned from our request to find the ControlUrl we will be using.
public bool IsConnected()
{ Connected = false;
try
{
Socket SocWeb = HelperDLNA.MakeSocket(this.IP, this.Port);
SocWeb.Send(UTF8Encoding.UTF8.GetBytes(HelperDLNA.MakeRequest("GET", this.SMP, 0, "", this.IP, this.Port)), SocketFlags.None);
this.HTML = HelperDLNA.ReadSocket(SocWeb, true, ref this.ReturnCode);
if (this.ReturnCode != 200) return false;
this.Services = DLNAService.ReadServices(HTML);
if (this.HTML.ToLower().IndexOf("<friendlyname>") > -1)
this.FriendlyName = this.HTML.ChopOffBefore("<friendlyName>").ChopOffAfter("</friendlyName>").Trim();
foreach (DLNAService S in this.Services.Values)
{
if (S.ServiceType.ToLower().IndexOf("avtransport:1") > -1) {
this.ControlURL = S.controlURL;
this.Connected = true;
return true;
}
}
}
catch { ;}
return false;
}
Now we are ready to rock and roll and play some music by sending the device listening to the "ControlUrl" a little bit of XML that contains information about our .mp3 file that could be hosted on another machine across the internet or in my case is a local virtual directory that is hosted by Microsoft Information Server (IIS-7) on a windows machine. UploadFile is called first and then StartPlay as shwon below.
private string UploadFileToPlay(string ControlURL, string UrlToPlay)
{ string XML = XMLHead;
XML += "<u:SetAVTransportURI xmlns:u=\"urn:schemas-upnp-org:service:AVTransport:1\">" + Environment.NewLine;
XML += "<InstanceID>0</InstanceID>" + Environment.NewLine;
XML += "<CurrentURI>" + UrlToPlay.Replace(" ", "%20") + "</CurrentURI>" + Environment.NewLine;
XML += "<CurrentURIMetaData>" + Desc() + "</CurrentURIMetaData>" + Environment.NewLine;
XML += "</u:SetAVTransportURI>" + Environment.NewLine;
XML += XMLFoot + Environment.NewLine;
Socket SocWeb = HelperDLNA.MakeSocket(this.IP, this.Port);
string Request = HelperDLNA.MakeRequest("POST", ControlURL, XML.Length, "urn:schemas-upnp-org:service:AVTransport:1#SetAVTransportURI", this.IP, this.Port) + XML;
SocWeb.Send(UTF8Encoding.UTF8.GetBytes(Request), SocketFlags.None);
return HelperDLNA.ReadSocket(SocWeb, true, ref this.ReturnCode);
}
private string StartPlay(string ControlURL, int Instance)
{ string XML = XMLHead;
XML += "<u:Play xmlns:u=\"urn:schemas-upnp-org:service:AVTransport:1\"><InstanceID>"+ Instance + "</InstanceID><Speed>1</Speed></u:Play>" + Environment.NewLine;
XML += XMLFoot + Environment.NewLine;
Socket SocWeb = HelperDLNA.MakeSocket(this.IP, this.Port);
string Request = HelperDLNA.MakeRequest("POST", ControlURL, XML.Length, "urn:schemas-upnp-org:service:AVTransport:1#Play", this.IP, this.Port) + XML;
SocWeb.Send(UTF8Encoding.UTF8.GetBytes(Request), SocketFlags.None);
return HelperDLNA.ReadSocket(SocWeb, true, ref this.ReturnCode);
}
Note that we upload the file (well stream if the truth be told) and that we only call "StartPlay()" if we get a good HTTP 200 OK reply from the device that will first checks that it can see the file before sending back the 200 OK response.
Pausing, Stopping or Starting the current play item is just as easy as is setting the volume level and all you need to do is to wrap the command up as an XML packet as shown above and then post the XML command to the DLNA Client using the ControlURL or we could use a command to request details about the current items position being played.
private string GetPosition(string ControlURL)
{ string XML = XMLHead + "<m:GetPositionInfo xmlns:m=\"urn:schemas-upnp-org:service:AVTransport:1\"><InstanceID xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"ui4\">0</InstanceID></m:GetPositionInfo>" + XMLFoot + Environment.NewLine;
Socket SocWeb = HelperDLNA.MakeSocket(this.IP, this.Port);
string Request = HelperDLNA.MakeRequest("POST", ControlURL, XML.Length, "urn:schemas-upnp-org:service:AVTransport:1#GetPositionInfo", this.IP, this.Port) + XML;
SocWeb.Send(UTF8Encoding.UTF8.GetBytes(Request), SocketFlags.None);
return HelperDLNA.ReadSocket(SocWeb, true, ref this.ReturnCode);
}
You now have all you need to play a movie or a single track from your music collection but if you would like to queue up tracks for a music album then things become a little more difficult because you cannot upload a play-list as such that will work on the devices that I have tested so far even if the device implements the "SetNextAVTransportURI" Interface like my Samsung and Sony "Smart TV's" because all that happens is that the current track stops playing and the new one starts or nothing happens at all.
Well that's the bad news but the good news is that I found the answer by using the above function "GetPosition" that amongst other things returned is how long the current item has left to play so by polling the DNLA device and using a collection of items in our play list it becomes possible to Upload and Start the next item to be played just at the right time.
All the C# Class files are included in the DLNACore.zip at the top of the page along with the play-list queue needed to play albums so you should not have too much trouble editing the test application to play a few movies or tracks.
Your starting code for a project might look something a bit like this.
string DLNAGood = "";
if (DLNA.SSDP.Servers.Length == 0)
{ DLNA.SSDP.Start();
Thread.Sleep(12000); Helper.SaveSetting("DLNA.SSDP.Servers", DLNA.SSDP.Servers);
} foreach (string Server in DLNA.SSDP.Servers.Split(' '))
{ DLNA.DLNADevice D = new DLNA.DLNADevice(Server); if (D.IsConnected())
{ D.TryToPlayFile("http://192.168.0.33/Vid/Music/MySong.mp3"); DLNAGood +=D.FriendlyName.Replace(" "," ") + "#URL#" + Server + " ";
}
}
Helper.SaveSetting("DLNAGood", DLNAGood.Trim());
Using "Play To" from a web-page (Part II)
Using a slow wifi connection to download a 1GB Movie from your two terabyte movie collection to a laptop takes time and that becomes a waste of time if you start streaming the movie to the TV and then decide its junk and that you don't want to watch it and devices like I-Pads or mobile phone just won't have any "Play To" Apps that will work but what if you could browse to a local web-site and click a "Play To" link and have the movie pop up on the TV screen without having to first stream the movie to the mobile device ?
Forget about having to open up all them ports in the windows firewall and all the services you need to have running or access permissions for hidden DLL's running in SvcHost because now you can simply stream the files directly to the TV or download the file to a pen-stick by simply putting a few web-pages on your server and mapping your USB external hard-drive as a virtual directory.
This function is used to read all the directories on the media drive to generate the HTM needed for the project and the code to add all the file in the current directory is just about the same and as easy to write.
if (Directory.Exists(Path))
{
DirectoryInfo DRoot = new DirectoryInfo(Path);
this.Title = DRoot.Name;
bool IsLeft = true;
foreach (DirectoryInfo DInfo in DRoot.GetDirectories())
{
if (DInfo.Name.ToLower().IndexOf("vti_cnf") == -1)
{
if (IsLeft) Output += "<tr><td width='350'><a href='" + RootUrl + "vid/Default.aspx?Path=" + Vids.Helper.EncodeUrl(DInfo.FullName) + "'>" +Vids.Helper.ShortString( DInfo.Name,45) + "</a></td><td><img src='images/folder.png' height='20' width='30' alt='folder' /></td>";
else
Output += "<td width='350'><a href='" + RootUrl + "vid/Default.aspx?Path=" + Vids.Helper.EncodeUrl(DInfo.FullName) + "'>" + Vids.Helper.ShortString(DInfo.Name, 45) + "</a></td><td><img src='images/folder.png' height='20' width='30' alt='folder' /></td></tr>" + Environment.NewLine;
}
IsLeft = !IsLeft;
}
}
if a movie file is clicked in the browser then it just becomes a question of passing in the URL of the movie and then calling "UploadFile" and "StartPlay" from the code behind for the .aspx page which is easy to do so here I will cover how we deal with the queued play list and that starts with a bit of javascript in the page that we used to do Ajax before Ajax or JSON was ever invented.
<script type="text/javascript">
setInterval("PollServer()", 5000);
var Count = 0;
function PollServer() {
Count++;
var I = new Image();
I.src = "PlayTo.aspx?Refresh=" + Count;
}
function Previous() {
Count++;
var I = new Image();
I.src = "PlayTo.aspx?Previous=true&Refresh=" + Count;
}
function Next() {
Count++;
var I = new Image();
I.src = "PlayTo.aspx?Next=true&Refresh=" + Count;
}
function Stop() {
Count++;
var I = new Image();
I.src = "PlayTo.aspx?Stop=true&Refresh=" + Count;
}
</script>
All the files in the albums folder were saved to an array from the folder containing the album to form our queue so in the code behind all we need to do is Something like PlayListPointer++; UploadFile(); StartPlay(); but the important part to notice here is that a timer in javascript is used (see function PollServer) to keep polling the web server which in the code behind then calls the GetPostion function which returns how long the current sound track has left to play and if it's less than a few seconds then the calling thread sleeps for a second or two and then increments the PlayListPointer before calling UploadFile(); StartPlay(); on the DNLA device to play the next track.
The server-side funtion for polling the DNLA Client TV is shown below and this function will also advance the queue if the "Force" flag is set to true because the "Next Track" button has been pressed by the user.
public int PlayNextQueue(bool Force)
{
if (Force)
{
PlayListPointer++;
if (PlayListQueue.Count == 0) return 0;
if (PlayListPointer > PlayListQueue.Count)
PlayListPointer = 1;
string Url = PlayListQueue[PlayListPointer];
StopPlay(false);
TryToPlayFile(Url);
NoPlayCount = 0;
return 310;
}
else
{
string HTMLPosition = GetPosition();
if (HTMLPosition.Length < 50) return 0;
string TrackDuration = HTMLPosition.ChopOffBefore("<TrackDuration>").ChopOffAfter("</TrackDuration>").Substring(2);
string RelTime = HTMLPosition.ChopOffBefore("<RelTime>").ChopOffAfter("</RelTime>").Substring(2);
int RTime = TotalSeconds(RelTime);
int TTime = TotalSeconds(TrackDuration);
int SecondsToPlay = TTime - RTime - 5;
if (SecondsToPlay < 0) SecondsToPlay = 0;
if (SecondsToPlay <10)
{
Thread.Sleep((SecondsToPlay * 1000) +100);
return PlayNextQueue(true);
}
return SecondsToPlay;
}
The download for this project includes all the files you need for browsing your movie collection and using some of the code we have covered already so that covers the TV and "Play To" but what about if you want to stream and watch a movie on your laptop ? Well of course thats included in the project but you may need to install the VLC plugin for windows because HTML5 is a little behind the times when playing 20 year old .mps files using the <AUDIO> tag in some browsers
Deployment to IIS-7 Web-server
Create a new web-site using something like port 8080 and set the physical path as an external hard-drive if that is where you store your media files so that the setup looks something like this from IIS-7 Service manager
Default Web-Site (80)
Media (8080)
|Sifi
|Horror
|War
|Music
Use Visual studio to create a new application named "Vid" on the 8080 website so that the setup looks like this
Default Web-Site (80)
Media (8080)
|Sifi
|Horror
|Vid (Application)
|War
|Music
Now place the contents of the DLNAWeb.zip vid folder into the sites Vid folder and then test the web-site is working by browsing to hxxp://localhost:8080/Vid/Default.aspx to view the home screen.
If you don't have a copy of Visual studio then copy the "Vid" folder to the physical path for the web-site and then convert the folder to an application by right clicking the "Vid" folder in IIS-7 Manager (Start-Programs-Admin Tools) and then selecting "Convert to application"
By default IIS-7 does not host all the file types that you might need for your movies so click on the "Media(8080)" node on the left-hand side of IIS-7 Manager and then double click to MINE Types to view all the suported file types. If .AVI is missing then right click and add a new MINE Type that should look like this.
.avi video/avi Inherited
Thats all, Enjoy Dr Gadgit
One last note.
The web-site project also contains a "Youtube.asxp" web-page that relays search requests over SSL to Youtube so that pages fit better in small android type devices and any spyware scripts are also removed but in order to do this a webhelper.cs file is included and I think you will find that the static GetWebPage() method is worth looking at since it deals with Encryption, Cookies, Chunking and Gzip and leaves you in a lot more control than using a HttPWebRequest
See http://www.codeproject.com/Tips/893013/Geolocation-wifi-Scanner-and-Finder for my wifi scanner or wait for my next project that is a fully working windows remote decktop application.