From eda8159b4408c4b33cbdd56e3e6ed2f82757bb0c Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Fri, 25 Apr 2014 13:30:41 -0400 Subject: [PATCH] improved device discovery --- MediaBrowser.Api/Dlna/DlnaServerService.cs | 3 +- MediaBrowser.Api/Playback/StreamState.cs | 6 +- .../Dlna/ControlRequest.cs | 2 + MediaBrowser.Dlna/Didl/DidlBuilder.cs | 11 +- MediaBrowser.Dlna/DlnaManager.cs | 21 +- .../DlnaEntryPoint.cs} | 137 +++--- MediaBrowser.Dlna/MediaBrowser.Dlna.csproj | 8 +- MediaBrowser.Dlna/PlayTo/DlnaController.cs | 2 +- MediaBrowser.Dlna/PlayTo/PlayToManager.cs | 22 +- MediaBrowser.Dlna/Profiles/LgTvProfile.cs | 5 +- .../Profiles/SamsungSmartTvProfile.cs | 12 +- .../Profiles/SonyBlurayPlayer2013Profile.cs | 5 +- .../Profiles/SonyBravia2010Profile.cs | 5 +- .../Profiles/Xml/Samsung Smart TV.xml | 4 +- MediaBrowser.Dlna/Server/ContentDirectory.cs | 6 +- .../{Server => Ssdp}/Datagram.cs | 31 +- .../{Server => Ssdp}/SsdpHandler.cs | 410 ++++++++++-------- MediaBrowser.Dlna/Ssdp/SsdpHelper.cs | 18 - MediaBrowser.Dlna/Ssdp/SsdpMessageBuilder.cs | 47 ++ .../Ssdp/SsdpMessageEventArgs.cs | 20 + MediaBrowser.Model/Dlna/StreamInfo.cs | 2 +- 21 files changed, 468 insertions(+), 309 deletions(-) rename MediaBrowser.Dlna/{Server/DlnaServerEntryPoint.cs => Main/DlnaEntryPoint.cs} (51%) rename MediaBrowser.Dlna/{Server => Ssdp}/Datagram.cs (63%) rename MediaBrowser.Dlna/{Server => Ssdp}/SsdpHandler.cs (57%) create mode 100644 MediaBrowser.Dlna/Ssdp/SsdpMessageBuilder.cs create mode 100644 MediaBrowser.Dlna/Ssdp/SsdpMessageEventArgs.cs diff --git a/MediaBrowser.Api/Dlna/DlnaServerService.cs b/MediaBrowser.Api/Dlna/DlnaServerService.cs index b357409c37..05d41c18bc 100644 --- a/MediaBrowser.Api/Dlna/DlnaServerService.cs +++ b/MediaBrowser.Api/Dlna/DlnaServerService.cs @@ -93,7 +93,8 @@ namespace MediaBrowser.Api.Dlna { Headers = GetRequestHeaders(), InputXml = await reader.ReadToEndAsync().ConfigureAwait(false), - TargetServerUuId = id + TargetServerUuId = id, + RequestedUrl = Request.AbsoluteUri }); } } diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs index c7a62a332b..38d734ff99 100644 --- a/MediaBrowser.Api/Playback/StreamState.cs +++ b/MediaBrowser.Api/Playback/StreamState.cs @@ -1,16 +1,16 @@ -using System.Globalization; -using MediaBrowser.Common.Net; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; +using MediaBrowser.Model.MediaInfo; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Threading; -using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Api.Playback { diff --git a/MediaBrowser.Controller/Dlna/ControlRequest.cs b/MediaBrowser.Controller/Dlna/ControlRequest.cs index a2b9f7a926..1bb5ddf8a9 100644 --- a/MediaBrowser.Controller/Dlna/ControlRequest.cs +++ b/MediaBrowser.Controller/Dlna/ControlRequest.cs @@ -10,6 +10,8 @@ namespace MediaBrowser.Controller.Dlna public string TargetServerUuId { get; set; } + public string RequestedUrl { get; set; } + public ControlRequest() { Headers = new Dictionary(); diff --git a/MediaBrowser.Dlna/Didl/DidlBuilder.cs b/MediaBrowser.Dlna/Didl/DidlBuilder.cs index 32b49ef9cc..e5eff90457 100644 --- a/MediaBrowser.Dlna/Didl/DidlBuilder.cs +++ b/MediaBrowser.Dlna/Didl/DidlBuilder.cs @@ -18,7 +18,7 @@ namespace MediaBrowser.Dlna.Didl public class DidlBuilder { private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - + private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; private const string NS_DC = "http://purl.org/dc/elements/1.1/"; private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/"; @@ -298,7 +298,7 @@ namespace MediaBrowser.Dlna.Didl container.AppendChild(res); } - + public XmlElement GetFolderElement(XmlDocument doc, Folder folder, int childCount, Filter filter) { var container = doc.CreateElement(string.Empty, "container", NS_DIDL); @@ -450,9 +450,14 @@ namespace MediaBrowser.Dlna.Didl private void AddPeople(BaseItem item, XmlElement element) { + var types = new[] { PersonType.Director, PersonType.Writer, PersonType.Producer, PersonType.Composer, "Creator" }; + foreach (var actor in item.People) { - AddValue(element, "upnp", (actor.Type ?? PersonType.Actor).ToLower(), actor.Name, NS_UPNP); + var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase)) + ?? PersonType.Actor; + + AddValue(element, "upnp", type.ToLower(), actor.Name, NS_UPNP); } } diff --git a/MediaBrowser.Dlna/DlnaManager.cs b/MediaBrowser.Dlna/DlnaManager.cs index 963c68d9a3..1f2d5b9c9d 100644 --- a/MediaBrowser.Dlna/DlnaManager.cs +++ b/MediaBrowser.Dlna/DlnaManager.cs @@ -25,10 +25,10 @@ namespace MediaBrowser.Dlna private readonly ILogger _logger; private readonly IJsonSerializer _jsonSerializer; - public DlnaManager(IXmlSerializer xmlSerializer, - IFileSystem fileSystem, - IApplicationPaths appPaths, - ILogger logger, + public DlnaManager(IXmlSerializer xmlSerializer, + IFileSystem fileSystem, + IApplicationPaths appPaths, + ILogger logger, IJsonSerializer jsonSerializer) { _xmlSerializer = xmlSerializer; @@ -230,6 +230,19 @@ namespace MediaBrowser.Dlna { _logger.Debug("Found matching device profile: {0}", profile.Name); } + else + { + string userAgent = null; + headers.TryGetValue("User-Agent", out userAgent); + + var msg = "No matching device profile found. The default will be used. "; + if (!string.IsNullOrEmpty(userAgent)) + { + msg += "User-agent: " + userAgent + ". "; + } + + _logger.Debug(msg); + } return profile; } diff --git a/MediaBrowser.Dlna/Server/DlnaServerEntryPoint.cs b/MediaBrowser.Dlna/Main/DlnaEntryPoint.cs similarity index 51% rename from MediaBrowser.Dlna/Server/DlnaServerEntryPoint.cs rename to MediaBrowser.Dlna/Main/DlnaEntryPoint.cs index d7a34c699c..3746630be3 100644 --- a/MediaBrowser.Dlna/Server/DlnaServerEntryPoint.cs +++ b/MediaBrowser.Dlna/Main/DlnaEntryPoint.cs @@ -3,81 +3,102 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Plugins; +using MediaBrowser.Dlna.Ssdp; using MediaBrowser.Model.Logging; using System; -using System.Linq; +using System.Collections.Generic; using System.Net; -namespace MediaBrowser.Dlna.Server +namespace MediaBrowser.Dlna.Main { - public class DlnaServerEntryPoint : IServerEntryPoint + public class DlnaEntryPoint : IServerEntryPoint { private readonly IServerConfigurationManager _config; private readonly ILogger _logger; - - private SsdpHandler _ssdpHandler; private readonly IApplicationHost _appHost; private readonly INetworkManager _network; - public static DlnaServerEntryPoint Instance; + private SsdpHandler _ssdpHandler; - public DlnaServerEntryPoint(IServerConfigurationManager config, ILogManager logManager, IApplicationHost appHost, INetworkManager network) - { - Instance = this; + private readonly List _registeredServerIds = new List(); + private bool _dlnaServerStarted; + public DlnaEntryPoint(IServerConfigurationManager config, ILogManager logManager, IApplicationHost appHost, INetworkManager network) + { _config = config; _appHost = appHost; _network = network; - _logger = logManager.GetLogger("DlnaServer"); + _logger = logManager.GetLogger("Dlna"); } public void Run() { - _config.ConfigurationUpdated += ConfigurationUpdated; + StartSsdpHandler(); + ReloadComponents(); - ReloadServer(); + _config.ConfigurationUpdated += ConfigurationUpdated; } void ConfigurationUpdated(object sender, EventArgs e) { - ReloadServer(); + ReloadComponents(); } - private void ReloadServer() + private void ReloadComponents() { - var isStarted = _ssdpHandler != null; + var isStarted = _dlnaServerStarted; if (_config.Configuration.DlnaOptions.EnableServer && !isStarted) { - StartServer(); + StartDlnaServer(); } else if (!_config.Configuration.DlnaOptions.EnableServer && isStarted) { - DisposeServer(); + DisposeDlnaServer(); } } - private readonly object _syncLock = new object(); - private void StartServer() + private void StartSsdpHandler() { - var signature = GenerateServerSignature(); + try + { + _ssdpHandler = new SsdpHandler(_logger, _config, GenerateServerSignature()); - lock (_syncLock) + _ssdpHandler.Start(); + } + catch (Exception ex) { - try - { - _ssdpHandler = new SsdpHandler(_logger, _config, signature); + _logger.ErrorException("Error starting Dlna server", ex); + } + } - RegisterEndpoints(); - } - catch (Exception ex) - { - _logger.ErrorException("Error starting Dlna server", ex); - } + private void DisposeSsdpHandler() + { + try + { + _ssdpHandler.Dispose(); + } + catch (Exception ex) + { + _logger.ErrorException("Error disposing ssdp handler", ex); } } - private void RegisterEndpoints() + public void StartDlnaServer() + { + try + { + RegisterServerEndpoints(); + + _dlnaServerStarted = true; + } + catch (Exception ex) + { + _logger.ErrorException("Error registering endpoint", ex); + } + } + + private void RegisterServerEndpoints() { foreach (var address in _network.GetLocalIpAddresses()) { @@ -87,31 +108,17 @@ namespace MediaBrowser.Dlna.Server var uri = new Uri(string.Format("http://{0}:{1}{2}", address, _config.Configuration.HttpServerPortNumber, descriptorURI)); - _ssdpHandler.RegisterNotification(guid, uri, IPAddress.Parse(address)); - } - } + var services = new List + { + "upnp:rootdevice", + "urn:schemas-upnp-org:device:MediaServer:1", + "urn:schemas-upnp-org:service:ContentDirectory:1", + "uuid:" + guid.ToString("N") + }; - public UpnpDevice GetServerUpnpDevice(string uuid) - { - return _ssdpHandler.Devices.FirstOrDefault(i => string.Equals(uuid, i.Uuid.ToString("N"), StringComparison.OrdinalIgnoreCase)); - } + _ssdpHandler.RegisterNotification(guid, uri, IPAddress.Parse(address), services); - private void DisposeServer() - { - lock (_syncLock) - { - if (_ssdpHandler != null) - { - try - { - _ssdpHandler.Dispose(); - } - catch (Exception ex) - { - _logger.ErrorException("Error disposing Dlna server", ex); - } - _ssdpHandler = null; - } + _registeredServerIds.Add(guid); } } @@ -140,7 +147,27 @@ namespace MediaBrowser.Dlna.Server public void Dispose() { - DisposeServer(); + DisposeDlnaServer(); + DisposeSsdpHandler(); + } + + public void DisposeDlnaServer() + { + foreach (var id in _registeredServerIds) + { + try + { + _ssdpHandler.UnregisterNotification(id); + } + catch (Exception ex) + { + _logger.ErrorException("Error unregistering server", ex); + } + } + + _registeredServerIds.Clear(); + + _dlnaServerStarted = false; } } } diff --git a/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj b/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj index a96838a4dd..97da7b697e 100644 --- a/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj +++ b/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj @@ -54,6 +54,7 @@ + Code @@ -79,7 +80,7 @@ - + @@ -108,10 +109,11 @@ - - + + + diff --git a/MediaBrowser.Dlna/PlayTo/DlnaController.cs b/MediaBrowser.Dlna/PlayTo/DlnaController.cs index 6b6152d85a..11514fb853 100644 --- a/MediaBrowser.Dlna/PlayTo/DlnaController.cs +++ b/MediaBrowser.Dlna/PlayTo/DlnaController.cs @@ -50,7 +50,7 @@ namespace MediaBrowser.Dlna.PlayTo if (_device == null || _device.UpdateTime == default(DateTime)) return false; - return DateTime.UtcNow <= _device.UpdateTime.AddSeconds(30); + return DateTime.UtcNow <= _device.UpdateTime.AddMinutes(10); } } diff --git a/MediaBrowser.Dlna/PlayTo/PlayToManager.cs b/MediaBrowser.Dlna/PlayTo/PlayToManager.cs index 10e82a2276..020bd21169 100644 --- a/MediaBrowser.Dlna/PlayTo/PlayToManager.cs +++ b/MediaBrowser.Dlna/PlayTo/PlayToManager.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Common.Net; +using System.Text; +using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dlna; @@ -68,7 +69,7 @@ namespace MediaBrowser.Dlna.PlayTo { _logger.Debug("Found interface: {0}. Type: {1}. Status: {2}", network.Name, network.NetworkInterfaceType, network.OperationalStatus); - if (!network.SupportsMulticast || !network.GetIPProperties().MulticastAddresses.Any()) + if (!network.SupportsMulticast || OperationalStatus.Up != network.OperationalStatus || !network.GetIPProperties().MulticastAddresses.Any()) continue; var ipV4 = network.GetIPProperties().GetIPv4Properties(); @@ -84,7 +85,7 @@ namespace MediaBrowser.Dlna.PlayTo { try { - CreateListener(localIp); + CreateListener(localIp, ipV4.Index); } catch (Exception e) { @@ -111,15 +112,15 @@ namespace MediaBrowser.Dlna.PlayTo /// Creates a socket for the interface and listends for data. /// /// The local ip. - private void CreateListener(IPAddress localIp) + private void CreateListener(IPAddress localIp, int networkInterfaceIndex) { Task.Factory.StartNew(async (o) => { try { - var socket = GetMulticastSocket(); + var socket = GetMulticastSocket(networkInterfaceIndex); - socket.Bind(new IPEndPoint(localIp, 0)); + socket.Bind(new IPEndPoint(localIp, 1900)); _logger.Info("Creating SSDP listener"); @@ -183,7 +184,8 @@ namespace MediaBrowser.Dlna.PlayTo { try { - var request = SsdpHelper.CreateRendererSSDP(3); + var msg = new SsdpMessageBuilder().BuildRendererDiscoveryMessage(); + var request = Encoding.UTF8.GetBytes(msg); while (true) { @@ -210,12 +212,12 @@ namespace MediaBrowser.Dlna.PlayTo /// Gets a socket configured for SDDP multicasting. /// /// - private Socket GetMulticastSocket() + private Socket GetMulticastSocket(int networkInterfaceIndex) { var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse("239.255.255.250"))); - //socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 3); + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse("239.255.255.250"), networkInterfaceIndex)); + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 4); return socket; } diff --git a/MediaBrowser.Dlna/Profiles/LgTvProfile.cs b/MediaBrowser.Dlna/Profiles/LgTvProfile.cs index cce33ae100..4ecfd3c5b9 100644 --- a/MediaBrowser.Dlna/Profiles/LgTvProfile.cs +++ b/MediaBrowser.Dlna/Profiles/LgTvProfile.cs @@ -1,6 +1,5 @@ -using System.Xml.Serialization; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dlna; +using System.Xml.Serialization; namespace MediaBrowser.Dlna.Profiles { diff --git a/MediaBrowser.Dlna/Profiles/SamsungSmartTvProfile.cs b/MediaBrowser.Dlna/Profiles/SamsungSmartTvProfile.cs index 30f62e81cd..d9581ff2ce 100644 --- a/MediaBrowser.Dlna/Profiles/SamsungSmartTvProfile.cs +++ b/MediaBrowser.Dlna/Profiles/SamsungSmartTvProfile.cs @@ -14,7 +14,17 @@ namespace MediaBrowser.Dlna.Profiles Identification = new DeviceIdentification { - ModelUrl = "samsung.com" + ModelUrl = "samsung.com", + + Headers = new[] + { + new HttpHeaderInfo + { + Name = "User-Agent", + Value = @"SEC_", + Match = HeaderMatchType.Substring + } + } }; XmlRootAttributes = new[] diff --git a/MediaBrowser.Dlna/Profiles/SonyBlurayPlayer2013Profile.cs b/MediaBrowser.Dlna/Profiles/SonyBlurayPlayer2013Profile.cs index 01ee7fd68d..baaccba5a7 100644 --- a/MediaBrowser.Dlna/Profiles/SonyBlurayPlayer2013Profile.cs +++ b/MediaBrowser.Dlna/Profiles/SonyBlurayPlayer2013Profile.cs @@ -1,6 +1,5 @@ -using System.Xml.Serialization; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dlna; +using System.Xml.Serialization; namespace MediaBrowser.Dlna.Profiles { diff --git a/MediaBrowser.Dlna/Profiles/SonyBravia2010Profile.cs b/MediaBrowser.Dlna/Profiles/SonyBravia2010Profile.cs index 6167f553c8..3897724959 100644 --- a/MediaBrowser.Dlna/Profiles/SonyBravia2010Profile.cs +++ b/MediaBrowser.Dlna/Profiles/SonyBravia2010Profile.cs @@ -1,6 +1,5 @@ -using System.Xml.Serialization; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dlna; +using System.Xml.Serialization; namespace MediaBrowser.Dlna.Profiles { diff --git a/MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml b/MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml index b99ecf8728..7b281abd1e 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml +++ b/MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml @@ -3,7 +3,9 @@ Samsung Smart TV samsung.com - + + + Media Browser Media Browser diff --git a/MediaBrowser.Dlna/Server/ContentDirectory.cs b/MediaBrowser.Dlna/Server/ContentDirectory.cs index c5b3360902..e657a2ff65 100644 --- a/MediaBrowser.Dlna/Server/ContentDirectory.cs +++ b/MediaBrowser.Dlna/Server/ContentDirectory.cs @@ -67,10 +67,8 @@ namespace MediaBrowser.Dlna.Server var profile = _dlna.GetProfile(request.Headers) ?? _dlna.GetDefaultProfile(); - var device = DlnaServerEntryPoint.Instance.GetServerUpnpDevice(request.TargetServerUuId); - - var serverAddress = device.Descriptor.ToString().Substring(0, device.Descriptor.ToString().IndexOf("/dlna", StringComparison.OrdinalIgnoreCase)); - + var serverAddress = request.RequestedUrl.Substring(0, request.RequestedUrl.IndexOf("/dlna", StringComparison.OrdinalIgnoreCase)); + var user = GetUser(profile); return new ControlHandler( diff --git a/MediaBrowser.Dlna/Server/Datagram.cs b/MediaBrowser.Dlna/Ssdp/Datagram.cs similarity index 63% rename from MediaBrowser.Dlna/Server/Datagram.cs rename to MediaBrowser.Dlna/Ssdp/Datagram.cs index 2432cbd062..0caf5c78fa 100644 --- a/MediaBrowser.Dlna/Server/Datagram.cs +++ b/MediaBrowser.Dlna/Ssdp/Datagram.cs @@ -4,24 +4,31 @@ using System.Net; using System.Net.Sockets; using System.Text; -namespace MediaBrowser.Dlna.Server +namespace MediaBrowser.Dlna.Ssdp { public class Datagram { public IPEndPoint EndPoint { get; private set; } public IPAddress LocalAddress { get; private set; } public string Message { get; private set; } - public bool Sticky { get; private set; } + /// + /// The number of times to send the message + /// + public int TotalSendCount { get; private set; } + + /// + /// The number of times the message has been sent + /// public int SendCount { get; private set; } private readonly ILogger _logger; - public Datagram(IPEndPoint endPoint, IPAddress localAddress, ILogger logger, string message, bool sticky) + public Datagram(IPEndPoint endPoint, IPAddress localAddress, ILogger logger, string message, int totalSendCount) { Message = message; _logger = logger; - Sticky = sticky; + TotalSendCount = totalSendCount; LocalAddress = localAddress; EndPoint = endPoint; } @@ -31,9 +38,11 @@ namespace MediaBrowser.Dlna.Server var msg = Encoding.ASCII.GetBytes(Message); try { - var client = new UdpClient(); - client.Client.Bind(new IPEndPoint(LocalAddress, 0)); - client.BeginSend(msg, msg.Length, EndPoint, result => + var client = CreateSocket(); + + client.Bind(new IPEndPoint(LocalAddress, 0)); + + client.BeginSendTo(msg, 0, msg.Length, SocketFlags.None, EndPoint, result => { try { @@ -61,5 +70,13 @@ namespace MediaBrowser.Dlna.Server } ++SendCount; } + + private Socket CreateSocket() + { + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + return socket; + } } } diff --git a/MediaBrowser.Dlna/Server/SsdpHandler.cs b/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs similarity index 57% rename from MediaBrowser.Dlna/Server/SsdpHandler.cs rename to MediaBrowser.Dlna/Ssdp/SsdpHandler.cs index 0430f6a02c..01393c6ce4 100644 --- a/MediaBrowser.Dlna/Server/SsdpHandler.cs +++ b/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs @@ -1,4 +1,5 @@ using MediaBrowser.Controller.Configuration; +using MediaBrowser.Dlna.Server; using MediaBrowser.Model.Logging; using System; using System.Collections.Concurrent; @@ -10,72 +11,199 @@ using System.Net.Sockets; using System.Text; using System.Threading; -namespace MediaBrowser.Dlna.Server +namespace MediaBrowser.Dlna.Ssdp { public class SsdpHandler : IDisposable { - private readonly AutoResetEvent _datagramPosted = new AutoResetEvent(false); - private readonly ConcurrentQueue _messageQueue = new ConcurrentQueue(); + private Socket _socket; private readonly ILogger _logger; private readonly IServerConfigurationManager _config; - private readonly string _serverSignature; - private bool _isDisposed; const string SSDPAddr = "239.255.255.250"; const int SSDPPort = 1900; + private readonly string _serverSignature; - private readonly IPEndPoint _ssdpEndp = new IPEndPoint(IPAddress.Parse(SSDPAddr), SSDPPort); private readonly IPAddress _ssdpIp = IPAddress.Parse(SSDPAddr); - - private UdpClient _udpClient; - - private readonly Dictionary> _devices = new Dictionary>(); + private readonly IPEndPoint _ssdpEndp = new IPEndPoint(IPAddress.Parse(SSDPAddr), SSDPPort); private Timer _queueTimer; private Timer _notificationTimer; + + private readonly AutoResetEvent _datagramPosted = new AutoResetEvent(false); + private readonly ConcurrentQueue _messageQueue = new ConcurrentQueue(); + private bool _isDisposed; + private readonly ConcurrentDictionary> _devices = new ConcurrentDictionary>(); + public SsdpHandler(ILogger logger, IServerConfigurationManager config, string serverSignature) { _logger = logger; _config = config; _serverSignature = serverSignature; + } + + public event EventHandler MessageReceived; - Start(); + private void OnMessageReceived(SsdpMessageEventArgs args) + { + if (string.Equals(args.Method, "M-SEARCH", StringComparison.OrdinalIgnoreCase)) + { + RespondToSearch(args.EndPoint, args.Headers["st"]); + } } - public IEnumerable Devices + public IEnumerable RegisteredDevices { get { - UpnpDevice[] devs; - lock (_devices) - { - devs = _devices.Values.SelectMany(i => i).ToArray(); - } - return devs; + return _devices.Values.SelectMany(i => i).ToList(); } } - - private void Start() + + public void Start() { - _udpClient = new UdpClient(); - _udpClient.Client.UseOnlyOverlappedIO = true; - _udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - _udpClient.ExclusiveAddressUse = false; - _udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, SSDPPort)); - _udpClient.JoinMulticastGroup(_ssdpIp, 2); + _socket = CreateMulticastSocket(); + _logger.Info("SSDP service started"); Receive(); StartNotificationTimer(); } + public void SendDatagram(string header, + Dictionary values, + IPAddress localAddress, + int sendCount = 1) + { + SendDatagram(header, values, _ssdpEndp, localAddress, sendCount); + } + + public void SendDatagram(string header, + Dictionary values, + IPEndPoint endpoint, + IPAddress localAddress, + int sendCount = 1) + { + var msg = new SsdpMessageBuilder().BuildMessage(header, values); + + var dgram = new Datagram(endpoint, localAddress, _logger, msg, sendCount); + if (_messageQueue.Count == 0) + { + dgram.Send(); + return; + } + + _messageQueue.Enqueue(dgram); + StartQueueTimer(); + } + + public void SendDatagramFromDevices(string header, + Dictionary values, + IPEndPoint endpoint, + string deviceType) + { + foreach (var d in RegisteredDevices) + { + if (string.Equals(deviceType, "ssdp:all", StringComparison.OrdinalIgnoreCase) || + string.Equals(deviceType, d.Type, StringComparison.OrdinalIgnoreCase)) + { + SendDatagram(header, values, endpoint, d.Address); + } + } + } + + private void RespondToSearch(IPEndPoint endpoint, string deviceType) + { + if (_config.Configuration.DlnaOptions.EnableDebugLogging) + { + _logger.Debug("RespondToSearch"); + } + + const string header = "HTTP/1.1 200 OK"; + + foreach (var d in RegisteredDevices) + { + if (string.Equals(deviceType, "ssdp:all", StringComparison.OrdinalIgnoreCase) || + string.Equals(deviceType, d.Type, StringComparison.OrdinalIgnoreCase)) + { + var values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + values["CACHE-CONTROL"] = "max-age = 600"; + values["DATE"] = DateTime.Now.ToString("R"); + values["EXT"] = ""; + values["LOCATION"] = d.Descriptor.ToString(); + values["SERVER"] = _serverSignature; + values["ST"] = d.Type; + values["USN"] = d.USN; + + SendDatagram(header, values, endpoint, d.Address); + + _logger.Info("{1} - Responded to a {0} request to {2}", d.Type, endpoint, d.Address.ToString()); + } + } + } + + private readonly object _queueTimerSyncLock = new object(); + private void StartQueueTimer() + { + lock (_queueTimerSyncLock) + { + if (_queueTimer == null) + { + _queueTimer = new Timer(QueueTimerCallback, null, 1000, Timeout.Infinite); + } + else + { + _queueTimer.Change(1000, Timeout.Infinite); + } + } + } + + private void QueueTimerCallback(object state) + { + while (_messageQueue.Count != 0) + { + Datagram msg; + if (!_messageQueue.TryPeek(out msg)) + { + continue; + } + + if (msg != null && (!_isDisposed || msg.TotalSendCount > 1)) + { + msg.Send(); + if (msg.SendCount > msg.TotalSendCount) + { + _messageQueue.TryDequeue(out msg); + } + break; + } + + _messageQueue.TryDequeue(out msg); + } + + _datagramPosted.Set(); + + if (_messageQueue.Count > 0) + { + StartQueueTimer(); + } + else + { + DisposeQueueTimer(); + } + } + private void Receive() { try { - _udpClient.BeginReceive(ReceiveCallback, null); + var buffer = new byte[1024]; + + EndPoint endpoint = new IPEndPoint(IPAddress.Any, SSDPPort); + + _socket.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref endpoint, ReceiveCallback, buffer); } catch (ObjectDisposedException) { @@ -84,10 +212,16 @@ namespace MediaBrowser.Dlna.Server private void ReceiveCallback(IAsyncResult result) { + if (_isDisposed) + { + return; + } + try { - var endpoint = new IPEndPoint(IPAddress.None, SSDPPort); - var received = _udpClient.EndReceive(result, ref endpoint); + EndPoint endpoint = new IPEndPoint(IPAddress.Any, SSDPPort); + var receivedCount = _socket.EndReceiveFrom(result, ref endpoint); + var received = (byte[])result.AsyncState; if (_config.Configuration.DlnaOptions.EnableDebugLogging) { @@ -98,7 +232,7 @@ namespace MediaBrowser.Dlna.Server { var proto = (reader.ReadLine() ?? string.Empty).Trim(); var method = proto.Split(new[] { ' ' }, 2)[0]; - var headers = new Headers(); + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); for (var line = reader.ReadLine(); line != null; line = reader.ReadLine()) { line = line.Trim(); @@ -119,10 +253,12 @@ namespace MediaBrowser.Dlna.Server _logger.Debug("{0} - Datagram method: {1}", endpoint, method); } - if (string.Equals(method, "M-SEARCH", StringComparison.OrdinalIgnoreCase)) + OnMessageReceived(new SsdpMessageEventArgs { - RespondToSearch(endpoint, headers["st"]); - } + Method = method, + Headers = headers, + EndPoint = (IPEndPoint)endpoint + }); } } catch (Exception ex) @@ -130,105 +266,60 @@ namespace MediaBrowser.Dlna.Server _logger.ErrorException("Failed to read SSDP message", ex); } - if (!_isDisposed) + if (_socket != null) { Receive(); } } - private void RespondToSearch(IPEndPoint endpoint, string req) + public void Dispose() { - if (string.Equals(req, "ssdp:all", StringComparison.OrdinalIgnoreCase)) - { - req = null; - } - - if (_config.Configuration.DlnaOptions.EnableDebugLogging) - { - _logger.Debug("RespondToSearch"); - } - - foreach (var d in Devices) + _isDisposed = true; + while (_messageQueue.Count != 0) { - if (!string.IsNullOrEmpty(req) && !string.Equals(req, d.Type, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - SendSearchResponse(endpoint, d); + _datagramPosted.WaitOne(); } - } - - private void SendSearchResponse(IPEndPoint endpoint, UpnpDevice dev) - { - var builder = new StringBuilder(); - - const string argFormat = "{0}: {1}\r\n"; - builder.Append("HTTP/1.1 200 OK\r\n"); - builder.AppendFormat(argFormat, "CACHE-CONTROL", "max-age = 600"); - builder.AppendFormat(argFormat, "DATE", DateTime.Now.ToString("R")); - builder.AppendFormat(argFormat, "EXT", ""); - builder.AppendFormat(argFormat, "LOCATION", dev.Descriptor); - builder.AppendFormat(argFormat, "SERVER", _serverSignature); - builder.AppendFormat(argFormat, "ST", dev.Type); - builder.AppendFormat(argFormat, "USN", dev.USN); - builder.Append("\r\n"); - - SendDatagram(endpoint, dev.Address, builder.ToString(), false); + DisposeSocket(); + DisposeQueueTimer(); + DisposeNotificationTimer(); - _logger.Info("{1} - Responded to a {0} request to {2}", dev.Type, endpoint, dev.Address.ToString()); + _datagramPosted.Dispose(); } - private void SendDatagram(IPEndPoint endpoint, IPAddress localAddress, string msg, bool sticky) + private void DisposeSocket() { - if (_isDisposed) + if (_socket != null) { - return; + _socket.Close(); + _socket.Dispose(); + _socket = null; } - - var dgram = new Datagram(endpoint, localAddress, _logger, msg, sticky); - if (_messageQueue.Count == 0) - { - dgram.Send(); - } - _messageQueue.Enqueue(dgram); - StartQueueTimer(); } - private void QueueTimerCallback(object state) + private void DisposeQueueTimer() { - while (_messageQueue.Count != 0) + lock (_queueTimerSyncLock) { - Datagram msg; - if (!_messageQueue.TryPeek(out msg)) - { - continue; - } - - if (msg != null && (!_isDisposed || msg.Sticky)) + if (_queueTimer != null) { - msg.Send(); - if (msg.SendCount > 2) - { - _messageQueue.TryDequeue(out msg); - } - break; + _queueTimer.Dispose(); + _queueTimer = null; } - - _messageQueue.TryDequeue(out msg); } + } - _datagramPosted.Set(); + private Socket CreateMulticastSocket() + { + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true); + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 4); + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(_ssdpIp, 0)); - if (_messageQueue.Count > 0) - { - StartQueueTimer(); - } - else - { - DisposeQueueTimer(); - } + socket.Bind(new IPEndPoint(IPAddress.Any, SSDPPort)); + + return socket; } private void NotifyAll() @@ -237,121 +328,64 @@ namespace MediaBrowser.Dlna.Server { _logger.Debug("Sending alive notifications"); } - foreach (var d in Devices) + foreach (var d in RegisteredDevices) { - NotifyDevice(d, "alive", false); + NotifyDevice(d, "alive"); } } - private void NotifyDevice(UpnpDevice dev, string type, bool sticky) + private void NotifyDevice(UpnpDevice dev, string type, int sendCount = 1) { - var builder = new StringBuilder(); + const string header = "NOTIFY * HTTP/1.1"; - const string argFormat = "{0}: {1}\r\n"; + var values = new Dictionary(StringComparer.OrdinalIgnoreCase); - builder.Append("NOTIFY * HTTP/1.1\r\n{0}\r\n"); - builder.AppendFormat(argFormat, "HOST", "239.255.255.250:1900"); - builder.AppendFormat(argFormat, "CACHE-CONTROL", "max-age = 600"); - builder.AppendFormat(argFormat, "LOCATION", dev.Descriptor); - builder.AppendFormat(argFormat, "SERVER", _serverSignature); - builder.AppendFormat(argFormat, "NTS", "ssdp:" + type); - builder.AppendFormat(argFormat, "NT", dev.Type); - builder.AppendFormat(argFormat, "USN", dev.USN); - builder.Append("\r\n"); + // If needed later for non-server devices, these headers will need to be dynamic + values["HOST"] = "239.255.255.250:1900"; + values["CACHE-CONTROL"] = "max-age = 600"; + values["LOCATION"] = dev.Descriptor.ToString(); + values["SERVER"] = _serverSignature; + values["NTS"] = "ssdp:" + type; + values["NT"] = dev.Type; + values["USN"] = dev.USN; if (_config.Configuration.DlnaOptions.EnableDebugLogging) { _logger.Debug("{0} said {1}", dev.USN, type); } - SendDatagram(_ssdpEndp, dev.Address, builder.ToString(), sticky); + SendDatagram(header, values, dev.Address, sendCount); } - public void RegisterNotification(Guid uuid, Uri descriptor, IPAddress address) + public void RegisterNotification(Guid uuid, Uri descriptionUri, IPAddress address, IEnumerable services) { List list; lock (_devices) { if (!_devices.TryGetValue(uuid, out list)) { - _devices.Add(uuid, list = new List()); + _devices.TryAdd(uuid, list = new List()); } } - foreach (var t in new[] - { - "upnp:rootdevice", - "urn:schemas-upnp-org:device:MediaServer:1", - "urn:schemas-upnp-org:service:ContentDirectory:1", - "uuid:" + uuid - }) - { - list.Add(new UpnpDevice(uuid, t, descriptor, address)); - } + list.AddRange(services.Select(i => new UpnpDevice(uuid, i, descriptionUri, address))); NotifyAll(); - _logger.Debug("Registered mount {0} at {1}", uuid, descriptor); + _logger.Debug("Registered mount {0} at {1}", uuid, descriptionUri); } - private void UnregisterNotification(Guid uuid) + public void UnregisterNotification(Guid uuid) { List dl; - lock (_devices) + if (_devices.TryRemove(uuid, out dl)) { - if (!_devices.TryGetValue(uuid, out dl)) - { - return; - } - _devices.Remove(uuid); - } - foreach (var d in dl) - { - NotifyDevice(d, "byebye", true); - } - _logger.Debug("Unregistered mount {0}", uuid); - } - - public void Dispose() - { - _isDisposed = true; - while (_messageQueue.Count != 0) - { - _datagramPosted.WaitOne(); - } - _udpClient.DropMulticastGroup(_ssdpIp); - _udpClient.Close(); - - DisposeNotificationTimer(); - DisposeQueueTimer(); - _datagramPosted.Dispose(); - } - - private readonly object _queueTimerSyncLock = new object(); - private void StartQueueTimer() - { - lock (_queueTimerSyncLock) - { - if (_queueTimer == null) - { - _queueTimer = new Timer(QueueTimerCallback, null, 1000, Timeout.Infinite); - } - else + foreach (var d in dl.ToList()) { - _queueTimer.Change(1000, Timeout.Infinite); + NotifyDevice(d, "byebye", 2); } - } - } - private void DisposeQueueTimer() - { - lock (_queueTimerSyncLock) - { - if (_queueTimer != null) - { - _queueTimer.Dispose(); - _queueTimer = null; - } + _logger.Debug("Unregistered mount {0}", uuid); } } diff --git a/MediaBrowser.Dlna/Ssdp/SsdpHelper.cs b/MediaBrowser.Dlna/Ssdp/SsdpHelper.cs index b22db781ad..2b5f386226 100644 --- a/MediaBrowser.Dlna/Ssdp/SsdpHelper.cs +++ b/MediaBrowser.Dlna/Ssdp/SsdpHelper.cs @@ -7,24 +7,6 @@ namespace MediaBrowser.Dlna.Ssdp { public class SsdpHelper { - private const string SsdpRenderer = "M-SEARCH * HTTP/1.1\r\n" + - "HOST: 239.255.255.250:1900\r\n" + - "User-Agent: UPnP/1.0 DLNADOC/1.50 Platinum/0.6.9.1\r\n" + - "ST: urn:schemas-upnp-org:device:MediaRenderer:1\r\n" + - "MAN: \"ssdp:discover\"\r\n" + - "MX: {0}\r\n" + - "\r\n"; - - /// - /// Creates a SSDP MSearch packet for DlnaRenderers. - /// - /// The mx. (Delaytime for device before responding) - /// - public static byte[] CreateRendererSSDP(int mx) - { - return Encoding.UTF8.GetBytes(string.Format(SsdpRenderer, mx)); - } - /// /// Parses the socket response into a location Uri for the DeviceDescription.xml. /// diff --git a/MediaBrowser.Dlna/Ssdp/SsdpMessageBuilder.cs b/MediaBrowser.Dlna/Ssdp/SsdpMessageBuilder.cs new file mode 100644 index 0000000000..4e60cfe2e3 --- /dev/null +++ b/MediaBrowser.Dlna/Ssdp/SsdpMessageBuilder.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MediaBrowser.Dlna.Ssdp +{ + public class SsdpMessageBuilder + { + public string BuildMessage(string header, Dictionary values) + { + var builder = new StringBuilder(); + + const string argFormat = "{0}: {1}\r\n"; + + builder.AppendFormat("{0}\r\n", header); + + foreach (var pair in values) + { + builder.AppendFormat(argFormat, pair.Key, pair.Value); + } + + builder.Append("\r\n"); + + return builder.ToString(); + } + + public string BuildDiscoveryMessage(string deviceSearchType, string mx) + { + const string header = "M-SEARCH * HTTP/1.1"; + + var values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + values["HOST"] = "239.255.255.250:1900"; + values["USER-AGENT"] = "UPnP/1.0 DLNADOC/1.50 Platinum/0.6.9.1"; + values["ST"] = deviceSearchType; + values["MAN"] = "\"ssdp:discover\""; + values["MX"] = mx; + + return BuildMessage(header, values); + } + + public string BuildRendererDiscoveryMessage() + { + return BuildDiscoveryMessage("urn:schemas-upnp-org:device:MediaRenderer:1", "3"); + } + } +} diff --git a/MediaBrowser.Dlna/Ssdp/SsdpMessageEventArgs.cs b/MediaBrowser.Dlna/Ssdp/SsdpMessageEventArgs.cs new file mode 100644 index 0000000000..d6368191b4 --- /dev/null +++ b/MediaBrowser.Dlna/Ssdp/SsdpMessageEventArgs.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Net; + +namespace MediaBrowser.Dlna.Ssdp +{ + public class SsdpMessageEventArgs + { + public string Method { get; set; } + + public IPEndPoint EndPoint { get; set; } + + public Dictionary Headers { get; set; } + + public SsdpMessageEventArgs() + { + Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 1e8ca6f20c..b6bf9b183d 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -298,7 +298,7 @@ namespace MediaBrowser.Model.Dlna { if (IsDirectStream) { - return MediaSource.Bitrate; + return MediaSource.Size; } if (RunTimeTicks.HasValue)