65.9K
CodeProject 正在变化。 阅读更多。
Home

使用任何设备的“播放到”功能,轻松实现 DLNA

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (4投票s)

2015 年 4 月 6 日

CPOL

9分钟阅读

viewsIcon

51815

downloadIcon

4352

涵盖 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(" ","&nbsp;") + "#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(" ","&nbsp;") + "#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 远程桌面应用程序。

 

© . All rights reserved.