stub out dlna server

This commit is contained in:
Luke Pulverenti 2014-03-24 08:47:39 -04:00
parent 1c3c12ebf6
commit 501dedb13c
9 changed files with 623 additions and 11 deletions

View File

@ -39,6 +39,8 @@ namespace MediaBrowser.Api.ScheduledTasks
TaskManager = taskManager; TaskManager = taskManager;
} }
private bool _lastResponseHadTasksRunning = true;
/// <summary> /// <summary>
/// Gets the data to send. /// Gets the data to send.
/// </summary> /// </summary>
@ -46,7 +48,25 @@ namespace MediaBrowser.Api.ScheduledTasks
/// <returns>Task{IEnumerable{TaskInfo}}.</returns> /// <returns>Task{IEnumerable{TaskInfo}}.</returns>
protected override Task<IEnumerable<TaskInfo>> GetDataToSend(object state) 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) .OrderBy(i => i.Name)
.Select(ScheduledTaskHelpers.GetTaskInfo) .Select(ScheduledTaskHelpers.GetTaskInfo)
.Where(i => !i.IsHidden)); .Where(i => !i.IsHidden));

View File

@ -1,11 +1,11 @@
using System.Globalization; using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Model.Net;
namespace MediaBrowser.Common.Net namespace MediaBrowser.Common.Net
{ {
@ -16,6 +16,7 @@ namespace MediaBrowser.Common.Net
/// <typeparam name="TStateType">The type of the T state type.</typeparam> /// <typeparam name="TStateType">The type of the T state type.</typeparam>
public abstract class BasePeriodicWebSocketListener<TReturnDataType, TStateType> : IWebSocketListener, IDisposable public abstract class BasePeriodicWebSocketListener<TReturnDataType, TStateType> : IWebSocketListener, IDisposable
where TStateType : class, new() where TStateType : class, new()
where TReturnDataType : class
{ {
/// <summary> /// <summary>
/// The _active connections /// The _active connections
@ -144,12 +145,15 @@ namespace MediaBrowser.Common.Net
var data = await GetDataToSend(tuple.Item4).ConfigureAwait(false); var data = await GetDataToSend(tuple.Item4).ConfigureAwait(false);
if (data != null)
{
await connection.SendAsync(new WebSocketMessage<TReturnDataType> await connection.SendAsync(new WebSocketMessage<TReturnDataType>
{ {
MessageType = Name, MessageType = Name,
Data = data Data = data
}, tuple.Item2.Token).ConfigureAwait(false); }, tuple.Item2.Token).ConfigureAwait(false);
}
tuple.Item5.Release(); tuple.Item5.Release();
} }

View File

@ -98,6 +98,11 @@
<Compile Include="Profiles\Xbox360Profile.cs" /> <Compile Include="Profiles\Xbox360Profile.cs" />
<Compile Include="Profiles\XboxOneProfile.cs" /> <Compile Include="Profiles\XboxOneProfile.cs" />
<Compile Include="Properties\AssemblyInfo.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>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj"> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
@ -113,9 +118,7 @@
<Name>MediaBrowser.Model</Name> <Name>MediaBrowser.Model</Name>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup />
<Folder Include="Server\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- 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. Other similar extension points exist, see Microsoft.Common.targets.

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}

View File

@ -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)
{
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -4,12 +4,14 @@ namespace MediaBrowser.Model.Configuration
public class DlnaOptions public class DlnaOptions
{ {
public bool EnablePlayTo { get; set; } public bool EnablePlayTo { get; set; }
public bool EnableServer { get; set; }
public bool EnableDebugLogging { get; set; } public bool EnableDebugLogging { get; set; }
public int ClientDiscoveryIntervalSeconds { get; set; } public int ClientDiscoveryIntervalSeconds { get; set; }
public DlnaOptions() public DlnaOptions()
{ {
EnablePlayTo = true; EnablePlayTo = true;
EnableServer = true;
ClientDiscoveryIntervalSeconds = 60; ClientDiscoveryIntervalSeconds = 60;
} }
} }