diff options
| author | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-03-24 08:47:39 -0400 |
|---|---|---|
| committer | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-03-24 08:47:39 -0400 |
| commit | 501dedb13cd59dc2683ac4192cd11289bd304cfb (patch) | |
| tree | f3c92b89ae3e8a7e744ee13eb1b16139da690622 | |
| parent | 1c3c12ebf6eb89f446d69900ccab2d3c4c48a85c (diff) | |
stub out dlna server
| -rw-r--r-- | MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs | 22 | ||||
| -rw-r--r-- | MediaBrowser.Common/Net/BasePeriodicWebSocketListener.cs | 18 | ||||
| -rw-r--r-- | MediaBrowser.Dlna/MediaBrowser.Dlna.csproj | 9 | ||||
| -rw-r--r-- | MediaBrowser.Dlna/Server/DlnaServerEntryPoint.cs | 115 | ||||
| -rw-r--r-- | MediaBrowser.Dlna/Server/Headers.cs | 164 | ||||
| -rw-r--r-- | MediaBrowser.Dlna/Server/RawHeaders.cs | 16 | ||||
| -rw-r--r-- | MediaBrowser.Dlna/Server/SsdpHandler.cs | 260 | ||||
| -rw-r--r-- | MediaBrowser.Dlna/Server/UpnpDevice.cs | 28 | ||||
| -rw-r--r-- | MediaBrowser.Model/Configuration/DlnaOptions.cs | 2 |
9 files changed, 623 insertions, 11 deletions
diff --git a/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs b/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs index c143635bf..0d3f5dfcd 100644 --- a/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs +++ b/MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs @@ -39,6 +39,8 @@ namespace MediaBrowser.Api.ScheduledTasks TaskManager = taskManager; } + private bool _lastResponseHadTasksRunning = true; + /// <summary> /// Gets the data to send. /// </summary> @@ -46,7 +48,25 @@ namespace MediaBrowser.Api.ScheduledTasks /// <returns>Task{IEnumerable{TaskInfo}}.</returns> protected override Task<IEnumerable<TaskInfo>> GetDataToSend(object state) { - return Task.FromResult(TaskManager.ScheduledTasks + var tasks = TaskManager.ScheduledTasks.ToList(); + + var anyRunning = tasks.Any(i => i.State != TaskState.Idle); + + if (anyRunning) + { + _lastResponseHadTasksRunning = true; + } + else + { + if (!_lastResponseHadTasksRunning) + { + return Task.FromResult<IEnumerable<TaskInfo>>(null); + } + + _lastResponseHadTasksRunning = false; + } + + return Task.FromResult(tasks .OrderBy(i => i.Name) .Select(ScheduledTaskHelpers.GetTaskInfo) .Where(i => !i.IsHidden)); diff --git a/MediaBrowser.Common/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Common/Net/BasePeriodicWebSocketListener.cs index 4ff34cfa1..33d3f368b 100644 --- a/MediaBrowser.Common/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Common/Net/BasePeriodicWebSocketListener.cs @@ -1,11 +1,11 @@ -using System.Globalization; -using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Model.Net; namespace MediaBrowser.Common.Net { @@ -16,6 +16,7 @@ namespace MediaBrowser.Common.Net /// <typeparam name="TStateType">The type of the T state type.</typeparam> public abstract class BasePeriodicWebSocketListener<TReturnDataType, TStateType> : IWebSocketListener, IDisposable where TStateType : class, new() + where TReturnDataType : class { /// <summary> /// The _active connections @@ -144,12 +145,15 @@ namespace MediaBrowser.Common.Net var data = await GetDataToSend(tuple.Item4).ConfigureAwait(false); - await connection.SendAsync(new WebSocketMessage<TReturnDataType> + if (data != null) { - MessageType = Name, - Data = data + await connection.SendAsync(new WebSocketMessage<TReturnDataType> + { + MessageType = Name, + Data = data - }, tuple.Item2.Token).ConfigureAwait(false); + }, tuple.Item2.Token).ConfigureAwait(false); + } tuple.Item5.Release(); } diff --git a/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj b/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj index 4eb305e0f..bea281b61 100644 --- a/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj +++ b/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj @@ -98,6 +98,11 @@ <Compile Include="Profiles\Xbox360Profile.cs" /> <Compile Include="Profiles\XboxOneProfile.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="Server\DlnaServerEntryPoint.cs" /> + <Compile Include="Server\Headers.cs" /> + <Compile Include="Server\RawHeaders.cs" /> + <Compile Include="Server\SsdpHandler.cs" /> + <Compile Include="Server\UpnpDevice.cs" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj"> @@ -113,9 +118,7 @@ <Name>MediaBrowser.Model</Name> </ProjectReference> </ItemGroup> - <ItemGroup> - <Folder Include="Server\" /> - </ItemGroup> + <ItemGroup /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <!-- To modify your build process, add your task inside one of the targets below and uncomment it. Other similar extension points exist, see Microsoft.Common.targets. diff --git a/MediaBrowser.Dlna/Server/DlnaServerEntryPoint.cs b/MediaBrowser.Dlna/Server/DlnaServerEntryPoint.cs new file mode 100644 index 000000000..f1af0af28 --- /dev/null +++ b/MediaBrowser.Dlna/Server/DlnaServerEntryPoint.cs @@ -0,0 +1,115 @@ +using MediaBrowser.Common; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.Logging; +using System; + +namespace MediaBrowser.Dlna.Server +{ + public class DlnaServerEntryPoint : IServerEntryPoint + { + private readonly IServerConfigurationManager _config; + private readonly ILogger _logger; + + private SsdpHandler _ssdpHandler; + private readonly IApplicationHost _appHost; + + public DlnaServerEntryPoint(IServerConfigurationManager config, ILogManager logManager, IApplicationHost appHost) + { + _config = config; + _appHost = appHost; + _logger = logManager.GetLogger("DlnaServer"); + } + + public void Run() + { + _config.ConfigurationUpdated += ConfigurationUpdated; + + //ReloadServer(); + } + + void ConfigurationUpdated(object sender, EventArgs e) + { + //ReloadServer(); + } + + private void ReloadServer() + { + var isStarted = _ssdpHandler != null; + + if (_config.Configuration.DlnaOptions.EnableServer && !isStarted) + { + StartServer(); + } + else if (!_config.Configuration.DlnaOptions.EnableServer && isStarted) + { + DisposeServer(); + } + } + + private readonly object _syncLock = new object(); + private void StartServer() + { + var signature = GenerateServerSignature(); + + lock (_syncLock) + { + try + { + _ssdpHandler = new SsdpHandler(_logger, _config, signature); + } + catch (Exception ex) + { + _logger.ErrorException("Error starting Dlna server", ex); + } + } + } + + private void DisposeServer() + { + lock (_syncLock) + { + if (_ssdpHandler != null) + { + try + { + _ssdpHandler.Dispose(); + } + catch (Exception ex) + { + _logger.ErrorException("Error disposing Dlna server", ex); + } + _ssdpHandler = null; + } + } + } + + private string GenerateServerSignature() + { + var os = Environment.OSVersion; + var pstring = os.Platform.ToString(); + switch (os.Platform) + { + case PlatformID.Win32NT: + case PlatformID.Win32S: + case PlatformID.Win32Windows: + pstring = "WIN"; + break; + } + + return String.Format( + "{0}{1}/{2}.{3} UPnP/1.0 DLNADOC/1.5 MediaBrowser/{4}", + pstring, + IntPtr.Size * 8, + os.Version.Major, + os.Version.Minor, + _appHost.ApplicationVersion + ); + } + + public void Dispose() + { + DisposeServer(); + } + } +} diff --git a/MediaBrowser.Dlna/Server/Headers.cs b/MediaBrowser.Dlna/Server/Headers.cs new file mode 100644 index 000000000..859ae7fbf --- /dev/null +++ b/MediaBrowser.Dlna/Server/Headers.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace MediaBrowser.Dlna.Server +{ + public class Headers : IDictionary<string, string> + { + private readonly bool _asIs = false; + private readonly Dictionary<string, string> _dict = new Dictionary<string, string>(); + private readonly static Regex Validator = new Regex(@"^[a-z\d][a-z\d_.-]+$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + protected Headers(bool asIs) + { + _asIs = asIs; + } + + public Headers() + : this(asIs: false) + { + } + + public int Count + { + get + { + return _dict.Count; + } + } + public string HeaderBlock + { + get + { + var hb = new StringBuilder(); + foreach (var h in this) + { + hb.AppendFormat("{0}: {1}\r\n", h.Key, h.Value); + } + return hb.ToString(); + } + } + public Stream HeaderStream + { + get + { + return new MemoryStream(Encoding.ASCII.GetBytes(HeaderBlock)); + } + } + public bool IsReadOnly + { + get + { + return false; + } + } + public ICollection<string> Keys + { + get + { + return _dict.Keys; + } + } + public ICollection<string> Values + { + get + { + return _dict.Values; + } + } + + + public string this[string key] + { + get + { + return _dict[Normalize(key)]; + } + set + { + _dict[Normalize(key)] = value; + } + } + + + private string Normalize(string header) + { + if (!_asIs) + { + header = header.ToLower(); + } + header = header.Trim(); + if (!Validator.IsMatch(header)) + { + throw new ArgumentException("Invalid header: " + header); + } + return header; + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return _dict.GetEnumerator(); + } + + public void Add(KeyValuePair<string, string> item) + { + Add(item.Key, item.Value); + } + + public void Add(string key, string value) + { + _dict.Add(Normalize(key), value); + } + + public void Clear() + { + _dict.Clear(); + } + + public bool Contains(KeyValuePair<string, string> item) + { + var p = new KeyValuePair<string, string>(Normalize(item.Key), item.Value); + return _dict.Contains(p); + } + + public bool ContainsKey(string key) + { + return _dict.ContainsKey(Normalize(key)); + } + + public void CopyTo(KeyValuePair<string, string>[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + public IEnumerator<KeyValuePair<string, string>> GetEnumerator() + { + return _dict.GetEnumerator(); + } + + public bool Remove(string key) + { + return _dict.Remove(Normalize(key)); + } + + public bool Remove(KeyValuePair<string, string> item) + { + return Remove(item.Key); + } + + public override string ToString() + { + return string.Format("({0})", string.Join(", ", (from x in _dict + select string.Format("{0}={1}", x.Key, x.Value)))); + } + + public bool TryGetValue(string key, out string value) + { + return _dict.TryGetValue(Normalize(key), out value); + } + } +} diff --git a/MediaBrowser.Dlna/Server/RawHeaders.cs b/MediaBrowser.Dlna/Server/RawHeaders.cs new file mode 100644 index 000000000..f57e6b9f3 --- /dev/null +++ b/MediaBrowser.Dlna/Server/RawHeaders.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Dlna.Server +{ + public class RawHeaders : Headers + { + public RawHeaders() + : base(true) + { + } + } +} diff --git a/MediaBrowser.Dlna/Server/SsdpHandler.cs b/MediaBrowser.Dlna/Server/SsdpHandler.cs new file mode 100644 index 000000000..63c2abbec --- /dev/null +++ b/MediaBrowser.Dlna/Server/SsdpHandler.cs @@ -0,0 +1,260 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace MediaBrowser.Dlna.Server +{ + public class SsdpHandler : IDisposable + { + private readonly ILogger _logger; + private readonly IServerConfigurationManager _config; + private readonly string _serverSignature; + private bool _isDisposed = false; + + const string SSDPAddr = "239.255.255.250"; + const int SSDPPort = 1900; + + private readonly IPEndPoint _ssdpEndp = new IPEndPoint(IPAddress.Parse(SSDPAddr), SSDPPort); + private readonly IPAddress _ssdpIp = IPAddress.Parse(SSDPAddr); + + private UdpClient _udpClient; + + private readonly Dictionary<Guid, List<UpnpDevice>> _devices = new Dictionary<Guid, List<UpnpDevice>>(); + + public SsdpHandler(ILogger logger, IServerConfigurationManager config, string serverSignature) + { + _logger = logger; + _config = config; + _serverSignature = serverSignature; + + Start(); + } + + private IEnumerable<UpnpDevice> Devices + { + get + { + UpnpDevice[] devs; + lock (_devices) + { + devs = _devices.Values.SelectMany(i => i).ToArray(); + } + return devs; + } + } + + private 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); + _logger.Info("SSDP service started"); + Receive(); + } + + private void Receive() + { + try + { + _udpClient.BeginReceive(ReceiveCallback, null); + } + catch (ObjectDisposedException) + { + } + } + + private void ReceiveCallback(IAsyncResult result) + { + try + { + var endpoint = new IPEndPoint(IPAddress.None, SSDPPort); + var received = _udpClient.EndReceive(result, ref endpoint); + + if (_config.Configuration.DlnaOptions.EnableDebugLogging) + { + _logger.Debug("{0} - SSDP Received a datagram", endpoint); + } + + using (var reader = new StreamReader(new MemoryStream(received), Encoding.ASCII)) + { + var proto = (reader.ReadLine() ?? string.Empty).Trim(); + var method = proto.Split(new[] { ' ' }, 2)[0]; + var headers = new Headers(); + for (var line = reader.ReadLine(); line != null; line = reader.ReadLine()) + { + line = line.Trim(); + if (string.IsNullOrEmpty(line)) + { + break; + } + var parts = line.Split(new char[] { ':' }, 2); + headers[parts[0]] = parts[1].Trim(); + } + + if (_config.Configuration.DlnaOptions.EnableDebugLogging) + { + _logger.Debug("{0} - Datagram method: {1}", endpoint, method); + //_logger.Debug(headers); + } + + if (string.Equals(method, "M-SEARCH", StringComparison.OrdinalIgnoreCase)) + { + RespondToSearch(endpoint, headers["st"]); + } + } + } + catch (Exception ex) + { + _logger.ErrorException("Failed to read SSDP message", ex); + } + + if (!_isDisposed) + { + Receive(); + } + } + + private void RespondToSearch(IPEndPoint endpoint, string req) + { + if (req == "ssdp:all") + { + req = null; + } + + if (_config.Configuration.DlnaOptions.EnableDebugLogging) + { + _logger.Debug("RespondToSearch"); + } + + foreach (var d in Devices) + { + if (!string.IsNullOrEmpty(req) && req != d.Type) + { + continue; + } + + SendSearchResponse(endpoint, d); + } + } + + private void SendSearchResponse(IPEndPoint endpoint, UpnpDevice dev) + { + var headers = new RawHeaders(); + headers.Add("CACHE-CONTROL", "max-age = 600"); + headers.Add("DATE", DateTime.Now.ToString("R")); + headers.Add("EXT", ""); + headers.Add("LOCATION", dev.Descriptor.ToString()); + headers.Add("SERVER", _serverSignature); + headers.Add("ST", dev.Type); + headers.Add("USN", dev.USN); + + SendDatagram(endpoint, String.Format("HTTP/1.1 200 OK\r\n{0}\r\n", headers.HeaderBlock), false); + _logger.Info("{1} - Responded to a {0} request", dev.Type, endpoint); + } + + private void SendDatagram(IPEndPoint endpoint, string msg, bool sticky) + { + if (_isDisposed) + { + return; + } + //var dgram = new Datagram(endpoint, msg, sticky); + //if (messageQueue.Count == 0) + //{ + // dgram.Send(); + //} + //messageQueue.Enqueue(dgram); + //queueTimer.Enabled = true; + } + + private void NotifyAll() + { + _logger.Debug("NotifyAll"); + foreach (var d in Devices) + { + NotifyDevice(d, "alive", false); + } + } + + private void NotifyDevice(UpnpDevice dev, string type, bool sticky) + { + _logger.Debug("NotifyDevice"); + var headers = new RawHeaders(); + headers.Add("HOST", "239.255.255.250:1900"); + headers.Add("CACHE-CONTROL", "max-age = 600"); + headers.Add("LOCATION", dev.Descriptor.ToString()); + headers.Add("SERVER", _serverSignature); + headers.Add("NTS", "ssdp:" + type); + headers.Add("NT", dev.Type); + headers.Add("USN", dev.USN); + + SendDatagram(_ssdpEndp, String.Format("NOTIFY * HTTP/1.1\r\n{0}\r\n", headers.HeaderBlock), sticky); + _logger.Debug("{0} said {1}", dev.USN, type); + } + + private void RegisterNotification(Guid UUID, Uri Descriptor) + { + List<UpnpDevice> list; + lock (_devices) + { + if (!_devices.TryGetValue(UUID, out list)) + { + _devices.Add(UUID, list = new List<UpnpDevice>()); + } + } + + 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)); + } + + NotifyAll(); + _logger.Debug("Registered mount {0}", UUID); + } + + internal void UnregisterNotification(Guid UUID) + { + List<UpnpDevice> dl; + lock (_devices) + { + 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(); + + //notificationTimer.Enabled = false; + //queueTimer.Enabled = false; + //notificationTimer.Dispose(); + //queueTimer.Dispose(); + //datagramPosted.Dispose(); + } + } +} diff --git a/MediaBrowser.Dlna/Server/UpnpDevice.cs b/MediaBrowser.Dlna/Server/UpnpDevice.cs new file mode 100644 index 000000000..96e37eb07 --- /dev/null +++ b/MediaBrowser.Dlna/Server/UpnpDevice.cs @@ -0,0 +1,28 @@ +using System; + +namespace MediaBrowser.Dlna.Server +{ + public sealed class UpnpDevice + { + public readonly Uri Descriptor; + public readonly string Type; + public readonly string USN; + public readonly Guid Uuid; + + public UpnpDevice(Guid aUuid, string aType, Uri aDescriptor) + { + Uuid = aUuid; + Type = aType; + Descriptor = aDescriptor; + + if (Type.StartsWith("uuid:")) + { + USN = Type; + } + else + { + USN = String.Format("uuid:{0}::{1}", Uuid.ToString(), Type); + } + } + } +} diff --git a/MediaBrowser.Model/Configuration/DlnaOptions.cs b/MediaBrowser.Model/Configuration/DlnaOptions.cs index b6398239f..00fdaa444 100644 --- a/MediaBrowser.Model/Configuration/DlnaOptions.cs +++ b/MediaBrowser.Model/Configuration/DlnaOptions.cs @@ -4,12 +4,14 @@ namespace MediaBrowser.Model.Configuration public class DlnaOptions { public bool EnablePlayTo { get; set; } + public bool EnableServer { get; set; } public bool EnableDebugLogging { get; set; } public int ClientDiscoveryIntervalSeconds { get; set; } public DlnaOptions() { EnablePlayTo = true; + EnableServer = true; ClientDiscoveryIntervalSeconds = 60; } } |
