aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Pulverenti <luke.pulverenti@gmail.com>2014-03-24 08:47:39 -0400
committerLuke Pulverenti <luke.pulverenti@gmail.com>2014-03-24 08:47:39 -0400
commit501dedb13cd59dc2683ac4192cd11289bd304cfb (patch)
treef3c92b89ae3e8a7e744ee13eb1b16139da690622
parent1c3c12ebf6eb89f446d69900ccab2d3c4c48a85c (diff)
stub out dlna server
-rw-r--r--MediaBrowser.Api/ScheduledTasks/ScheduledTasksWebSocketListener.cs22
-rw-r--r--MediaBrowser.Common/Net/BasePeriodicWebSocketListener.cs18
-rw-r--r--MediaBrowser.Dlna/MediaBrowser.Dlna.csproj9
-rw-r--r--MediaBrowser.Dlna/Server/DlnaServerEntryPoint.cs115
-rw-r--r--MediaBrowser.Dlna/Server/Headers.cs164
-rw-r--r--MediaBrowser.Dlna/Server/RawHeaders.cs16
-rw-r--r--MediaBrowser.Dlna/Server/SsdpHandler.cs260
-rw-r--r--MediaBrowser.Dlna/Server/UpnpDevice.cs28
-rw-r--r--MediaBrowser.Model/Configuration/DlnaOptions.cs2
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;
}
}