rework dlna project
|
@ -5,23 +5,30 @@
|
|||
"type": "project",
|
||||
"framework": ".NETPortable,Version=v4.5,Profile=Profile7",
|
||||
"compile": {
|
||||
"bin/Release/MediaBrowser.Common.dll": {}
|
||||
"bin/Debug/MediaBrowser.Common.dll": {}
|
||||
},
|
||||
"runtime": {
|
||||
"bin/Release/MediaBrowser.Common.dll": {}
|
||||
"bin/Debug/MediaBrowser.Common.dll": {}
|
||||
},
|
||||
"contentFiles": {
|
||||
"bin/Debug/MediaBrowser.Common.pdb": {
|
||||
"buildAction": "None",
|
||||
"codeLanguage": "any",
|
||||
"copyToOutput": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"MediaBrowser.Model/1.0.0": {
|
||||
"type": "project",
|
||||
"framework": ".NETPortable,Version=v4.5,Profile=Profile7",
|
||||
"compile": {
|
||||
"bin/Release/MediaBrowser.Model.dll": {}
|
||||
"bin/Debug/MediaBrowser.Model.dll": {}
|
||||
},
|
||||
"runtime": {
|
||||
"bin/Release/MediaBrowser.Model.dll": {}
|
||||
"bin/Debug/MediaBrowser.Model.dll": {}
|
||||
},
|
||||
"contentFiles": {
|
||||
"bin/Release/MediaBrowser.Model.pdb": {
|
||||
"bin/Debug/MediaBrowser.Model.pdb": {
|
||||
"buildAction": "None",
|
||||
"codeLanguage": "any",
|
||||
"copyToOutput": true
|
||||
|
|
12
Emby.Dlna/Common/Argument.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
|
||||
namespace MediaBrowser.Dlna.Common
|
||||
{
|
||||
public class Argument
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public string Direction { get; set; }
|
||||
|
||||
public string RelatedStateVariable { get; set; }
|
||||
}
|
||||
}
|
21
Emby.Dlna/Common/DeviceIcon.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
|
||||
namespace MediaBrowser.Dlna.Common
|
||||
{
|
||||
public class DeviceIcon
|
||||
{
|
||||
public string Url { get; set; }
|
||||
|
||||
public string MimeType { get; set; }
|
||||
|
||||
public int Width { get; set; }
|
||||
|
||||
public int Height { get; set; }
|
||||
|
||||
public string Depth { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("{0}x{1}", Height, Width);
|
||||
}
|
||||
}
|
||||
}
|
21
Emby.Dlna/Common/DeviceService.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
|
||||
namespace MediaBrowser.Dlna.Common
|
||||
{
|
||||
public class DeviceService
|
||||
{
|
||||
public string ServiceType { get; set; }
|
||||
|
||||
public string ServiceId { get; set; }
|
||||
|
||||
public string ScpdUrl { get; set; }
|
||||
|
||||
public string ControlUrl { get; set; }
|
||||
|
||||
public string EventSubUrl { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("{0}", ServiceId);
|
||||
}
|
||||
}
|
||||
}
|
21
Emby.Dlna/Common/ServiceAction.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Dlna.Common
|
||||
{
|
||||
public class ServiceAction
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public List<Argument> ArgumentList { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
|
||||
public ServiceAction()
|
||||
{
|
||||
ArgumentList = new List<Argument>();
|
||||
}
|
||||
}
|
||||
}
|
25
Emby.Dlna/Common/StateVariable.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Dlna.Common
|
||||
{
|
||||
public class StateVariable
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public string DataType { get; set; }
|
||||
|
||||
public bool SendsEvents { get; set; }
|
||||
|
||||
public List<string> AllowedValues { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
|
||||
public StateVariable()
|
||||
{
|
||||
AllowedValues = new List<string>();
|
||||
}
|
||||
}
|
||||
}
|
29
Emby.Dlna/ConfigurationExtension.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Dlna
|
||||
{
|
||||
public static class ConfigurationExtension
|
||||
{
|
||||
public static DlnaOptions GetDlnaConfiguration(this IConfigurationManager manager)
|
||||
{
|
||||
return manager.GetConfiguration<DlnaOptions>("dlna");
|
||||
}
|
||||
}
|
||||
|
||||
public class DlnaConfigurationFactory : IConfigurationFactory
|
||||
{
|
||||
public IEnumerable<ConfigurationStore> GetConfigurations()
|
||||
{
|
||||
return new List<ConfigurationStore>
|
||||
{
|
||||
new ConfigurationStore
|
||||
{
|
||||
Key = "dlna",
|
||||
ConfigurationType = typeof (DlnaOptions)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
37
Emby.Dlna/ConnectionManager/ConnectionManager.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Dlna.Service;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Dlna.ConnectionManager
|
||||
{
|
||||
public class ConnectionManager : BaseService, IConnectionManager
|
||||
{
|
||||
private readonly IDlnaManager _dlna;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
||||
public ConnectionManager(IDlnaManager dlna, IServerConfigurationManager config, ILogger logger, IHttpClient httpClient)
|
||||
: base(logger, httpClient)
|
||||
{
|
||||
_dlna = dlna;
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string GetServiceXml(IDictionary<string, string> headers)
|
||||
{
|
||||
return new ConnectionManagerXmlBuilder().GetXml();
|
||||
}
|
||||
|
||||
public ControlResponse ProcessControlRequest(ControlRequest request)
|
||||
{
|
||||
var profile = _dlna.GetProfile(request.Headers) ??
|
||||
_dlna.GetDefaultProfile();
|
||||
|
||||
return new ControlHandler(_logger, profile, _config).ProcessControlRequest(request);
|
||||
}
|
||||
}
|
||||
}
|
106
Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs
Normal file
|
@ -0,0 +1,106 @@
|
|||
using MediaBrowser.Dlna.Common;
|
||||
using MediaBrowser.Dlna.Service;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Dlna.ConnectionManager
|
||||
{
|
||||
public class ConnectionManagerXmlBuilder
|
||||
{
|
||||
public string GetXml()
|
||||
{
|
||||
return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(), GetStateVariables());
|
||||
}
|
||||
|
||||
private IEnumerable<StateVariable> GetStateVariables()
|
||||
{
|
||||
var list = new List<StateVariable>();
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "SourceProtocolInfo",
|
||||
DataType = "string",
|
||||
SendsEvents = true
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "SinkProtocolInfo",
|
||||
DataType = "string",
|
||||
SendsEvents = true
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "CurrentConnectionIDs",
|
||||
DataType = "string",
|
||||
SendsEvents = true
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ConnectionStatus",
|
||||
DataType = "string",
|
||||
SendsEvents = false,
|
||||
|
||||
AllowedValues = new List<string>
|
||||
{
|
||||
"OK",
|
||||
"ContentFormatMismatch",
|
||||
"InsufficientBandwidth",
|
||||
"UnreliableChannel",
|
||||
"Unknown"
|
||||
}
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ConnectionManager",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Direction",
|
||||
DataType = "string",
|
||||
SendsEvents = false,
|
||||
|
||||
AllowedValues = new List<string>
|
||||
{
|
||||
"Output",
|
||||
"Input"
|
||||
}
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ProtocolInfo",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ConnectionID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_AVTransportID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RcsID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
41
Emby.Dlna/ConnectionManager/ControlHandler.cs
Normal file
|
@ -0,0 +1,41 @@
|
|||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Dlna.Server;
|
||||
using MediaBrowser.Dlna.Service;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Dlna.ConnectionManager
|
||||
{
|
||||
public class ControlHandler : BaseControlHandler
|
||||
{
|
||||
private readonly DeviceProfile _profile;
|
||||
|
||||
public ControlHandler(ILogger logger, DeviceProfile profile, IServerConfigurationManager config)
|
||||
: base(config, logger)
|
||||
{
|
||||
_profile = profile;
|
||||
}
|
||||
|
||||
protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, Headers methodParams)
|
||||
{
|
||||
if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return HandleGetProtocolInfo();
|
||||
}
|
||||
|
||||
throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
|
||||
}
|
||||
|
||||
private IEnumerable<KeyValuePair<string, string>> HandleGetProtocolInfo()
|
||||
{
|
||||
return new Headers(true)
|
||||
{
|
||||
{ "Source", _profile.ProtocolInfo },
|
||||
{ "Sink", "" }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
205
Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs
Normal file
|
@ -0,0 +1,205 @@
|
|||
using MediaBrowser.Dlna.Common;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Dlna.ConnectionManager
|
||||
{
|
||||
public class ServiceActionListBuilder
|
||||
{
|
||||
public IEnumerable<ServiceAction> GetActions()
|
||||
{
|
||||
var list = new List<ServiceAction>
|
||||
{
|
||||
GetCurrentConnectionInfo(),
|
||||
GetProtocolInfo(),
|
||||
GetCurrentConnectionIDs(),
|
||||
ConnectionComplete(),
|
||||
PrepareForConnection()
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private ServiceAction PrepareForConnection()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "PrepareForConnection"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "RemoteProtocolInfo",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ProtocolInfo"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "PeerConnectionManager",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ConnectionManager"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "PeerConnectionID",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ConnectionID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Direction",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Direction"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ConnectionID",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ConnectionID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "AVTransportID",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_AVTransportID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "RcsID",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_RcsID"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private ServiceAction GetCurrentConnectionInfo()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetCurrentConnectionInfo"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ConnectionID",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ConnectionID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "RcsID",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_RcsID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "AVTransportID",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_AVTransportID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ProtocolInfo",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ProtocolInfo"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "PeerConnectionManager",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ConnectionManager"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "PeerConnectionID",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ConnectionID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Direction",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Direction"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Status",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ConnectionStatus"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private ServiceAction GetProtocolInfo()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetProtocolInfo"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Source",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "SourceProtocolInfo"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Sink",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "SinkProtocolInfo"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private ServiceAction GetCurrentConnectionIDs()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetCurrentConnectionIDs"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ConnectionIDs",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "CurrentConnectionIDs"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private ServiceAction ConnectionComplete()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "ConnectionComplete"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ConnectionID",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ConnectionID"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
}
|
||||
}
|
133
Emby.Dlna/ContentDirectory/ContentDirectory.cs
Normal file
|
@ -0,0 +1,133 @@
|
|||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Dlna.Service;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
|
||||
namespace MediaBrowser.Dlna.ContentDirectory
|
||||
{
|
||||
public class ContentDirectory : BaseService, IContentDirectory, IDisposable
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly IDlnaManager _dlna;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IChannelManager _channelManager;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IUserViewManager _userViewManager;
|
||||
private readonly Func<IMediaEncoder> _mediaEncoder;
|
||||
|
||||
public ContentDirectory(IDlnaManager dlna,
|
||||
IUserDataManager userDataManager,
|
||||
IImageProcessor imageProcessor,
|
||||
ILibraryManager libraryManager,
|
||||
IServerConfigurationManager config,
|
||||
IUserManager userManager,
|
||||
ILogger logger,
|
||||
IHttpClient httpClient, ILocalizationManager localization, IChannelManager channelManager, IMediaSourceManager mediaSourceManager, IUserViewManager userViewManager, Func<IMediaEncoder> mediaEncoder)
|
||||
: base(logger, httpClient)
|
||||
{
|
||||
_dlna = dlna;
|
||||
_userDataManager = userDataManager;
|
||||
_imageProcessor = imageProcessor;
|
||||
_libraryManager = libraryManager;
|
||||
_config = config;
|
||||
_userManager = userManager;
|
||||
_localization = localization;
|
||||
_channelManager = channelManager;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_userViewManager = userViewManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
}
|
||||
|
||||
private int SystemUpdateId
|
||||
{
|
||||
get
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
return now.Year + now.DayOfYear + now.Hour;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetServiceXml(IDictionary<string, string> headers)
|
||||
{
|
||||
return new ContentDirectoryXmlBuilder().GetXml();
|
||||
}
|
||||
|
||||
public ControlResponse ProcessControlRequest(ControlRequest request)
|
||||
{
|
||||
var profile = _dlna.GetProfile(request.Headers) ??
|
||||
_dlna.GetDefaultProfile();
|
||||
|
||||
var serverAddress = request.RequestedUrl.Substring(0, request.RequestedUrl.IndexOf("/dlna", StringComparison.OrdinalIgnoreCase));
|
||||
string accessToken = null;
|
||||
|
||||
var user = GetUser(profile);
|
||||
|
||||
return new ControlHandler(
|
||||
Logger,
|
||||
_libraryManager,
|
||||
profile,
|
||||
serverAddress,
|
||||
accessToken,
|
||||
_imageProcessor,
|
||||
_userDataManager,
|
||||
user,
|
||||
SystemUpdateId,
|
||||
_config,
|
||||
_localization,
|
||||
_channelManager,
|
||||
_mediaSourceManager,
|
||||
_userViewManager,
|
||||
_mediaEncoder())
|
||||
.ProcessControlRequest(request);
|
||||
}
|
||||
|
||||
private User GetUser(DeviceProfile profile)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(profile.UserId))
|
||||
{
|
||||
var user = _userManager.GetUserById(profile.UserId);
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
var userId = _config.GetDlnaConfiguration().DefaultUserId;
|
||||
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
// No configuration so it's going to be pretty arbitrary
|
||||
return _userManager.Users.First();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
126
Emby.Dlna/ContentDirectory/ContentDirectoryBrowser.cs
Normal file
|
@ -0,0 +1,126 @@
|
|||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Dlna.Server;
|
||||
|
||||
namespace MediaBrowser.Dlna.ContentDirectory
|
||||
{
|
||||
public class ContentDirectoryBrowser
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public ContentDirectoryBrowser(IHttpClient httpClient, ILogger logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private static XNamespace UNamespace = "u";
|
||||
|
||||
public async Task<QueryResult<ChannelItemInfo>> Browse(ContentDirectoryBrowseRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
CancellationToken = cancellationToken,
|
||||
UserAgent = "Emby",
|
||||
RequestContentType = "text/xml; charset=\"utf-8\"",
|
||||
LogErrorResponseBody = true,
|
||||
Url = request.ContentDirectoryUrl,
|
||||
BufferContent = false
|
||||
};
|
||||
|
||||
options.RequestHeaders["SOAPACTION"] = "urn:schemas-upnp-org:service:ContentDirectory:1#Browse";
|
||||
|
||||
options.RequestContent = GetRequestBody(request);
|
||||
|
||||
var response = await _httpClient.SendAsync(options, "POST");
|
||||
|
||||
using (var reader = new StreamReader(response.Content))
|
||||
{
|
||||
var doc = XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace);
|
||||
|
||||
var queryResult = new QueryResult<ChannelItemInfo>();
|
||||
|
||||
if (doc.Document == null)
|
||||
return queryResult;
|
||||
|
||||
var responseElement = doc.Document.Descendants(UNamespace + "BrowseResponse").ToList();
|
||||
|
||||
var countElement = responseElement.Select(i => i.Element("TotalMatches")).FirstOrDefault(i => i != null);
|
||||
var countValue = countElement == null ? null : countElement.Value;
|
||||
|
||||
int count;
|
||||
if (!string.IsNullOrWhiteSpace(countValue) && int.TryParse(countValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out count))
|
||||
{
|
||||
queryResult.TotalRecordCount = count;
|
||||
|
||||
var resultElement = responseElement.Select(i => i.Element("Result")).FirstOrDefault(i => i != null);
|
||||
var resultString = (string)resultElement;
|
||||
|
||||
if (resultElement != null)
|
||||
{
|
||||
var xElement = XElement.Parse(resultString);
|
||||
}
|
||||
}
|
||||
|
||||
return queryResult;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetRequestBody(ContentDirectoryBrowseRequest request)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
||||
|
||||
builder.Append("<s:Envelope s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"><s:Body>");
|
||||
builder.Append("<u:Browse xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.ParentId))
|
||||
{
|
||||
request.ParentId = "1";
|
||||
}
|
||||
|
||||
builder.AppendFormat("<ObjectID>{0}</ObjectID>", DescriptionXmlBuilder.Escape(request.ParentId));
|
||||
builder.Append("<BrowseFlag>BrowseDirectChildren</BrowseFlag>");
|
||||
|
||||
//builder.Append("<BrowseFlag>BrowseMetadata</BrowseFlag>");
|
||||
|
||||
builder.Append("<Filter>*</Filter>");
|
||||
|
||||
request.StartIndex = request.StartIndex ?? 0;
|
||||
builder.AppendFormat("<StartingIndex>{0}</StartingIndex>", DescriptionXmlBuilder.Escape(request.StartIndex.Value.ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
request.Limit = request.Limit ?? 20;
|
||||
if (request.Limit.HasValue)
|
||||
{
|
||||
builder.AppendFormat("<RequestedCount>{0}</RequestedCount>", DescriptionXmlBuilder.Escape(request.Limit.Value.ToString(CultureInfo.InvariantCulture)));
|
||||
}
|
||||
|
||||
builder.Append("<SortCriteria></SortCriteria>");
|
||||
|
||||
builder.Append("</u:Browse>");
|
||||
builder.Append("</s:Body></s:Envelope>");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class ContentDirectoryBrowseRequest
|
||||
{
|
||||
public int? StartIndex { get; set; }
|
||||
public int? Limit { get; set; }
|
||||
public string ParentId { get; set; }
|
||||
public string ContentDirectoryUrl { get; set; }
|
||||
}
|
||||
}
|
147
Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs
Normal file
|
@ -0,0 +1,147 @@
|
|||
using MediaBrowser.Dlna.Common;
|
||||
using MediaBrowser.Dlna.Service;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Dlna.ContentDirectory
|
||||
{
|
||||
public class ContentDirectoryXmlBuilder
|
||||
{
|
||||
public string GetXml()
|
||||
{
|
||||
return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(),
|
||||
GetStateVariables());
|
||||
}
|
||||
|
||||
private IEnumerable<StateVariable> GetStateVariables()
|
||||
{
|
||||
var list = new List<StateVariable>();
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Filter",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_SortCriteria",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Index",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Count",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_UpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "SearchCapabilities",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "SortCapabilities",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "SystemUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_SearchCriteria",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Result",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ObjectID",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_BrowseFlag",
|
||||
DataType = "string",
|
||||
SendsEvents = false,
|
||||
|
||||
AllowedValues = new List<string>
|
||||
{
|
||||
"BrowseMetadata",
|
||||
"BrowseDirectChildren"
|
||||
}
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_BrowseLetter",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_CategoryType",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_PosSec",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Featurelist",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
588
Emby.Dlna/ContentDirectory/ControlHandler.cs
Normal file
|
@ -0,0 +1,588 @@
|
|||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Dlna.Didl;
|
||||
using MediaBrowser.Dlna.Server;
|
||||
using MediaBrowser.Dlna.Service;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
|
||||
namespace MediaBrowser.Dlna.ContentDirectory
|
||||
{
|
||||
public class ControlHandler : BaseControlHandler
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IChannelManager _channelManager;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly User _user;
|
||||
private readonly IUserViewManager _userViewManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
|
||||
private const string NS_DC = "http://purl.org/dc/elements/1.1/";
|
||||
private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
|
||||
private const string NS_DLNA = "urn:schemas-dlna-org:metadata-1-0/";
|
||||
private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/";
|
||||
|
||||
private readonly int _systemUpdateId;
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
|
||||
private readonly DidlBuilder _didlBuilder;
|
||||
|
||||
private readonly DeviceProfile _profile;
|
||||
|
||||
public ControlHandler(ILogger logger, ILibraryManager libraryManager, DeviceProfile profile, string serverAddress, string accessToken, IImageProcessor imageProcessor, IUserDataManager userDataManager, User user, int systemUpdateId, IServerConfigurationManager config, ILocalizationManager localization, IChannelManager channelManager, IMediaSourceManager mediaSourceManager, IUserViewManager userViewManager, IMediaEncoder mediaEncoder)
|
||||
: base(config, logger)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_userDataManager = userDataManager;
|
||||
_user = user;
|
||||
_systemUpdateId = systemUpdateId;
|
||||
_channelManager = channelManager;
|
||||
_userViewManager = userViewManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_profile = profile;
|
||||
_config = config;
|
||||
|
||||
_didlBuilder = new DidlBuilder(profile, user, imageProcessor, serverAddress, accessToken, userDataManager, localization, mediaSourceManager, Logger, libraryManager, _mediaEncoder);
|
||||
}
|
||||
|
||||
protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, Headers methodParams)
|
||||
{
|
||||
var deviceId = "test";
|
||||
|
||||
var user = _user;
|
||||
|
||||
if (string.Equals(methodName, "GetSearchCapabilities", StringComparison.OrdinalIgnoreCase))
|
||||
return HandleGetSearchCapabilities();
|
||||
|
||||
if (string.Equals(methodName, "GetSortCapabilities", StringComparison.OrdinalIgnoreCase))
|
||||
return HandleGetSortCapabilities();
|
||||
|
||||
if (string.Equals(methodName, "GetSortExtensionCapabilities", StringComparison.OrdinalIgnoreCase))
|
||||
return HandleGetSortExtensionCapabilities();
|
||||
|
||||
if (string.Equals(methodName, "GetSystemUpdateID", StringComparison.OrdinalIgnoreCase))
|
||||
return HandleGetSystemUpdateID();
|
||||
|
||||
if (string.Equals(methodName, "Browse", StringComparison.OrdinalIgnoreCase))
|
||||
return HandleBrowse(methodParams, user, deviceId).Result;
|
||||
|
||||
if (string.Equals(methodName, "X_GetFeatureList", StringComparison.OrdinalIgnoreCase))
|
||||
return HandleXGetFeatureList();
|
||||
|
||||
if (string.Equals(methodName, "GetFeatureList", StringComparison.OrdinalIgnoreCase))
|
||||
return HandleGetFeatureList();
|
||||
|
||||
if (string.Equals(methodName, "X_SetBookmark", StringComparison.OrdinalIgnoreCase))
|
||||
return HandleXSetBookmark(methodParams, user);
|
||||
|
||||
if (string.Equals(methodName, "Search", StringComparison.OrdinalIgnoreCase))
|
||||
return HandleSearch(methodParams, user, deviceId).Result;
|
||||
|
||||
throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
|
||||
}
|
||||
|
||||
private IEnumerable<KeyValuePair<string, string>> HandleXSetBookmark(IDictionary<string, string> sparams, User user)
|
||||
{
|
||||
var id = sparams["ObjectID"];
|
||||
|
||||
var serverItem = GetItemFromObjectId(id, user);
|
||||
|
||||
var item = serverItem.Item;
|
||||
|
||||
var newbookmark = int.Parse(sparams["PosSecond"], _usCulture);
|
||||
|
||||
var userdata = _userDataManager.GetUserData(user, item);
|
||||
|
||||
userdata.PlaybackPositionTicks = TimeSpan.FromSeconds(newbookmark).Ticks;
|
||||
|
||||
_userDataManager.SaveUserData(user.Id, item, userdata, UserDataSaveReason.TogglePlayed,
|
||||
CancellationToken.None);
|
||||
|
||||
return new Headers();
|
||||
}
|
||||
|
||||
private IEnumerable<KeyValuePair<string, string>> HandleGetSearchCapabilities()
|
||||
{
|
||||
return new Headers(true) { { "SearchCaps", "res@resolution,res@size,res@duration,dc:title,dc:creator,upnp:actor,upnp:artist,upnp:genre,upnp:album,dc:date,upnp:class,@id,@refID,@protocolInfo,upnp:author,dc:description,pv:avKeywords" } };
|
||||
}
|
||||
|
||||
private IEnumerable<KeyValuePair<string, string>> HandleGetSortCapabilities()
|
||||
{
|
||||
return new Headers(true)
|
||||
{
|
||||
{ "SortCaps", "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating" }
|
||||
};
|
||||
}
|
||||
|
||||
private IEnumerable<KeyValuePair<string, string>> HandleGetSortExtensionCapabilities()
|
||||
{
|
||||
return new Headers(true)
|
||||
{
|
||||
{ "SortExtensionCaps", "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating" }
|
||||
};
|
||||
}
|
||||
|
||||
private IEnumerable<KeyValuePair<string, string>> HandleGetSystemUpdateID()
|
||||
{
|
||||
var headers = new Headers(true);
|
||||
headers.Add("Id", _systemUpdateId.ToString(_usCulture));
|
||||
return headers;
|
||||
}
|
||||
|
||||
private IEnumerable<KeyValuePair<string, string>> HandleGetFeatureList()
|
||||
{
|
||||
return new Headers(true)
|
||||
{
|
||||
{ "FeatureList", GetFeatureListXml() }
|
||||
};
|
||||
}
|
||||
|
||||
private IEnumerable<KeyValuePair<string, string>> HandleXGetFeatureList()
|
||||
{
|
||||
return new Headers(true)
|
||||
{
|
||||
{ "FeatureList", GetFeatureListXml() }
|
||||
};
|
||||
}
|
||||
|
||||
private string GetFeatureListXml()
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
||||
builder.Append("<Features xmlns=\"urn:schemas-upnp-org:av:avs\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\">");
|
||||
|
||||
builder.Append("<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">");
|
||||
builder.Append("<container id=\"I\" type=\"object.item.imageItem\"/>");
|
||||
builder.Append("<container id=\"A\" type=\"object.item.audioItem\"/>");
|
||||
builder.Append("<container id=\"V\" type=\"object.item.videoItem\"/>");
|
||||
builder.Append("</Feature>");
|
||||
|
||||
builder.Append("</Features>");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<KeyValuePair<string, string>>> HandleBrowse(Headers sparams, User user, string deviceId)
|
||||
{
|
||||
var id = sparams["ObjectID"];
|
||||
var flag = sparams["BrowseFlag"];
|
||||
var filter = new Filter(sparams.GetValueOrDefault("Filter", "*"));
|
||||
var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", ""));
|
||||
|
||||
var provided = 0;
|
||||
|
||||
// Default to null instead of 0
|
||||
// Upnp inspector sends 0 as requestedCount when it wants everything
|
||||
int? requestedCount = null;
|
||||
int? start = 0;
|
||||
|
||||
int requestedVal;
|
||||
if (sparams.ContainsKey("RequestedCount") && int.TryParse(sparams["RequestedCount"], out requestedVal) && requestedVal > 0)
|
||||
{
|
||||
requestedCount = requestedVal;
|
||||
}
|
||||
|
||||
int startVal;
|
||||
if (sparams.ContainsKey("StartingIndex") && int.TryParse(sparams["StartingIndex"], out startVal) && startVal > 0)
|
||||
{
|
||||
start = startVal;
|
||||
}
|
||||
|
||||
//var root = GetItem(id) as IMediaFolder;
|
||||
var result = new XmlDocument();
|
||||
|
||||
var didl = result.CreateElement(string.Empty, "DIDL-Lite", NS_DIDL);
|
||||
didl.SetAttribute("xmlns:dc", NS_DC);
|
||||
didl.SetAttribute("xmlns:dlna", NS_DLNA);
|
||||
didl.SetAttribute("xmlns:upnp", NS_UPNP);
|
||||
//didl.SetAttribute("xmlns:sec", NS_SEC);
|
||||
result.AppendChild(didl);
|
||||
|
||||
var serverItem = GetItemFromObjectId(id, user);
|
||||
var item = serverItem.Item;
|
||||
|
||||
int totalCount;
|
||||
|
||||
if (string.Equals(flag, "BrowseMetadata"))
|
||||
{
|
||||
totalCount = 1;
|
||||
|
||||
if (item.IsFolder || serverItem.StubType.HasValue)
|
||||
{
|
||||
var childrenResult = (await GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount).ConfigureAwait(false));
|
||||
|
||||
result.DocumentElement.AppendChild(_didlBuilder.GetFolderElement(result, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.DocumentElement.AppendChild(_didlBuilder.GetItemElement(_config.GetDlnaConfiguration(), result, item, null, null, deviceId, filter));
|
||||
}
|
||||
|
||||
provided++;
|
||||
}
|
||||
else
|
||||
{
|
||||
var childrenResult = (await GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount).ConfigureAwait(false));
|
||||
totalCount = childrenResult.TotalRecordCount;
|
||||
|
||||
provided = childrenResult.Items.Length;
|
||||
|
||||
foreach (var i in childrenResult.Items)
|
||||
{
|
||||
var childItem = i.Item;
|
||||
var displayStubType = i.StubType;
|
||||
|
||||
if (childItem.IsFolder || displayStubType.HasValue)
|
||||
{
|
||||
var childCount = (await GetUserItems(childItem, displayStubType, user, sortCriteria, null, 0).ConfigureAwait(false))
|
||||
.TotalRecordCount;
|
||||
|
||||
result.DocumentElement.AppendChild(_didlBuilder.GetFolderElement(result, childItem, displayStubType, item, childCount, filter));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.DocumentElement.AppendChild(_didlBuilder.GetItemElement(_config.GetDlnaConfiguration(), result, childItem, item, serverItem.StubType, deviceId, filter));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var resXML = result.OuterXml;
|
||||
|
||||
return new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string,string>("Result", resXML),
|
||||
new KeyValuePair<string,string>("NumberReturned", provided.ToString(_usCulture)),
|
||||
new KeyValuePair<string,string>("TotalMatches", totalCount.ToString(_usCulture)),
|
||||
new KeyValuePair<string,string>("UpdateID", _systemUpdateId.ToString(_usCulture))
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<KeyValuePair<string, string>>> HandleSearch(Headers sparams, User user, string deviceId)
|
||||
{
|
||||
var searchCriteria = new SearchCriteria(sparams.GetValueOrDefault("SearchCriteria", ""));
|
||||
var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", ""));
|
||||
var filter = new Filter(sparams.GetValueOrDefault("Filter", "*"));
|
||||
|
||||
// sort example: dc:title, dc:date
|
||||
|
||||
// Default to null instead of 0
|
||||
// Upnp inspector sends 0 as requestedCount when it wants everything
|
||||
int? requestedCount = null;
|
||||
int? start = 0;
|
||||
|
||||
int requestedVal;
|
||||
if (sparams.ContainsKey("RequestedCount") && int.TryParse(sparams["RequestedCount"], out requestedVal) && requestedVal > 0)
|
||||
{
|
||||
requestedCount = requestedVal;
|
||||
}
|
||||
|
||||
int startVal;
|
||||
if (sparams.ContainsKey("StartingIndex") && int.TryParse(sparams["StartingIndex"], out startVal) && startVal > 0)
|
||||
{
|
||||
start = startVal;
|
||||
}
|
||||
|
||||
//var root = GetItem(id) as IMediaFolder;
|
||||
var result = new XmlDocument();
|
||||
|
||||
var didl = result.CreateElement(string.Empty, "DIDL-Lite", NS_DIDL);
|
||||
didl.SetAttribute("xmlns:dc", NS_DC);
|
||||
didl.SetAttribute("xmlns:dlna", NS_DLNA);
|
||||
didl.SetAttribute("xmlns:upnp", NS_UPNP);
|
||||
|
||||
foreach (var att in _profile.XmlRootAttributes)
|
||||
{
|
||||
didl.SetAttribute(att.Name, att.Value);
|
||||
}
|
||||
|
||||
result.AppendChild(didl);
|
||||
|
||||
var serverItem = GetItemFromObjectId(sparams["ContainerID"], user);
|
||||
|
||||
var item = serverItem.Item;
|
||||
|
||||
var childrenResult = (await GetChildrenSorted(item, user, searchCriteria, sortCriteria, start, requestedCount).ConfigureAwait(false));
|
||||
|
||||
var totalCount = childrenResult.TotalRecordCount;
|
||||
|
||||
var provided = childrenResult.Items.Length;
|
||||
|
||||
foreach (var i in childrenResult.Items)
|
||||
{
|
||||
if (i.IsFolder)
|
||||
{
|
||||
var childCount = (await GetChildrenSorted(i, user, searchCriteria, sortCriteria, null, 0).ConfigureAwait(false))
|
||||
.TotalRecordCount;
|
||||
|
||||
result.DocumentElement.AppendChild(_didlBuilder.GetFolderElement(result, i, null, item, childCount, filter));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.DocumentElement.AppendChild(_didlBuilder.GetItemElement(_config.GetDlnaConfiguration(), result, i, item, serverItem.StubType, deviceId, filter));
|
||||
}
|
||||
}
|
||||
|
||||
var resXML = result.OuterXml;
|
||||
|
||||
return new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string,string>("Result", resXML),
|
||||
new KeyValuePair<string,string>("NumberReturned", provided.ToString(_usCulture)),
|
||||
new KeyValuePair<string,string>("TotalMatches", totalCount.ToString(_usCulture)),
|
||||
new KeyValuePair<string,string>("UpdateID", _systemUpdateId.ToString(_usCulture))
|
||||
};
|
||||
}
|
||||
|
||||
private Task<QueryResult<BaseItem>> GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit)
|
||||
{
|
||||
var folder = (Folder)item;
|
||||
|
||||
var sortOrders = new List<string>();
|
||||
if (!folder.IsPreSorted)
|
||||
{
|
||||
sortOrders.Add(ItemSortBy.SortName);
|
||||
}
|
||||
|
||||
var mediaTypes = new List<string>();
|
||||
bool? isFolder = null;
|
||||
|
||||
if (search.SearchType == SearchType.Audio)
|
||||
{
|
||||
mediaTypes.Add(MediaType.Audio);
|
||||
isFolder = false;
|
||||
}
|
||||
else if (search.SearchType == SearchType.Video)
|
||||
{
|
||||
mediaTypes.Add(MediaType.Video);
|
||||
isFolder = false;
|
||||
}
|
||||
else if (search.SearchType == SearchType.Image)
|
||||
{
|
||||
mediaTypes.Add(MediaType.Photo);
|
||||
isFolder = false;
|
||||
}
|
||||
else if (search.SearchType == SearchType.Playlist)
|
||||
{
|
||||
//items = items.OfType<Playlist>();
|
||||
isFolder = true;
|
||||
}
|
||||
else if (search.SearchType == SearchType.MusicAlbum)
|
||||
{
|
||||
//items = items.OfType<MusicAlbum>();
|
||||
isFolder = true;
|
||||
}
|
||||
|
||||
return folder.GetItems(new InternalItemsQuery
|
||||
{
|
||||
Limit = limit,
|
||||
StartIndex = startIndex,
|
||||
SortBy = sortOrders.ToArray(),
|
||||
SortOrder = sort.SortOrder,
|
||||
User = user,
|
||||
Recursive = true,
|
||||
IsMissing = false,
|
||||
ExcludeItemTypes = new[] { typeof(Game).Name, typeof(Book).Name },
|
||||
IsFolder = isFolder,
|
||||
MediaTypes = mediaTypes.ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<QueryResult<ServerItem>> GetUserItems(BaseItem item, StubType? stubType, User user, SortCriteria sort, int? startIndex, int? limit)
|
||||
{
|
||||
if (stubType.HasValue)
|
||||
{
|
||||
if (stubType.Value == StubType.People)
|
||||
{
|
||||
var items = _libraryManager.GetPeopleItems(new InternalPeopleQuery
|
||||
{
|
||||
ItemId = item.Id
|
||||
|
||||
}).ToArray();
|
||||
|
||||
var result = new QueryResult<ServerItem>
|
||||
{
|
||||
Items = items.Select(i => new ServerItem { Item = i, StubType = StubType.Folder }).ToArray(),
|
||||
TotalRecordCount = items.Length
|
||||
};
|
||||
|
||||
return ApplyPaging(result, startIndex, limit);
|
||||
}
|
||||
|
||||
var person = item as Person;
|
||||
if (person != null)
|
||||
{
|
||||
return GetItemsFromPerson(person, user, startIndex, limit);
|
||||
}
|
||||
|
||||
return ApplyPaging(new QueryResult<ServerItem>(), startIndex, limit);
|
||||
}
|
||||
|
||||
var folder = (Folder)item;
|
||||
|
||||
var sortOrders = new List<string>();
|
||||
if (!folder.IsPreSorted)
|
||||
{
|
||||
sortOrders.Add(ItemSortBy.SortName);
|
||||
}
|
||||
|
||||
var queryResult = await folder.GetItems(new InternalItemsQuery
|
||||
{
|
||||
Limit = limit,
|
||||
StartIndex = startIndex,
|
||||
SortBy = sortOrders.ToArray(),
|
||||
SortOrder = sort.SortOrder,
|
||||
User = user,
|
||||
IsMissing = false,
|
||||
PresetViews = new[] { CollectionType.Movies, CollectionType.TvShows, CollectionType.Music },
|
||||
ExcludeItemTypes = new[] { typeof(Game).Name, typeof(Book).Name },
|
||||
IsPlaceHolder = false
|
||||
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
var serverItems = queryResult
|
||||
.Items
|
||||
.Select(i => new ServerItem
|
||||
{
|
||||
Item = i
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
return new QueryResult<ServerItem>
|
||||
{
|
||||
TotalRecordCount = queryResult.TotalRecordCount,
|
||||
Items = serverItems
|
||||
};
|
||||
}
|
||||
|
||||
private QueryResult<ServerItem> GetItemsFromPerson(Person person, User user, int? startIndex, int? limit)
|
||||
{
|
||||
var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
|
||||
{
|
||||
Person = person.Name,
|
||||
IncludeItemTypes = new[] { typeof(Movie).Name, typeof(Series).Name, typeof(Trailer).Name },
|
||||
SortBy = new[] { ItemSortBy.SortName },
|
||||
Limit = limit,
|
||||
StartIndex = startIndex
|
||||
|
||||
});
|
||||
|
||||
var serverItems = itemsResult.Items.Select(i => new ServerItem
|
||||
{
|
||||
Item = i,
|
||||
StubType = null
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
return new QueryResult<ServerItem>
|
||||
{
|
||||
TotalRecordCount = itemsResult.TotalRecordCount,
|
||||
Items = serverItems
|
||||
};
|
||||
}
|
||||
|
||||
private QueryResult<ServerItem> ApplyPaging(QueryResult<ServerItem> result, int? startIndex, int? limit)
|
||||
{
|
||||
result.Items = result.Items.Skip(startIndex ?? 0).Take(limit ?? int.MaxValue).ToArray();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool EnablePeopleDisplay(BaseItem item)
|
||||
{
|
||||
if (_libraryManager.GetPeopleNames(new InternalPeopleQuery
|
||||
{
|
||||
ItemId = item.Id
|
||||
|
||||
}).Count > 0)
|
||||
{
|
||||
return item is Movie;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private ServerItem GetItemFromObjectId(string id, User user)
|
||||
{
|
||||
return DidlBuilder.IsIdRoot(id)
|
||||
|
||||
? new ServerItem { Item = user.RootFolder }
|
||||
: ParseItemId(id, user);
|
||||
}
|
||||
|
||||
private ServerItem ParseItemId(string id, User user)
|
||||
{
|
||||
Guid itemId;
|
||||
StubType? stubType = null;
|
||||
|
||||
// After using PlayTo, MediaMonkey sends a request to the server trying to get item info
|
||||
const string paramsSrch = "Params=";
|
||||
var paramsIndex = id.IndexOf(paramsSrch, StringComparison.OrdinalIgnoreCase);
|
||||
if (paramsIndex != -1)
|
||||
{
|
||||
id = id.Substring(paramsIndex + paramsSrch.Length);
|
||||
|
||||
var parts = id.Split(';');
|
||||
id = parts[23];
|
||||
}
|
||||
|
||||
if (id.StartsWith("folder_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stubType = StubType.Folder;
|
||||
id = id.Split(new[] { '_' }, 2)[1];
|
||||
}
|
||||
else if (id.StartsWith("people_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stubType = StubType.People;
|
||||
id = id.Split(new[] { '_' }, 2)[1];
|
||||
}
|
||||
|
||||
if (Guid.TryParse(id, out itemId))
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
|
||||
return new ServerItem
|
||||
{
|
||||
Item = item,
|
||||
StubType = stubType
|
||||
};
|
||||
}
|
||||
|
||||
Logger.Error("Error parsing item Id: {0}. Returning user root folder.", id);
|
||||
|
||||
return new ServerItem { Item = user.RootFolder };
|
||||
}
|
||||
}
|
||||
|
||||
internal class ServerItem
|
||||
{
|
||||
public BaseItem Item { get; set; }
|
||||
public StubType? StubType { get; set; }
|
||||
}
|
||||
|
||||
public enum StubType
|
||||
{
|
||||
Folder = 0,
|
||||
People = 1
|
||||
}
|
||||
}
|
378
Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs
Normal file
|
@ -0,0 +1,378 @@
|
|||
using MediaBrowser.Dlna.Common;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Dlna.ContentDirectory
|
||||
{
|
||||
public class ServiceActionListBuilder
|
||||
{
|
||||
public IEnumerable<ServiceAction> GetActions()
|
||||
{
|
||||
var list = new List<ServiceAction>
|
||||
{
|
||||
GetSearchCapabilitiesAction(),
|
||||
GetSortCapabilitiesAction(),
|
||||
GetGetSystemUpdateIDAction(),
|
||||
GetBrowseAction(),
|
||||
GetSearchAction(),
|
||||
GetX_GetFeatureListAction(),
|
||||
GetXSetBookmarkAction(),
|
||||
GetBrowseByLetterAction()
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private ServiceAction GetGetSystemUpdateIDAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetSystemUpdateID"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Id",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "SystemUpdateID"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private ServiceAction GetSearchCapabilitiesAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetSearchCapabilities"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "SearchCaps",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "SearchCapabilities"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private ServiceAction GetSortCapabilitiesAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetSortCapabilities"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "SortCaps",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "SortCapabilities"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private ServiceAction GetX_GetFeatureListAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "X_GetFeatureList"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "FeatureList",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Featurelist"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private ServiceAction GetSearchAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "Search"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ContainerID",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ObjectID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "SearchCriteria",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_SearchCriteria"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Filter",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Filter"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "StartingIndex",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Index"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "RequestedCount",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Count"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "SortCriteria",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_SortCriteria"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Result",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Result"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "NumberReturned",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Count"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "TotalMatches",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Count"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "UpdateID",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_UpdateID"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private ServiceAction GetBrowseAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "Browse"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ObjectID",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ObjectID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "BrowseFlag",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_BrowseFlag"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Filter",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Filter"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "StartingIndex",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Index"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "RequestedCount",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Count"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "SortCriteria",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_SortCriteria"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Result",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Result"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "NumberReturned",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Count"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "TotalMatches",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Count"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "UpdateID",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_UpdateID"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private ServiceAction GetBrowseByLetterAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "X_BrowseByLetter"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ObjectID",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ObjectID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "BrowseFlag",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_BrowseFlag"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Filter",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Filter"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "StartingLetter",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_BrowseLetter"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "RequestedCount",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Count"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "SortCriteria",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_SortCriteria"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Result",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Result"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "NumberReturned",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Count"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "TotalMatches",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Count"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "UpdateID",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_UpdateID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "StartingIndex",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "A_ARG_TYPE_Index"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private ServiceAction GetXSetBookmarkAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "X_SetBookmark"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "CategoryType",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_CategoryType"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "RID",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_RID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ObjectID",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ObjectID"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "PosSecond",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_PosSec"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
}
|
||||
}
|
1163
Emby.Dlna/Didl/DidlBuilder.cs
Normal file
35
Emby.Dlna/Didl/Filter.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using MediaBrowser.Model.Extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Dlna.Didl
|
||||
{
|
||||
public class Filter
|
||||
{
|
||||
private readonly List<string> _fields;
|
||||
private readonly bool _all;
|
||||
|
||||
public Filter()
|
||||
: this("*")
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public Filter(string filter)
|
||||
{
|
||||
_all = StringHelper.EqualsIgnoreCase(filter, "*");
|
||||
|
||||
var list = (filter ?? string.Empty).Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
|
||||
_fields = list;
|
||||
}
|
||||
|
||||
public bool Contains(string field)
|
||||
{
|
||||
// Don't bother with this. Some clients (media monkey) use the filter and then don't display very well when very little data comes back.
|
||||
return true;
|
||||
//return _all || ListHelper.ContainsIgnoreCase(_fields, field);
|
||||
}
|
||||
}
|
||||
}
|
648
Emby.Dlna/DlnaManager.cs
Normal file
|
@ -0,0 +1,648 @@
|
|||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Dlna.Profiles;
|
||||
using MediaBrowser.Dlna.Server;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Model.IO;
|
||||
#if NETSTANDARD1_6
|
||||
using System.Reflection;
|
||||
#endif
|
||||
|
||||
namespace MediaBrowser.Dlna
|
||||
{
|
||||
public class DlnaManager : IDlnaManager
|
||||
{
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly IXmlSerializer _xmlSerializer;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
|
||||
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
|
||||
|
||||
public DlnaManager(IXmlSerializer xmlSerializer,
|
||||
IFileSystem fileSystem,
|
||||
IApplicationPaths appPaths,
|
||||
ILogger logger,
|
||||
IJsonSerializer jsonSerializer, IServerApplicationHost appHost)
|
||||
{
|
||||
_xmlSerializer = xmlSerializer;
|
||||
_fileSystem = fileSystem;
|
||||
_appPaths = appPaths;
|
||||
_logger = logger;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_appHost = appHost;
|
||||
}
|
||||
|
||||
public void InitProfiles()
|
||||
{
|
||||
try
|
||||
{
|
||||
ExtractSystemProfiles();
|
||||
LoadProfiles();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error extracting DLNA profiles.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadProfiles()
|
||||
{
|
||||
var list = GetProfiles(UserProfilesPath, DeviceProfileType.User)
|
||||
.OrderBy(i => i.Name)
|
||||
.ToList();
|
||||
|
||||
list.AddRange(GetProfiles(SystemProfilesPath, DeviceProfileType.System)
|
||||
.OrderBy(i => i.Name));
|
||||
}
|
||||
|
||||
public IEnumerable<DeviceProfile> GetProfiles()
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
var list = _profiles.Values.ToList();
|
||||
return list
|
||||
.OrderBy(i => i.Item1.Info.Type == DeviceProfileType.User ? 0 : 1)
|
||||
.ThenBy(i => i.Item1.Info.Name)
|
||||
.Select(i => i.Item2)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public DeviceProfile GetDefaultProfile()
|
||||
{
|
||||
return new DefaultProfile();
|
||||
}
|
||||
|
||||
public DeviceProfile GetProfile(DeviceIdentification deviceInfo)
|
||||
{
|
||||
if (deviceInfo == null)
|
||||
{
|
||||
throw new ArgumentNullException("deviceInfo");
|
||||
}
|
||||
|
||||
var profile = GetProfiles()
|
||||
.FirstOrDefault(i => i.Identification != null && IsMatch(deviceInfo, i.Identification));
|
||||
|
||||
if (profile != null)
|
||||
{
|
||||
_logger.Debug("Found matching device profile: {0}", profile.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("No matching device profile found. The default will need to be used.");
|
||||
LogUnmatchedProfile(deviceInfo);
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
private void LogUnmatchedProfile(DeviceIdentification profile)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.AppendLine(string.Format("DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty));
|
||||
builder.AppendLine(string.Format("FriendlyName:{0}", profile.FriendlyName ?? string.Empty));
|
||||
builder.AppendLine(string.Format("Manufacturer:{0}", profile.Manufacturer ?? string.Empty));
|
||||
builder.AppendLine(string.Format("ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty));
|
||||
builder.AppendLine(string.Format("ModelDescription:{0}", profile.ModelDescription ?? string.Empty));
|
||||
builder.AppendLine(string.Format("ModelName:{0}", profile.ModelName ?? string.Empty));
|
||||
builder.AppendLine(string.Format("ModelNumber:{0}", profile.ModelNumber ?? string.Empty));
|
||||
builder.AppendLine(string.Format("ModelUrl:{0}", profile.ModelUrl ?? string.Empty));
|
||||
builder.AppendLine(string.Format("SerialNumber:{0}", profile.SerialNumber ?? string.Empty));
|
||||
|
||||
_logger.LogMultiline("No matching device profile found. The default will need to be used.", LogSeverity.Info, builder);
|
||||
}
|
||||
|
||||
private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(profileInfo.DeviceDescription))
|
||||
{
|
||||
if (deviceInfo.DeviceDescription == null || !IsRegexMatch(deviceInfo.DeviceDescription, profileInfo.DeviceDescription))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(profileInfo.FriendlyName))
|
||||
{
|
||||
if (deviceInfo.FriendlyName == null || !IsRegexMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(profileInfo.Manufacturer))
|
||||
{
|
||||
if (deviceInfo.Manufacturer == null || !IsRegexMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(profileInfo.ManufacturerUrl))
|
||||
{
|
||||
if (deviceInfo.ManufacturerUrl == null || !IsRegexMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(profileInfo.ModelDescription))
|
||||
{
|
||||
if (deviceInfo.ModelDescription == null || !IsRegexMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(profileInfo.ModelName))
|
||||
{
|
||||
if (deviceInfo.ModelName == null || !IsRegexMatch(deviceInfo.ModelName, profileInfo.ModelName))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(profileInfo.ModelNumber))
|
||||
{
|
||||
if (deviceInfo.ModelNumber == null || !IsRegexMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(profileInfo.ModelUrl))
|
||||
{
|
||||
if (deviceInfo.ModelUrl == null || !IsRegexMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(profileInfo.SerialNumber))
|
||||
{
|
||||
if (deviceInfo.SerialNumber == null || !IsRegexMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsRegexMatch(string input, string pattern)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Regex.IsMatch(input, pattern);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
_logger.ErrorException("Error evaluating regex pattern {0}", ex, pattern);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public DeviceProfile GetProfile(IDictionary<string, string> headers)
|
||||
{
|
||||
if (headers == null)
|
||||
{
|
||||
throw new ArgumentNullException("headers");
|
||||
}
|
||||
|
||||
// Convert to case insensitive
|
||||
headers = new Dictionary<string, string>(headers, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification));
|
||||
|
||||
if (profile != null)
|
||||
{
|
||||
_logger.Debug("Found matching device profile: {0}", profile.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
var msg = new StringBuilder();
|
||||
foreach (var header in headers)
|
||||
{
|
||||
msg.AppendLine(header.Key + ": " + header.Value);
|
||||
}
|
||||
_logger.LogMultiline("No matching device profile found. The default will need to be used.", LogSeverity.Info, msg);
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
private bool IsMatch(IDictionary<string, string> headers, DeviceIdentification profileInfo)
|
||||
{
|
||||
return profileInfo.Headers.Any(i => IsMatch(headers, i));
|
||||
}
|
||||
|
||||
private bool IsMatch(IDictionary<string, string> headers, HttpHeaderInfo header)
|
||||
{
|
||||
string value;
|
||||
|
||||
if (headers.TryGetValue(header.Name, out value))
|
||||
{
|
||||
switch (header.Match)
|
||||
{
|
||||
case HeaderMatchType.Equals:
|
||||
return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase);
|
||||
case HeaderMatchType.Substring:
|
||||
var isMatch = value.IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1;
|
||||
//_logger.Debug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch);
|
||||
return isMatch;
|
||||
case HeaderMatchType.Regex:
|
||||
return Regex.IsMatch(value, header.Value, RegexOptions.IgnoreCase);
|
||||
default:
|
||||
throw new ArgumentException("Unrecognized HeaderMatchType");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private string UserProfilesPath
|
||||
{
|
||||
get
|
||||
{
|
||||
return Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
|
||||
}
|
||||
}
|
||||
|
||||
private string SystemProfilesPath
|
||||
{
|
||||
get
|
||||
{
|
||||
return Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type)
|
||||
{
|
||||
try
|
||||
{
|
||||
var allFiles = _fileSystem.GetFiles(path)
|
||||
.ToList();
|
||||
|
||||
var xmlFies = allFiles
|
||||
.Where(i => string.Equals(i.Extension, ".xml", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
var jsonFiles = allFiles
|
||||
.Where(i => string.Equals(i.Extension, ".json", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
var jsonFileNames = jsonFiles
|
||||
.Select(i => Path.GetFileNameWithoutExtension(i.Name))
|
||||
.ToList();
|
||||
|
||||
var parseFiles = jsonFiles.ToList();
|
||||
|
||||
parseFiles.AddRange(xmlFies.Where(i => !jsonFileNames.Contains(Path.GetFileNameWithoutExtension(i.Name), StringComparer.Ordinal)));
|
||||
|
||||
return parseFiles
|
||||
.Select(i => ParseProfileFile(i.FullName, type))
|
||||
.Where(i => i != null)
|
||||
.ToList();
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
return new List<DeviceProfile>();
|
||||
}
|
||||
}
|
||||
|
||||
private DeviceProfile ParseProfileFile(string path, DeviceProfileType type)
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
Tuple<InternalProfileInfo, DeviceProfile> profileTuple;
|
||||
if (_profiles.TryGetValue(path, out profileTuple))
|
||||
{
|
||||
return profileTuple.Item2;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
DeviceProfile profile;
|
||||
|
||||
if (string.Equals(Path.GetExtension(path), ".xml", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var tempProfile = (MediaBrowser.Dlna.ProfileSerialization.DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(MediaBrowser.Dlna.ProfileSerialization.DeviceProfile), path);
|
||||
|
||||
var json = _jsonSerializer.SerializeToString(tempProfile);
|
||||
profile = (DeviceProfile)_jsonSerializer.DeserializeFromString<DeviceProfile>(json);
|
||||
}
|
||||
else
|
||||
{
|
||||
profile = (DeviceProfile)_jsonSerializer.DeserializeFromFile(typeof(DeviceProfile), path);
|
||||
}
|
||||
|
||||
profile.Id = path.ToLower().GetMD5().ToString("N");
|
||||
profile.ProfileType = type;
|
||||
|
||||
_profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
|
||||
|
||||
return profile;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error parsing profile file: {0}", ex, path);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DeviceProfile GetProfile(string id)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
throw new ArgumentNullException("id");
|
||||
}
|
||||
|
||||
var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return ParseProfileFile(info.Path, info.Info.Type);
|
||||
}
|
||||
|
||||
private IEnumerable<InternalProfileInfo> GetProfileInfosInternal()
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
var list = _profiles.Values.ToList();
|
||||
return list
|
||||
.Select(i => i.Item1)
|
||||
.OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1)
|
||||
.ThenBy(i => i.Info.Name);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<DeviceProfileInfo> GetProfileInfos()
|
||||
{
|
||||
return GetProfileInfosInternal().Select(i => i.Info);
|
||||
}
|
||||
|
||||
private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type)
|
||||
{
|
||||
return new InternalProfileInfo
|
||||
{
|
||||
Path = file.FullName,
|
||||
|
||||
Info = new DeviceProfileInfo
|
||||
{
|
||||
Id = file.FullName.ToLower().GetMD5().ToString("N"),
|
||||
Name = _fileSystem.GetFileNameWithoutExtension(file),
|
||||
Type = type
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void ExtractSystemProfiles()
|
||||
{
|
||||
#if NET46
|
||||
var assembly = GetType().Assembly;
|
||||
var namespaceName = GetType().Namespace + ".Profiles.Json.";
|
||||
#elif NETSTANDARD1_6
|
||||
var assembly = GetType().GetTypeInfo().Assembly;
|
||||
var namespaceName = GetType().GetTypeInfo().Namespace + ".Profiles.Json.";
|
||||
#endif
|
||||
|
||||
var systemProfilesPath = SystemProfilesPath;
|
||||
|
||||
foreach (var name in assembly.GetManifestResourceNames()
|
||||
.Where(i => i.StartsWith(namespaceName))
|
||||
.ToList())
|
||||
{
|
||||
var filename = Path.GetFileName(name).Substring(namespaceName.Length);
|
||||
|
||||
var path = Path.Combine(systemProfilesPath, filename);
|
||||
|
||||
using (var stream = assembly.GetManifestResourceStream(name))
|
||||
{
|
||||
var fileInfo = new FileInfo(path);
|
||||
|
||||
if (!fileInfo.Exists || fileInfo.Length != stream.Length)
|
||||
{
|
||||
_fileSystem.CreateDirectory(systemProfilesPath);
|
||||
|
||||
using (var fileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
|
||||
{
|
||||
stream.CopyTo(fileStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not necessary, but just to make it easy to find
|
||||
_fileSystem.CreateDirectory(UserProfilesPath);
|
||||
}
|
||||
|
||||
public void DeleteProfile(string id)
|
||||
{
|
||||
var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (info.Info.Type == DeviceProfileType.System)
|
||||
{
|
||||
throw new ArgumentException("System profiles cannot be deleted.");
|
||||
}
|
||||
|
||||
_fileSystem.DeleteFile(info.Path);
|
||||
|
||||
lock (_profiles)
|
||||
{
|
||||
_profiles.Remove(info.Path);
|
||||
}
|
||||
}
|
||||
|
||||
public void CreateProfile(DeviceProfile profile)
|
||||
{
|
||||
profile = ReserializeProfile(profile);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(profile.Name))
|
||||
{
|
||||
throw new ArgumentException("Profile is missing Name");
|
||||
}
|
||||
|
||||
var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".json";
|
||||
var path = Path.Combine(UserProfilesPath, newFilename);
|
||||
|
||||
SaveProfile(profile, path, DeviceProfileType.User);
|
||||
}
|
||||
|
||||
public void UpdateProfile(DeviceProfile profile)
|
||||
{
|
||||
profile = ReserializeProfile(profile);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(profile.Id))
|
||||
{
|
||||
throw new ArgumentException("Profile is missing Id");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(profile.Name))
|
||||
{
|
||||
throw new ArgumentException("Profile is missing Name");
|
||||
}
|
||||
|
||||
var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profile.Id, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".json";
|
||||
var path = Path.Combine(UserProfilesPath, newFilename);
|
||||
|
||||
if (!string.Equals(path, current.Path, StringComparison.Ordinal) &&
|
||||
current.Info.Type != DeviceProfileType.System)
|
||||
{
|
||||
_fileSystem.DeleteFile(current.Path);
|
||||
}
|
||||
|
||||
SaveProfile(profile, path, DeviceProfileType.User);
|
||||
}
|
||||
|
||||
private void SaveProfile(DeviceProfile profile, string path, DeviceProfileType type)
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
_profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
|
||||
}
|
||||
SerializeToJson(profile, path);
|
||||
}
|
||||
|
||||
internal void SerializeToJson(DeviceProfile profile, string path)
|
||||
{
|
||||
_jsonSerializer.SerializeToFile(profile, path);
|
||||
|
||||
try
|
||||
{
|
||||
File.Delete(Path.ChangeExtension(path, ".xml"));
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recreates the object using serialization, to ensure it's not a subclass.
|
||||
/// If it's a subclass it may not serlialize properly to xml (different root element tag name)
|
||||
/// </summary>
|
||||
/// <param name="profile"></param>
|
||||
/// <returns></returns>
|
||||
private DeviceProfile ReserializeProfile(DeviceProfile profile)
|
||||
{
|
||||
if (profile.GetType() == typeof(DeviceProfile))
|
||||
{
|
||||
return profile;
|
||||
}
|
||||
|
||||
var json = _jsonSerializer.SerializeToString(profile);
|
||||
|
||||
return _jsonSerializer.DeserializeFromString<DeviceProfile>(json);
|
||||
}
|
||||
|
||||
class InternalProfileInfo
|
||||
{
|
||||
internal DeviceProfileInfo Info { get; set; }
|
||||
internal string Path { get; set; }
|
||||
}
|
||||
|
||||
public string GetServerDescriptionXml(IDictionary<string, string> headers, string serverUuId, string serverAddress)
|
||||
{
|
||||
var profile = GetProfile(headers) ??
|
||||
GetDefaultProfile();
|
||||
|
||||
var serverId = _appHost.SystemId;
|
||||
|
||||
return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
|
||||
}
|
||||
|
||||
public ImageStream GetIcon(string filename)
|
||||
{
|
||||
var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
|
||||
? ImageFormat.Png
|
||||
: ImageFormat.Jpg;
|
||||
|
||||
#if NET46
|
||||
return new ImageStream
|
||||
{
|
||||
Format = format,
|
||||
Stream = GetType().Assembly.GetManifestResourceStream("MediaBrowser.Dlna.Images." + filename.ToLower())
|
||||
};
|
||||
#elif NETSTANDARD1_6
|
||||
return new ImageStream
|
||||
{
|
||||
Format = format,
|
||||
Stream = GetType().GetTypeInfo().Assembly.GetManifestResourceStream("MediaBrowser.Dlna.Images." + filename.ToLower())
|
||||
};
|
||||
#endif
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
class DlnaProfileEntryPoint : IServerEntryPoint
|
||||
{
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
public DlnaProfileEntryPoint(IApplicationPaths appPaths, IFileSystem fileSystem, IJsonSerializer jsonSerializer)
|
||||
{
|
||||
_appPaths = appPaths;
|
||||
_fileSystem = fileSystem;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
//DumpProfiles();
|
||||
}
|
||||
|
||||
private void DumpProfiles()
|
||||
{
|
||||
var list = new List<DeviceProfile>
|
||||
{
|
||||
new SamsungSmartTvProfile(),
|
||||
new Xbox360Profile(),
|
||||
new XboxOneProfile(),
|
||||
new SonyPs3Profile(),
|
||||
new SonyPs4Profile(),
|
||||
new SonyBravia2010Profile(),
|
||||
new SonyBravia2011Profile(),
|
||||
new SonyBravia2012Profile(),
|
||||
new SonyBravia2013Profile(),
|
||||
new SonyBravia2014Profile(),
|
||||
new SonyBlurayPlayer2013(),
|
||||
new SonyBlurayPlayer2014(),
|
||||
new SonyBlurayPlayer2015(),
|
||||
new SonyBlurayPlayer2016(),
|
||||
new SonyBlurayPlayerProfile(),
|
||||
new PanasonicVieraProfile(),
|
||||
new WdtvLiveProfile(),
|
||||
new DenonAvrProfile(),
|
||||
new LinksysDMA2100Profile(),
|
||||
new LgTvProfile(),
|
||||
new Foobar2000Profile(),
|
||||
new MediaMonkeyProfile(),
|
||||
//new Windows81Profile(),
|
||||
//new WindowsMediaCenterProfile(),
|
||||
//new WindowsPhoneProfile(),
|
||||
new DirectTvProfile(),
|
||||
new DishHopperJoeyProfile(),
|
||||
new DefaultProfile(),
|
||||
new PopcornHourProfile(),
|
||||
new VlcProfile(),
|
||||
new BubbleUpnpProfile(),
|
||||
new KodiProfile(),
|
||||
};
|
||||
|
||||
foreach (var item in list)
|
||||
{
|
||||
var path = Path.Combine(_appPaths.ProgramDataPath, _fileSystem.GetValidFilename(item.Name) + ".json");
|
||||
|
||||
_jsonSerializer.SerializeToFile(item, path);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
24
Emby.Dlna/Emby.Dlna.xproj
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>f40e364d-01d9-4bbf-b82c-5d6c55e0a1f5</ProjectGuid>
|
||||
<RootNamespace>Emby.Dlna</RootNamespace>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
170
Emby.Dlna/Eventing/EventManager.cs
Normal file
|
@ -0,0 +1,170 @@
|
|||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Dlna.Eventing
|
||||
{
|
||||
public class EventManager : IEventManager
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, EventSubscription> _subscriptions =
|
||||
new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
public EventManager(ILogger logger, IHttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, int? timeoutSeconds)
|
||||
{
|
||||
var timeout = timeoutSeconds ?? 300;
|
||||
|
||||
var subscription = GetSubscription(subscriptionId, true);
|
||||
|
||||
_logger.Debug("Renewing event subscription for {0} with timeout of {1} to {2}",
|
||||
subscription.NotificationType,
|
||||
timeout,
|
||||
subscription.CallbackUrl);
|
||||
|
||||
subscription.TimeoutSeconds = timeout;
|
||||
subscription.SubscriptionTime = DateTime.UtcNow;
|
||||
|
||||
return GetEventSubscriptionResponse(subscriptionId, timeout);
|
||||
}
|
||||
|
||||
public EventSubscriptionResponse CreateEventSubscription(string notificationType, int? timeoutSeconds, string callbackUrl)
|
||||
{
|
||||
var timeout = timeoutSeconds ?? 300;
|
||||
var id = "uuid:" + Guid.NewGuid().ToString("N");
|
||||
|
||||
_logger.Debug("Creating event subscription for {0} with timeout of {1} to {2}",
|
||||
notificationType,
|
||||
timeout,
|
||||
callbackUrl);
|
||||
|
||||
_subscriptions.TryAdd(id, new EventSubscription
|
||||
{
|
||||
Id = id,
|
||||
CallbackUrl = callbackUrl,
|
||||
SubscriptionTime = DateTime.UtcNow,
|
||||
TimeoutSeconds = timeout
|
||||
});
|
||||
|
||||
return GetEventSubscriptionResponse(id, timeout);
|
||||
}
|
||||
|
||||
public EventSubscriptionResponse CancelEventSubscription(string subscriptionId)
|
||||
{
|
||||
_logger.Debug("Cancelling event subscription {0}", subscriptionId);
|
||||
|
||||
EventSubscription sub;
|
||||
_subscriptions.TryRemove(subscriptionId, out sub);
|
||||
|
||||
return new EventSubscriptionResponse
|
||||
{
|
||||
Content = "\r\n",
|
||||
ContentType = "text/plain"
|
||||
};
|
||||
}
|
||||
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, int timeoutSeconds)
|
||||
{
|
||||
var response = new EventSubscriptionResponse
|
||||
{
|
||||
Content = "\r\n",
|
||||
ContentType = "text/plain"
|
||||
};
|
||||
|
||||
response.Headers["SID"] = subscriptionId;
|
||||
response.Headers["TIMEOUT"] = "SECOND-" + timeoutSeconds.ToString(_usCulture);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public EventSubscription GetSubscription(string id)
|
||||
{
|
||||
return GetSubscription(id, false);
|
||||
}
|
||||
|
||||
private EventSubscription GetSubscription(string id, bool throwOnMissing)
|
||||
{
|
||||
EventSubscription e;
|
||||
|
||||
if (!_subscriptions.TryGetValue(id, out e) && throwOnMissing)
|
||||
{
|
||||
throw new ResourceNotFoundException("Event with Id " + id + " not found.");
|
||||
}
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
public Task TriggerEvent(string notificationType, IDictionary<string, string> stateVariables)
|
||||
{
|
||||
var subs = _subscriptions.Values
|
||||
.Where(i => !i.IsExpired && string.Equals(notificationType, i.NotificationType, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
var tasks = subs.Select(i => TriggerEvent(i, stateVariables));
|
||||
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
private async Task TriggerEvent(EventSubscription subscription, IDictionary<string, string> stateVariables)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.Append("<?xml version=\"1.0\"?>");
|
||||
builder.Append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">");
|
||||
foreach (var key in stateVariables.Keys)
|
||||
{
|
||||
builder.Append("<e:property>");
|
||||
builder.Append("<" + key + ">");
|
||||
builder.Append(stateVariables[key]);
|
||||
builder.Append("</" + key + ">");
|
||||
builder.Append("</e:property>");
|
||||
}
|
||||
builder.Append("</e:propertyset>");
|
||||
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
RequestContent = builder.ToString(),
|
||||
RequestContentType = "text/xml",
|
||||
Url = subscription.CallbackUrl,
|
||||
BufferContent = false
|
||||
};
|
||||
|
||||
options.RequestHeaders.Add("NT", subscription.NotificationType);
|
||||
options.RequestHeaders.Add("NTS", "upnp:propchange");
|
||||
options.RequestHeaders.Add("SID", subscription.Id);
|
||||
options.RequestHeaders.Add("SEQ", subscription.TriggerCount.ToString(_usCulture));
|
||||
|
||||
try
|
||||
{
|
||||
await _httpClient.SendAsync(options, "NOTIFY").ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Already logged at lower levels
|
||||
}
|
||||
finally
|
||||
{
|
||||
subscription.IncrementTriggerCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34
Emby.Dlna/Eventing/EventSubscription.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
|
||||
namespace MediaBrowser.Dlna.Eventing
|
||||
{
|
||||
public class EventSubscription
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string CallbackUrl { get; set; }
|
||||
public string NotificationType { get; set; }
|
||||
|
||||
public DateTime SubscriptionTime { get; set; }
|
||||
public int TimeoutSeconds { get; set; }
|
||||
|
||||
public long TriggerCount { get; set; }
|
||||
|
||||
public void IncrementTriggerCount()
|
||||
{
|
||||
if (TriggerCount == long.MaxValue)
|
||||
{
|
||||
TriggerCount = 0;
|
||||
}
|
||||
|
||||
TriggerCount++;
|
||||
}
|
||||
|
||||
public bool IsExpired
|
||||
{
|
||||
get
|
||||
{
|
||||
return SubscriptionTime.AddSeconds(TimeoutSeconds) >= DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
BIN
Emby.Dlna/Images/logo120.jpg
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
Emby.Dlna/Images/logo120.png
Normal file
After Width: | Height: | Size: 840 B |
BIN
Emby.Dlna/Images/logo240.jpg
Normal file
After Width: | Height: | Size: 8.0 KiB |
BIN
Emby.Dlna/Images/logo240.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
Emby.Dlna/Images/logo48.jpg
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
Emby.Dlna/Images/logo48.png
Normal file
After Width: | Height: | Size: 699 B |
BIN
Emby.Dlna/Images/people48.jpg
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
Emby.Dlna/Images/people48.png
Normal file
After Width: | Height: | Size: 688 B |
BIN
Emby.Dlna/Images/people480.jpg
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
Emby.Dlna/Images/people480.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
393
Emby.Dlna/Main/DlnaEntryPoint.cs
Normal file
|
@ -0,0 +1,393 @@
|
|||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Dlna.PlayTo;
|
||||
using MediaBrowser.Dlna.Ssdp;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using Rssdp;
|
||||
using Rssdp.Infrastructure;
|
||||
|
||||
namespace MediaBrowser.Dlna.Main
|
||||
{
|
||||
public class DlnaEntryPoint : IServerEntryPoint
|
||||
{
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly INetworkManager _network;
|
||||
|
||||
private PlayToManager _manager;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
|
||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
||||
|
||||
private bool _ssdpHandlerStarted;
|
||||
private bool _dlnaServerStarted;
|
||||
private SsdpDevicePublisher _Publisher;
|
||||
|
||||
public DlnaEntryPoint(IServerConfigurationManager config,
|
||||
ILogManager logManager,
|
||||
IServerApplicationHost appHost,
|
||||
INetworkManager network,
|
||||
ISessionManager sessionManager,
|
||||
IHttpClient httpClient,
|
||||
ILibraryManager libraryManager,
|
||||
IUserManager userManager,
|
||||
IDlnaManager dlnaManager,
|
||||
IImageProcessor imageProcessor,
|
||||
IUserDataManager userDataManager,
|
||||
ILocalizationManager localization,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IDeviceDiscovery deviceDiscovery, IMediaEncoder mediaEncoder)
|
||||
{
|
||||
_config = config;
|
||||
_appHost = appHost;
|
||||
_network = network;
|
||||
_sessionManager = sessionManager;
|
||||
_httpClient = httpClient;
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
_dlnaManager = dlnaManager;
|
||||
_imageProcessor = imageProcessor;
|
||||
_userDataManager = userDataManager;
|
||||
_localization = localization;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_logger = logManager.GetLogger("Dlna");
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
((DlnaManager)_dlnaManager).InitProfiles();
|
||||
|
||||
ReloadComponents();
|
||||
|
||||
_config.ConfigurationUpdated += _config_ConfigurationUpdated;
|
||||
_config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;
|
||||
}
|
||||
|
||||
private bool _lastEnableUpnP;
|
||||
void _config_ConfigurationUpdated(object sender, EventArgs e)
|
||||
{
|
||||
if (_lastEnableUpnP != _config.Configuration.EnableUPnP)
|
||||
{
|
||||
ReloadComponents();
|
||||
}
|
||||
_lastEnableUpnP = _config.Configuration.EnableUPnP;
|
||||
}
|
||||
|
||||
void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
|
||||
{
|
||||
if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ReloadComponents();
|
||||
}
|
||||
}
|
||||
|
||||
private async void ReloadComponents()
|
||||
{
|
||||
var options = _config.GetDlnaConfiguration();
|
||||
|
||||
if (!_ssdpHandlerStarted)
|
||||
{
|
||||
StartSsdpHandler();
|
||||
}
|
||||
|
||||
var isServerStarted = _dlnaServerStarted;
|
||||
|
||||
if (options.EnableServer && !isServerStarted)
|
||||
{
|
||||
await StartDlnaServer().ConfigureAwait(false);
|
||||
}
|
||||
else if (!options.EnableServer && isServerStarted)
|
||||
{
|
||||
DisposeDlnaServer();
|
||||
}
|
||||
|
||||
var isPlayToStarted = _manager != null;
|
||||
|
||||
if (options.EnablePlayTo && !isPlayToStarted)
|
||||
{
|
||||
StartPlayToManager();
|
||||
}
|
||||
else if (!options.EnablePlayTo && isPlayToStarted)
|
||||
{
|
||||
DisposePlayToManager();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartSsdpHandler()
|
||||
{
|
||||
try
|
||||
{
|
||||
StartPublishing();
|
||||
_ssdpHandlerStarted = true;
|
||||
|
||||
StartDeviceDiscovery();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error starting ssdp handlers", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void LogMessage(string msg)
|
||||
{
|
||||
//_logger.Debug(msg);
|
||||
}
|
||||
|
||||
private void StartPublishing()
|
||||
{
|
||||
SsdpDevicePublisherBase.LogFunction = LogMessage;
|
||||
_Publisher = new SsdpDevicePublisher();
|
||||
}
|
||||
|
||||
private void StartDeviceDiscovery()
|
||||
{
|
||||
try
|
||||
{
|
||||
((DeviceDiscovery)_deviceDiscovery).Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error starting device discovery", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeDeviceDiscovery()
|
||||
{
|
||||
try
|
||||
{
|
||||
((DeviceDiscovery)_deviceDiscovery).Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error stopping device discovery", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeSsdpHandler()
|
||||
{
|
||||
DisposeDeviceDiscovery();
|
||||
|
||||
try
|
||||
{
|
||||
((DeviceDiscovery)_deviceDiscovery).Dispose();
|
||||
|
||||
_ssdpHandlerStarted = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error stopping ssdp handlers", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartDlnaServer()
|
||||
{
|
||||
try
|
||||
{
|
||||
await RegisterServerEndpoints().ConfigureAwait(false);
|
||||
|
||||
_dlnaServerStarted = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error registering endpoint", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RegisterServerEndpoints()
|
||||
{
|
||||
if (!_config.GetDlnaConfiguration().BlastAliveMessages)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var cacheLength = _config.GetDlnaConfiguration().BlastAliveMessageIntervalSeconds;
|
||||
_Publisher.SupportPnpRootDevice = false;
|
||||
|
||||
var addresses = (await _appHost.GetLocalIpAddresses().ConfigureAwait(false)).ToList();
|
||||
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
//if (IPAddress.IsLoopback(address))
|
||||
//{
|
||||
// // Should we allow this?
|
||||
// continue;
|
||||
//}
|
||||
|
||||
var addressString = address.ToString();
|
||||
|
||||
var udn = CreateUuid(addressString);
|
||||
|
||||
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
|
||||
|
||||
_logger.Info("Registering publisher for {0} on {1}", fullService, addressString);
|
||||
|
||||
var descriptorUri = "/dlna/" + udn + "/description.xml";
|
||||
var uri = new Uri(_appHost.GetLocalApiUrl(addressString, address.IsIpv6) + descriptorUri);
|
||||
|
||||
var device = new SsdpRootDevice
|
||||
{
|
||||
CacheLifetime = TimeSpan.FromSeconds(cacheLength), //How long SSDP clients can cache this info.
|
||||
Location = uri, // Must point to the URL that serves your devices UPnP description document.
|
||||
FriendlyName = "Emby Server",
|
||||
Manufacturer = "Emby",
|
||||
ModelName = "Emby Server",
|
||||
Uuid = udn
|
||||
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
|
||||
};
|
||||
|
||||
SetProperies(device, fullService);
|
||||
_Publisher.AddDevice(device);
|
||||
|
||||
var embeddedDevices = new List<string>
|
||||
{
|
||||
"urn:schemas-upnp-org:service:ContentDirectory:1",
|
||||
"urn:schemas-upnp-org:service:ConnectionManager:1",
|
||||
"urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1"
|
||||
};
|
||||
|
||||
foreach (var subDevice in embeddedDevices)
|
||||
{
|
||||
var embeddedDevice = new SsdpEmbeddedDevice
|
||||
{
|
||||
FriendlyName = device.FriendlyName,
|
||||
Manufacturer = device.Manufacturer,
|
||||
ModelName = device.ModelName,
|
||||
Uuid = udn
|
||||
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
|
||||
};
|
||||
|
||||
SetProperies(embeddedDevice, subDevice);
|
||||
device.AddDevice(embeddedDevice);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string CreateUuid(string text)
|
||||
{
|
||||
return text.GetMD5().ToString("N");
|
||||
}
|
||||
|
||||
private void SetProperies(SsdpDevice device, string fullDeviceType)
|
||||
{
|
||||
var service = fullDeviceType.Replace("urn:", string.Empty).Replace(":1", string.Empty);
|
||||
|
||||
var serviceParts = service.Split(':');
|
||||
|
||||
var deviceTypeNamespace = serviceParts[0].Replace('.', '-');
|
||||
|
||||
device.DeviceTypeNamespace = deviceTypeNamespace;
|
||||
device.DeviceClass = serviceParts[1];
|
||||
device.DeviceType = serviceParts[2];
|
||||
}
|
||||
|
||||
private readonly object _syncLock = new object();
|
||||
private void StartPlayToManager()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
_manager = new PlayToManager(_logger,
|
||||
_sessionManager,
|
||||
_libraryManager,
|
||||
_userManager,
|
||||
_dlnaManager,
|
||||
_appHost,
|
||||
_imageProcessor,
|
||||
_deviceDiscovery,
|
||||
_httpClient,
|
||||
_config,
|
||||
_userDataManager,
|
||||
_localization,
|
||||
_mediaSourceManager,
|
||||
_mediaEncoder);
|
||||
|
||||
_manager.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error starting PlayTo manager", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposePlayToManager()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_manager != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_manager.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error disposing PlayTo manager", ex);
|
||||
}
|
||||
_manager = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeDlnaServer();
|
||||
DisposePlayToManager();
|
||||
DisposeSsdpHandler();
|
||||
}
|
||||
|
||||
public void DisposeDlnaServer()
|
||||
{
|
||||
if (_Publisher != null)
|
||||
{
|
||||
var devices = _Publisher.Devices.ToList();
|
||||
var tasks = devices.Select(i => _Publisher.RemoveDevice(i)).ToArray();
|
||||
|
||||
Task.WaitAll(tasks);
|
||||
//foreach (var device in devices)
|
||||
//{
|
||||
// try
|
||||
// {
|
||||
// _Publisher.RemoveDevice(device);
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// _logger.ErrorException("Error sending bye bye", ex);
|
||||
// }
|
||||
//}
|
||||
_Publisher.Dispose();
|
||||
}
|
||||
|
||||
_dlnaServerStarted = false;
|
||||
}
|
||||
}
|
||||
}
|
43
Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
Normal file
|
@ -0,0 +1,43 @@
|
|||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Dlna.Server;
|
||||
using MediaBrowser.Dlna.Service;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Dlna.MediaReceiverRegistrar
|
||||
{
|
||||
public class ControlHandler : BaseControlHandler
|
||||
{
|
||||
public ControlHandler(IServerConfigurationManager config, ILogger logger) : base(config, logger)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, Headers methodParams)
|
||||
{
|
||||
if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase))
|
||||
return HandleIsAuthorized();
|
||||
if (string.Equals(methodName, "IsValidated", StringComparison.OrdinalIgnoreCase))
|
||||
return HandleIsValidated();
|
||||
|
||||
throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
|
||||
}
|
||||
|
||||
private IEnumerable<KeyValuePair<string, string>> HandleIsAuthorized()
|
||||
{
|
||||
return new Headers(true)
|
||||
{
|
||||
{ "Result", "1" }
|
||||
};
|
||||
}
|
||||
|
||||
private IEnumerable<KeyValuePair<string, string>> HandleIsValidated()
|
||||
{
|
||||
return new Headers(true)
|
||||
{
|
||||
{ "Result", "1" }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
39
Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Dlna.Service;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Dlna.MediaReceiverRegistrar
|
||||
{
|
||||
public class MediaReceiverRegistrar : BaseService, IMediaReceiverRegistrar, IDisposable
|
||||
{
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
||||
public MediaReceiverRegistrar(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config)
|
||||
: base(logger, httpClient)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public string GetServiceXml(IDictionary<string, string> headers)
|
||||
{
|
||||
return new MediaReceiverRegistrarXmlBuilder().GetXml();
|
||||
}
|
||||
|
||||
public ControlResponse ProcessControlRequest(ControlRequest request)
|
||||
{
|
||||
return new ControlHandler(
|
||||
_config,
|
||||
Logger)
|
||||
.ProcessControlRequest(request);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
using MediaBrowser.Dlna.Common;
|
||||
using MediaBrowser.Dlna.Service;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Dlna.MediaReceiverRegistrar
|
||||
{
|
||||
public class MediaReceiverRegistrarXmlBuilder
|
||||
{
|
||||
public string GetXml()
|
||||
{
|
||||
return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(),
|
||||
GetStateVariables());
|
||||
}
|
||||
|
||||
private IEnumerable<StateVariable> GetStateVariables()
|
||||
{
|
||||
var list = new List<StateVariable>();
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "AuthorizationGrantedUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_DeviceID",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "AuthorizationDeniedUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "ValidationSucceededUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RegistrationRespMsg",
|
||||
DataType = "bin.base64",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RegistrationReqMsg",
|
||||
DataType = "bin.base64",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "ValidationRevokedUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
});
|
||||
|
||||
list.Add(new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Result",
|
||||
DataType = "int",
|
||||
SendsEvents = false
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
154
Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs
Normal file
|
@ -0,0 +1,154 @@
|
|||
using MediaBrowser.Dlna.Common;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Dlna.MediaReceiverRegistrar
|
||||
{
|
||||
public class ServiceActionListBuilder
|
||||
{
|
||||
public IEnumerable<ServiceAction> GetActions()
|
||||
{
|
||||
var list = new List<ServiceAction>
|
||||
{
|
||||
GetIsValidated(),
|
||||
GetIsAuthorized(),
|
||||
GetRegisterDevice(),
|
||||
GetGetAuthorizationDeniedUpdateID(),
|
||||
GetGetAuthorizationGrantedUpdateID(),
|
||||
GetGetValidationRevokedUpdateID(),
|
||||
GetGetValidationSucceededUpdateID()
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private ServiceAction GetIsValidated()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "IsValidated"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "DeviceID",
|
||||
Direction = "in"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Result",
|
||||
Direction = "out"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private ServiceAction GetIsAuthorized()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "IsAuthorized"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "DeviceID",
|
||||
Direction = "in"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Result",
|
||||
Direction = "out"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private ServiceAction GetRegisterDevice()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "RegisterDevice"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "RegistrationReqMsg",
|
||||
Direction = "in"
|
||||
});
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "RegistrationRespMsg",
|
||||
Direction = "out"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private ServiceAction GetGetValidationSucceededUpdateID()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetValidationSucceededUpdateID"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ValidationSucceededUpdateID",
|
||||
Direction = "out"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private ServiceAction GetGetAuthorizationDeniedUpdateID()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetAuthorizationDeniedUpdateID"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "AuthorizationDeniedUpdateID",
|
||||
Direction = "out"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private ServiceAction GetGetValidationRevokedUpdateID()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetValidationRevokedUpdateID"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ValidationRevokedUpdateID",
|
||||
Direction = "out"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private ServiceAction GetGetAuthorizationGrantedUpdateID()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetAuthorizationGrantedUpdateID"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "AuthorizationGrantedUpdateID",
|
||||
Direction = "out"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
}
|
||||
}
|
9
Emby.Dlna/PlayTo/CurrentIdEventArgs.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
using System;
|
||||
|
||||
namespace MediaBrowser.Dlna.PlayTo
|
||||
{
|
||||
public class CurrentIdEventArgs : EventArgs
|
||||
{
|
||||
public string Id { get; set; }
|
||||
}
|
||||
}
|
1092
Emby.Dlna/PlayTo/Device.cs
Normal file
76
Emby.Dlna/PlayTo/DeviceInfo.cs
Normal file
|
@ -0,0 +1,76 @@
|
|||
using MediaBrowser.Dlna.Common;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Dlna.PlayTo
|
||||
{
|
||||
public class DeviceInfo
|
||||
{
|
||||
public DeviceInfo()
|
||||
{
|
||||
ClientType = "DLNA";
|
||||
Name = "Generic Device";
|
||||
}
|
||||
|
||||
public string UUID { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string ClientType { get; set; }
|
||||
|
||||
public string ModelName { get; set; }
|
||||
|
||||
public string ModelNumber { get; set; }
|
||||
|
||||
public string ModelDescription { get; set; }
|
||||
|
||||
public string ModelUrl { get; set; }
|
||||
|
||||
public string Manufacturer { get; set; }
|
||||
|
||||
public string SerialNumber { get; set; }
|
||||
|
||||
public string ManufacturerUrl { get; set; }
|
||||
|
||||
public string PresentationUrl { get; set; }
|
||||
|
||||
private string _baseUrl = string.Empty;
|
||||
public string BaseUrl
|
||||
{
|
||||
get
|
||||
{
|
||||
return _baseUrl;
|
||||
}
|
||||
set
|
||||
{
|
||||
_baseUrl = value;
|
||||
}
|
||||
}
|
||||
|
||||
public DeviceIcon Icon { get; set; }
|
||||
|
||||
private readonly List<DeviceService> _services = new List<DeviceService>();
|
||||
public List<DeviceService> Services
|
||||
{
|
||||
get
|
||||
{
|
||||
return _services;
|
||||
}
|
||||
}
|
||||
|
||||
public DeviceIdentification ToDeviceIdentification()
|
||||
{
|
||||
return new DeviceIdentification
|
||||
{
|
||||
Manufacturer = Manufacturer,
|
||||
ModelName = ModelName,
|
||||
ModelNumber = ModelNumber,
|
||||
FriendlyName = Name,
|
||||
ManufacturerUrl = ManufacturerUrl,
|
||||
ModelUrl = ModelUrl,
|
||||
ModelDescription = ModelDescription,
|
||||
SerialNumber = SerialNumber
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
941
Emby.Dlna/PlayTo/PlayToController.cs
Normal file
|
@ -0,0 +1,941 @@
|
|||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Dlna.Didl;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Session;
|
||||
using MediaBrowser.Model.System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
|
||||
namespace MediaBrowser.Dlna.PlayTo
|
||||
{
|
||||
public class PlayToController : ISessionController, IDisposable
|
||||
{
|
||||
private Device _device;
|
||||
private readonly SessionInfo _session;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IConfigurationManager _config;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
|
||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
||||
private readonly string _serverAddress;
|
||||
private readonly string _accessToken;
|
||||
private readonly DateTime _creationTime;
|
||||
|
||||
public bool IsSessionActive
|
||||
{
|
||||
get
|
||||
{
|
||||
var lastDateKnownActivity = new[] { _creationTime, _device.DateLastActivity }.Max();
|
||||
|
||||
if (DateTime.UtcNow >= lastDateKnownActivity.AddSeconds(120))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Session is inactive, mark it for Disposal and don't start the elapsed timer.
|
||||
_sessionManager.ReportSessionEnded(_session.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error in ReportSessionEnded", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return _device != null;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnActivity()
|
||||
{
|
||||
}
|
||||
|
||||
public bool SupportsMediaControl
|
||||
{
|
||||
get { return IsSessionActive; }
|
||||
}
|
||||
|
||||
public PlayToController(SessionInfo session, ISessionManager sessionManager, ILibraryManager libraryManager, ILogger logger, IDlnaManager dlnaManager, IUserManager userManager, IImageProcessor imageProcessor, string serverAddress, string accessToken, IDeviceDiscovery deviceDiscovery, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IConfigurationManager config, IMediaEncoder mediaEncoder)
|
||||
{
|
||||
_session = session;
|
||||
_sessionManager = sessionManager;
|
||||
_libraryManager = libraryManager;
|
||||
_dlnaManager = dlnaManager;
|
||||
_userManager = userManager;
|
||||
_imageProcessor = imageProcessor;
|
||||
_serverAddress = serverAddress;
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
_userDataManager = userDataManager;
|
||||
_localization = localization;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_config = config;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_accessToken = accessToken;
|
||||
_logger = logger;
|
||||
_creationTime = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void Init(Device device)
|
||||
{
|
||||
_device = device;
|
||||
_device.OnDeviceUnavailable = OnDeviceUnavailable;
|
||||
_device.PlaybackStart += _device_PlaybackStart;
|
||||
_device.PlaybackProgress += _device_PlaybackProgress;
|
||||
_device.PlaybackStopped += _device_PlaybackStopped;
|
||||
_device.MediaChanged += _device_MediaChanged;
|
||||
|
||||
_device.Start();
|
||||
|
||||
_deviceDiscovery.DeviceLeft += _deviceDiscovery_DeviceLeft;
|
||||
}
|
||||
|
||||
private void OnDeviceUnavailable()
|
||||
{
|
||||
try
|
||||
{
|
||||
_sessionManager.ReportSessionEnded(_session.Id);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Could throw if the session is already gone
|
||||
}
|
||||
}
|
||||
|
||||
void _deviceDiscovery_DeviceLeft(object sender, GenericEventArgs<UpnpDeviceInfo> e)
|
||||
{
|
||||
var info = e.Argument;
|
||||
|
||||
string nts;
|
||||
info.Headers.TryGetValue("NTS", out nts);
|
||||
|
||||
string usn;
|
||||
if (!info.Headers.TryGetValue("USN", out usn)) usn = String.Empty;
|
||||
|
||||
string nt;
|
||||
if (!info.Headers.TryGetValue("NT", out nt)) nt = String.Empty;
|
||||
|
||||
if (usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1 &&
|
||||
!_disposed)
|
||||
{
|
||||
if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1 ||
|
||||
nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
OnDeviceUnavailable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async void _device_MediaChanged(object sender, MediaChangedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var streamInfo = await StreamParams.ParseFromUrl(e.OldMediaInfo.Url, _libraryManager, _mediaSourceManager).ConfigureAwait(false);
|
||||
if (streamInfo.Item != null)
|
||||
{
|
||||
var progress = GetProgressInfo(e.OldMediaInfo, streamInfo);
|
||||
|
||||
var positionTicks = progress.PositionTicks;
|
||||
|
||||
ReportPlaybackStopped(e.OldMediaInfo, streamInfo, positionTicks);
|
||||
}
|
||||
|
||||
streamInfo = await StreamParams.ParseFromUrl(e.NewMediaInfo.Url, _libraryManager, _mediaSourceManager).ConfigureAwait(false);
|
||||
if (streamInfo.Item == null) return;
|
||||
|
||||
var newItemProgress = GetProgressInfo(e.NewMediaInfo, streamInfo);
|
||||
|
||||
await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error reporting progress", ex);
|
||||
}
|
||||
}
|
||||
|
||||
async void _device_PlaybackStopped(object sender, PlaybackStoppedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var streamInfo = await StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (streamInfo.Item == null) return;
|
||||
|
||||
var progress = GetProgressInfo(e.MediaInfo, streamInfo);
|
||||
|
||||
var positionTicks = progress.PositionTicks;
|
||||
|
||||
ReportPlaybackStopped(e.MediaInfo, streamInfo, positionTicks);
|
||||
|
||||
var duration = streamInfo.MediaSource == null ?
|
||||
(_device.Duration == null ? (long?)null : _device.Duration.Value.Ticks) :
|
||||
streamInfo.MediaSource.RunTimeTicks;
|
||||
|
||||
var playedToCompletion = (positionTicks.HasValue && positionTicks.Value == 0);
|
||||
|
||||
if (!playedToCompletion && duration.HasValue && positionTicks.HasValue)
|
||||
{
|
||||
double percent = positionTicks.Value;
|
||||
percent /= duration.Value;
|
||||
|
||||
playedToCompletion = Math.Abs(1 - percent) <= .1;
|
||||
}
|
||||
|
||||
if (playedToCompletion)
|
||||
{
|
||||
await SetPlaylistIndex(_currentPlaylistIndex + 1).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Playlist.Clear();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error reporting playback stopped", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async void ReportPlaybackStopped(uBaseObject mediaInfo, StreamParams streamInfo, long? positionTicks)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _sessionManager.OnPlaybackStopped(new PlaybackStopInfo
|
||||
{
|
||||
ItemId = streamInfo.ItemId,
|
||||
SessionId = _session.Id,
|
||||
PositionTicks = positionTicks,
|
||||
MediaSourceId = streamInfo.MediaSourceId
|
||||
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error reporting progress", ex);
|
||||
}
|
||||
}
|
||||
|
||||
async void _device_PlaybackStart(object sender, PlaybackStartEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = await StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager).ConfigureAwait(false);
|
||||
|
||||
if (info.Item != null)
|
||||
{
|
||||
var progress = GetProgressInfo(e.MediaInfo, info);
|
||||
|
||||
await _sessionManager.OnPlaybackStart(progress).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error reporting progress", ex);
|
||||
}
|
||||
}
|
||||
|
||||
async void _device_PlaybackProgress(object sender, PlaybackProgressEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = await StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager).ConfigureAwait(false);
|
||||
|
||||
if (info.Item != null)
|
||||
{
|
||||
var progress = GetProgressInfo(e.MediaInfo, info);
|
||||
|
||||
await _sessionManager.OnPlaybackProgress(progress).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error reporting progress", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private PlaybackStartInfo GetProgressInfo(uBaseObject mediaInfo, StreamParams info)
|
||||
{
|
||||
var ticks = _device.Position.Ticks;
|
||||
|
||||
if (!EnableClientSideSeek(info))
|
||||
{
|
||||
ticks += info.StartPositionTicks;
|
||||
}
|
||||
|
||||
return new PlaybackStartInfo
|
||||
{
|
||||
ItemId = info.ItemId,
|
||||
SessionId = _session.Id,
|
||||
PositionTicks = ticks,
|
||||
IsMuted = _device.IsMuted,
|
||||
IsPaused = _device.IsPaused,
|
||||
MediaSourceId = info.MediaSourceId,
|
||||
AudioStreamIndex = info.AudioStreamIndex,
|
||||
SubtitleStreamIndex = info.SubtitleStreamIndex,
|
||||
VolumeLevel = _device.Volume,
|
||||
|
||||
CanSeek = info.MediaSource == null ? _device.Duration.HasValue : info.MediaSource.RunTimeTicks.HasValue,
|
||||
|
||||
PlayMethod = info.IsDirectStream ? PlayMethod.DirectStream : PlayMethod.Transcode,
|
||||
QueueableMediaTypes = new List<string> { mediaInfo.MediaType }
|
||||
};
|
||||
}
|
||||
|
||||
#region SendCommands
|
||||
|
||||
public async Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.Debug("{0} - Received PlayRequest: {1}", this._session.DeviceName, command.PlayCommand);
|
||||
|
||||
var user = String.IsNullOrEmpty(command.ControllingUserId) ? null : _userManager.GetUserById(command.ControllingUserId);
|
||||
|
||||
var items = new List<BaseItem>();
|
||||
foreach (string id in command.ItemIds)
|
||||
{
|
||||
AddItemFromId(Guid.Parse(id), items);
|
||||
}
|
||||
|
||||
var playlist = new List<PlaylistItem>();
|
||||
var isFirst = true;
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (isFirst && command.StartPositionTicks.HasValue)
|
||||
{
|
||||
playlist.Add(CreatePlaylistItem(item, user, command.StartPositionTicks.Value, null, null, null));
|
||||
isFirst = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
playlist.Add(CreatePlaylistItem(item, user, 0, null, null, null));
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug("{0} - Playlist created", _session.DeviceName);
|
||||
|
||||
if (command.PlayCommand == PlayCommand.PlayLast)
|
||||
{
|
||||
Playlist.AddRange(playlist);
|
||||
}
|
||||
if (command.PlayCommand == PlayCommand.PlayNext)
|
||||
{
|
||||
Playlist.AddRange(playlist);
|
||||
}
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(command.ControllingUserId))
|
||||
{
|
||||
await _sessionManager.LogSessionActivity(_session.Client, _session.ApplicationVersion, _session.DeviceId,
|
||||
_session.DeviceName, _session.RemoteEndPoint, user).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await PlayItems(playlist).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken)
|
||||
{
|
||||
switch (command.Command)
|
||||
{
|
||||
case PlaystateCommand.Stop:
|
||||
Playlist.Clear();
|
||||
return _device.SetStop();
|
||||
|
||||
case PlaystateCommand.Pause:
|
||||
return _device.SetPause();
|
||||
|
||||
case PlaystateCommand.Unpause:
|
||||
return _device.SetPlay();
|
||||
|
||||
case PlaystateCommand.Seek:
|
||||
{
|
||||
return Seek(command.SeekPositionTicks ?? 0);
|
||||
}
|
||||
|
||||
case PlaystateCommand.NextTrack:
|
||||
return SetPlaylistIndex(_currentPlaylistIndex + 1);
|
||||
|
||||
case PlaystateCommand.PreviousTrack:
|
||||
return SetPlaylistIndex(_currentPlaylistIndex - 1);
|
||||
}
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
private async Task Seek(long newPosition)
|
||||
{
|
||||
var media = _device.CurrentMediaInfo;
|
||||
|
||||
if (media != null)
|
||||
{
|
||||
var info = await StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager).ConfigureAwait(false);
|
||||
|
||||
if (info.Item != null && !EnableClientSideSeek(info))
|
||||
{
|
||||
var user = _session.UserId.HasValue ? _userManager.GetUserById(_session.UserId.Value) : null;
|
||||
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
|
||||
|
||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
await SeekAfterTransportChange(newPosition).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private bool EnableClientSideSeek(StreamParams info)
|
||||
{
|
||||
return info.IsDirectStream;
|
||||
}
|
||||
|
||||
private bool EnableClientSideSeek(StreamInfo info)
|
||||
{
|
||||
return info.IsDirectStream;
|
||||
}
|
||||
|
||||
public Task SendUserDataChangeInfo(UserDataChangeInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task SendRestartRequiredNotification(SystemInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task SendServerRestartNotification(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task SendSessionEndedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task SendPlaybackStartNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task SendPlaybackStoppedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task SendServerShutdownNotification(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task SendLibraryUpdateInfo(LibraryUpdateInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Playlist
|
||||
|
||||
private int _currentPlaylistIndex;
|
||||
private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
|
||||
private List<PlaylistItem> Playlist
|
||||
{
|
||||
get
|
||||
{
|
||||
return _playlist;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddItemFromId(Guid id, List<BaseItem> list)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(id);
|
||||
if (item.MediaType == MediaType.Audio || item.MediaType == MediaType.Video)
|
||||
{
|
||||
list.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
private PlaylistItem CreatePlaylistItem(BaseItem item, User user, long startPostionTicks, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
|
||||
{
|
||||
var deviceInfo = _device.Properties;
|
||||
|
||||
var profile = _dlnaManager.GetProfile(deviceInfo.ToDeviceIdentification()) ??
|
||||
_dlnaManager.GetDefaultProfile();
|
||||
|
||||
var hasMediaSources = item as IHasMediaSources;
|
||||
var mediaSources = hasMediaSources != null
|
||||
? (_mediaSourceManager.GetStaticMediaSources(hasMediaSources, true, user)).ToList()
|
||||
: new List<MediaSourceInfo>();
|
||||
|
||||
var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex);
|
||||
playlistItem.StreamInfo.StartPositionTicks = startPostionTicks;
|
||||
|
||||
playlistItem.StreamUrl = playlistItem.StreamInfo.ToDlnaUrl(_serverAddress, _accessToken);
|
||||
|
||||
var itemXml = new DidlBuilder(profile, user, _imageProcessor, _serverAddress, _accessToken, _userDataManager, _localization, _mediaSourceManager, _logger, _libraryManager, _mediaEncoder)
|
||||
.GetItemDidl(_config.GetDlnaConfiguration(), item, null, _session.DeviceId, new Filter(), playlistItem.StreamInfo);
|
||||
|
||||
playlistItem.Didl = itemXml;
|
||||
|
||||
return playlistItem;
|
||||
}
|
||||
|
||||
private string GetDlnaHeaders(PlaylistItem item)
|
||||
{
|
||||
var profile = item.Profile;
|
||||
var streamInfo = item.StreamInfo;
|
||||
|
||||
if (streamInfo.MediaType == DlnaProfileType.Audio)
|
||||
{
|
||||
return new ContentFeatureBuilder(profile)
|
||||
.BuildAudioHeader(streamInfo.Container,
|
||||
streamInfo.TargetAudioCodec,
|
||||
streamInfo.TargetAudioBitrate,
|
||||
streamInfo.TargetAudioSampleRate,
|
||||
streamInfo.TargetAudioChannels,
|
||||
streamInfo.IsDirectStream,
|
||||
streamInfo.RunTimeTicks,
|
||||
streamInfo.TranscodeSeekInfo);
|
||||
}
|
||||
|
||||
if (streamInfo.MediaType == DlnaProfileType.Video)
|
||||
{
|
||||
var list = new ContentFeatureBuilder(profile)
|
||||
.BuildVideoHeader(streamInfo.Container,
|
||||
streamInfo.VideoCodec,
|
||||
streamInfo.TargetAudioCodec,
|
||||
streamInfo.TargetWidth,
|
||||
streamInfo.TargetHeight,
|
||||
streamInfo.TargetVideoBitDepth,
|
||||
streamInfo.TargetVideoBitrate,
|
||||
streamInfo.TargetTimestamp,
|
||||
streamInfo.IsDirectStream,
|
||||
streamInfo.RunTimeTicks,
|
||||
streamInfo.TargetVideoProfile,
|
||||
streamInfo.TargetVideoLevel,
|
||||
streamInfo.TargetFramerate,
|
||||
streamInfo.TargetPacketLength,
|
||||
streamInfo.TranscodeSeekInfo,
|
||||
streamInfo.IsTargetAnamorphic,
|
||||
streamInfo.TargetRefFrames,
|
||||
streamInfo.TargetVideoStreamCount,
|
||||
streamInfo.TargetAudioStreamCount,
|
||||
streamInfo.TargetVideoCodecTag,
|
||||
streamInfo.IsTargetAVC);
|
||||
|
||||
return list.FirstOrDefault();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private ILogger GetStreamBuilderLogger()
|
||||
{
|
||||
if (_config.GetDlnaConfiguration().EnableDebugLog)
|
||||
{
|
||||
return _logger;
|
||||
}
|
||||
|
||||
return new NullLogger();
|
||||
}
|
||||
|
||||
private PlaylistItem GetPlaylistItem(BaseItem item, List<MediaSourceInfo> mediaSources, DeviceProfile profile, string deviceId, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
|
||||
{
|
||||
if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new PlaylistItem
|
||||
{
|
||||
StreamInfo = new StreamBuilder(_mediaEncoder, GetStreamBuilderLogger()).BuildVideoItem(new VideoOptions
|
||||
{
|
||||
ItemId = item.Id.ToString("N"),
|
||||
MediaSources = mediaSources,
|
||||
Profile = profile,
|
||||
DeviceId = deviceId,
|
||||
MaxBitrate = profile.MaxStreamingBitrate,
|
||||
MediaSourceId = mediaSourceId,
|
||||
AudioStreamIndex = audioStreamIndex,
|
||||
SubtitleStreamIndex = subtitleStreamIndex
|
||||
}),
|
||||
|
||||
Profile = profile
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new PlaylistItem
|
||||
{
|
||||
StreamInfo = new StreamBuilder(_mediaEncoder, GetStreamBuilderLogger()).BuildAudioItem(new AudioOptions
|
||||
{
|
||||
ItemId = item.Id.ToString("N"),
|
||||
MediaSources = mediaSources,
|
||||
Profile = profile,
|
||||
DeviceId = deviceId,
|
||||
MaxBitrate = profile.MaxStreamingBitrate,
|
||||
MediaSourceId = mediaSourceId
|
||||
}),
|
||||
|
||||
Profile = profile
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new PlaylistItemFactory().Create((Photo)item, profile);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Unrecognized item type.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays the items.
|
||||
/// </summary>
|
||||
/// <param name="items">The items.</param>
|
||||
/// <returns></returns>
|
||||
private async Task<bool> PlayItems(IEnumerable<PlaylistItem> items)
|
||||
{
|
||||
Playlist.Clear();
|
||||
Playlist.AddRange(items);
|
||||
_logger.Debug("{0} - Playing {1} items", _session.DeviceName, Playlist.Count);
|
||||
|
||||
await SetPlaylistIndex(0).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task SetPlaylistIndex(int index)
|
||||
{
|
||||
if (index < 0 || index >= Playlist.Count)
|
||||
{
|
||||
Playlist.Clear();
|
||||
await _device.SetStop();
|
||||
return;
|
||||
}
|
||||
|
||||
_currentPlaylistIndex = index;
|
||||
var currentitem = Playlist[index];
|
||||
|
||||
await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl);
|
||||
|
||||
var streamInfo = currentitem.StreamInfo;
|
||||
if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
|
||||
{
|
||||
await SeekAfterTransportChange(streamInfo.StartPositionTicks).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_disposed = true;
|
||||
|
||||
_device.PlaybackStart -= _device_PlaybackStart;
|
||||
_device.PlaybackProgress -= _device_PlaybackProgress;
|
||||
_device.PlaybackStopped -= _device_PlaybackStopped;
|
||||
_device.MediaChanged -= _device_MediaChanged;
|
||||
//_deviceDiscovery.DeviceLeft -= _deviceDiscovery_DeviceLeft;
|
||||
_device.OnDeviceUnavailable = null;
|
||||
|
||||
_device.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
|
||||
public Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
GeneralCommandType commandType;
|
||||
|
||||
if (Enum.TryParse(command.Name, true, out commandType))
|
||||
{
|
||||
switch (commandType)
|
||||
{
|
||||
case GeneralCommandType.VolumeDown:
|
||||
return _device.VolumeDown();
|
||||
case GeneralCommandType.VolumeUp:
|
||||
return _device.VolumeUp();
|
||||
case GeneralCommandType.Mute:
|
||||
return _device.Mute();
|
||||
case GeneralCommandType.Unmute:
|
||||
return _device.Unmute();
|
||||
case GeneralCommandType.ToggleMute:
|
||||
return _device.ToggleMute();
|
||||
case GeneralCommandType.SetAudioStreamIndex:
|
||||
{
|
||||
string arg;
|
||||
|
||||
if (command.Arguments.TryGetValue("Index", out arg))
|
||||
{
|
||||
int val;
|
||||
|
||||
if (Int32.TryParse(arg, NumberStyles.Any, _usCulture, out val))
|
||||
{
|
||||
return SetAudioStreamIndex(val);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
|
||||
}
|
||||
|
||||
throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
|
||||
}
|
||||
case GeneralCommandType.SetSubtitleStreamIndex:
|
||||
{
|
||||
string arg;
|
||||
|
||||
if (command.Arguments.TryGetValue("Index", out arg))
|
||||
{
|
||||
int val;
|
||||
|
||||
if (Int32.TryParse(arg, NumberStyles.Any, _usCulture, out val))
|
||||
{
|
||||
return SetSubtitleStreamIndex(val);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
|
||||
}
|
||||
|
||||
throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
|
||||
}
|
||||
case GeneralCommandType.SetVolume:
|
||||
{
|
||||
string arg;
|
||||
|
||||
if (command.Arguments.TryGetValue("Volume", out arg))
|
||||
{
|
||||
int volume;
|
||||
|
||||
if (Int32.TryParse(arg, NumberStyles.Any, _usCulture, out volume))
|
||||
{
|
||||
return _device.SetVolume(volume);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Unsupported volume value supplied.");
|
||||
}
|
||||
|
||||
throw new ArgumentException("Volume argument cannot be null");
|
||||
}
|
||||
default:
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
private async Task SetAudioStreamIndex(int? newIndex)
|
||||
{
|
||||
var media = _device.CurrentMediaInfo;
|
||||
|
||||
if (media != null)
|
||||
{
|
||||
var info = await StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager).ConfigureAwait(false);
|
||||
|
||||
if (info.Item != null)
|
||||
{
|
||||
var progress = GetProgressInfo(media, info);
|
||||
var newPosition = progress.PositionTicks ?? 0;
|
||||
|
||||
var user = _session.UserId.HasValue ? _userManager.GetUserById(_session.UserId.Value) : null;
|
||||
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, newIndex, info.SubtitleStreamIndex);
|
||||
|
||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl).ConfigureAwait(false);
|
||||
|
||||
if (EnableClientSideSeek(newItem.StreamInfo))
|
||||
{
|
||||
await SeekAfterTransportChange(newPosition).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetSubtitleStreamIndex(int? newIndex)
|
||||
{
|
||||
var media = _device.CurrentMediaInfo;
|
||||
|
||||
if (media != null)
|
||||
{
|
||||
var info = await StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager).ConfigureAwait(false);
|
||||
|
||||
if (info.Item != null)
|
||||
{
|
||||
var progress = GetProgressInfo(media, info);
|
||||
var newPosition = progress.PositionTicks ?? 0;
|
||||
|
||||
var user = _session.UserId.HasValue ? _userManager.GetUserById(_session.UserId.Value) : null;
|
||||
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, newIndex);
|
||||
|
||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl).ConfigureAwait(false);
|
||||
|
||||
if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
|
||||
{
|
||||
await SeekAfterTransportChange(newPosition).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SeekAfterTransportChange(long positionTicks)
|
||||
{
|
||||
const int maxWait = 15000000;
|
||||
const int interval = 500;
|
||||
var currentWait = 0;
|
||||
while (_device.TransportState != TRANSPORTSTATE.PLAYING && currentWait < maxWait)
|
||||
{
|
||||
await Task.Delay(interval).ConfigureAwait(false);
|
||||
currentWait += interval;
|
||||
}
|
||||
|
||||
await _device.Seek(TimeSpan.FromTicks(positionTicks)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private class StreamParams
|
||||
{
|
||||
public string ItemId { get; set; }
|
||||
|
||||
public bool IsDirectStream { get; set; }
|
||||
|
||||
public long StartPositionTicks { get; set; }
|
||||
|
||||
public int? AudioStreamIndex { get; set; }
|
||||
|
||||
public int? SubtitleStreamIndex { get; set; }
|
||||
|
||||
public string DeviceProfileId { get; set; }
|
||||
public string DeviceId { get; set; }
|
||||
|
||||
public string MediaSourceId { get; set; }
|
||||
public string LiveStreamId { get; set; }
|
||||
|
||||
public BaseItem Item { get; set; }
|
||||
public MediaSourceInfo MediaSource { get; set; }
|
||||
|
||||
private static string GetItemId(string url)
|
||||
{
|
||||
var parts = url.Split('/');
|
||||
|
||||
for (var i = 0; i < parts.Length; i++)
|
||||
{
|
||||
var part = parts[i];
|
||||
|
||||
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (parts.Length > i + 1)
|
||||
{
|
||||
return parts[i + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static async Task<StreamParams> ParseFromUrl(string url, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager)
|
||||
{
|
||||
var request = new StreamParams
|
||||
{
|
||||
ItemId = GetItemId(url)
|
||||
};
|
||||
|
||||
Guid parsedId;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.ItemId) || !Guid.TryParse(request.ItemId, out parsedId))
|
||||
{
|
||||
return request;
|
||||
}
|
||||
|
||||
const string srch = "params=";
|
||||
var index = url.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (index == -1) return request;
|
||||
|
||||
var vals = url.Substring(index + srch.Length).Split(';');
|
||||
|
||||
for (var i = 0; i < vals.Length; i++)
|
||||
{
|
||||
var val = vals[i];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
request.DeviceProfileId = val;
|
||||
}
|
||||
else if (i == 1)
|
||||
{
|
||||
request.DeviceId = val;
|
||||
}
|
||||
else if (i == 2)
|
||||
{
|
||||
request.MediaSourceId = val;
|
||||
}
|
||||
else if (i == 3)
|
||||
{
|
||||
request.IsDirectStream = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else if (i == 6)
|
||||
{
|
||||
request.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
else if (i == 7)
|
||||
{
|
||||
request.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
else if (i == 14)
|
||||
{
|
||||
request.StartPositionTicks = long.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
else if (i == 22)
|
||||
{
|
||||
request.LiveStreamId = val;
|
||||
}
|
||||
}
|
||||
|
||||
request.Item = string.IsNullOrWhiteSpace(request.ItemId)
|
||||
? null
|
||||
: libraryManager.GetItemById(parsedId);
|
||||
|
||||
var hasMediaSources = request.Item as IHasMediaSources;
|
||||
|
||||
request.MediaSource = hasMediaSources == null
|
||||
? null
|
||||
: (await mediaSourceManager.GetMediaSource(hasMediaSources, request.MediaSourceId, request.LiveStreamId, false, CancellationToken.None).ConfigureAwait(false));
|
||||
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
public Task SendMessage<T>(string name, T data, CancellationToken cancellationToken)
|
||||
{
|
||||
// Not supported or needed right now
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
}
|
205
Emby.Dlna/PlayTo/PlayToManager.cs
Normal file
|
@ -0,0 +1,205 @@
|
|||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Session;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
|
||||
namespace MediaBrowser.Dlna.PlayTo
|
||||
{
|
||||
class PlayToManager : IDisposable
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
|
||||
private readonly List<string> _nonRendererUrls = new List<string>();
|
||||
private DateTime _lastRendererClear;
|
||||
|
||||
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClient httpClient, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
|
||||
{
|
||||
_logger = logger;
|
||||
_sessionManager = sessionManager;
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
_dlnaManager = dlnaManager;
|
||||
_appHost = appHost;
|
||||
_imageProcessor = imageProcessor;
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
_httpClient = httpClient;
|
||||
_config = config;
|
||||
_userDataManager = userDataManager;
|
||||
_localization = localization;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered;
|
||||
}
|
||||
|
||||
async void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
|
||||
{
|
||||
var info = e.Argument;
|
||||
|
||||
string usn;
|
||||
if (!info.Headers.TryGetValue("USN", out usn)) usn = string.Empty;
|
||||
|
||||
string nt;
|
||||
if (!info.Headers.TryGetValue("NT", out nt)) nt = string.Empty;
|
||||
|
||||
string location = info.Location.ToString();
|
||||
|
||||
// It has to report that it's a media renderer
|
||||
if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1 &&
|
||||
nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_sessionManager.Sessions.Any(i => usn.IndexOf(i.DeviceId, StringComparison.OrdinalIgnoreCase) != -1))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
lock (_nonRendererUrls)
|
||||
{
|
||||
if ((DateTime.UtcNow - _lastRendererClear).TotalMinutes >= 10)
|
||||
{
|
||||
_nonRendererUrls.Clear();
|
||||
_lastRendererClear = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
if (_nonRendererUrls.Contains(location, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var uri = info.Location;
|
||||
_logger.Debug("Attempting to create PlayToController from location {0}", location);
|
||||
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _config, _logger).ConfigureAwait(false);
|
||||
|
||||
if (device.RendererCommands == null)
|
||||
{
|
||||
lock (_nonRendererUrls)
|
||||
{
|
||||
_nonRendererUrls.Add(location);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug("Logging session activity from location {0}", location);
|
||||
var sessionInfo = await _sessionManager.LogSessionActivity(device.Properties.ClientType, _appHost.ApplicationVersion.ToString(), device.Properties.UUID, device.Properties.Name, uri.OriginalString, null)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var controller = sessionInfo.SessionController as PlayToController;
|
||||
|
||||
if (controller == null)
|
||||
{
|
||||
string serverAddress;
|
||||
if (info.LocalIpAddress == null)
|
||||
{
|
||||
serverAddress = await GetServerAddress(null, false).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
serverAddress = await GetServerAddress(info.LocalIpAddress.Address, info.LocalIpAddress.IsIpv6).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
string accessToken = null;
|
||||
|
||||
sessionInfo.SessionController = controller = new PlayToController(sessionInfo,
|
||||
_sessionManager,
|
||||
_libraryManager,
|
||||
_logger,
|
||||
_dlnaManager,
|
||||
_userManager,
|
||||
_imageProcessor,
|
||||
serverAddress,
|
||||
accessToken,
|
||||
_deviceDiscovery,
|
||||
_userDataManager,
|
||||
_localization,
|
||||
_mediaSourceManager,
|
||||
_config,
|
||||
_mediaEncoder);
|
||||
|
||||
controller.Init(device);
|
||||
|
||||
var profile = _dlnaManager.GetProfile(device.Properties.ToDeviceIdentification()) ??
|
||||
_dlnaManager.GetDefaultProfile();
|
||||
|
||||
_sessionManager.ReportCapabilities(sessionInfo.Id, new ClientCapabilities
|
||||
{
|
||||
PlayableMediaTypes = profile.GetSupportedMediaTypes(),
|
||||
|
||||
SupportedCommands = new List<string>
|
||||
{
|
||||
GeneralCommandType.VolumeDown.ToString(),
|
||||
GeneralCommandType.VolumeUp.ToString(),
|
||||
GeneralCommandType.Mute.ToString(),
|
||||
GeneralCommandType.Unmute.ToString(),
|
||||
GeneralCommandType.ToggleMute.ToString(),
|
||||
GeneralCommandType.SetVolume.ToString(),
|
||||
GeneralCommandType.SetAudioStreamIndex.ToString(),
|
||||
GeneralCommandType.SetSubtitleStreamIndex.ToString()
|
||||
},
|
||||
|
||||
SupportsMediaControl = true
|
||||
});
|
||||
|
||||
_logger.Info("DLNA Session created for {0} - {1}", device.Properties.Name, device.Properties.ModelName);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error creating PlayTo device.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Task<string> GetServerAddress(string ipAddress, bool isIpv6)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ipAddress))
|
||||
{
|
||||
return _appHost.GetLocalApiUrl();
|
||||
}
|
||||
|
||||
return Task.FromResult(_appHost.GetLocalApiUrl(ipAddress, isIpv6));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_deviceDiscovery.DeviceDiscovered -= _deviceDiscovery_DeviceDiscovered;
|
||||
}
|
||||
}
|
||||
}
|
9
Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
using System;
|
||||
|
||||
namespace MediaBrowser.Dlna.PlayTo
|
||||
{
|
||||
public class PlaybackProgressEventArgs : EventArgs
|
||||
{
|
||||
public uBaseObject MediaInfo { get; set; }
|
||||
}
|
||||
}
|
9
Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
using System;
|
||||
|
||||
namespace MediaBrowser.Dlna.PlayTo
|
||||
{
|
||||
public class PlaybackStartEventArgs : EventArgs
|
||||
{
|
||||
public uBaseObject MediaInfo { get; set; }
|
||||
}
|
||||
}
|
15
Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
|
||||
namespace MediaBrowser.Dlna.PlayTo
|
||||
{
|
||||
public class PlaybackStoppedEventArgs : EventArgs
|
||||
{
|
||||
public uBaseObject MediaInfo { get; set; }
|
||||
}
|
||||
|
||||
public class MediaChangedEventArgs : EventArgs
|
||||
{
|
||||
public uBaseObject OldMediaInfo { get; set; }
|
||||
public uBaseObject NewMediaInfo { get; set; }
|
||||
}
|
||||
}
|
15
Emby.Dlna/PlayTo/PlaylistItem.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace MediaBrowser.Dlna.PlayTo
|
||||
{
|
||||
public class PlaylistItem
|
||||
{
|
||||
public string StreamUrl { get; set; }
|
||||
|
||||
public string Didl { get; set; }
|
||||
|
||||
public StreamInfo StreamInfo { get; set; }
|
||||
|
||||
public DeviceProfile Profile { get; set; }
|
||||
}
|
||||
}
|
69
Emby.Dlna/PlayTo/PlaylistItemFactory.cs
Normal file
|
@ -0,0 +1,69 @@
|
|||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Session;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Dlna.PlayTo
|
||||
{
|
||||
public class PlaylistItemFactory
|
||||
{
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
|
||||
public PlaylistItem Create(Photo item, DeviceProfile profile)
|
||||
{
|
||||
var playlistItem = new PlaylistItem
|
||||
{
|
||||
StreamInfo = new StreamInfo
|
||||
{
|
||||
ItemId = item.Id.ToString("N"),
|
||||
MediaType = DlnaProfileType.Photo,
|
||||
DeviceProfile = profile
|
||||
},
|
||||
|
||||
Profile = profile
|
||||
};
|
||||
|
||||
var directPlay = profile.DirectPlayProfiles
|
||||
.FirstOrDefault(i => i.Type == DlnaProfileType.Photo && IsSupported(i, item));
|
||||
|
||||
if (directPlay != null)
|
||||
{
|
||||
playlistItem.StreamInfo.PlayMethod = PlayMethod.DirectStream;
|
||||
playlistItem.StreamInfo.Container = Path.GetExtension(item.Path);
|
||||
|
||||
return playlistItem;
|
||||
}
|
||||
|
||||
var transcodingProfile = profile.TranscodingProfiles
|
||||
.FirstOrDefault(i => i.Type == DlnaProfileType.Photo);
|
||||
|
||||
if (transcodingProfile != null)
|
||||
{
|
||||
playlistItem.StreamInfo.PlayMethod = PlayMethod.Transcode;
|
||||
playlistItem.StreamInfo.Container = "." + transcodingProfile.Container.TrimStart('.');
|
||||
}
|
||||
|
||||
return playlistItem;
|
||||
}
|
||||
|
||||
private bool IsSupported(DirectPlayProfile profile, Photo item)
|
||||
{
|
||||
var mediaPath = item.Path;
|
||||
|
||||
if (profile.Container.Length > 0)
|
||||
{
|
||||
// Check container type
|
||||
var mediaContainer = Path.GetExtension(mediaPath);
|
||||
if (!profile.GetContainers().Any(i => string.Equals("." + i.TrimStart('.'), mediaContainer, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
139
Emby.Dlna/PlayTo/SsdpHttpClient.cs
Normal file
|
@ -0,0 +1,139 @@
|
|||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Dlna.Common;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace MediaBrowser.Dlna.PlayTo
|
||||
{
|
||||
public class SsdpHttpClient
|
||||
{
|
||||
private const string USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50";
|
||||
private const string FriendlyName = "Emby";
|
||||
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
||||
public SsdpHttpClient(IHttpClient httpClient, IServerConfigurationManager config)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public async Task<XDocument> SendCommandAsync(string baseUrl,
|
||||
DeviceService service,
|
||||
string command,
|
||||
string postData,
|
||||
bool logRequest = true,
|
||||
string header = null)
|
||||
{
|
||||
var response = await PostSoapDataAsync(NormalizeServiceUrl(baseUrl, service.ControlUrl), "\"" + service.ServiceType + "#" + command + "\"", postData, header, logRequest)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
using (var stream = response.Content)
|
||||
{
|
||||
using (var reader = new StreamReader(stream, Encoding.UTF8))
|
||||
{
|
||||
return XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string NormalizeServiceUrl(string baseUrl, string serviceUrl)
|
||||
{
|
||||
// If it's already a complete url, don't stick anything onto the front of it
|
||||
if (serviceUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return serviceUrl;
|
||||
}
|
||||
|
||||
if (!serviceUrl.StartsWith("/"))
|
||||
serviceUrl = "/" + serviceUrl;
|
||||
|
||||
return baseUrl + serviceUrl;
|
||||
}
|
||||
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
|
||||
public async Task SubscribeAsync(string url,
|
||||
string ip,
|
||||
int port,
|
||||
string localIp,
|
||||
int eventport,
|
||||
int timeOut = 3600)
|
||||
{
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
UserAgent = USERAGENT,
|
||||
LogErrorResponseBody = true,
|
||||
BufferContent = false
|
||||
};
|
||||
|
||||
options.RequestHeaders["HOST"] = ip + ":" + port.ToString(_usCulture);
|
||||
options.RequestHeaders["CALLBACK"] = "<" + localIp + ":" + eventport.ToString(_usCulture) + ">";
|
||||
options.RequestHeaders["NT"] = "upnp:event";
|
||||
options.RequestHeaders["TIMEOUT"] = "Second-" + timeOut.ToString(_usCulture);
|
||||
|
||||
await _httpClient.SendAsync(options, "SUBSCRIBE").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<XDocument> GetDataAsync(string url)
|
||||
{
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
UserAgent = USERAGENT,
|
||||
LogErrorResponseBody = true,
|
||||
BufferContent = false
|
||||
};
|
||||
|
||||
options.RequestHeaders["FriendlyName.DLNA.ORG"] = FriendlyName;
|
||||
|
||||
using (var stream = await _httpClient.Get(options).ConfigureAwait(false))
|
||||
{
|
||||
using (var reader = new StreamReader(stream, Encoding.UTF8))
|
||||
{
|
||||
return XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Task<HttpResponseInfo> PostSoapDataAsync(string url,
|
||||
string soapAction,
|
||||
string postData,
|
||||
string header,
|
||||
bool logRequest)
|
||||
{
|
||||
if (!soapAction.StartsWith("\""))
|
||||
soapAction = "\"" + soapAction + "\"";
|
||||
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
UserAgent = USERAGENT,
|
||||
LogRequest = logRequest || _config.GetDlnaConfiguration().EnableDebugLog,
|
||||
LogErrorResponseBody = true,
|
||||
BufferContent = false
|
||||
};
|
||||
|
||||
options.RequestHeaders["SOAPAction"] = soapAction;
|
||||
options.RequestHeaders["Pragma"] = "no-cache";
|
||||
options.RequestHeaders["FriendlyName.DLNA.ORG"] = FriendlyName;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(header))
|
||||
{
|
||||
options.RequestHeaders["contentFeatures.dlna.org"] = header;
|
||||
}
|
||||
|
||||
options.RequestContentType = "text/xml; charset=\"utf-8\"";
|
||||
options.RequestContent = postData;
|
||||
|
||||
return _httpClient.Post(options);
|
||||
}
|
||||
}
|
||||
}
|
11
Emby.Dlna/PlayTo/TRANSPORTSTATE.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
namespace MediaBrowser.Dlna.PlayTo
|
||||
{
|
||||
public enum TRANSPORTSTATE
|
||||
{
|
||||
STOPPED,
|
||||
PLAYING,
|
||||
TRANSITIONING,
|
||||
PAUSED_PLAYBACK,
|
||||
PAUSED
|
||||
}
|
||||
}
|
185
Emby.Dlna/PlayTo/TransportCommands.cs
Normal file
|
@ -0,0 +1,185 @@
|
|||
using System;
|
||||
using MediaBrowser.Dlna.Common;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using MediaBrowser.Dlna.Ssdp;
|
||||
|
||||
namespace MediaBrowser.Dlna.PlayTo
|
||||
{
|
||||
public class TransportCommands
|
||||
{
|
||||
private List<StateVariable> _stateVariables = new List<StateVariable>();
|
||||
public List<StateVariable> StateVariables
|
||||
{
|
||||
get
|
||||
{
|
||||
return _stateVariables;
|
||||
}
|
||||
set
|
||||
{
|
||||
_stateVariables = value;
|
||||
}
|
||||
}
|
||||
|
||||
private List<ServiceAction> _serviceActions = new List<ServiceAction>();
|
||||
public List<ServiceAction> ServiceActions
|
||||
{
|
||||
get
|
||||
{
|
||||
return _serviceActions;
|
||||
}
|
||||
set
|
||||
{
|
||||
_serviceActions = value;
|
||||
}
|
||||
}
|
||||
|
||||
public static TransportCommands Create(XDocument document)
|
||||
{
|
||||
var command = new TransportCommands();
|
||||
|
||||
var actionList = document.Descendants(uPnpNamespaces.svc + "actionList");
|
||||
|
||||
foreach (var container in actionList.Descendants(uPnpNamespaces.svc + "action"))
|
||||
{
|
||||
command.ServiceActions.Add(ServiceActionFromXml(container));
|
||||
}
|
||||
|
||||
var stateValues = document.Descendants(uPnpNamespaces.ServiceStateTable).FirstOrDefault();
|
||||
|
||||
if (stateValues != null)
|
||||
{
|
||||
foreach (var container in stateValues.Elements(uPnpNamespaces.svc + "stateVariable"))
|
||||
{
|
||||
command.StateVariables.Add(FromXml(container));
|
||||
}
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static ServiceAction ServiceActionFromXml(XElement container)
|
||||
{
|
||||
var argumentList = new List<Argument>();
|
||||
|
||||
foreach (var arg in container.Descendants(uPnpNamespaces.svc + "argument"))
|
||||
{
|
||||
argumentList.Add(ArgumentFromXml(arg));
|
||||
}
|
||||
|
||||
return new ServiceAction
|
||||
{
|
||||
Name = container.GetValue(uPnpNamespaces.svc + "name"),
|
||||
|
||||
ArgumentList = argumentList
|
||||
};
|
||||
}
|
||||
|
||||
private static Argument ArgumentFromXml(XElement container)
|
||||
{
|
||||
if (container == null)
|
||||
{
|
||||
throw new ArgumentNullException("container");
|
||||
}
|
||||
|
||||
return new Argument
|
||||
{
|
||||
Name = container.GetValue(uPnpNamespaces.svc + "name"),
|
||||
Direction = container.GetValue(uPnpNamespaces.svc + "direction"),
|
||||
RelatedStateVariable = container.GetValue(uPnpNamespaces.svc + "relatedStateVariable")
|
||||
};
|
||||
}
|
||||
|
||||
public static StateVariable FromXml(XElement container)
|
||||
{
|
||||
var allowedValues = new List<string>();
|
||||
var element = container.Descendants(uPnpNamespaces.svc + "allowedValueList")
|
||||
.FirstOrDefault();
|
||||
|
||||
if (element != null)
|
||||
{
|
||||
var values = element.Descendants(uPnpNamespaces.svc + "allowedValue");
|
||||
|
||||
allowedValues.AddRange(values.Select(child => child.Value));
|
||||
}
|
||||
|
||||
return new StateVariable
|
||||
{
|
||||
Name = container.GetValue(uPnpNamespaces.svc + "name"),
|
||||
DataType = container.GetValue(uPnpNamespaces.svc + "dataType"),
|
||||
AllowedValues = allowedValues
|
||||
};
|
||||
}
|
||||
|
||||
public string BuildPost(ServiceAction action, string xmlNamespace)
|
||||
{
|
||||
var stateString = string.Empty;
|
||||
|
||||
foreach (var arg in action.ArgumentList)
|
||||
{
|
||||
if (arg.Direction == "out")
|
||||
continue;
|
||||
|
||||
if (arg.Name == "InstanceID")
|
||||
stateString += BuildArgumentXml(arg, "0");
|
||||
else
|
||||
stateString += BuildArgumentXml(arg, null);
|
||||
}
|
||||
|
||||
return string.Format(CommandBase, action.Name, xmlNamespace, stateString);
|
||||
}
|
||||
|
||||
public string BuildPost(ServiceAction action, string xmlNamesapce, object value, string commandParameter = "")
|
||||
{
|
||||
var stateString = string.Empty;
|
||||
|
||||
foreach (var arg in action.ArgumentList)
|
||||
{
|
||||
if (arg.Direction == "out")
|
||||
continue;
|
||||
if (arg.Name == "InstanceID")
|
||||
stateString += BuildArgumentXml(arg, "0");
|
||||
else
|
||||
stateString += BuildArgumentXml(arg, value.ToString(), commandParameter);
|
||||
}
|
||||
|
||||
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
|
||||
}
|
||||
|
||||
public string BuildPost(ServiceAction action, string xmlNamesapce, object value, Dictionary<string, string> dictionary)
|
||||
{
|
||||
var stateString = string.Empty;
|
||||
|
||||
foreach (var arg in action.ArgumentList)
|
||||
{
|
||||
if (arg.Name == "InstanceID")
|
||||
stateString += BuildArgumentXml(arg, "0");
|
||||
else if (dictionary.ContainsKey(arg.Name))
|
||||
stateString += BuildArgumentXml(arg, dictionary[arg.Name]);
|
||||
else
|
||||
stateString += BuildArgumentXml(arg, value.ToString());
|
||||
}
|
||||
|
||||
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
|
||||
}
|
||||
|
||||
private string BuildArgumentXml(Argument argument, string value, string commandParameter = "")
|
||||
{
|
||||
var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (state != null)
|
||||
{
|
||||
var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ??
|
||||
state.AllowedValues.FirstOrDefault() ??
|
||||
value;
|
||||
|
||||
return string.Format("<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue);
|
||||
}
|
||||
|
||||
return string.Format("<{0}>{1}</{0}>", argument.Name, value);
|
||||
}
|
||||
|
||||
private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
|
||||
}
|
||||
}
|
9
Emby.Dlna/PlayTo/TransportStateEventArgs.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
using System;
|
||||
|
||||
namespace MediaBrowser.Dlna.PlayTo
|
||||
{
|
||||
public class TransportStateEventArgs : EventArgs
|
||||
{
|
||||
public TRANSPORTSTATE State { get; set; }
|
||||
}
|
||||
}
|
26
Emby.Dlna/PlayTo/UpnpContainer.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
using System;
|
||||
using System.Xml.Linq;
|
||||
using MediaBrowser.Dlna.Ssdp;
|
||||
|
||||
namespace MediaBrowser.Dlna.PlayTo
|
||||
{
|
||||
public class UpnpContainer : uBaseObject
|
||||
{
|
||||
public static uBaseObject Create(XElement container)
|
||||
{
|
||||
if (container == null)
|
||||
{
|
||||
throw new ArgumentNullException("container");
|
||||
}
|
||||
|
||||
return new uBaseObject
|
||||
{
|
||||
Id = container.GetAttributeValue(uPnpNamespaces.Id),
|
||||
ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId),
|
||||
Title = container.GetValue(uPnpNamespaces.title),
|
||||
IconUrl = container.GetValue(uPnpNamespaces.Artwork),
|
||||
UpnpClass = container.GetValue(uPnpNamespaces.uClass)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
58
Emby.Dlna/PlayTo/uBaseObject.cs
Normal file
|
@ -0,0 +1,58 @@
|
|||
using System;
|
||||
|
||||
namespace MediaBrowser.Dlna.PlayTo
|
||||
{
|
||||
public class uBaseObject
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string ParentId { get; set; }
|
||||
|
||||
public string Title { get; set; }
|
||||
|
||||
public string SecondText { get; set; }
|
||||
|
||||
public string IconUrl { get; set; }
|
||||
|
||||
public string MetaData { get; set; }
|
||||
|
||||
public string Url { get; set; }
|
||||
|
||||
public string[] ProtocolInfo { get; set; }
|
||||
|
||||
public string UpnpClass { get; set; }
|
||||
|
||||
public bool Equals(uBaseObject obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
throw new ArgumentNullException("obj");
|
||||
}
|
||||
|
||||
return string.Equals(Id, obj.Id);
|
||||
}
|
||||
|
||||
public string MediaType
|
||||
{
|
||||
get
|
||||
{
|
||||
var classType = UpnpClass ?? string.Empty;
|
||||
|
||||
if (classType.IndexOf(Model.Entities.MediaType.Audio, StringComparison.Ordinal) != -1)
|
||||
{
|
||||
return Model.Entities.MediaType.Audio;
|
||||
}
|
||||
if (classType.IndexOf(Model.Entities.MediaType.Video, StringComparison.Ordinal) != -1)
|
||||
{
|
||||
return Model.Entities.MediaType.Video;
|
||||
}
|
||||
if (classType.IndexOf("image", StringComparison.Ordinal) != -1)
|
||||
{
|
||||
return Model.Entities.MediaType.Photo;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
47
Emby.Dlna/PlayTo/uParser.cs
Normal file
|
@ -0,0 +1,47 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace MediaBrowser.Dlna.PlayTo
|
||||
{
|
||||
public class uParser
|
||||
{
|
||||
public static IList<uBaseObject> ParseBrowseXml(XDocument doc)
|
||||
{
|
||||
if (doc == null)
|
||||
{
|
||||
throw new ArgumentException("doc");
|
||||
}
|
||||
|
||||
var list = new List<uBaseObject>();
|
||||
|
||||
var document = doc.Document;
|
||||
|
||||
if (document == null)
|
||||
return list;
|
||||
|
||||
var item = (from result in document.Descendants("Result") select result).FirstOrDefault();
|
||||
|
||||
if (item == null)
|
||||
return list;
|
||||
|
||||
var uPnpResponse = XElement.Parse((String)item);
|
||||
|
||||
var uObjects = from container in uPnpResponse.Elements(uPnpNamespaces.containers)
|
||||
select new uParserObject { Element = container };
|
||||
|
||||
var uObjects2 = from container in uPnpResponse.Elements(uPnpNamespaces.items)
|
||||
select new uParserObject { Element = container };
|
||||
|
||||
list.AddRange(uObjects.Concat(uObjects2).Select(CreateObjectFromXML).Where(uObject => uObject != null));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public static uBaseObject CreateObjectFromXML(uParserObject uItem)
|
||||
{
|
||||
return UpnpContainer.Create(uItem.Element);
|
||||
}
|
||||
}
|
||||
}
|
9
Emby.Dlna/PlayTo/uParserObject.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
using System.Xml.Linq;
|
||||
|
||||
namespace MediaBrowser.Dlna.PlayTo
|
||||
{
|
||||
public class uParserObject
|
||||
{
|
||||
public XElement Element { get; set; }
|
||||
}
|
||||
}
|
39
Emby.Dlna/PlayTo/uPnpNamespaces.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using System.Xml.Linq;
|
||||
|
||||
namespace MediaBrowser.Dlna.PlayTo
|
||||
{
|
||||
public class uPnpNamespaces
|
||||
{
|
||||
public static XNamespace dc = "http://purl.org/dc/elements/1.1/";
|
||||
public static XNamespace ns = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
|
||||
public static XNamespace svc = "urn:schemas-upnp-org:service-1-0";
|
||||
public static XNamespace ud = "urn:schemas-upnp-org:device-1-0";
|
||||
public static XNamespace upnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
|
||||
public static XNamespace RenderingControl = "urn:schemas-upnp-org:service:RenderingControl:1";
|
||||
public static XNamespace AvTransport = "urn:schemas-upnp-org:service:AVTransport:1";
|
||||
public static XNamespace ContentDirectory = "urn:schemas-upnp-org:service:ContentDirectory:1";
|
||||
|
||||
public static XName containers = ns + "container";
|
||||
public static XName items = ns + "item";
|
||||
public static XName title = dc + "title";
|
||||
public static XName creator = dc + "creator";
|
||||
public static XName artist = upnp + "artist";
|
||||
public static XName Id = "id";
|
||||
public static XName ParentId = "parentID";
|
||||
public static XName uClass = upnp + "class";
|
||||
public static XName Artwork = upnp + "albumArtURI";
|
||||
public static XName Description = dc + "description";
|
||||
public static XName LongDescription = upnp + "longDescription";
|
||||
public static XName Album = upnp + "album";
|
||||
public static XName Author = upnp + "author";
|
||||
public static XName Director = upnp + "director";
|
||||
public static XName PlayCount = upnp + "playbackCount";
|
||||
public static XName Tracknumber = upnp + "originalTrackNumber";
|
||||
public static XName Res = ns + "res";
|
||||
public static XName Duration = "duration";
|
||||
public static XName ProtocolInfo = "protocolInfo";
|
||||
|
||||
public static XName ServiceStateTable = svc + "serviceStateTable";
|
||||
public static XName StateVariable = svc + "stateVariable";
|
||||
}
|
||||
}
|
68
Emby.Dlna/ProfileSerialization/CodecProfile.cs
Normal file
|
@ -0,0 +1,68 @@
|
|||
using MediaBrowser.Model.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Serialization;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace MediaBrowser.Dlna.ProfileSerialization
|
||||
{
|
||||
public class CodecProfile
|
||||
{
|
||||
[XmlAttribute("type")]
|
||||
public CodecType Type { get; set; }
|
||||
|
||||
public ProfileCondition[] Conditions { get; set; }
|
||||
|
||||
public ProfileCondition[] ApplyConditions { get; set; }
|
||||
|
||||
[XmlAttribute("codec")]
|
||||
public string Codec { get; set; }
|
||||
|
||||
[XmlAttribute("container")]
|
||||
public string Container { get; set; }
|
||||
|
||||
public CodecProfile()
|
||||
{
|
||||
Conditions = new ProfileCondition[] {};
|
||||
ApplyConditions = new ProfileCondition[] { };
|
||||
}
|
||||
|
||||
public List<string> GetCodecs()
|
||||
{
|
||||
List<string> list = new List<string>();
|
||||
foreach (string i in (Codec ?? string.Empty).Split(','))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(i)) list.Add(i);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<string> GetContainers()
|
||||
{
|
||||
List<string> list = new List<string>();
|
||||
foreach (string i in (Container ?? string.Empty).Split(','))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(i)) list.Add(i);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private bool ContainsContainer(string container)
|
||||
{
|
||||
List<string> containers = GetContainers();
|
||||
|
||||
return containers.Count == 0 || ListHelper.ContainsIgnoreCase(containers, container ?? string.Empty);
|
||||
}
|
||||
|
||||
public bool ContainsCodec(string codec, string container)
|
||||
{
|
||||
if (!ContainsContainer(container))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
List<string> codecs = GetCodecs();
|
||||
|
||||
return codecs.Count == 0 || ListHelper.ContainsIgnoreCase(codecs, codec);
|
||||
}
|
||||
}
|
||||
}
|
31
Emby.Dlna/ProfileSerialization/ContainerProfile.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Xml.Serialization;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace MediaBrowser.Dlna.ProfileSerialization
|
||||
{
|
||||
public class ContainerProfile
|
||||
{
|
||||
[XmlAttribute("type")]
|
||||
public DlnaProfileType Type { get; set; }
|
||||
public ProfileCondition[] Conditions { get; set; }
|
||||
|
||||
[XmlAttribute("container")]
|
||||
public string Container { get; set; }
|
||||
|
||||
public ContainerProfile()
|
||||
{
|
||||
Conditions = new ProfileCondition[] { };
|
||||
}
|
||||
|
||||
public List<string> GetContainers()
|
||||
{
|
||||
List<string> list = new List<string>();
|
||||
foreach (string i in (Container ?? string.Empty).Split(','))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(i)) list.Add(i);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
351
Emby.Dlna/ProfileSerialization/DeviceProfile.cs
Normal file
|
@ -0,0 +1,351 @@
|
|||
using MediaBrowser.Model.Extensions;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace MediaBrowser.Dlna.ProfileSerialization
|
||||
{
|
||||
[XmlRoot("Profile")]
|
||||
public class DeviceProfile
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name { get; set; }
|
||||
|
||||
[XmlIgnore]
|
||||
public string Id { get; set; }
|
||||
|
||||
[XmlIgnore]
|
||||
public MediaBrowser.Model.Dlna.DeviceProfileType ProfileType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the identification.
|
||||
/// </summary>
|
||||
/// <value>The identification.</value>
|
||||
public MediaBrowser.Model.Dlna.DeviceIdentification Identification { get; set; }
|
||||
|
||||
public string FriendlyName { get; set; }
|
||||
public string Manufacturer { get; set; }
|
||||
public string ManufacturerUrl { get; set; }
|
||||
public string ModelName { get; set; }
|
||||
public string ModelDescription { get; set; }
|
||||
public string ModelNumber { get; set; }
|
||||
public string ModelUrl { get; set; }
|
||||
public string SerialNumber { get; set; }
|
||||
|
||||
public bool EnableAlbumArtInDidl { get; set; }
|
||||
public bool EnableSingleAlbumArtLimit { get; set; }
|
||||
public bool EnableSingleSubtitleLimit { get; set; }
|
||||
|
||||
public string SupportedMediaTypes { get; set; }
|
||||
|
||||
public string UserId { get; set; }
|
||||
|
||||
public string AlbumArtPn { get; set; }
|
||||
|
||||
public int MaxAlbumArtWidth { get; set; }
|
||||
public int MaxAlbumArtHeight { get; set; }
|
||||
|
||||
public int? MaxIconWidth { get; set; }
|
||||
public int? MaxIconHeight { get; set; }
|
||||
|
||||
public int? MaxStreamingBitrate { get; set; }
|
||||
public int? MaxStaticBitrate { get; set; }
|
||||
|
||||
public int? MusicStreamingTranscodingBitrate { get; set; }
|
||||
public int? MaxStaticMusicBitrate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Controls the content of the X_DLNADOC element in the urn:schemas-dlna-org:device-1-0 namespace.
|
||||
/// </summary>
|
||||
public string XDlnaDoc { get; set; }
|
||||
/// <summary>
|
||||
/// Controls the content of the X_DLNACAP element in the urn:schemas-dlna-org:device-1-0 namespace.
|
||||
/// </summary>
|
||||
public string XDlnaCap { get; set; }
|
||||
/// <summary>
|
||||
/// Controls the content of the aggregationFlags element in the urn:schemas-sonycom:av namespace.
|
||||
/// </summary>
|
||||
public string SonyAggregationFlags { get; set; }
|
||||
|
||||
public string ProtocolInfo { get; set; }
|
||||
|
||||
public int TimelineOffsetSeconds { get; set; }
|
||||
public bool RequiresPlainVideoItems { get; set; }
|
||||
public bool RequiresPlainFolders { get; set; }
|
||||
|
||||
public bool EnableMSMediaReceiverRegistrar { get; set; }
|
||||
public bool IgnoreTranscodeByteRangeRequests { get; set; }
|
||||
|
||||
public XmlAttribute[] XmlRootAttributes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the direct play profiles.
|
||||
/// </summary>
|
||||
/// <value>The direct play profiles.</value>
|
||||
public DirectPlayProfile[] DirectPlayProfiles { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transcoding profiles.
|
||||
/// </summary>
|
||||
/// <value>The transcoding profiles.</value>
|
||||
public TranscodingProfile[] TranscodingProfiles { get; set; }
|
||||
|
||||
public ContainerProfile[] ContainerProfiles { get; set; }
|
||||
|
||||
public CodecProfile[] CodecProfiles { get; set; }
|
||||
public ResponseProfile[] ResponseProfiles { get; set; }
|
||||
|
||||
public SubtitleProfile[] SubtitleProfiles { get; set; }
|
||||
|
||||
public DeviceProfile()
|
||||
{
|
||||
DirectPlayProfiles = new DirectPlayProfile[] { };
|
||||
TranscodingProfiles = new TranscodingProfile[] { };
|
||||
ResponseProfiles = new ResponseProfile[] { };
|
||||
CodecProfiles = new CodecProfile[] { };
|
||||
ContainerProfiles = new ContainerProfile[] { };
|
||||
SubtitleProfiles = new SubtitleProfile[] { };
|
||||
|
||||
XmlRootAttributes = new XmlAttribute[] { };
|
||||
|
||||
SupportedMediaTypes = "Audio,Photo,Video";
|
||||
MaxStreamingBitrate = 8000000;
|
||||
MaxStaticBitrate = 8000000;
|
||||
MusicStreamingTranscodingBitrate = 128000;
|
||||
}
|
||||
|
||||
public List<string> GetSupportedMediaTypes()
|
||||
{
|
||||
List<string> list = new List<string>();
|
||||
foreach (string i in (SupportedMediaTypes ?? string.Empty).Split(','))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(i))
|
||||
list.Add(i);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public TranscodingProfile GetAudioTranscodingProfile(string container, string audioCodec)
|
||||
{
|
||||
container = StringHelper.TrimStart(container ?? string.Empty, '.');
|
||||
|
||||
foreach (var i in TranscodingProfiles)
|
||||
{
|
||||
if (i.Type != MediaBrowser.Model.Dlna.DlnaProfileType.Audio)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!StringHelper.EqualsIgnoreCase(container, i.Container))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ListHelper.ContainsIgnoreCase(i.GetAudioCodecs(), audioCodec ?? string.Empty))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public TranscodingProfile GetVideoTranscodingProfile(string container, string audioCodec, string videoCodec)
|
||||
{
|
||||
container = StringHelper.TrimStart(container ?? string.Empty, '.');
|
||||
|
||||
foreach (var i in TranscodingProfiles)
|
||||
{
|
||||
if (i.Type != MediaBrowser.Model.Dlna.DlnaProfileType.Video)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!StringHelper.EqualsIgnoreCase(container, i.Container))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ListHelper.ContainsIgnoreCase(i.GetAudioCodecs(), audioCodec ?? string.Empty))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!StringHelper.EqualsIgnoreCase(videoCodec, i.VideoCodec ?? string.Empty))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ResponseProfile GetAudioMediaProfile(string container, string audioCodec, int? audioChannels, int? audioBitrate)
|
||||
{
|
||||
container = StringHelper.TrimStart(container ?? string.Empty, '.');
|
||||
|
||||
foreach (var i in ResponseProfiles)
|
||||
{
|
||||
if (i.Type != MediaBrowser.Model.Dlna.DlnaProfileType.Audio)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
List<string> containers = i.GetContainers();
|
||||
if (containers.Count > 0 && !ListHelper.ContainsIgnoreCase(containers, container))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
List<string> audioCodecs = i.GetAudioCodecs();
|
||||
if (audioCodecs.Count > 0 && !ListHelper.ContainsIgnoreCase(audioCodecs, audioCodec ?? string.Empty))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var conditionProcessor = new MediaBrowser.Model.Dlna.ConditionProcessor();
|
||||
|
||||
var anyOff = false;
|
||||
foreach (ProfileCondition c in i.Conditions)
|
||||
{
|
||||
if (!conditionProcessor.IsAudioConditionSatisfied(GetModelProfileCondition(c), audioChannels, audioBitrate))
|
||||
{
|
||||
anyOff = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (anyOff)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private MediaBrowser.Model.Dlna.ProfileCondition GetModelProfileCondition(ProfileCondition c)
|
||||
{
|
||||
return new Model.Dlna.ProfileCondition
|
||||
{
|
||||
Condition = c.Condition,
|
||||
IsRequired = c.IsRequired,
|
||||
Property = c.Property,
|
||||
Value = c.Value
|
||||
};
|
||||
}
|
||||
|
||||
public ResponseProfile GetImageMediaProfile(string container, int? width, int? height)
|
||||
{
|
||||
container = StringHelper.TrimStart(container ?? string.Empty, '.');
|
||||
|
||||
foreach (var i in ResponseProfiles)
|
||||
{
|
||||
if (i.Type != MediaBrowser.Model.Dlna.DlnaProfileType.Photo)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
List<string> containers = i.GetContainers();
|
||||
if (containers.Count > 0 && !ListHelper.ContainsIgnoreCase(containers, container))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var conditionProcessor = new MediaBrowser.Model.Dlna.ConditionProcessor();
|
||||
|
||||
var anyOff = false;
|
||||
foreach (ProfileCondition c in i.Conditions)
|
||||
{
|
||||
if (!conditionProcessor.IsImageConditionSatisfied(GetModelProfileCondition(c), width, height))
|
||||
{
|
||||
anyOff = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (anyOff)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ResponseProfile GetVideoMediaProfile(string container,
|
||||
string audioCodec,
|
||||
string videoCodec,
|
||||
int? width,
|
||||
int? height,
|
||||
int? bitDepth,
|
||||
int? videoBitrate,
|
||||
string videoProfile,
|
||||
double? videoLevel,
|
||||
float? videoFramerate,
|
||||
int? packetLength,
|
||||
TransportStreamTimestamp timestamp,
|
||||
bool? isAnamorphic,
|
||||
int? refFrames,
|
||||
int? numVideoStreams,
|
||||
int? numAudioStreams,
|
||||
string videoCodecTag,
|
||||
bool? isAvc)
|
||||
{
|
||||
container = StringHelper.TrimStart(container ?? string.Empty, '.');
|
||||
|
||||
foreach (var i in ResponseProfiles)
|
||||
{
|
||||
if (i.Type != MediaBrowser.Model.Dlna.DlnaProfileType.Video)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
List<string> containers = i.GetContainers();
|
||||
if (containers.Count > 0 && !ListHelper.ContainsIgnoreCase(containers, container ?? string.Empty))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
List<string> audioCodecs = i.GetAudioCodecs();
|
||||
if (audioCodecs.Count > 0 && !ListHelper.ContainsIgnoreCase(audioCodecs, audioCodec ?? string.Empty))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
List<string> videoCodecs = i.GetVideoCodecs();
|
||||
if (videoCodecs.Count > 0 && !ListHelper.ContainsIgnoreCase(videoCodecs, videoCodec ?? string.Empty))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var conditionProcessor = new MediaBrowser.Model.Dlna.ConditionProcessor();
|
||||
|
||||
var anyOff = false;
|
||||
foreach (ProfileCondition c in i.Conditions)
|
||||
{
|
||||
if (!conditionProcessor.IsVideoConditionSatisfied(GetModelProfileCondition(c), width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))
|
||||
{
|
||||
anyOff = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (anyOff)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
51
Emby.Dlna/ProfileSerialization/DirectPlayProfile.cs
Normal file
|
@ -0,0 +1,51 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Xml.Serialization;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace MediaBrowser.Dlna.ProfileSerialization
|
||||
{
|
||||
public class DirectPlayProfile
|
||||
{
|
||||
[XmlAttribute("container")]
|
||||
public string Container { get; set; }
|
||||
|
||||
[XmlAttribute("audioCodec")]
|
||||
public string AudioCodec { get; set; }
|
||||
|
||||
[XmlAttribute("videoCodec")]
|
||||
public string VideoCodec { get; set; }
|
||||
|
||||
[XmlAttribute("type")]
|
||||
public DlnaProfileType Type { get; set; }
|
||||
|
||||
public List<string> GetContainers()
|
||||
{
|
||||
List<string> list = new List<string>();
|
||||
foreach (string i in (Container ?? string.Empty).Split(','))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(i)) list.Add(i);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<string> GetAudioCodecs()
|
||||
{
|
||||
List<string> list = new List<string>();
|
||||
foreach (string i in (AudioCodec ?? string.Empty).Split(','))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(i)) list.Add(i);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<string> GetVideoCodecs()
|
||||
{
|
||||
List<string> list = new List<string>();
|
||||
foreach (string i in (VideoCodec ?? string.Empty).Split(','))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(i)) list.Add(i);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
17
Emby.Dlna/ProfileSerialization/HttpHeaderInfo.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
using System.Xml.Serialization;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace MediaBrowser.Dlna.ProfileSerialization
|
||||
{
|
||||
public class HttpHeaderInfo
|
||||
{
|
||||
[XmlAttribute("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[XmlAttribute("value")]
|
||||
public string Value { get; set; }
|
||||
|
||||
[XmlAttribute("match")]
|
||||
public HeaderMatchType Match { get; set; }
|
||||
}
|
||||
}
|
39
Emby.Dlna/ProfileSerialization/ProfileCondition.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using System.Xml.Serialization;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace MediaBrowser.Dlna.ProfileSerialization
|
||||
{
|
||||
public class ProfileCondition
|
||||
{
|
||||
[XmlAttribute("condition")]
|
||||
public ProfileConditionType Condition { get; set; }
|
||||
|
||||
[XmlAttribute("property")]
|
||||
public ProfileConditionValue Property { get; set; }
|
||||
|
||||
[XmlAttribute("value")]
|
||||
public string Value { get; set; }
|
||||
|
||||
[XmlAttribute("isRequired")]
|
||||
public bool IsRequired { get; set; }
|
||||
|
||||
public ProfileCondition()
|
||||
{
|
||||
IsRequired = true;
|
||||
}
|
||||
|
||||
public ProfileCondition(ProfileConditionType condition, ProfileConditionValue property, string value)
|
||||
: this(condition, property, value, false)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public ProfileCondition(ProfileConditionType condition, ProfileConditionValue property, string value, bool isRequired)
|
||||
{
|
||||
Condition = condition;
|
||||
Property = property;
|
||||
Value = value;
|
||||
IsRequired = isRequired;
|
||||
}
|
||||
}
|
||||
}
|
64
Emby.Dlna/ProfileSerialization/ResponseProfile.cs
Normal file
|
@ -0,0 +1,64 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Xml.Serialization;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace MediaBrowser.Dlna.ProfileSerialization
|
||||
{
|
||||
public class ResponseProfile
|
||||
{
|
||||
[XmlAttribute("container")]
|
||||
public string Container { get; set; }
|
||||
|
||||
[XmlAttribute("audioCodec")]
|
||||
public string AudioCodec { get; set; }
|
||||
|
||||
[XmlAttribute("videoCodec")]
|
||||
public string VideoCodec { get; set; }
|
||||
|
||||
[XmlAttribute("type")]
|
||||
public DlnaProfileType Type { get; set; }
|
||||
|
||||
[XmlAttribute("orgPn")]
|
||||
public string OrgPn { get; set; }
|
||||
|
||||
[XmlAttribute("mimeType")]
|
||||
public string MimeType { get; set; }
|
||||
|
||||
public ProfileCondition[] Conditions { get; set; }
|
||||
|
||||
public ResponseProfile()
|
||||
{
|
||||
Conditions = new ProfileCondition[] {};
|
||||
}
|
||||
|
||||
public List<string> GetContainers()
|
||||
{
|
||||
List<string> list = new List<string>();
|
||||
foreach (string i in (Container ?? string.Empty).Split(','))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(i)) list.Add(i);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<string> GetAudioCodecs()
|
||||
{
|
||||
List<string> list = new List<string>();
|
||||
foreach (string i in (AudioCodec ?? string.Empty).Split(','))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(i)) list.Add(i);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<string> GetVideoCodecs()
|
||||
{
|
||||
List<string> list = new List<string>();
|
||||
foreach (string i in (VideoCodec ?? string.Empty).Split(','))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(i)) list.Add(i);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
48
Emby.Dlna/ProfileSerialization/SubtitleProfile.cs
Normal file
|
@ -0,0 +1,48 @@
|
|||
using MediaBrowser.Model.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Serialization;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace MediaBrowser.Dlna.ProfileSerialization
|
||||
{
|
||||
public class SubtitleProfile
|
||||
{
|
||||
[XmlAttribute("format")]
|
||||
public string Format { get; set; }
|
||||
|
||||
[XmlAttribute("method")]
|
||||
public SubtitleDeliveryMethod Method { get; set; }
|
||||
|
||||
[XmlAttribute("didlMode")]
|
||||
public string DidlMode { get; set; }
|
||||
|
||||
[XmlAttribute("language")]
|
||||
public string Language { get; set; }
|
||||
|
||||
public List<string> GetLanguages()
|
||||
{
|
||||
List<string> list = new List<string>();
|
||||
foreach (string i in (Language ?? string.Empty).Split(','))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(i)) list.Add(i);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public bool SupportsLanguage(string subLanguage)
|
||||
{
|
||||
if (string.IsNullOrEmpty(Language))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(subLanguage))
|
||||
{
|
||||
subLanguage = "und";
|
||||
}
|
||||
|
||||
List<string> languages = GetLanguages();
|
||||
return languages.Count == 0 || ListHelper.ContainsIgnoreCase(languages, subLanguage);
|
||||
}
|
||||
}
|
||||
}
|
58
Emby.Dlna/ProfileSerialization/TranscodingProfile.cs
Normal file
|
@ -0,0 +1,58 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Xml.Serialization;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace MediaBrowser.Dlna.ProfileSerialization
|
||||
{
|
||||
public class TranscodingProfile
|
||||
{
|
||||
[XmlAttribute("container")]
|
||||
public string Container { get; set; }
|
||||
|
||||
[XmlAttribute("type")]
|
||||
public DlnaProfileType Type { get; set; }
|
||||
|
||||
[XmlAttribute("videoCodec")]
|
||||
public string VideoCodec { get; set; }
|
||||
|
||||
[XmlAttribute("audioCodec")]
|
||||
public string AudioCodec { get; set; }
|
||||
|
||||
[XmlAttribute("protocol")]
|
||||
public string Protocol { get; set; }
|
||||
|
||||
[XmlAttribute("estimateContentLength")]
|
||||
public bool EstimateContentLength { get; set; }
|
||||
|
||||
[XmlAttribute("enableMpegtsM2TsMode")]
|
||||
public bool EnableMpegtsM2TsMode { get; set; }
|
||||
|
||||
[XmlAttribute("transcodeSeekInfo")]
|
||||
public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
|
||||
|
||||
[XmlAttribute("copyTimestamps")]
|
||||
public bool CopyTimestamps { get; set; }
|
||||
|
||||
[XmlAttribute("context")]
|
||||
public EncodingContext Context { get; set; }
|
||||
|
||||
[XmlAttribute("enableSubtitlesInManifest")]
|
||||
public bool EnableSubtitlesInManifest { get; set; }
|
||||
|
||||
[XmlAttribute("enableSplittingOnNonKeyFrames")]
|
||||
public bool EnableSplittingOnNonKeyFrames { get; set; }
|
||||
|
||||
[XmlAttribute("maxAudioChannels")]
|
||||
public string MaxAudioChannels { get; set; }
|
||||
|
||||
public List<string> GetAudioCodecs()
|
||||
{
|
||||
List<string> list = new List<string>();
|
||||
foreach (string i in (AudioCodec ?? string.Empty).Split(','))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(i)) list.Add(i);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
13
Emby.Dlna/ProfileSerialization/XmlAttribute.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
using System.Xml.Serialization;
|
||||
|
||||
namespace MediaBrowser.Dlna.ProfileSerialization
|
||||
{
|
||||
public class XmlAttribute
|
||||
{
|
||||
[XmlAttribute("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[XmlAttribute("value")]
|
||||
public string Value { get; set; }
|
||||
}
|
||||
}
|
146
Emby.Dlna/Profiles/BubbleUpnpProfile.cs
Normal file
|
@ -0,0 +1,146 @@
|
|||
using MediaBrowser.Model.Dlna;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace MediaBrowser.Dlna.Profiles
|
||||
{
|
||||
[XmlRoot("Profile")]
|
||||
public class BubbleUpnpProfile : DefaultProfile
|
||||
{
|
||||
public BubbleUpnpProfile()
|
||||
{
|
||||
Name = "BubbleUPnp";
|
||||
|
||||
Identification = new DeviceIdentification
|
||||
{
|
||||
ModelName = "BubbleUPnp",
|
||||
|
||||
Headers = new[]
|
||||
{
|
||||
new HttpHeaderInfo {Name = "User-Agent", Value = "BubbleUPnp", Match = HeaderMatchType.Substring}
|
||||
}
|
||||
};
|
||||
|
||||
TranscodingProfiles = new[]
|
||||
{
|
||||
new TranscodingProfile
|
||||
{
|
||||
Container = "mp3",
|
||||
AudioCodec = "mp3",
|
||||
Type = DlnaProfileType.Audio
|
||||
},
|
||||
|
||||
new TranscodingProfile
|
||||
{
|
||||
Container = "ts",
|
||||
Type = DlnaProfileType.Video,
|
||||
AudioCodec = "aac",
|
||||
VideoCodec = "h264"
|
||||
},
|
||||
|
||||
new TranscodingProfile
|
||||
{
|
||||
Container = "jpeg",
|
||||
Type = DlnaProfileType.Photo
|
||||
}
|
||||
};
|
||||
|
||||
DirectPlayProfiles = new[]
|
||||
{
|
||||
new DirectPlayProfile
|
||||
{
|
||||
Container = "",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
|
||||
new DirectPlayProfile
|
||||
{
|
||||
Container = "",
|
||||
Type = DlnaProfileType.Audio
|
||||
},
|
||||
|
||||
new DirectPlayProfile
|
||||
{
|
||||
Container = "",
|
||||
Type = DlnaProfileType.Photo,
|
||||
}
|
||||
};
|
||||
|
||||
ResponseProfiles = new ResponseProfile[] { };
|
||||
|
||||
ContainerProfiles = new ContainerProfile[] { };
|
||||
|
||||
CodecProfiles = new CodecProfile[] { };
|
||||
|
||||
SubtitleProfiles = new[]
|
||||
{
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "srt",
|
||||
Method = SubtitleDeliveryMethod.External,
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "sub",
|
||||
Method = SubtitleDeliveryMethod.External,
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "srt",
|
||||
Method = SubtitleDeliveryMethod.Embed,
|
||||
DidlMode = "",
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "ass",
|
||||
Method = SubtitleDeliveryMethod.Embed,
|
||||
DidlMode = "",
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "ssa",
|
||||
Method = SubtitleDeliveryMethod.Embed,
|
||||
DidlMode = "",
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "smi",
|
||||
Method = SubtitleDeliveryMethod.Embed,
|
||||
DidlMode = "",
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "dvdsub",
|
||||
Method = SubtitleDeliveryMethod.Embed,
|
||||
DidlMode = "",
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "pgs",
|
||||
Method = SubtitleDeliveryMethod.Embed,
|
||||
DidlMode = "",
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "pgssub",
|
||||
Method = SubtitleDeliveryMethod.Embed,
|
||||
DidlMode = "",
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "sub",
|
||||
Method = SubtitleDeliveryMethod.Embed,
|
||||
DidlMode = "",
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
114
Emby.Dlna/Profiles/DefaultProfile.cs
Normal file
31
Emby.Dlna/Profiles/DenonAvrProfile.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
using MediaBrowser.Model.Dlna;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace MediaBrowser.Dlna.Profiles
|
||||
{
|
||||
[XmlRoot("Profile")]
|
||||
public class DenonAvrProfile : DefaultProfile
|
||||
{
|
||||
public DenonAvrProfile()
|
||||
{
|
||||
Name = "Denon AVR";
|
||||
|
||||
Identification = new DeviceIdentification
|
||||
{
|
||||
FriendlyName = @"Denon:\[AVR:.*",
|
||||
Manufacturer = "Denon"
|
||||
};
|
||||
|
||||
DirectPlayProfiles = new[]
|
||||
{
|
||||
new DirectPlayProfile
|
||||
{
|
||||
Container = "mp3,flac,m4a,wma",
|
||||
Type = DlnaProfileType.Audio
|
||||
},
|
||||
};
|
||||
|
||||
ResponseProfiles = new ResponseProfile[] { };
|
||||
}
|
||||
}
|
||||
}
|
119
Emby.Dlna/Profiles/DirectTvProfile.cs
Normal file
|
@ -0,0 +1,119 @@
|
|||
using MediaBrowser.Model.Dlna;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace MediaBrowser.Dlna.Profiles
|
||||
{
|
||||
[XmlRoot("Profile")]
|
||||
public class DirectTvProfile : DefaultProfile
|
||||
{
|
||||
public DirectTvProfile()
|
||||
{
|
||||
Name = "DirecTV HD-DVR";
|
||||
|
||||
TimelineOffsetSeconds = 10;
|
||||
RequiresPlainFolders = true;
|
||||
RequiresPlainVideoItems = true;
|
||||
|
||||
Identification = new DeviceIdentification
|
||||
{
|
||||
Headers = new[]
|
||||
{
|
||||
new HttpHeaderInfo
|
||||
{
|
||||
Match = HeaderMatchType.Substring,
|
||||
Name = "User-Agent",
|
||||
Value = "DIRECTV"
|
||||
}
|
||||
},
|
||||
|
||||
FriendlyName = "^DIRECTV.*$"
|
||||
};
|
||||
|
||||
DirectPlayProfiles = new[]
|
||||
{
|
||||
new DirectPlayProfile
|
||||
{
|
||||
Container = "mpeg",
|
||||
VideoCodec = "mpeg2video",
|
||||
AudioCodec = "mp2",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
new DirectPlayProfile
|
||||
{
|
||||
Container = "jpeg,jpg",
|
||||
Type = DlnaProfileType.Photo
|
||||
}
|
||||
};
|
||||
|
||||
TranscodingProfiles = new[]
|
||||
{
|
||||
new TranscodingProfile
|
||||
{
|
||||
Container = "mpeg",
|
||||
VideoCodec = "mpeg2video",
|
||||
AudioCodec = "mp2",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
new TranscodingProfile
|
||||
{
|
||||
Container = "jpeg",
|
||||
Type = DlnaProfileType.Photo
|
||||
}
|
||||
};
|
||||
|
||||
CodecProfiles = new[]
|
||||
{
|
||||
new CodecProfile
|
||||
{
|
||||
Codec = "mpeg2video",
|
||||
Type = CodecType.Video,
|
||||
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
Condition = ProfileConditionType.LessThanEqual,
|
||||
Property = ProfileConditionValue.Width,
|
||||
Value = "1920"
|
||||
},
|
||||
new ProfileCondition
|
||||
{
|
||||
Condition = ProfileConditionType.LessThanEqual,
|
||||
Property = ProfileConditionValue.Height,
|
||||
Value = "1080"
|
||||
},
|
||||
new ProfileCondition
|
||||
{
|
||||
Condition = ProfileConditionType.LessThanEqual,
|
||||
Property = ProfileConditionValue.VideoFramerate,
|
||||
Value = "30"
|
||||
},
|
||||
new ProfileCondition
|
||||
{
|
||||
Condition = ProfileConditionType.LessThanEqual,
|
||||
Property = ProfileConditionValue.VideoBitrate,
|
||||
Value = "8192000"
|
||||
}
|
||||
}
|
||||
},
|
||||
new CodecProfile
|
||||
{
|
||||
Codec = "mp2",
|
||||
Type = CodecType.Audio,
|
||||
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
Condition = ProfileConditionType.LessThanEqual,
|
||||
Property = ProfileConditionValue.AudioChannels,
|
||||
Value = "2"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ResponseProfiles = new ResponseProfile[] { };
|
||||
}
|
||||
}
|
||||
}
|
219
Emby.Dlna/Profiles/DishHopperJoeyProfile.cs
Normal file
|
@ -0,0 +1,219 @@
|
|||
using MediaBrowser.Model.Dlna;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace MediaBrowser.Dlna.Profiles
|
||||
{
|
||||
[XmlRoot("Profile")]
|
||||
public class DishHopperJoeyProfile : DefaultProfile
|
||||
{
|
||||
public DishHopperJoeyProfile()
|
||||
{
|
||||
Name = "Dish Hopper-Joey";
|
||||
|
||||
ProtocolInfo = "http-get:*:video/mp2t:*,http-get:*:video/MP1S:*,http-get:*:video/mpeg2:*,http-get:*:video/mp4:*,http-get:*:video/x-matroska:*,http-get:*:audio/mpeg:*,http-get:*:audio/mpeg3:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/mp4a-latm:*,http-get:*:image/jpeg:*";
|
||||
|
||||
Identification = new DeviceIdentification
|
||||
{
|
||||
Manufacturer = "Echostar Technologies LLC",
|
||||
ManufacturerUrl = "http://www.echostar.com",
|
||||
|
||||
Headers = new[]
|
||||
{
|
||||
new HttpHeaderInfo
|
||||
{
|
||||
Match = HeaderMatchType.Substring,
|
||||
Name = "User-Agent",
|
||||
Value ="XiP"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TranscodingProfiles = new[]
|
||||
{
|
||||
new TranscodingProfile
|
||||
{
|
||||
Container = "mp3",
|
||||
AudioCodec = "mp3",
|
||||
Type = DlnaProfileType.Audio
|
||||
},
|
||||
new TranscodingProfile
|
||||
{
|
||||
Container = "mp4",
|
||||
Type = DlnaProfileType.Video,
|
||||
AudioCodec = "aac",
|
||||
VideoCodec = "h264"
|
||||
},
|
||||
|
||||
new TranscodingProfile
|
||||
{
|
||||
Container = "jpeg",
|
||||
Type = DlnaProfileType.Photo
|
||||
}
|
||||
};
|
||||
|
||||
DirectPlayProfiles = new[]
|
||||
{
|
||||
new DirectPlayProfile
|
||||
{
|
||||
Container = "mp4,mkv,mpeg,ts",
|
||||
VideoCodec = "h264,mpeg2video",
|
||||
AudioCodec = "mp3,ac3,aac,he-aac,pcm",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
|
||||
new DirectPlayProfile
|
||||
{
|
||||
Container = "mp3",
|
||||
AudioCodec = "mp3",
|
||||
Type = DlnaProfileType.Audio
|
||||
},
|
||||
|
||||
new DirectPlayProfile
|
||||
{
|
||||
Container = "alac",
|
||||
AudioCodec = "alac",
|
||||
Type = DlnaProfileType.Audio
|
||||
},
|
||||
|
||||
new DirectPlayProfile
|
||||
{
|
||||
Container = "flac",
|
||||
AudioCodec = "flac",
|
||||
Type = DlnaProfileType.Audio
|
||||
},
|
||||
|
||||
new DirectPlayProfile
|
||||
{
|
||||
Container = "jpeg",
|
||||
Type = DlnaProfileType.Photo
|
||||
}
|
||||
};
|
||||
|
||||
CodecProfiles = new[]
|
||||
{
|
||||
new CodecProfile
|
||||
{
|
||||
Type = CodecType.Video,
|
||||
Codec = "h264",
|
||||
Conditions = new []
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
Condition = ProfileConditionType.LessThanEqual,
|
||||
Property = ProfileConditionValue.Width,
|
||||
Value = "1920",
|
||||
IsRequired = true
|
||||
},
|
||||
new ProfileCondition
|
||||
{
|
||||
Condition = ProfileConditionType.LessThanEqual,
|
||||
Property = ProfileConditionValue.Height,
|
||||
Value = "1080",
|
||||
IsRequired = true
|
||||
},
|
||||
new ProfileCondition
|
||||
{
|
||||
Condition = ProfileConditionType.LessThanEqual,
|
||||
Property = ProfileConditionValue.VideoFramerate,
|
||||
Value = "30",
|
||||
IsRequired = true
|
||||
},
|
||||
new ProfileCondition
|
||||
{
|
||||
Condition = ProfileConditionType.LessThanEqual,
|
||||
Property = ProfileConditionValue.VideoBitrate,
|
||||
Value = "20000000",
|
||||
IsRequired = true
|
||||
},
|
||||
new ProfileCondition
|
||||
{
|
||||
Condition = ProfileConditionType.LessThanEqual,
|
||||
Property = ProfileConditionValue.VideoLevel,
|
||||
Value = "41",
|
||||
IsRequired = true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
new CodecProfile
|
||||
{
|
||||
Type = CodecType.Video,
|
||||
Conditions = new []
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
Condition = ProfileConditionType.LessThanEqual,
|
||||
Property = ProfileConditionValue.Width,
|
||||
Value = "1920",
|
||||
IsRequired = true
|
||||
},
|
||||
new ProfileCondition
|
||||
{
|
||||
Condition = ProfileConditionType.LessThanEqual,
|
||||
Property = ProfileConditionValue.Height,
|
||||
Value = "1080",
|
||||
IsRequired = true
|
||||
},
|
||||
new ProfileCondition
|
||||
{
|
||||
Condition = ProfileConditionType.LessThanEqual,
|
||||
Property = ProfileConditionValue.VideoFramerate,
|
||||
Value = "30",
|
||||
IsRequired = true
|
||||
},
|
||||
new ProfileCondition
|
||||
{
|
||||
Condition = ProfileConditionType.LessThanEqual,
|
||||
Property = ProfileConditionValue.VideoBitrate,
|
||||
Value = "20000000",
|
||||
IsRequired = true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
new CodecProfile
|
||||
{
|
||||
Type = CodecType.VideoAudio,
|
||||
Codec = "ac3,he-aac",
|
||||
Conditions = new []
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
Condition = ProfileConditionType.LessThanEqual,
|
||||
Property = ProfileConditionValue.AudioChannels,
|
||||
Value = "6",
|
||||
IsRequired = true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
new CodecProfile
|
||||
{
|
||||
Type = CodecType.VideoAudio,
|
||||
Codec = "aac",
|
||||
Conditions = new []
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
Condition = ProfileConditionType.LessThanEqual,
|
||||
Property = ProfileConditionValue.AudioChannels,
|
||||
Value = "2",
|
||||
IsRequired = true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ResponseProfiles = new[]
|
||||
{
|
||||
new ResponseProfile
|
||||
{
|
||||
Container = "mkv,ts",
|
||||
Type = DlnaProfileType.Video,
|
||||
MimeType = "video/mp4"
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
77
Emby.Dlna/Profiles/Foobar2000Profile.cs
Normal file
|
@ -0,0 +1,77 @@
|
|||
using MediaBrowser.Model.Dlna;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace MediaBrowser.Dlna.Profiles
|
||||
{
|
||||
[XmlRoot("Profile")]
|
||||
public class Foobar2000Profile : DefaultProfile
|
||||
{
|
||||
public Foobar2000Profile()
|
||||
{
|
||||
Name = "foobar2000";
|
||||
|
||||
SupportedMediaTypes = "Audio";
|
||||
|
||||
Identification = new DeviceIdentification
|
||||
{
|
||||
FriendlyName = @"foobar",
|
||||
|
||||
Headers = new[]
|
||||
{
|
||||
new HttpHeaderInfo
|
||||
{
|
||||
Name = "User-Agent",
|
||||
Value = "foobar",
|
||||
Match = HeaderMatchType.Substring
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DirectPlayProfiles = new[]
|
||||
{
|
||||
new DirectPlayProfile
|
||||
{
|
||||
Container = "mp3",
|
||||
AudioCodec = "mp2,mp3",
|
||||
Type = DlnaProfileType.Audio
|
||||
},
|
||||
|
||||
new DirectPlayProfile
|
||||
{
|
||||
Container = "mp4",
|
||||
AudioCodec = "mp4",
|
||||
Type = DlnaProfileType.Audio
|
||||
},
|
||||
|
||||
new DirectPlayProfile
|
||||
{
|
||||
Container = "aac,wav",
|
||||
Type = DlnaProfileType.Audio
|
||||
},
|
||||
|
||||
new DirectPlayProfile
|
||||
{
|
||||
Container = "flac",
|
||||
AudioCodec = "flac",
|
||||
Type = DlnaProfileType.Audio
|
||||
},
|
||||
|
||||
new DirectPlayProfile
|
||||
{
|
||||
Container = "asf",
|
||||
AudioCodec = "wmav2,wmapro,wmavoice",
|
||||
Type = DlnaProfileType.Audio
|
||||
},
|
||||
|
||||
new DirectPlayProfile
|
||||
{
|
||||
Container = "ogg",
|
||||
AudioCodec = "vorbis",
|
||||
Type = DlnaProfileType.Audio
|
||||
}
|
||||
};
|
||||
|
||||
ResponseProfiles = new ResponseProfile[] { };
|
||||
}
|
||||
}
|
||||
}
|
1
Emby.Dlna/Profiles/Json/BubbleUPnp.json
Normal file
1
Emby.Dlna/Profiles/Json/Default.json
Normal file
1
Emby.Dlna/Profiles/Json/Denon AVR.json
Normal file
1
Emby.Dlna/Profiles/Json/DirecTV HD-DVR.json
Normal file
1
Emby.Dlna/Profiles/Json/Dish Hopper-Joey.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"Name":"Dish Hopper-Joey","Identification":{"Manufacturer":"Echostar Technologies LLC","ManufacturerUrl":"http://www.echostar.com","Headers":[{"Name":"User-Agent","Value":"XiP","Match":"Substring"}]},"Manufacturer":"Emby","ManufacturerUrl":"http://emby.media/","ModelName":"Emby Server","ModelDescription":"Emby","ModelNumber":"Emby","ModelUrl":"http://emby.media/","EnableAlbumArtInDidl":false,"EnableSingleAlbumArtLimit":false,"EnableSingleSubtitleLimit":false,"SupportedMediaTypes":"Audio,Photo,Video","AlbumArtPn":"JPEG_SM","MaxAlbumArtWidth":480,"MaxAlbumArtHeight":480,"MaxIconWidth":48,"MaxIconHeight":48,"MaxStreamingBitrate":20000000,"MaxStaticBitrate":20000000,"MusicStreamingTranscodingBitrate":192000,"XDlnaDoc":"DMS-1.50","ProtocolInfo":"http-get:*:video/mp2t:*,http-get:*:video/MP1S:*,http-get:*:video/mpeg2:*,http-get:*:video/mp4:*,http-get:*:video/x-matroska:*,http-get:*:audio/mpeg:*,http-get:*:audio/mpeg3:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/mp4a-latm:*,http-get:*:image/jpeg:*","TimelineOffsetSeconds":0,"RequiresPlainVideoItems":false,"RequiresPlainFolders":false,"EnableMSMediaReceiverRegistrar":false,"IgnoreTranscodeByteRangeRequests":false,"XmlRootAttributes":[],"DirectPlayProfiles":[{"Container":"mp4,mkv,mpeg,ts","AudioCodec":"mp3,ac3,aac,he-aac,pcm","VideoCodec":"h264,mpeg2video","Type":"Video"},{"Container":"mp3","AudioCodec":"mp3","Type":"Audio"},{"Container":"alac","AudioCodec":"alac","Type":"Audio"},{"Container":"flac","AudioCodec":"flac","Type":"Audio"},{"Container":"jpeg","Type":"Photo"}],"TranscodingProfiles":[{"Container":"mp3","Type":"Audio","AudioCodec":"mp3","EstimateContentLength":false,"EnableMpegtsM2TsMode":false,"TranscodeSeekInfo":"Auto","CopyTimestamps":false,"Context":"Streaming","EnableSubtitlesInManifest":false,"EnableSplittingOnNonKeyFrames":false},{"Container":"mp4","Type":"Video","VideoCodec":"h264","AudioCodec":"aac","EstimateContentLength":false,"EnableMpegtsM2TsMode":false,"TranscodeSeekInfo":"Auto","CopyTimestamps":false,"Context":"Streaming","EnableSubtitlesInManifest":false,"EnableSplittingOnNonKeyFrames":false},{"Container":"jpeg","Type":"Photo","EstimateContentLength":false,"EnableMpegtsM2TsMode":false,"TranscodeSeekInfo":"Auto","CopyTimestamps":false,"Context":"Streaming","EnableSubtitlesInManifest":false,"EnableSplittingOnNonKeyFrames":false}],"ContainerProfiles":[],"CodecProfiles":[{"Type":"Video","Conditions":[{"Condition":"LessThanEqual","Property":"Width","Value":"1920","IsRequired":true},{"Condition":"LessThanEqual","Property":"Height","Value":"1080","IsRequired":true},{"Condition":"LessThanEqual","Property":"VideoFramerate","Value":"30","IsRequired":true},{"Condition":"LessThanEqual","Property":"VideoBitrate","Value":"20000000","IsRequired":true},{"Condition":"LessThanEqual","Property":"VideoLevel","Value":"41","IsRequired":true}],"ApplyConditions":[],"Codec":"h264"},{"Type":"Video","Conditions":[{"Condition":"LessThanEqual","Property":"Width","Value":"1920","IsRequired":true},{"Condition":"LessThanEqual","Property":"Height","Value":"1080","IsRequired":true},{"Condition":"LessThanEqual","Property":"VideoFramerate","Value":"30","IsRequired":true},{"Condition":"LessThanEqual","Property":"VideoBitrate","Value":"20000000","IsRequired":true}],"ApplyConditions":[]},{"Type":"VideoAudio","Conditions":[{"Condition":"LessThanEqual","Property":"AudioChannels","Value":"6","IsRequired":true}],"ApplyConditions":[],"Codec":"ac3,he-aac"},{"Type":"VideoAudio","Conditions":[{"Condition":"LessThanEqual","Property":"AudioChannels","Value":"2","IsRequired":true}],"ApplyConditions":[],"Codec":"aac"}],"ResponseProfiles":[{"Container":"mkv,ts","Type":"Video","MimeType":"video/mp4","Conditions":[]}],"SubtitleProfiles":[{"Format":"srt","Method":"Embed"}]}
|