使用任何设备的“播放到”功能,轻松实现 DLNA
涵盖 SSDP 消息和 XML 回复,以及如何播放播放列表
使用应用程序“播放到”(第一部分)
此项目使用 VS2010 编写,分为两个单元,第一部分包含两个 C# .cs 核心文件(DLNADevice.cs, SSDP.cs),位于一个测试应用程序中,该应用程序具有一个表单,可从 DLNACore.zip 下载,其中这些 C# 文件用于向数字生活网络联盟 (DLNA) 设备发送 SSDP 请求,通过 LAN 上的多播消息,然后使用 UDP 在端口 1900 上等待任何回复。
下一步需要将文件流式传输到媒体设备,就是通过 TCP 请求每个 DLNA 设备的可用服务列表,然后处理 XML 响应,以便我们知道每个设备正在侦听的地址和端口,以便我们可以将媒体流式传输到设备或电视,使用正确的 **ControlUrl**。
项目的第二部分涉及在我们已有的“播放到”应用程序的基础上,创建一个网站,使我们能够使用手机和浏览器控制电视,在“智能电视”上观看电影。
以下代码使用 UDP 发送 SSDP 请求,在本地网络 (LAN) 上多播/广播消息,然后等待网络上的 DLNA 设备回复,这可能需要长达十四秒才能收到回复,所以我将其封装为一个服务,使用一个进程线程,如果调用 stop 方法,则在将 "Running" 设置为 false 后中止该线程。
private static void SendRequestNow()
{//Uses UDP Multicast on 239.255.255.250 with port 1900 to send out invitations that are slow to be answered
IPEndPoint LocalEndPoint = new IPEndPoint(IPAddress.Any, 6000);
IPEndPoint MulticastEndPoint = new IPEndPoint(IPAddress.Parse("239.255.255.250"), 1900);//SSDP port
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)
{//Keep loopping until we timeout or stop is called but do wait for at least ten seconds
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)
{//ChopOffAfter is an extended string method added in Helper.cs
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();//Bank in our new servers nice and quick with minute risk of thread error due to not locking
UdpSocket.Close();
THSend = null;
UdpSocket = null;
}
}
上面的 SSDP 函数将返回一个由所有回复我们 UDP 广播的 DLNA 客户端组成的空格分隔字符串,其中包含客户端正在侦听的端口和地址,返回的字符串看起来像这样:
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
这个字符串应该被持久化,因为我们不需要也无法反复轮询网络上的 DLNA 客户端,稍后我们可以直接与客户端通信,例如 "http://192.168.0.40:7676/smp_24_" 来知道设备是否已连接。因此,接下来的步骤是将上述字符串分割成一个数组,并创建我们的 DLNADevice 对象,使它们能够进行通信。
foreach (string Server in DLNA.SSDP.Servers.Split(' '))
{//Test each DLNA client to see if we can talk to them
DLNA.DLNADevice D = new DLNA.DLNADevice(Server);
if (D.IsConnected())
{//Don't worry about the HTML just if the device is connected or not
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;
每个 DNLADevice 的构造函数如下所示。
public DLNADevice(string url)
{//Constructor like "http://192.168.0.41:7676/smp_14_"
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);
}
现在我们需要通过 TCP(不像 SSDP 那样使用 UDP)在正确的地址上调用设备,以测试设备是否已连接以及设备提供了哪些服务。我们正在寻找且最常用的服务是 "avtransport",我们可以通过解析我们请求返回的 XML 来读取它,以找到我们将使用的 **ControlUrl**。
public bool IsConnected()
{//Will send a request to the DLNA client and then see if we get a valid reply
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)//avtransport is the one we will be using to control the device
{
this.ControlURL = S.controlURL;
this.Connected = true;
return true;
}
}
}
catch { ;}
return false;
}
现在我们准备好播放音乐了,通过向侦听 "ControlUrl" 的设备发送一段 XML,其中包含有关我们的 .mp3 文件的信息,这些文件可以托管在互联网上的另一台机器上,或者在我的例子中,它由 Windows 机器上的 Microsoft Internet Information Server (IIS-7) 托管为一个本地虚拟目录。首先调用 UploadFile,然后调用 StartPlay,如下所示。
private string UploadFileToPlay(string ControlURL, string UrlToPlay)
{///Later we will send a message to the DLNA server to start the file playing
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)
{//Start playing the new upload film or music track
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);
}
请注意,我们上传文件(如果说实话,就是流式传输),并且只有在我们收到设备良好的 HTTP 200 OK 回复后才调用 "StartPlay()",设备会先检查它是否能看到文件,然后才返回 200 OK 响应。
暂停、停止或开始当前播放项与设置音量级别一样简单,您只需将命令封装为如上所示的 XML 数据包,然后将 XML 命令通过 ControlURL 发布到 DLNA 客户端,或者我们可以使用一个命令来请求有关当前播放项位置的详细信息。
private string GetPosition(string ControlURL)
{//Returns the current position for the track that is playing on the DLNA server
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);
}
您现在拥有播放电影或单曲所需的一切,**但如果您想将音乐专辑排队播放,事情就会变得更加困难**,因为到目前为止,我测试过的设备不支持像 "SetNextAVTransportURI" 接口那样的播放列表上传,即使设备实现了该接口,也会发生的情况是当前曲目停止播放,然后新的曲目开始播放,或者根本什么也不发生。
这确实是个坏消息,但好消息是,我通过使用上面的 "GetPosition" 函数找到了解决方案,该函数除了其他之外,还可以返回当前项目剩余的播放时间。通过轮询 DNLA 设备并使用播放列表中的项目集合,就可以在正确的时间上传和开始播放下一项。
此页面顶部的 DLNACore.zip 包含所有 C# 类文件以及播放列表队列,用于播放专辑,因此您应该不难编辑测试应用程序来播放一些电影或曲目。
一个项目的起始代码可能看起来像这样。
string DLNAGood = "";
if (DLNA.SSDP.Servers.Length == 0)
{//Will send out a UDP message and then we need to wait for the relies to come in from DLNA server on the LAN
DLNA.SSDP.Start();
Thread.Sleep(12000);//Wait for our service to complete
Helper.SaveSetting("DLNA.SSDP.Servers", DLNA.SSDP.Servers);
} //Save the above values because we don't want to do this very often
foreach (string Server in DLNA.SSDP.Servers.Split(' '))
{//Test each DLNA client to see if we can talk to them
DLNA.DLNADevice D = new DLNA.DLNADevice(Server);//Should be called Client
if (D.IsConnected())
{//The TV is switched on and is ready to play
D.TryToPlayFile("http://192.168.0.33/Vid/Music/MySong.mp3");//Calls upload and start
DLNAGood +=D.FriendlyName.Replace(" "," ") + "#URL#" + Server + " ";
}
}
Helper.SaveSetting("DLNAGood", DLNAGood.Trim());
使用网页“播放到”(第二部分)
使用缓慢的 Wi-Fi 连接从您两 TB 的电影收藏中下载 1GB 的电影到笔记本电脑需要时间,如果您开始将电影流式传输到电视,然后发现它是垃圾并且您不想观看,那么这就浪费了时间。像 iPad 或手机这样的设备将没有可用的“播放到”应用程序。但是,如果您可以浏览到本地网站并单击“播放到”链接,而电影却出现在电视屏幕上,而无需先将电影流式传输到移动设备,那会怎么样?
忘掉在 Windows 防火墙中打开所有端口以及您需要运行的所有服务或访问 SvcHost 中运行的隐藏 DLL 的权限吧,因为现在您只需将文件直接流式传输到电视,或将文件下载到 U 盘,只需在您的服务器上放置几个网页并将您的 USB 外部硬盘驱动器映射为虚拟目录即可。
此函数用于读取媒体驱动器上的所有目录,以生成项目所需的 HTM。添加当前目录中所有文件的代码几乎相同,并且易于编写。
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)//Rows containtans two folders, left or right
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;
}
}
如果浏览器中单击了一个电影文件,那么问题就变成了将电影的 URL 传递给它,然后在 .aspx 页面的代码隐藏中调用 "UploadFile" 和 "StartPlay",这很容易做到。在这里,我将介绍我们如何处理排队的播放列表,它以页面中的一些 JavaScript 开始,我们用它来在 Ajax 或 JSON 发明之前就实现 Ajax。
<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>
专辑文件夹中的所有文件都从包含专辑的文件夹保存到一个数组中,以形成我们的队列。因此,在代码隐藏中,我们只需要做类似 PlayListPointer++; UploadFile(); StartPlay(); 的事情。但需要注意的是,JavaScript 中的计时器(请参阅 PollServer 函数)用于持续轮询 Web 服务器,在代码隐藏中,它会调用 GetPostion 函数,该函数返回当前音轨还剩多少时间播放。如果少于几秒钟,则调用线程会休眠一到两秒钟,然后递增 PlayListPointer,再调用 UploadFile(); StartPlay(); 在 DNLA 设备上播放下一曲。
轮询 DLNA 客户端电视的服务器端函数如下所示,该函数还会推进队列,如果 "Force" 标志设置为 true,因为用户按下了 "下一曲" 按钮。
public int PlayNextQueue(bool Force)
{//Play the next track in our queue but only if the current track is about to end or unless we are being forced
if (Force)
{//Looks like someone has pressed the next track button
PlayListPointer++;
if (PlayListQueue.Count == 0) return 0;
if (PlayListPointer > PlayListQueue.Count)
PlayListPointer = 1;
string Url = PlayListQueue[PlayListPointer];
StopPlay(false);
TryToPlayFile(Url);//Just play it
NoPlayCount = 0;
return 310;//Just guess for now how long the track is
}
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;//Just a safeguard
if (SecondsToPlay <10)
{//Current track is about to end so wait a few seconds and then force the next track in our queue to play
Thread.Sleep((SecondsToPlay * 1000) +100);
return PlayNextQueue(true);//Calls uploadFile and StartPlay
}
return SecondsToPlay;//Will have to wait to be polled again before playing the next track in our queue
}
此项目的下载包含了浏览电影收藏所需的所有文件,以及我们已经介绍过的一些代码,所以这就涵盖了电视和“播放到”。但是,如果您想在笔记本电脑上流式传输和观看电影怎么办?当然,这包含在项目中,但您可能需要安装 VLC 插件 for windows,因为 HTML5 在某些浏览器中使用 <AUDIO> 标签播放 20 年前的 .mps 文件方面有点落后。
部署到 IIS-7 Web 服务器
使用 8080 端口等创建一个新的网站,并将物理路径设置为外部硬盘驱动器,如果您的媒体文件存储在那里,那么设置从 IIS-7 服务管理器看起来就像这样。
默认网站 (80)
媒体 (8080)
|科幻
|恐怖
|战争
|音乐
使用 Visual Studio 在 8080 网站上创建一个名为“Vid”的新应用程序,这样设置看起来就像这样。
默认网站 (80)
媒体 (8080)
|科幻
|恐怖
|Vid (应用程序)
|战争
|音乐
现在将 DLNAWeb.zip 的 vid 文件夹内容放入网站的 Vid 文件夹中,然后通过浏览 hxxp://:8080/Vid/Default.aspx 来测试网站是否正常工作,以查看主屏幕。
如果您没有 Visual Studio,请将“Vid”文件夹复制到网站的物理路径,然后通过右键单击 IIS-7 管理器中的“Vid”文件夹(开始-程序-管理工具)将其转换为应用程序,然后选择“转换为应用程序”。
默认情况下,IIS-7 不托管您电影所需的所有文件类型,因此请单击 IIS-7 管理器左侧的“Media(8080)”节点,然后双击 MINE Types 以查看所有支持的文件类型。如果缺少 .AVI,则右键单击并添加一个新的 MINE Type,如下所示。
.avi video/avi 继承
就是这样,尽情享用吧!Dr Gadgit
最后一点说明。
网站项目还包含一个 "Youtube.asxp" 网页,它通过 SSL 中继搜索请求到 Youtube,以便页面更适合 Android 等小型设备,并且还会删除任何间谍软件脚本。但是,为了做到这一点,包含了一个 webhelper.cs 文件,我认为您会发现静态 GetWebPage() 方法值得一看,因为它处理加密、Cookie、分块和 Gzip,并且比使用 HttPWebRequest 提供了更多的控制。
请参阅 https://codeproject.org.cn/Tips/893013/Geolocation-wifi-Scanner-and-Finder 了解我的 Wi-Fi 扫描仪,或者等待我的下一个项目,那是一个完全工作的 Windows 远程桌面应用程序。