Merge pull request #10558 from barronpm/dlna-plugin2
Move DLNA to Plugin (Part 2)
|
@ -1,23 +0,0 @@
|
|||
namespace Emby.Dlna.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// DLNA Query parameter type, used when querying DLNA devices via SOAP.
|
||||
/// </summary>
|
||||
public class Argument
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets name of the DLNA argument.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the direction of the parameter.
|
||||
/// </summary>
|
||||
public string Direction { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the related DLNA state variable for this argument.
|
||||
/// </summary>
|
||||
public string RelatedStateVariable { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
using System.Globalization;
|
||||
|
||||
namespace Emby.Dlna.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="DeviceIcon" />.
|
||||
/// </summary>
|
||||
public class DeviceIcon
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Url.
|
||||
/// </summary>
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MimeType.
|
||||
/// </summary>
|
||||
public string MimeType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Width.
|
||||
/// </summary>
|
||||
public int Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Height.
|
||||
/// </summary>
|
||||
public int Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Depth.
|
||||
/// </summary>
|
||||
public string Depth { get; set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0}x{1}", Height, Width);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
namespace Emby.Dlna.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="DeviceService" />.
|
||||
/// </summary>
|
||||
public class DeviceService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Service Type.
|
||||
/// </summary>
|
||||
public string ServiceType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Service Id.
|
||||
/// </summary>
|
||||
public string ServiceId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Scpd Url.
|
||||
/// </summary>
|
||||
public string ScpdUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Control Url.
|
||||
/// </summary>
|
||||
public string ControlUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the EventSubUrl.
|
||||
/// </summary>
|
||||
public string EventSubUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => ServiceId;
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Emby.Dlna.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ServiceAction" />.
|
||||
/// </summary>
|
||||
public class ServiceAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServiceAction"/> class.
|
||||
/// </summary>
|
||||
public ServiceAction()
|
||||
{
|
||||
ArgumentList = new List<Argument>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the action.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ArgumentList.
|
||||
/// </summary>
|
||||
public List<Argument> ArgumentList { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Emby.Dlna.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="StateVariable" />.
|
||||
/// </summary>
|
||||
public class StateVariable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the state variable.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the data type of the state variable.
|
||||
/// </summary>
|
||||
public string DataType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether it sends events.
|
||||
/// </summary>
|
||||
public bool SendsEvents { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the allowed values range.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> AllowedValues { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
namespace Emby.Dlna.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// The DlnaOptions class contains the user definable parameters for the dlna subsystems.
|
||||
/// </summary>
|
||||
public class DlnaOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DlnaOptions"/> class.
|
||||
/// </summary>
|
||||
public DlnaOptions()
|
||||
{
|
||||
EnablePlayTo = true;
|
||||
EnableServer = false;
|
||||
BlastAliveMessages = true;
|
||||
SendOnlyMatchedHost = true;
|
||||
ClientDiscoveryIntervalSeconds = 60;
|
||||
AliveMessageIntervalSeconds = 180;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether gets or sets a value to indicate the status of the dlna playTo subsystem.
|
||||
/// </summary>
|
||||
public bool EnablePlayTo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether gets or sets a value to indicate the status of the dlna server subsystem.
|
||||
/// </summary>
|
||||
public bool EnableServer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether detailed dlna server logs are sent to the console/log.
|
||||
/// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work.
|
||||
/// </summary>
|
||||
public bool EnableDebugLog { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether whether detailed playTo debug logs are sent to the console/log.
|
||||
/// If the setting "Emby.Dlna.PlayTo": "Debug" msut be set in logging.default.json for this property to work.
|
||||
/// </summary>
|
||||
public bool EnablePlayToTracing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ssdp client discovery interval time (in seconds).
|
||||
/// This is the time after which the server will send a ssdp search request.
|
||||
/// </summary>
|
||||
public int ClientDiscoveryIntervalSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the frequency at which ssdp alive notifications are transmitted.
|
||||
/// </summary>
|
||||
public int AliveMessageIntervalSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the frequency at which ssdp alive notifications are transmitted. MIGRATING - TO BE REMOVED ONCE WEB HAS BEEN ALTERED.
|
||||
/// </summary>
|
||||
public int BlastAliveMessageIntervalSeconds
|
||||
{
|
||||
get
|
||||
{
|
||||
return AliveMessageIntervalSeconds;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
AliveMessageIntervalSeconds = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default user account that the dlna server uses.
|
||||
/// </summary>
|
||||
public string? DefaultUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether playTo device profiles should be created.
|
||||
/// </summary>
|
||||
public bool AutoCreatePlayToProfiles { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to blast alive messages.
|
||||
/// </summary>
|
||||
public bool BlastAliveMessages { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// gets or sets a value indicating whether to send only matched host.
|
||||
/// </summary>
|
||||
public bool SendOnlyMatchedHost { get; set; } = true;
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using Emby.Dlna.Configuration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public static class ConfigurationExtension
|
||||
{
|
||||
public static DlnaOptions GetDlnaConfiguration(this IConfigurationManager manager)
|
||||
{
|
||||
return manager.GetConfiguration<DlnaOptions>("dlna");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.Service;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.ConnectionManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ConnectionManagerService" />.
|
||||
/// </summary>
|
||||
public class ConnectionManagerService : BaseService, IConnectionManager
|
||||
{
|
||||
private readonly IDlnaManager _dlna;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectionManagerService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dlna">The <see cref="IDlnaManager"/> for use with the <see cref="ConnectionManagerService"/> instance.</param>
|
||||
/// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ConnectionManagerService"/> instance.</param>
|
||||
/// <param name="logger">The <see cref="ILogger{ConnectionManagerService}"/> for use with the <see cref="ConnectionManagerService"/> instance..</param>
|
||||
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for use with the <see cref="ConnectionManagerService"/> instance..</param>
|
||||
public ConnectionManagerService(
|
||||
IDlnaManager dlna,
|
||||
IServerConfigurationManager config,
|
||||
ILogger<ConnectionManagerService> logger,
|
||||
IHttpClientFactory httpClientFactory)
|
||||
: base(logger, httpClientFactory)
|
||||
{
|
||||
_dlna = dlna;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetServiceXml()
|
||||
{
|
||||
return ConnectionManagerXmlBuilder.GetXml();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
|
||||
{
|
||||
var profile = _dlna.GetProfile(request.Headers) ??
|
||||
_dlna.GetDefaultProfile();
|
||||
|
||||
return new ControlHandler(_config, Logger, profile).ProcessControlRequestAsync(request);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
using Emby.Dlna.Service;
|
||||
|
||||
namespace Emby.Dlna.ConnectionManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ConnectionManagerXmlBuilder" />.
|
||||
/// </summary>
|
||||
public static class ConnectionManagerXmlBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the ConnectionManager:1 service template.
|
||||
/// See http://upnp.org/specs/av/UPnP-av-ConnectionManager-v1-Service.pdf.
|
||||
/// </summary>
|
||||
/// <returns>An XML description of this service.</returns>
|
||||
public static string GetXml()
|
||||
{
|
||||
return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the list of state variables for this invocation.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
|
||||
private static IEnumerable<StateVariable> GetStateVariables()
|
||||
{
|
||||
return new StateVariable[]
|
||||
{
|
||||
new StateVariable
|
||||
{
|
||||
Name = "SourceProtocolInfo",
|
||||
DataType = "string",
|
||||
SendsEvents = true
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "SinkProtocolInfo",
|
||||
DataType = "string",
|
||||
SendsEvents = true
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "CurrentConnectionIDs",
|
||||
DataType = "string",
|
||||
SendsEvents = true
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ConnectionStatus",
|
||||
DataType = "string",
|
||||
SendsEvents = false,
|
||||
|
||||
AllowedValues = new[]
|
||||
{
|
||||
"OK",
|
||||
"ContentFormatMismatch",
|
||||
"InsufficientBandwidth",
|
||||
"UnreliableChannel",
|
||||
"Unknown"
|
||||
}
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ConnectionManager",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Direction",
|
||||
DataType = "string",
|
||||
SendsEvents = false,
|
||||
|
||||
AllowedValues = new[]
|
||||
{
|
||||
"Output",
|
||||
"Input"
|
||||
}
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ProtocolInfo",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ConnectionID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_AVTransportID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RcsID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml;
|
||||
using Emby.Dlna.Service;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.ConnectionManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ControlHandler" />.
|
||||
/// </summary>
|
||||
public class ControlHandler : BaseControlHandler
|
||||
{
|
||||
private readonly DeviceProfile _profile;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ControlHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param>
|
||||
/// <param name="profile">The <see cref="DeviceProfile"/> for use with the <see cref="ControlHandler"/> instance.</param>
|
||||
public ControlHandler(IServerConfigurationManager config, ILogger logger, DeviceProfile profile)
|
||||
: base(config, logger)
|
||||
{
|
||||
_profile = profile;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
||||
{
|
||||
if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
HandleGetProtocolInfo(xmlWriter);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the response to the GetProtocolInfo request.
|
||||
/// </summary>
|
||||
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
|
||||
private void HandleGetProtocolInfo(XmlWriter xmlWriter)
|
||||
{
|
||||
xmlWriter.WriteElementString("Source", _profile.ProtocolInfo);
|
||||
xmlWriter.WriteElementString("Sink", string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,234 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
|
||||
namespace Emby.Dlna.ConnectionManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ServiceActionListBuilder" />.
|
||||
/// </summary>
|
||||
public static class ServiceActionListBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns an enumerable of the ConnectionManagar:1 DLNA actions.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
|
||||
public static IEnumerable<ServiceAction> GetActions()
|
||||
{
|
||||
var list = new List<ServiceAction>
|
||||
{
|
||||
GetCurrentConnectionInfo(),
|
||||
GetProtocolInfo(),
|
||||
GetCurrentConnectionIDs(),
|
||||
ConnectionComplete(),
|
||||
PrepareForConnection()
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "PrepareForConnection".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetCurrentConnectionInfo".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetProtocolInfo".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetCurrentConnectionIDs".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetCurrentConnectionIDs()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetCurrentConnectionIDs"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ConnectionIDs",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "CurrentConnectionIDs"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "ConnectionComplete".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction ConnectionComplete()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "ConnectionComplete"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ConnectionID",
|
||||
Direction = "in",
|
||||
RelatedStateVariable = "A_ARG_TYPE_ConnectionID"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,173 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.Service;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.TV;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ContentDirectoryService" />.
|
||||
/// </summary>
|
||||
public class ContentDirectoryService : BaseService, IContentDirectory
|
||||
{
|
||||
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 IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IUserViewManager _userViewManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly ITVSeriesManager _tvSeriesManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ContentDirectoryService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dlna">The <see cref="IDlnaManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="userDataManager">The <see cref="IUserDataManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="imageProcessor">The <see cref="IImageProcessor"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="libraryManager">The <see cref="ILibraryManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="config">The <see cref="IServerConfigurationManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="userManager">The <see cref="IUserManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="logger">The <see cref="ILogger{ContentDirectoryService}"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="httpClient">The <see cref="IHttpClientFactory"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="localization">The <see cref="ILocalizationManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="userViewManager">The <see cref="IUserViewManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="mediaEncoder">The <see cref="IMediaEncoder"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
/// <param name="tvSeriesManager">The <see cref="ITVSeriesManager"/> to use in the <see cref="ContentDirectoryService"/> instance.</param>
|
||||
public ContentDirectoryService(
|
||||
IDlnaManager dlna,
|
||||
IUserDataManager userDataManager,
|
||||
IImageProcessor imageProcessor,
|
||||
ILibraryManager libraryManager,
|
||||
IServerConfigurationManager config,
|
||||
IUserManager userManager,
|
||||
ILogger<ContentDirectoryService> logger,
|
||||
IHttpClientFactory httpClient,
|
||||
ILocalizationManager localization,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IUserViewManager userViewManager,
|
||||
IMediaEncoder mediaEncoder,
|
||||
ITVSeriesManager tvSeriesManager)
|
||||
: base(logger, httpClient)
|
||||
{
|
||||
_dlna = dlna;
|
||||
_userDataManager = userDataManager;
|
||||
_imageProcessor = imageProcessor;
|
||||
_libraryManager = libraryManager;
|
||||
_config = config;
|
||||
_userManager = userManager;
|
||||
_localization = localization;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_userViewManager = userViewManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_tvSeriesManager = tvSeriesManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the system id. (A unique id which changes on when our definition changes.)
|
||||
/// </summary>
|
||||
private static int SystemUpdateId
|
||||
{
|
||||
get
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
return now.Year + now.DayOfYear + now.Hour;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetServiceXml()
|
||||
{
|
||||
return ContentDirectoryXmlBuilder.GetXml();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var profile = _dlna.GetProfile(request.Headers) ?? _dlna.GetDefaultProfile();
|
||||
|
||||
var serverAddress = request.RequestedUrl.Substring(0, request.RequestedUrl.IndexOf("/dlna", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var user = GetUser(profile);
|
||||
|
||||
return new ControlHandler(
|
||||
Logger,
|
||||
_libraryManager,
|
||||
profile,
|
||||
serverAddress,
|
||||
null,
|
||||
_imageProcessor,
|
||||
_userDataManager,
|
||||
user,
|
||||
SystemUpdateId,
|
||||
_config,
|
||||
_localization,
|
||||
_mediaSourceManager,
|
||||
_userViewManager,
|
||||
_mediaEncoder,
|
||||
_tvSeriesManager)
|
||||
.ProcessControlRequestAsync(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the user stored in the device profile.
|
||||
/// </summary>
|
||||
/// <param name="profile">The <see cref="DeviceProfile"/>.</param>
|
||||
/// <returns>The <see cref="User"/>.</returns>
|
||||
private User? GetUser(DeviceProfile profile)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(profile.UserId))
|
||||
{
|
||||
var user = _userManager.GetUserById(Guid.Parse(profile.UserId));
|
||||
|
||||
if (user is not null)
|
||||
{
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
var userId = _config.GetDlnaConfiguration().DefaultUserId;
|
||||
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
{
|
||||
var user = _userManager.GetUserById(Guid.Parse(userId));
|
||||
|
||||
if (user is not null)
|
||||
{
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var user in _userManager.Users)
|
||||
{
|
||||
if (user.HasPermission(PermissionKind.IsAdministrator))
|
||||
{
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
return _userManager.Users.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,159 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
using Emby.Dlna.Service;
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ContentDirectoryXmlBuilder" />.
|
||||
/// </summary>
|
||||
public static class ContentDirectoryXmlBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the ContentDirectory:1 service template.
|
||||
/// See http://upnp.org/specs/av/UPnP-av-ContentDirectory-v1-Service.pdf.
|
||||
/// </summary>
|
||||
/// <returns>An XML description of this service.</returns>
|
||||
public static string GetXml()
|
||||
{
|
||||
return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the list of state variables for this invocation.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
|
||||
private static IEnumerable<StateVariable> GetStateVariables()
|
||||
{
|
||||
return new StateVariable[]
|
||||
{
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Filter",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_SortCriteria",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Index",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Count",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_UpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "SearchCapabilities",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "SortCapabilities",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "SystemUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_SearchCriteria",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Result",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_ObjectID",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_BrowseFlag",
|
||||
DataType = "string",
|
||||
SendsEvents = false,
|
||||
|
||||
AllowedValues = new[]
|
||||
{
|
||||
"BrowseMetadata",
|
||||
"BrowseDirectChildren"
|
||||
}
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_BrowseLetter",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_CategoryType",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_PosSec",
|
||||
DataType = "ui4",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Featurelist",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
using MediaBrowser.Controller.Entities;
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ServerItem" />.
|
||||
/// </summary>
|
||||
internal class ServerItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerItem"/> class.
|
||||
/// </summary>
|
||||
/// <param name="item">The <see cref="BaseItem"/>.</param>
|
||||
/// <param name="stubType">The stub type.</param>
|
||||
public ServerItem(BaseItem item, StubType? stubType)
|
||||
{
|
||||
Item = item;
|
||||
|
||||
if (stubType.HasValue)
|
||||
{
|
||||
StubType = stubType;
|
||||
}
|
||||
else if (item is IItemByName and not Folder)
|
||||
{
|
||||
StubType = Dlna.ContentDirectory.StubType.Folder;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying base item.
|
||||
/// </summary>
|
||||
public BaseItem Item { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DLNA item type.
|
||||
/// </summary>
|
||||
public StubType? StubType { get; }
|
||||
}
|
||||
}
|
|
@ -1,415 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ServiceActionListBuilder" />.
|
||||
/// </summary>
|
||||
public static class ServiceActionListBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a list of services that this instance provides.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
|
||||
public static IEnumerable<ServiceAction> GetActions()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
GetSearchCapabilitiesAction(),
|
||||
GetSortCapabilitiesAction(),
|
||||
GetGetSystemUpdateIDAction(),
|
||||
GetBrowseAction(),
|
||||
GetSearchAction(),
|
||||
GetX_GetFeatureListAction(),
|
||||
GetXSetBookmarkAction(),
|
||||
GetBrowseByLetterAction()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetSystemUpdateID".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetGetSystemUpdateIDAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetSystemUpdateID"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "Id",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "SystemUpdateID"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetSearchCapabilities".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetSearchCapabilitiesAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetSearchCapabilities"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "SearchCaps",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "SearchCapabilities"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetSortCapabilities".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetSortCapabilitiesAction()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetSortCapabilities"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "SortCaps",
|
||||
Direction = "out",
|
||||
RelatedStateVariable = "SortCapabilities"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "X_GetFeatureList".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "Search".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "Browse".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "X_BrowseByLetter".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "X_SetBookmark".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the DLNA item types.
|
||||
/// </summary>
|
||||
public enum StubType
|
||||
{
|
||||
Folder = 0,
|
||||
Latest = 2,
|
||||
Playlists = 3,
|
||||
Albums = 4,
|
||||
AlbumArtists = 5,
|
||||
Artists = 6,
|
||||
Songs = 7,
|
||||
Genres = 8,
|
||||
FavoriteSongs = 9,
|
||||
FavoriteArtists = 10,
|
||||
FavoriteAlbums = 11,
|
||||
ContinueWatching = 12,
|
||||
Movies = 13,
|
||||
Collections = 14,
|
||||
Favorites = 15,
|
||||
NextUp = 16,
|
||||
Series = 17,
|
||||
FavoriteSeries = 18,
|
||||
FavoriteEpisodes = 19
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.IO;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public class ControlRequest
|
||||
{
|
||||
public ControlRequest(IHeaderDictionary headers)
|
||||
{
|
||||
Headers = headers;
|
||||
}
|
||||
|
||||
public IHeaderDictionary Headers { get; }
|
||||
|
||||
public Stream InputXml { get; set; }
|
||||
|
||||
public string TargetServerUuId { get; set; }
|
||||
|
||||
public string RequestedUrl { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public class ControlResponse
|
||||
{
|
||||
public ControlResponse(string xml, bool isSuccessful)
|
||||
{
|
||||
Headers = new Dictionary<string, string>();
|
||||
Xml = xml;
|
||||
IsSuccessful = isSuccessful;
|
||||
}
|
||||
|
||||
public IDictionary<string, string> Headers { get; }
|
||||
|
||||
public string Xml { get; set; }
|
||||
|
||||
public bool IsSuccessful { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Xml;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace Emby.Dlna.Didl
|
||||
{
|
||||
public class Filter
|
||||
{
|
||||
private readonly string[] _fields;
|
||||
private readonly bool _all;
|
||||
|
||||
public Filter()
|
||||
: this("*")
|
||||
{
|
||||
}
|
||||
|
||||
public Filter(string filter)
|
||||
{
|
||||
_all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase);
|
||||
_fields = filter.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
public bool Contains(string field)
|
||||
{
|
||||
return _all || Array.Exists(_fields, x => x.Equals(field, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
#pragma warning disable CA1305
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace Emby.Dlna.Didl
|
||||
{
|
||||
public class StringWriterWithEncoding : StringWriter
|
||||
{
|
||||
private readonly Encoding? _encoding;
|
||||
|
||||
public StringWriterWithEncoding()
|
||||
{
|
||||
}
|
||||
|
||||
public StringWriterWithEncoding(IFormatProvider formatProvider)
|
||||
: base(formatProvider)
|
||||
{
|
||||
}
|
||||
|
||||
public StringWriterWithEncoding(StringBuilder sb)
|
||||
: base(sb)
|
||||
{
|
||||
}
|
||||
|
||||
public StringWriterWithEncoding(StringBuilder sb, IFormatProvider formatProvider)
|
||||
: base(sb, formatProvider)
|
||||
{
|
||||
}
|
||||
|
||||
public StringWriterWithEncoding(Encoding encoding)
|
||||
{
|
||||
_encoding = encoding;
|
||||
}
|
||||
|
||||
public StringWriterWithEncoding(IFormatProvider formatProvider, Encoding encoding)
|
||||
: base(formatProvider)
|
||||
{
|
||||
_encoding = encoding;
|
||||
}
|
||||
|
||||
public StringWriterWithEncoding(StringBuilder sb, Encoding encoding)
|
||||
: base(sb)
|
||||
{
|
||||
_encoding = encoding;
|
||||
}
|
||||
|
||||
public StringWriterWithEncoding(StringBuilder sb, IFormatProvider formatProvider, Encoding encoding)
|
||||
: base(sb, formatProvider)
|
||||
{
|
||||
_encoding = encoding;
|
||||
}
|
||||
|
||||
public override Encoding Encoding => _encoding ?? base.Encoding;
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Configuration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public class DlnaConfigurationFactory : IConfigurationFactory
|
||||
{
|
||||
public IEnumerable<ConfigurationStore> GetConfigurations()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new ConfigurationStore
|
||||
{
|
||||
Key = "dlna",
|
||||
ConfigurationType = typeof(DlnaOptions)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,491 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.Profiles;
|
||||
using Emby.Dlna.Server;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public class DlnaManager : IDlnaManager
|
||||
{
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly IXmlSerializer _xmlSerializer;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILogger<DlnaManager> _logger;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
|
||||
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
|
||||
|
||||
public DlnaManager(
|
||||
IXmlSerializer xmlSerializer,
|
||||
IFileSystem fileSystem,
|
||||
IApplicationPaths appPaths,
|
||||
ILoggerFactory loggerFactory,
|
||||
IServerApplicationHost appHost)
|
||||
{
|
||||
_xmlSerializer = xmlSerializer;
|
||||
_fileSystem = fileSystem;
|
||||
_appPaths = appPaths;
|
||||
_logger = loggerFactory.CreateLogger<DlnaManager>();
|
||||
_appHost = appHost;
|
||||
}
|
||||
|
||||
private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
|
||||
|
||||
private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
|
||||
|
||||
public async Task InitProfilesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await ExtractSystemProfilesAsync().ConfigureAwait(false);
|
||||
Directory.CreateDirectory(UserProfilesPath);
|
||||
LoadProfiles();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error extracting DLNA profiles.");
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return _profiles.Values
|
||||
.OrderBy(i => i.Item1.Info.Type == DeviceProfileType.User ? 0 : 1)
|
||||
.ThenBy(i => i.Item1.Info.Name)
|
||||
.Select(i => i.Item2)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DeviceProfile GetDefaultProfile()
|
||||
{
|
||||
return new DefaultProfile();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DeviceProfile? GetProfile(DeviceIdentification deviceInfo)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(deviceInfo);
|
||||
|
||||
var profile = GetProfiles()
|
||||
.FirstOrDefault(i => i.Identification is not null && IsMatch(deviceInfo, i.Identification));
|
||||
|
||||
if (profile is null)
|
||||
{
|
||||
_logger.LogInformation("No matching device profile found. The default will need to be used. \n{@Profile}", deviceInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to match a device with a profile.
|
||||
/// Rules:
|
||||
/// - If the profile field has no value, the field matches regardless of its contents.
|
||||
/// - the profile field can be an exact match, or a reg exp.
|
||||
/// </summary>
|
||||
/// <param name="deviceInfo">The <see cref="DeviceIdentification"/> of the device.</param>
|
||||
/// <param name="profileInfo">The <see cref="DeviceIdentification"/> of the profile.</param>
|
||||
/// <returns><b>True</b> if they match.</returns>
|
||||
public bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
|
||||
{
|
||||
return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)
|
||||
&& IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber);
|
||||
}
|
||||
|
||||
private bool IsRegexOrSubstringMatch(string input, string pattern)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pattern))
|
||||
{
|
||||
// In profile identification: An empty pattern matches anything.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
// The profile contains a value, and the device doesn't.
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return input.Equals(pattern, StringComparison.OrdinalIgnoreCase)
|
||||
|| Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error evaluating regex pattern {Pattern}", pattern);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DeviceProfile? GetProfile(IHeaderDictionary headers)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(headers);
|
||||
|
||||
var profile = GetProfiles().FirstOrDefault(i => i.Identification is not null && IsMatch(headers, i.Identification));
|
||||
if (profile is null)
|
||||
{
|
||||
_logger.LogDebug("No matching device profile found. {@Headers}", headers);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
private bool IsMatch(IHeaderDictionary headers, DeviceIdentification profileInfo)
|
||||
{
|
||||
return profileInfo.Headers.Any(i => IsMatch(headers, i));
|
||||
}
|
||||
|
||||
private bool IsMatch(IHeaderDictionary headers, HttpHeaderInfo header)
|
||||
{
|
||||
// Handle invalid user setup
|
||||
if (string.IsNullOrEmpty(header.Name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (headers.TryGetValue(header.Name, out StringValues value))
|
||||
{
|
||||
if (StringValues.IsNullOrEmpty(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (header.Match)
|
||||
{
|
||||
case HeaderMatchType.Equals:
|
||||
return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase);
|
||||
case HeaderMatchType.Substring:
|
||||
var isMatch = value.ToString().IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1;
|
||||
// _logger.LogDebug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch);
|
||||
return isMatch;
|
||||
case HeaderMatchType.Regex:
|
||||
// Can't be null, we checked above the switch statement
|
||||
return Regex.IsMatch(value!, header.Value, RegexOptions.IgnoreCase);
|
||||
default:
|
||||
throw new ArgumentException("Unrecognized HeaderMatchType");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _fileSystem.GetFilePaths(path)
|
||||
.Where(i => Path.GetExtension(i.AsSpan()).Equals(".xml", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(i => ParseProfileFile(i, type))
|
||||
.Where(i => i is not null)
|
||||
.ToList()!; // We just filtered out all the nulls
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return Array.Empty<DeviceProfile>();
|
||||
}
|
||||
}
|
||||
|
||||
private DeviceProfile? ParseProfileFile(string path, DeviceProfileType type)
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile>? profileTuple))
|
||||
{
|
||||
return profileTuple.Item2;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var tempProfile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path);
|
||||
var profile = ReserializeProfile(tempProfile);
|
||||
|
||||
profile.Id = path.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
_profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
|
||||
|
||||
return profile;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error parsing profile file: {Path}", path);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DeviceProfile? GetProfile(string id)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(id);
|
||||
|
||||
var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (info is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ParseProfileFile(info.Path, info.Info.Type);
|
||||
}
|
||||
|
||||
private IEnumerable<InternalProfileInfo> GetProfileInfosInternal()
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
return _profiles.Values
|
||||
.Select(i => i.Item1)
|
||||
.OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1)
|
||||
.ThenBy(i => i.Info.Name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<DeviceProfileInfo> GetProfileInfos()
|
||||
{
|
||||
return GetProfileInfosInternal().Select(i => i.Info);
|
||||
}
|
||||
|
||||
private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type)
|
||||
{
|
||||
return new InternalProfileInfo(
|
||||
new DeviceProfileInfo
|
||||
{
|
||||
Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture),
|
||||
Name = _fileSystem.GetFileNameWithoutExtension(file),
|
||||
Type = type
|
||||
},
|
||||
file.FullName);
|
||||
}
|
||||
|
||||
private async Task ExtractSystemProfilesAsync()
|
||||
{
|
||||
var namespaceName = GetType().Namespace + ".Profiles.Xml.";
|
||||
|
||||
var systemProfilesPath = SystemProfilesPath;
|
||||
|
||||
foreach (var name in _assembly.GetManifestResourceNames())
|
||||
{
|
||||
if (!name.StartsWith(namespaceName, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var path = Path.Join(
|
||||
systemProfilesPath,
|
||||
Path.GetFileName(name.AsSpan())[namespaceName.Length..]);
|
||||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// The stream should exist as we just got its name from GetManifestResourceNames
|
||||
using (var stream = _assembly.GetManifestResourceStream(name)!)
|
||||
{
|
||||
Directory.CreateDirectory(systemProfilesPath);
|
||||
|
||||
var fileOptions = AsyncFile.WriteOptions;
|
||||
fileOptions.Mode = FileMode.CreateNew;
|
||||
fileOptions.PreallocationSize = stream.Length;
|
||||
var fileStream = new FileStream(path, fileOptions);
|
||||
await using (fileStream.ConfigureAwait(false))
|
||||
{
|
||||
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CreateProfile(DeviceProfile profile)
|
||||
{
|
||||
profile = ReserializeProfile(profile);
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(profile.Name);
|
||||
|
||||
var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
|
||||
var path = Path.Combine(UserProfilesPath, newFilename);
|
||||
|
||||
SaveProfile(profile, path, DeviceProfileType.User);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UpdateProfile(string profileId, DeviceProfile profile)
|
||||
{
|
||||
profile = ReserializeProfile(profile);
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(profile.Id);
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(profile.Name);
|
||||
|
||||
var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profileId, StringComparison.OrdinalIgnoreCase));
|
||||
if (current.Info.Type == DeviceProfileType.System)
|
||||
{
|
||||
throw new ArgumentException("System profiles can't be edited");
|
||||
}
|
||||
|
||||
var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
|
||||
var path = Path.Join(UserProfilesPath, newFilename);
|
||||
|
||||
if (!string.Equals(path, current.Path, StringComparison.Ordinal))
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
_profiles.Remove(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);
|
||||
}
|
||||
|
||||
SerializeToXml(profile, path);
|
||||
}
|
||||
|
||||
internal void SerializeToXml(DeviceProfile profile, string path)
|
||||
{
|
||||
_xmlSerializer.SerializeToFile(profile, path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recreates the object using serialization, to ensure it's not a subclass.
|
||||
/// If it's a subclass it may not serialize properly to xml (different root element tag name).
|
||||
/// </summary>
|
||||
/// <param name="profile">The device profile.</param>
|
||||
/// <returns>The re-serialized device profile.</returns>
|
||||
private DeviceProfile ReserializeProfile(DeviceProfile profile)
|
||||
{
|
||||
if (profile.GetType() == typeof(DeviceProfile))
|
||||
{
|
||||
return profile;
|
||||
}
|
||||
|
||||
var json = JsonSerializer.Serialize(profile, _jsonOptions);
|
||||
|
||||
// Output can't be null if the input isn't null
|
||||
return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions)!;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
|
||||
{
|
||||
var profile = GetProfile(headers) ?? GetDefaultProfile();
|
||||
|
||||
var serverId = _appHost.SystemId;
|
||||
|
||||
return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ImageStream? GetIcon(string filename)
|
||||
{
|
||||
var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
|
||||
? ImageFormat.Png
|
||||
: ImageFormat.Jpg;
|
||||
|
||||
var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant();
|
||||
var stream = _assembly.GetManifestResourceStream(resource);
|
||||
if (stream is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ImageStream(stream)
|
||||
{
|
||||
Format = format
|
||||
};
|
||||
}
|
||||
|
||||
private class InternalProfileInfo
|
||||
{
|
||||
internal InternalProfileInfo(DeviceProfileInfo info, string path)
|
||||
{
|
||||
Info = info;
|
||||
Path = path;
|
||||
}
|
||||
|
||||
internal DeviceProfileInfo Info { get; }
|
||||
|
||||
internal string Path { get; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{805844AB-E92F-45E6-9D99-4F6D48D129A5}</ProjectGuid>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SharedVersion.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
||||
<ProjectReference Include="..\RSSDP\RSSDP.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Code Analyzers -->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="IDisposableAnalyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Images\logo120.jpg" />
|
||||
<EmbeddedResource Include="Images\logo120.png" />
|
||||
<EmbeddedResource Include="Images\logo240.jpg" />
|
||||
<EmbeddedResource Include="Images\logo240.png" />
|
||||
<EmbeddedResource Include="Images\logo48.jpg" />
|
||||
<EmbeddedResource Include="Images\logo48.png" />
|
||||
<EmbeddedResource Include="Images\people48.jpg" />
|
||||
<EmbeddedResource Include="Images\people48.png" />
|
||||
<EmbeddedResource Include="Images\people480.jpg" />
|
||||
<EmbeddedResource Include="Images\people480.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Profiles\Xml\Default.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Denon AVR.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\DirecTV HD-DVR.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Dish Hopper-Joey.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\foobar2000.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\LG Smart TV.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Linksys DMA2100.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Marantz.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\MediaMonkey.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Panasonic Viera.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Popcorn Hour.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Samsung Smart TV.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2013.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2014.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2015.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2016.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony Bravia %282010%29.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony Bravia %282011%29.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony Bravia %282012%29.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony Bravia %282013%29.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony Bravia %282014%29.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony PlayStation 3.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Sony PlayStation 4.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\WDTV Live.xml" />
|
||||
<EmbeddedResource Include="Profiles\Xml\Xbox One.xml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -1,22 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public class EventSubscriptionResponse
|
||||
{
|
||||
public EventSubscriptionResponse(string content, string contentType)
|
||||
{
|
||||
Content = content;
|
||||
ContentType = contentType;
|
||||
Headers = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public string Content { get; set; }
|
||||
|
||||
public string ContentType { get; set; }
|
||||
|
||||
public Dictionary<string, string> Headers { get; }
|
||||
}
|
||||
}
|
|
@ -1,183 +0,0 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.Eventing
|
||||
{
|
||||
public class DlnaEventManager : IDlnaEventManager
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, EventSubscription> _subscriptions =
|
||||
new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl)
|
||||
{
|
||||
var subscription = GetSubscription(subscriptionId, false);
|
||||
if (subscription is not null)
|
||||
{
|
||||
subscription.TimeoutSeconds = ParseTimeout(requestedTimeoutString) ?? 300;
|
||||
int timeoutSeconds = subscription.TimeoutSeconds;
|
||||
subscription.SubscriptionTime = DateTime.UtcNow;
|
||||
|
||||
_logger.LogDebug(
|
||||
"Renewing event subscription for {0} with timeout of {1} to {2}",
|
||||
subscription.NotificationType,
|
||||
timeoutSeconds,
|
||||
subscription.CallbackUrl);
|
||||
|
||||
return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
|
||||
}
|
||||
|
||||
return new EventSubscriptionResponse(string.Empty, "text/plain");
|
||||
}
|
||||
|
||||
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
|
||||
{
|
||||
var timeout = ParseTimeout(requestedTimeoutString) ?? 300;
|
||||
var id = "uuid:" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
_logger.LogDebug(
|
||||
"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,
|
||||
NotificationType = notificationType
|
||||
});
|
||||
|
||||
return GetEventSubscriptionResponse(id, requestedTimeoutString, timeout);
|
||||
}
|
||||
|
||||
private int? ParseTimeout(string header)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(header))
|
||||
{
|
||||
// Starts with SECOND-
|
||||
if (int.TryParse(header.AsSpan().RightPart('-'), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||
{
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public EventSubscriptionResponse CancelEventSubscription(string subscriptionId)
|
||||
{
|
||||
_logger.LogDebug("Cancelling event subscription {0}", subscriptionId);
|
||||
|
||||
_subscriptions.TryRemove(subscriptionId, out _);
|
||||
|
||||
return new EventSubscriptionResponse(string.Empty, "text/plain");
|
||||
}
|
||||
|
||||
private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds)
|
||||
{
|
||||
var response = new EventSubscriptionResponse(string.Empty, "text/plain");
|
||||
|
||||
response.Headers["SID"] = subscriptionId;
|
||||
response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(CultureInfo.InvariantCulture)) : requestedTimeoutString;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public EventSubscription GetSubscription(string id)
|
||||
{
|
||||
return GetSubscription(id, false);
|
||||
}
|
||||
|
||||
private EventSubscription GetSubscription(string id, bool throwOnMissing)
|
||||
{
|
||||
if (!_subscriptions.TryGetValue(id, out EventSubscription 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));
|
||||
|
||||
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>")
|
||||
.Append('<')
|
||||
.Append(key)
|
||||
.Append('>')
|
||||
.Append(stateVariables[key])
|
||||
.Append("</")
|
||||
.Append(key)
|
||||
.Append('>')
|
||||
.Append("</e:property>");
|
||||
}
|
||||
|
||||
builder.Append("</e:propertyset>");
|
||||
|
||||
using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl);
|
||||
options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml);
|
||||
options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
|
||||
options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
|
||||
options.Headers.TryAddWithoutValidation("SID", subscription.Id);
|
||||
options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
try
|
||||
{
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.DirectIp)
|
||||
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Already logged at lower levels
|
||||
}
|
||||
finally
|
||||
{
|
||||
subscription.IncrementTriggerCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace Emby.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 bool IsExpired => SubscriptionTime.AddSeconds(TimeoutSeconds) >= DateTime.UtcNow;
|
||||
|
||||
public void IncrementTriggerCount()
|
||||
{
|
||||
if (TriggerCount == long.MaxValue)
|
||||
{
|
||||
TriggerCount = 0;
|
||||
}
|
||||
|
||||
TriggerCount++;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using Emby.Dlna.ConnectionManager;
|
||||
using Emby.Dlna.ContentDirectory;
|
||||
using Emby.Dlna.Main;
|
||||
using Emby.Dlna.MediaReceiverRegistrar;
|
||||
using Emby.Dlna.Ssdp;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Net;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Rssdp.Infrastructure;
|
||||
|
||||
namespace Emby.Dlna.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for adding DLNA services.
|
||||
/// </summary>
|
||||
public static class DlnaServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds DLNA services to the provided <see cref="IServiceCollection"/>.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
|
||||
/// <param name="applicationHost">The <see cref="IServerApplicationHost"/>.</param>
|
||||
public static void AddDlnaServices(
|
||||
this IServiceCollection services,
|
||||
IServerApplicationHost applicationHost)
|
||||
{
|
||||
services.AddHttpClient(NamedClient.Dlna, c =>
|
||||
{
|
||||
c.DefaultRequestHeaders.UserAgent.ParseAdd(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}/{1} UPnP/1.0 {2}/{3}",
|
||||
Environment.OSVersion.Platform,
|
||||
Environment.OSVersion,
|
||||
applicationHost.Name,
|
||||
applicationHost.ApplicationVersionString));
|
||||
|
||||
c.DefaultRequestHeaders.Add("CPFN.UPNP.ORG", applicationHost.FriendlyName); // Required for UPnP DeviceArchitecture v2.0
|
||||
c.DefaultRequestHeaders.Add("FriendlyName.DLNA.ORG", applicationHost.FriendlyName); // REVIEW: where does this come from?
|
||||
})
|
||||
.ConfigurePrimaryHttpMessageHandler(_ => new SocketsHttpHandler
|
||||
{
|
||||
AutomaticDecompression = DecompressionMethods.All,
|
||||
RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8
|
||||
});
|
||||
|
||||
services.AddSingleton<IDlnaManager, DlnaManager>();
|
||||
services.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
|
||||
services.AddSingleton<IContentDirectory, ContentDirectoryService>();
|
||||
services.AddSingleton<IConnectionManager, ConnectionManagerService>();
|
||||
services.AddSingleton<IMediaReceiverRegistrar, MediaReceiverRegistrarService>();
|
||||
|
||||
services.AddSingleton<ISsdpCommunicationsServer>(provider => new SsdpCommunicationsServer(
|
||||
provider.GetRequiredService<ISocketFactory>(),
|
||||
provider.GetRequiredService<INetworkManager>(),
|
||||
provider.GetRequiredService<ILogger<SsdpCommunicationsServer>>())
|
||||
{
|
||||
IsShared = true
|
||||
});
|
||||
|
||||
services.AddHostedService<DlnaHost>();
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public interface IConnectionManager : IDlnaEventManager, IUpnpService
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public interface IContentDirectory : IDlnaEventManager, IUpnpService
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public interface IDlnaEventManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Cancels the event subscription.
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">The subscription identifier.</param>
|
||||
/// <returns>The response.</returns>
|
||||
EventSubscriptionResponse CancelEventSubscription(string subscriptionId);
|
||||
|
||||
/// <summary>
|
||||
/// Renews the event subscription.
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">The subscription identifier.</param>
|
||||
/// <param name="notificationType">The notification type.</param>
|
||||
/// <param name="requestedTimeoutString">The requested timeout as a string.</param>
|
||||
/// <param name="callbackUrl">The callback url.</param>
|
||||
/// <returns>The response.</returns>
|
||||
EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl);
|
||||
|
||||
/// <summary>
|
||||
/// Creates the event subscription.
|
||||
/// </summary>
|
||||
/// <param name="notificationType">The notification type.</param>
|
||||
/// <param name="requestedTimeoutString">The requested timeout as a string.</param>
|
||||
/// <param name="callbackUrl">The callback url.</param>
|
||||
/// <returns>The response.</returns>
|
||||
EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl);
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public interface IMediaReceiverRegistrar : IDlnaEventManager, IUpnpService
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public interface IUpnpService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the content directory XML.
|
||||
/// </summary>
|
||||
/// <returns>System.String.</returns>
|
||||
string GetServiceXml();
|
||||
|
||||
/// <summary>
|
||||
/// Processes the control request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>ControlResponse.</returns>
|
||||
Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request);
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 740 B |
Before Width: | Height: | Size: 278 B |
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 2.1 KiB |
|
@ -1,387 +0,0 @@
|
|||
#pragma warning disable CA1031 // Do not catch general exception types.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.PlayTo;
|
||||
using Emby.Dlna.Ssdp;
|
||||
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.MediaEncoding;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Rssdp;
|
||||
using Rssdp.Infrastructure;
|
||||
|
||||
namespace Emby.Dlna.Main;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="IHostedService"/> that manages a DLNA server.
|
||||
/// </summary>
|
||||
public sealed class DlnaHost : IHostedService, IDisposable
|
||||
{
|
||||
private readonly ILogger<DlnaHost> _logger;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
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 readonly ISsdpCommunicationsServer _communicationsServer;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly object _syncLock = new();
|
||||
|
||||
private SsdpDevicePublisher? _publisher;
|
||||
private PlayToManager? _manager;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DlnaHost"/> class.
|
||||
/// </summary>
|
||||
/// <param name="config">The <see cref="IServerConfigurationManager"/>.</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
/// <param name="appHost">The <see cref="IServerApplicationHost"/>.</param>
|
||||
/// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
|
||||
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
|
||||
/// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
|
||||
/// <param name="userManager">The <see cref="IUserManager"/>.</param>
|
||||
/// <param name="dlnaManager">The <see cref="IDlnaManager"/>.</param>
|
||||
/// <param name="imageProcessor">The <see cref="IImageProcessor"/>.</param>
|
||||
/// <param name="userDataManager">The <see cref="IUserDataManager"/>.</param>
|
||||
/// <param name="localizationManager">The <see cref="ILocalizationManager"/>.</param>
|
||||
/// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/>.</param>
|
||||
/// <param name="deviceDiscovery">The <see cref="IDeviceDiscovery"/>.</param>
|
||||
/// <param name="mediaEncoder">The <see cref="IMediaEncoder"/>.</param>
|
||||
/// <param name="communicationsServer">The <see cref="ISsdpCommunicationsServer"/>.</param>
|
||||
/// <param name="networkManager">The <see cref="INetworkManager"/>.</param>
|
||||
public DlnaHost(
|
||||
IServerConfigurationManager config,
|
||||
ILoggerFactory loggerFactory,
|
||||
IServerApplicationHost appHost,
|
||||
ISessionManager sessionManager,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILibraryManager libraryManager,
|
||||
IUserManager userManager,
|
||||
IDlnaManager dlnaManager,
|
||||
IImageProcessor imageProcessor,
|
||||
IUserDataManager userDataManager,
|
||||
ILocalizationManager localizationManager,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IDeviceDiscovery deviceDiscovery,
|
||||
IMediaEncoder mediaEncoder,
|
||||
ISsdpCommunicationsServer communicationsServer,
|
||||
INetworkManager networkManager)
|
||||
{
|
||||
_config = config;
|
||||
_appHost = appHost;
|
||||
_sessionManager = sessionManager;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
_dlnaManager = dlnaManager;
|
||||
_imageProcessor = imageProcessor;
|
||||
_userDataManager = userDataManager;
|
||||
_localization = localizationManager;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_communicationsServer = communicationsServer;
|
||||
_networkManager = networkManager;
|
||||
_logger = loggerFactory.CreateLogger<DlnaHost>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var netConfig = _config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
|
||||
if (_appHost.ListenWithHttps && netConfig.RequireHttps)
|
||||
{
|
||||
if (_config.GetDlnaConfiguration().EnableServer)
|
||||
{
|
||||
_logger.LogError("The DLNA specification does not support HTTPS.");
|
||||
}
|
||||
|
||||
// No use starting as dlna won't work, as we're running purely on HTTPS.
|
||||
return;
|
||||
}
|
||||
|
||||
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
|
||||
ReloadComponents();
|
||||
|
||||
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Stop();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
Stop();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNamedConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs e)
|
||||
{
|
||||
if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ReloadComponents();
|
||||
}
|
||||
}
|
||||
|
||||
private void ReloadComponents()
|
||||
{
|
||||
var options = _config.GetDlnaConfiguration();
|
||||
StartDeviceDiscovery();
|
||||
|
||||
if (options.EnableServer)
|
||||
{
|
||||
StartDevicePublisher(options);
|
||||
}
|
||||
else
|
||||
{
|
||||
DisposeDevicePublisher();
|
||||
}
|
||||
|
||||
if (options.EnablePlayTo)
|
||||
{
|
||||
StartPlayToManager();
|
||||
}
|
||||
else
|
||||
{
|
||||
DisposePlayToManager();
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateUuid(string text)
|
||||
{
|
||||
if (!Guid.TryParse(text, out var guid))
|
||||
{
|
||||
guid = text.GetMD5();
|
||||
}
|
||||
|
||||
return guid.ToString("D", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static void SetProperties(SsdpDevice device, string fullDeviceType)
|
||||
{
|
||||
var serviceParts = fullDeviceType
|
||||
.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Split(':');
|
||||
|
||||
device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-');
|
||||
device.DeviceClass = serviceParts[1];
|
||||
device.DeviceType = serviceParts[2];
|
||||
}
|
||||
|
||||
private void StartDeviceDiscovery()
|
||||
{
|
||||
try
|
||||
{
|
||||
((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error starting device discovery");
|
||||
}
|
||||
}
|
||||
|
||||
private void StartDevicePublisher(Configuration.DlnaOptions options)
|
||||
{
|
||||
if (_publisher is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_publisher = new SsdpDevicePublisher(
|
||||
_communicationsServer,
|
||||
Environment.OSVersion.Platform.ToString(),
|
||||
// Can not use VersionString here since that includes OS and version
|
||||
Environment.OSVersion.Version.ToString(),
|
||||
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
||||
{
|
||||
LogFunction = msg => _logger.LogDebug("{Msg}", msg),
|
||||
SupportPnpRootDevice = false
|
||||
};
|
||||
|
||||
RegisterServerEndpoints();
|
||||
|
||||
if (options.BlastAliveMessages)
|
||||
{
|
||||
_publisher.StartSendingAliveNotifications(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error registering endpoint");
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterServerEndpoints()
|
||||
{
|
||||
var udn = CreateUuid(_appHost.SystemId);
|
||||
var descriptorUri = "/dlna/" + udn + "/description.xml";
|
||||
|
||||
// Only get bind addresses in LAN
|
||||
// IPv6 is currently unsupported
|
||||
var validInterfaces = _networkManager.GetInternalBindAddresses()
|
||||
.Where(x => x.AddressFamily != AddressFamily.InterNetworkV6)
|
||||
.ToList();
|
||||
|
||||
if (validInterfaces.Count == 0)
|
||||
{
|
||||
// No interfaces returned, fall back to loopback
|
||||
validInterfaces = _networkManager.GetLoopbacks().ToList();
|
||||
}
|
||||
|
||||
foreach (var intf in validInterfaces)
|
||||
{
|
||||
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
|
||||
|
||||
_logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, intf.Address);
|
||||
|
||||
var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(intf.Address, false) + descriptorUri);
|
||||
|
||||
var device = new SsdpRootDevice
|
||||
{
|
||||
CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
|
||||
Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document.
|
||||
Address = intf.Address,
|
||||
PrefixLength = NetworkUtils.MaskToCidr(intf.Subnet.Prefix),
|
||||
FriendlyName = "Jellyfin",
|
||||
Manufacturer = "Jellyfin",
|
||||
ModelName = "Jellyfin Server",
|
||||
Uuid = udn
|
||||
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
|
||||
};
|
||||
|
||||
SetProperties(device, fullService);
|
||||
_publisher!.AddDevice(device);
|
||||
|
||||
var embeddedDevices = new[]
|
||||
{
|
||||
"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.
|
||||
};
|
||||
|
||||
SetProperties(embeddedDevice, subDevice);
|
||||
device.AddDevice(embeddedDevice);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void StartPlayToManager()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_manager is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_manager = new PlayToManager(
|
||||
_logger,
|
||||
_sessionManager,
|
||||
_libraryManager,
|
||||
_userManager,
|
||||
_dlnaManager,
|
||||
_appHost,
|
||||
_imageProcessor,
|
||||
_deviceDiscovery,
|
||||
_httpClientFactory,
|
||||
_userDataManager,
|
||||
_localization,
|
||||
_mediaSourceManager,
|
||||
_mediaEncoder);
|
||||
|
||||
_manager.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error starting PlayTo manager");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposePlayToManager()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_manager is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Disposing PlayToManager");
|
||||
_manager.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error disposing PlayTo manager");
|
||||
}
|
||||
|
||||
_manager = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeDevicePublisher()
|
||||
{
|
||||
if (_publisher is not null)
|
||||
{
|
||||
_logger.LogInformation("Disposing SsdpDevicePublisher");
|
||||
_publisher.Dispose();
|
||||
_publisher = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void Stop()
|
||||
{
|
||||
DisposeDevicePublisher();
|
||||
DisposePlayToManager();
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml;
|
||||
using Emby.Dlna.Service;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ControlHandler" />.
|
||||
/// </summary>
|
||||
public class ControlHandler : BaseControlHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ControlHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param>
|
||||
public ControlHandler(IServerConfigurationManager config, ILogger logger)
|
||||
: base(config, logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter)
|
||||
{
|
||||
if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
HandleIsAuthorized(xmlWriter);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(methodName, "IsValidated", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
HandleIsValidated(xmlWriter);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records that the handle is authorized in the xml stream.
|
||||
/// </summary>
|
||||
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
|
||||
private static void HandleIsAuthorized(XmlWriter xmlWriter)
|
||||
=> xmlWriter.WriteElementString("Result", "1");
|
||||
|
||||
/// <summary>
|
||||
/// Records that the handle is validated in the xml stream.
|
||||
/// </summary>
|
||||
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
|
||||
private static void HandleIsValidated(XmlWriter xmlWriter)
|
||||
=> xmlWriter.WriteElementString("Result", "1");
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.Service;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="MediaReceiverRegistrarService" />.
|
||||
/// </summary>
|
||||
public class MediaReceiverRegistrarService : BaseService, IMediaReceiverRegistrar
|
||||
{
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MediaReceiverRegistrarService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The <see cref="ILogger{MediaReceiverRegistrarService}"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
|
||||
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
|
||||
/// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="MediaReceiverRegistrarService"/> instance.</param>
|
||||
public MediaReceiverRegistrarService(
|
||||
ILogger<MediaReceiverRegistrarService> logger,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IServerConfigurationManager config)
|
||||
: base(logger, httpClientFactory)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetServiceXml()
|
||||
{
|
||||
return MediaReceiverRegistrarXmlBuilder.GetXml();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
|
||||
{
|
||||
return new ControlHandler(
|
||||
_config,
|
||||
Logger)
|
||||
.ProcessControlRequestAsync(request);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
using Emby.Dlna.Service;
|
||||
|
||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="MediaReceiverRegistrarXmlBuilder" />.
|
||||
/// See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-drmnd/5d37515e-7a63-4709-8258-8fd4e0ed4482.
|
||||
/// </summary>
|
||||
public static class MediaReceiverRegistrarXmlBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves an XML description of the X_MS_MediaReceiverRegistrar.
|
||||
/// </summary>
|
||||
/// <returns>An XML representation of this service.</returns>
|
||||
public static string GetXml()
|
||||
{
|
||||
return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The a list of all the state variables for this invocation.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
|
||||
private static IEnumerable<StateVariable> GetStateVariables()
|
||||
{
|
||||
var list = new List<StateVariable>
|
||||
{
|
||||
new StateVariable
|
||||
{
|
||||
Name = "AuthorizationGrantedUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_DeviceID",
|
||||
DataType = "string",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "AuthorizationDeniedUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "ValidationSucceededUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RegistrationRespMsg",
|
||||
DataType = "bin.base64",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_RegistrationReqMsg",
|
||||
DataType = "bin.base64",
|
||||
SendsEvents = false
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "ValidationRevokedUpdateID",
|
||||
DataType = "ui4",
|
||||
SendsEvents = true
|
||||
},
|
||||
|
||||
new StateVariable
|
||||
{
|
||||
Name = "A_ARG_TYPE_Result",
|
||||
DataType = "int",
|
||||
SendsEvents = false
|
||||
}
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,187 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
|
||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ServiceActionListBuilder" />.
|
||||
/// </summary>
|
||||
public static class ServiceActionListBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a list of services that this instance provides.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
|
||||
public static IEnumerable<ServiceAction> GetActions()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
GetIsValidated(),
|
||||
GetIsAuthorized(),
|
||||
GetRegisterDevice(),
|
||||
GetGetAuthorizationDeniedUpdateID(),
|
||||
GetGetAuthorizationGrantedUpdateID(),
|
||||
GetGetValidationRevokedUpdateID(),
|
||||
GetGetValidationSucceededUpdateID()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "IsValidated".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "IsAuthorized".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "RegisterDevice".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetValidationSucceededUpdateID".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetGetValidationSucceededUpdateID()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetValidationSucceededUpdateID"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ValidationSucceededUpdateID",
|
||||
Direction = "out"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetGetAuthorizationDeniedUpdateID".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetGetAuthorizationDeniedUpdateID()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetAuthorizationDeniedUpdateID"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "AuthorizationDeniedUpdateID",
|
||||
Direction = "out"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetValidationRevokedUpdateID".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetGetValidationRevokedUpdateID()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetValidationRevokedUpdateID"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "ValidationRevokedUpdateID",
|
||||
Direction = "out"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action details for "GetAuthorizationGrantedUpdateID".
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ServiceAction"/>.</returns>
|
||||
private static ServiceAction GetGetAuthorizationGrantedUpdateID()
|
||||
{
|
||||
var action = new ServiceAction
|
||||
{
|
||||
Name = "GetAuthorizationGrantedUpdateID"
|
||||
};
|
||||
|
||||
action.ArgumentList.Add(new Argument
|
||||
{
|
||||
Name = "AuthorizationGrantedUpdateID",
|
||||
Direction = "out"
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Common;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class DeviceInfo
|
||||
{
|
||||
private readonly List<DeviceService> _services = new List<DeviceService>();
|
||||
private string _baseUrl = string.Empty;
|
||||
|
||||
public DeviceInfo()
|
||||
{
|
||||
Name = "Generic Device";
|
||||
}
|
||||
|
||||
public string UUID { get; set; }
|
||||
|
||||
public string Name { 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; }
|
||||
|
||||
public string BaseUrl
|
||||
{
|
||||
get => _baseUrl;
|
||||
set => _baseUrl = value;
|
||||
}
|
||||
|
||||
public DeviceIcon Icon { get; set; }
|
||||
|
||||
public List<DeviceService> Services => _services;
|
||||
|
||||
public DeviceIdentification ToDeviceIdentification()
|
||||
{
|
||||
return new DeviceIdentification
|
||||
{
|
||||
Manufacturer = Manufacturer,
|
||||
ModelName = ModelName,
|
||||
ModelNumber = ModelNumber,
|
||||
FriendlyName = Name,
|
||||
ManufacturerUrl = ManufacturerUrl,
|
||||
ModelUrl = ModelUrl,
|
||||
ModelDescription = ModelDescription,
|
||||
SerialNumber = SerialNumber
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,137 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Emby.Dlna.Common;
|
||||
using MediaBrowser.Common.Net;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
/// <summary>
|
||||
/// Http client for Dlna PlayTo function.
|
||||
/// </summary>
|
||||
public partial class DlnaHttpClient
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public DlnaHttpClient(ILogger logger, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
[GeneratedRegex("(&(?![a-z]*;))")]
|
||||
private static partial Regex EscapeAmpersandRegex();
|
||||
|
||||
private static 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 async Task<XDocument?> SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient(NamedClient.Dlna);
|
||||
using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
try
|
||||
{
|
||||
return await XDocument.LoadAsync(
|
||||
stream,
|
||||
LoadOptions.None,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (XmlException)
|
||||
{
|
||||
// try correcting the Xml response with common errors
|
||||
stream.Position = 0;
|
||||
using StreamReader sr = new StreamReader(stream);
|
||||
var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// find and replace unescaped ampersands (&)
|
||||
xmlString = EscapeAmpersandRegex().Replace(xmlString, "&");
|
||||
|
||||
try
|
||||
{
|
||||
// retry reading Xml
|
||||
using var xmlReader = new StringReader(xmlString);
|
||||
return await XDocument.LoadAsync(
|
||||
xmlReader,
|
||||
LoadOptions.None,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (XmlException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to parse response");
|
||||
_logger.LogDebug("Malformed response: {Content}\n", xmlString);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<XDocument?> GetDataAsync(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
|
||||
// Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
|
||||
return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<XDocument?> SendCommandAsync(
|
||||
string baseUrl,
|
||||
DeviceService service,
|
||||
string command,
|
||||
string postData,
|
||||
string? header = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, NormalizeServiceUrl(baseUrl, service.ControlUrl))
|
||||
{
|
||||
Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml)
|
||||
};
|
||||
|
||||
request.Headers.TryAddWithoutValidation(
|
||||
"SOAPACTION",
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"\"{0}#{1}\"",
|
||||
service.ServiceType,
|
||||
command));
|
||||
request.Headers.Pragma.ParseAdd("no-cache");
|
||||
|
||||
if (!string.IsNullOrEmpty(header))
|
||||
{
|
||||
request.Headers.TryAddWithoutValidation("contentFeatures.dlna.org", header);
|
||||
}
|
||||
|
||||
// Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
|
||||
return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class MediaChangedEventArgs : EventArgs
|
||||
{
|
||||
public MediaChangedEventArgs(UBaseObject oldMediaInfo, UBaseObject newMediaInfo)
|
||||
{
|
||||
OldMediaInfo = oldMediaInfo;
|
||||
NewMediaInfo = newMediaInfo;
|
||||
}
|
||||
|
||||
public UBaseObject OldMediaInfo { get; set; }
|
||||
|
||||
public UBaseObject NewMediaInfo { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,980 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.Didl;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Photo = MediaBrowser.Controller.Entities.Photo;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class PlayToController : ISessionController, IDisposable
|
||||
{
|
||||
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 IMediaEncoder _mediaEncoder;
|
||||
|
||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
||||
private readonly string _serverAddress;
|
||||
private readonly string? _accessToken;
|
||||
|
||||
private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
|
||||
private Device _device;
|
||||
private int _currentPlaylistIndex;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
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,
|
||||
IMediaEncoder mediaEncoder,
|
||||
Device device)
|
||||
{
|
||||
_session = session;
|
||||
_sessionManager = sessionManager;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_dlnaManager = dlnaManager;
|
||||
_userManager = userManager;
|
||||
_imageProcessor = imageProcessor;
|
||||
_serverAddress = serverAddress;
|
||||
_accessToken = accessToken;
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
_userDataManager = userDataManager;
|
||||
_localization = localization;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
|
||||
_device = device;
|
||||
_device.OnDeviceUnavailable = OnDeviceUnavailable;
|
||||
_device.PlaybackStart += OnDevicePlaybackStart;
|
||||
_device.PlaybackProgress += OnDevicePlaybackProgress;
|
||||
_device.PlaybackStopped += OnDevicePlaybackStopped;
|
||||
_device.MediaChanged += OnDeviceMediaChanged;
|
||||
|
||||
_device.Start();
|
||||
|
||||
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
|
||||
}
|
||||
|
||||
public bool IsSessionActive => !_disposed;
|
||||
|
||||
public bool SupportsMediaControl => IsSessionActive;
|
||||
|
||||
/*
|
||||
* Send a message to the DLNA device to notify what is the next track in the playlist.
|
||||
*/
|
||||
private async Task SendNextTrackMessage(int currentPlayListItemIndex, CancellationToken cancellationToken)
|
||||
{
|
||||
if (currentPlayListItemIndex >= 0 && currentPlayListItemIndex < _playlist.Count - 1)
|
||||
{
|
||||
// The current playing item is indeed in the play list and we are not yet at the end of the playlist.
|
||||
var nextItemIndex = currentPlayListItemIndex + 1;
|
||||
var nextItem = _playlist[nextItemIndex];
|
||||
|
||||
// Send the SetNextAvTransport message.
|
||||
await _device.SetNextAvTransport(nextItem.StreamUrl, GetDlnaHeaders(nextItem), nextItem.Didl, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDeviceUnavailable()
|
||||
{
|
||||
try
|
||||
{
|
||||
_sessionManager.ReportSessionEnded(_session.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Could throw if the session is already gone
|
||||
_logger.LogError(ex, "Error reporting the end of session {Id}", _session.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDeviceDiscoveryDeviceLeft(object? sender, GenericEventArgs<UpnpDeviceInfo> e)
|
||||
{
|
||||
var info = e.Argument;
|
||||
|
||||
if (!_disposed
|
||||
&& info.Headers.TryGetValue("USN", out string? usn)
|
||||
&& usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1
|
||||
&& (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1
|
||||
|| (info.Headers.TryGetValue("NT", out string? nt)
|
||||
&& nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)))
|
||||
{
|
||||
OnDeviceUnavailable();
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnDeviceMediaChanged(object? sender, MediaChangedEventArgs e)
|
||||
{
|
||||
if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var streamInfo = StreamParams.ParseFromUrl(e.OldMediaInfo.Url, _libraryManager, _mediaSourceManager);
|
||||
if (streamInfo.Item is not null)
|
||||
{
|
||||
var positionTicks = GetProgressPositionTicks(streamInfo);
|
||||
|
||||
await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
streamInfo = StreamParams.ParseFromUrl(e.NewMediaInfo.Url, _libraryManager, _mediaSourceManager);
|
||||
if (streamInfo.Item is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var newItemProgress = GetProgressInfo(streamInfo);
|
||||
|
||||
await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
|
||||
|
||||
// Send a message to the DLNA device to notify what is the next track in the playlist.
|
||||
var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId.Equals(streamInfo.ItemId));
|
||||
if (currentItemIndex >= 0)
|
||||
{
|
||||
_currentPlaylistIndex = currentItemIndex;
|
||||
}
|
||||
|
||||
await SendNextTrackMessage(currentItemIndex, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error reporting progress");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnDevicePlaybackStopped(object? sender, PlaybackStoppedEventArgs e)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var streamInfo = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager);
|
||||
|
||||
if (streamInfo.Item is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var positionTicks = GetProgressPositionTicks(streamInfo);
|
||||
|
||||
await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
|
||||
|
||||
var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var duration = mediaSource is null
|
||||
? _device.Duration?.Ticks
|
||||
: 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.LogError(ex, "Error reporting playback stopped");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReportPlaybackStopped(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.LogError(ex, "Error reporting progress");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnDevicePlaybackStart(object? sender, PlaybackStartEventArgs e)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var info = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager);
|
||||
|
||||
if (info.Item is not null)
|
||||
{
|
||||
var progress = GetProgressInfo(info);
|
||||
|
||||
await _sessionManager.OnPlaybackStart(progress).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error reporting progress");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnDevicePlaybackProgress(object? sender, PlaybackProgressEventArgs e)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var mediaUrl = e.MediaInfo.Url;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(mediaUrl))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var info = StreamParams.ParseFromUrl(mediaUrl, _libraryManager, _mediaSourceManager);
|
||||
|
||||
if (info.Item is not null)
|
||||
{
|
||||
var progress = GetProgressInfo(info);
|
||||
|
||||
await _sessionManager.OnPlaybackProgress(progress).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error reporting progress");
|
||||
}
|
||||
}
|
||||
|
||||
private long? GetProgressPositionTicks(StreamParams info)
|
||||
{
|
||||
var ticks = _device.Position.Ticks;
|
||||
|
||||
if (!EnableClientSideSeek(info))
|
||||
{
|
||||
ticks += info.StartPositionTicks;
|
||||
}
|
||||
|
||||
return ticks;
|
||||
}
|
||||
|
||||
private PlaybackStartInfo GetProgressInfo(StreamParams info)
|
||||
{
|
||||
return new PlaybackStartInfo
|
||||
{
|
||||
ItemId = info.ItemId,
|
||||
SessionId = _session.Id,
|
||||
PositionTicks = GetProgressPositionTicks(info),
|
||||
IsMuted = _device.IsMuted,
|
||||
IsPaused = _device.IsPaused,
|
||||
MediaSourceId = info.MediaSourceId,
|
||||
AudioStreamIndex = info.AudioStreamIndex,
|
||||
SubtitleStreamIndex = info.SubtitleStreamIndex,
|
||||
VolumeLevel = _device.Volume,
|
||||
|
||||
CanSeek = true,
|
||||
|
||||
PlayMethod = info.IsDirectStream ? PlayMethod.DirectStream : PlayMethod.Transcode
|
||||
};
|
||||
}
|
||||
|
||||
public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogDebug("{0} - Received PlayRequest: {1}", _session.DeviceName, command.PlayCommand);
|
||||
|
||||
var user = command.ControllingUserId.Equals(default)
|
||||
? null :
|
||||
_userManager.GetUserById(command.ControllingUserId);
|
||||
|
||||
var items = new List<BaseItem>();
|
||||
foreach (var id in command.ItemIds)
|
||||
{
|
||||
AddItemFromId(id, items);
|
||||
}
|
||||
|
||||
var startIndex = command.StartIndex ?? 0;
|
||||
int len = items.Count - startIndex;
|
||||
if (startIndex > 0)
|
||||
{
|
||||
items = items.GetRange(startIndex, len);
|
||||
}
|
||||
|
||||
var playlist = new PlaylistItem[len];
|
||||
|
||||
// Not nullable enabled - so this is required.
|
||||
playlist[0] = CreatePlaylistItem(
|
||||
items[0],
|
||||
user,
|
||||
command.StartPositionTicks ?? 0,
|
||||
command.MediaSourceId ?? string.Empty,
|
||||
command.AudioStreamIndex,
|
||||
command.SubtitleStreamIndex);
|
||||
|
||||
for (int i = 1; i < len; i++)
|
||||
{
|
||||
playlist[i] = CreatePlaylistItem(items[i], user, 0, string.Empty, null, null);
|
||||
}
|
||||
|
||||
_logger.LogDebug("{0} - Playlist created", _session.DeviceName);
|
||||
|
||||
if (command.PlayCommand == PlayCommand.PlayLast)
|
||||
{
|
||||
_playlist.AddRange(playlist);
|
||||
}
|
||||
|
||||
if (command.PlayCommand == PlayCommand.PlayNext)
|
||||
{
|
||||
_playlist.AddRange(playlist);
|
||||
}
|
||||
|
||||
if (!command.ControllingUserId.Equals(default))
|
||||
{
|
||||
_sessionManager.LogSessionActivity(
|
||||
_session.Client,
|
||||
_session.ApplicationVersion,
|
||||
_session.DeviceId,
|
||||
_session.DeviceName,
|
||||
_session.RemoteEndPoint,
|
||||
user);
|
||||
}
|
||||
|
||||
return PlayItems(playlist, cancellationToken);
|
||||
}
|
||||
|
||||
private Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken)
|
||||
{
|
||||
switch (command.Command)
|
||||
{
|
||||
case PlaystateCommand.Stop:
|
||||
_playlist.Clear();
|
||||
return _device.SetStop(CancellationToken.None);
|
||||
|
||||
case PlaystateCommand.Pause:
|
||||
return _device.SetPause(CancellationToken.None);
|
||||
|
||||
case PlaystateCommand.Unpause:
|
||||
return _device.SetPlay(CancellationToken.None);
|
||||
|
||||
case PlaystateCommand.PlayPause:
|
||||
return _device.IsPaused ? _device.SetPlay(CancellationToken.None) : _device.SetPause(CancellationToken.None);
|
||||
|
||||
case PlaystateCommand.Seek:
|
||||
return Seek(command.SeekPositionTicks ?? 0);
|
||||
|
||||
case PlaystateCommand.NextTrack:
|
||||
return SetPlaylistIndex(_currentPlaylistIndex + 1, cancellationToken);
|
||||
|
||||
case PlaystateCommand.PreviousTrack:
|
||||
return SetPlaylistIndex(_currentPlaylistIndex - 1, cancellationToken);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task Seek(long newPosition)
|
||||
{
|
||||
var media = _device.CurrentMediaInfo;
|
||||
|
||||
if (media is not null)
|
||||
{
|
||||
var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager);
|
||||
|
||||
if (info.Item is not null && !EnableClientSideSeek(info))
|
||||
{
|
||||
var user = _session.UserId.Equals(default)
|
||||
? null
|
||||
: _userManager.GetUserById(_session.UserId);
|
||||
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
|
||||
|
||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
|
||||
await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private bool EnableClientSideSeek(StreamParams info)
|
||||
{
|
||||
return info.IsDirectStream;
|
||||
}
|
||||
|
||||
private bool EnableClientSideSeek(StreamInfo info)
|
||||
{
|
||||
return info.IsDirectStream;
|
||||
}
|
||||
|
||||
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 mediaSources = item is IHasMediaSources
|
||||
? _mediaSourceManager.GetStaticMediaSources(item, true, user).ToArray()
|
||||
: Array.Empty<MediaSourceInfo>();
|
||||
|
||||
var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex);
|
||||
playlistItem.StreamInfo.StartPositionTicks = startPostionTicks;
|
||||
|
||||
playlistItem.StreamUrl = DidlBuilder.NormalizeDlnaMediaUrl(playlistItem.StreamInfo.ToUrl(_serverAddress, _accessToken));
|
||||
|
||||
var itemXml = new DidlBuilder(
|
||||
profile,
|
||||
user,
|
||||
_imageProcessor,
|
||||
_serverAddress,
|
||||
_accessToken,
|
||||
_userDataManager,
|
||||
_localization,
|
||||
_mediaSourceManager,
|
||||
_logger,
|
||||
_mediaEncoder,
|
||||
_libraryManager)
|
||||
.GetItemDidl(item, user, 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 ContentFeatureBuilder.BuildAudioHeader(
|
||||
profile,
|
||||
streamInfo.Container,
|
||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||
streamInfo.TargetAudioBitrate,
|
||||
streamInfo.TargetAudioSampleRate,
|
||||
streamInfo.TargetAudioChannels,
|
||||
streamInfo.TargetAudioBitDepth,
|
||||
streamInfo.IsDirectStream,
|
||||
streamInfo.RunTimeTicks ?? 0,
|
||||
streamInfo.TranscodeSeekInfo);
|
||||
}
|
||||
|
||||
if (streamInfo.MediaType == DlnaProfileType.Video)
|
||||
{
|
||||
var list = ContentFeatureBuilder.BuildVideoHeader(
|
||||
profile,
|
||||
streamInfo.Container,
|
||||
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||
streamInfo.TargetWidth,
|
||||
streamInfo.TargetHeight,
|
||||
streamInfo.TargetVideoBitDepth,
|
||||
streamInfo.TargetVideoBitrate,
|
||||
streamInfo.TargetTimestamp,
|
||||
streamInfo.IsDirectStream,
|
||||
streamInfo.RunTimeTicks ?? 0,
|
||||
streamInfo.TargetVideoProfile,
|
||||
streamInfo.TargetVideoRangeType,
|
||||
streamInfo.TargetVideoLevel,
|
||||
streamInfo.TargetFramerate ?? 0,
|
||||
streamInfo.TargetPacketLength,
|
||||
streamInfo.TranscodeSeekInfo,
|
||||
streamInfo.IsTargetAnamorphic,
|
||||
streamInfo.IsTargetInterlaced,
|
||||
streamInfo.TargetRefFrames,
|
||||
streamInfo.TargetVideoStreamCount,
|
||||
streamInfo.TargetAudioStreamCount,
|
||||
streamInfo.TargetVideoCodecTag,
|
||||
streamInfo.IsTargetAVC);
|
||||
|
||||
return list.FirstOrDefault();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string? mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
|
||||
{
|
||||
if (item.MediaType == MediaType.Video)
|
||||
{
|
||||
return new PlaylistItem
|
||||
{
|
||||
StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions
|
||||
{
|
||||
ItemId = item.Id,
|
||||
MediaSources = mediaSources,
|
||||
Profile = profile,
|
||||
DeviceId = deviceId,
|
||||
MaxBitrate = profile.MaxStreamingBitrate,
|
||||
MediaSourceId = mediaSourceId,
|
||||
AudioStreamIndex = audioStreamIndex,
|
||||
SubtitleStreamIndex = subtitleStreamIndex
|
||||
}),
|
||||
|
||||
Profile = profile
|
||||
};
|
||||
}
|
||||
|
||||
if (item.MediaType == MediaType.Audio)
|
||||
{
|
||||
return new PlaylistItem
|
||||
{
|
||||
StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions
|
||||
{
|
||||
ItemId = item.Id,
|
||||
MediaSources = mediaSources,
|
||||
Profile = profile,
|
||||
DeviceId = deviceId,
|
||||
MaxBitrate = profile.MaxStreamingBitrate,
|
||||
MediaSourceId = mediaSourceId
|
||||
}),
|
||||
|
||||
Profile = profile
|
||||
};
|
||||
}
|
||||
|
||||
if (item.MediaType == MediaType.Photo)
|
||||
{
|
||||
return PlaylistItemFactory.Create((Photo)item, profile);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Unrecognized item type.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays the items.
|
||||
/// </summary>
|
||||
/// <param name="items">The items.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns><c>true</c> on success.</returns>
|
||||
private async Task<bool> PlayItems(IEnumerable<PlaylistItem> items, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_playlist.Clear();
|
||||
_playlist.AddRange(items);
|
||||
_logger.LogDebug("{0} - Playing {1} items", _session.DeviceName, _playlist.Count);
|
||||
|
||||
await SetPlaylistIndex(0, cancellationToken).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task SetPlaylistIndex(int index, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (index < 0 || index >= _playlist.Count)
|
||||
{
|
||||
_playlist.Clear();
|
||||
await _device.SetStop(cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_currentPlaylistIndex = index;
|
||||
var currentitem = _playlist[index];
|
||||
|
||||
await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||
await SendNextTrackMessage(index, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var streamInfo = currentitem.StreamInfo;
|
||||
if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
|
||||
{
|
||||
await SeekAfterTransportChange(streamInfo.StartPositionTicks, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and optionally managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_device.PlaybackStart -= OnDevicePlaybackStart;
|
||||
_device.PlaybackProgress -= OnDevicePlaybackProgress;
|
||||
_device.PlaybackStopped -= OnDevicePlaybackStopped;
|
||||
_device.MediaChanged -= OnDeviceMediaChanged;
|
||||
_deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
|
||||
_device.OnDeviceUnavailable = null;
|
||||
_device.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
switch (command.Name)
|
||||
{
|
||||
case GeneralCommandType.VolumeDown:
|
||||
return _device.VolumeDown(cancellationToken);
|
||||
case GeneralCommandType.VolumeUp:
|
||||
return _device.VolumeUp(cancellationToken);
|
||||
case GeneralCommandType.Mute:
|
||||
return _device.Mute(cancellationToken);
|
||||
case GeneralCommandType.Unmute:
|
||||
return _device.Unmute(cancellationToken);
|
||||
case GeneralCommandType.ToggleMute:
|
||||
return _device.ToggleMute(cancellationToken);
|
||||
case GeneralCommandType.SetAudioStreamIndex:
|
||||
if (command.Arguments.TryGetValue("Index", out string? index))
|
||||
{
|
||||
if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||
{
|
||||
return SetAudioStreamIndex(val);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
|
||||
}
|
||||
|
||||
throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
|
||||
case GeneralCommandType.SetSubtitleStreamIndex:
|
||||
if (command.Arguments.TryGetValue("Index", out index))
|
||||
{
|
||||
if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||
{
|
||||
return SetSubtitleStreamIndex(val);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
|
||||
}
|
||||
|
||||
throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
|
||||
case GeneralCommandType.SetVolume:
|
||||
if (command.Arguments.TryGetValue("Volume", out string? vol))
|
||||
{
|
||||
if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
|
||||
{
|
||||
return _device.SetVolume(volume, cancellationToken);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Unsupported volume value supplied.");
|
||||
}
|
||||
|
||||
throw new ArgumentException("Volume argument cannot be null");
|
||||
default:
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetAudioStreamIndex(int? newIndex)
|
||||
{
|
||||
var media = _device.CurrentMediaInfo;
|
||||
|
||||
if (media is not null)
|
||||
{
|
||||
var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager);
|
||||
|
||||
if (info.Item is not null)
|
||||
{
|
||||
var newPosition = GetProgressPositionTicks(info) ?? 0;
|
||||
|
||||
var user = _session.UserId.Equals(default)
|
||||
? null
|
||||
: _userManager.GetUserById(_session.UserId);
|
||||
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, newIndex, info.SubtitleStreamIndex);
|
||||
|
||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
|
||||
await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
if (EnableClientSideSeek(newItem.StreamInfo))
|
||||
{
|
||||
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetSubtitleStreamIndex(int? newIndex)
|
||||
{
|
||||
var media = _device.CurrentMediaInfo;
|
||||
|
||||
if (media is not null)
|
||||
{
|
||||
var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager);
|
||||
|
||||
if (info.Item is not null)
|
||||
{
|
||||
var newPosition = GetProgressPositionTicks(info) ?? 0;
|
||||
|
||||
var user = _session.UserId.Equals(default)
|
||||
? null
|
||||
: _userManager.GetUserById(_session.UserId);
|
||||
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, newIndex);
|
||||
|
||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
|
||||
await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
|
||||
{
|
||||
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SeekAfterTransportChange(long positionTicks, CancellationToken cancellationToken)
|
||||
{
|
||||
const int MaxWait = 15000000;
|
||||
const int Interval = 500;
|
||||
|
||||
var currentWait = 0;
|
||||
while (_device.TransportState != TransportState.PLAYING && currentWait < MaxWait)
|
||||
{
|
||||
await Task.Delay(Interval, cancellationToken).ConfigureAwait(false);
|
||||
currentWait += Interval;
|
||||
}
|
||||
|
||||
await _device.Seek(TimeSpan.FromTicks(positionTicks), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static int? GetIntValue(IReadOnlyDictionary<string, string> values, string name)
|
||||
{
|
||||
var value = values.GetValueOrDefault(name);
|
||||
|
||||
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static long GetLongValue(IReadOnlyDictionary<string, string> values, string name)
|
||||
{
|
||||
var value = values.GetValueOrDefault(name);
|
||||
|
||||
if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(GetType().Name);
|
||||
}
|
||||
|
||||
return name switch
|
||||
{
|
||||
SessionMessageType.Play => SendPlayCommand((data as PlayRequest)!, cancellationToken),
|
||||
SessionMessageType.Playstate => SendPlaystateCommand((data as PlaystateRequest)!, cancellationToken),
|
||||
SessionMessageType.GeneralCommand => SendGeneralCommand((data as GeneralCommand)!, cancellationToken),
|
||||
_ => Task.CompletedTask // Not supported or needed right now
|
||||
};
|
||||
}
|
||||
|
||||
private class StreamParams
|
||||
{
|
||||
private MediaSourceInfo? _mediaSource;
|
||||
private IMediaSourceManager? _mediaSourceManager;
|
||||
|
||||
public Guid 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 async Task<MediaSourceInfo?> GetMediaSource(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_mediaSource is not null)
|
||||
{
|
||||
return _mediaSource;
|
||||
}
|
||||
|
||||
if (Item is not IHasMediaSources)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_mediaSourceManager is not null)
|
||||
{
|
||||
_mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return _mediaSource;
|
||||
}
|
||||
|
||||
private static Guid GetItemId(string url)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(url);
|
||||
|
||||
var parts = url.Split('/');
|
||||
|
||||
for (var i = 0; i < parts.Length - 1; i++)
|
||||
{
|
||||
var part = parts[i];
|
||||
|
||||
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (Guid.TryParse(parts[i + 1], out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public static StreamParams ParseFromUrl(string url, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(url);
|
||||
|
||||
var request = new StreamParams
|
||||
{
|
||||
ItemId = GetItemId(url)
|
||||
};
|
||||
|
||||
if (request.ItemId.Equals(default))
|
||||
{
|
||||
return request;
|
||||
}
|
||||
|
||||
var index = url.IndexOf('?', StringComparison.Ordinal);
|
||||
if (index == -1)
|
||||
{
|
||||
return request;
|
||||
}
|
||||
|
||||
var query = url.Substring(index + 1);
|
||||
Dictionary<string, string> values = QueryHelpers.ParseQuery(query).ToDictionary(kv => kv.Key, kv => kv.Value.ToString());
|
||||
|
||||
request.DeviceProfileId = values.GetValueOrDefault("DeviceProfileId");
|
||||
request.DeviceId = values.GetValueOrDefault("DeviceId");
|
||||
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
|
||||
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
|
||||
request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
|
||||
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
|
||||
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
|
||||
request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");
|
||||
|
||||
request.Item = libraryManager.GetItemById(request.ItemId);
|
||||
|
||||
request._mediaSourceManager = mediaSourceManager;
|
||||
|
||||
return request;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,258 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public sealed 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 IHttpClientFactory _httpClientFactory;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
|
||||
private readonly SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
|
||||
private readonly CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
|
||||
private bool _disposed;
|
||||
|
||||
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
|
||||
{
|
||||
_logger = logger;
|
||||
_sessionManager = sessionManager;
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
_dlnaManager = dlnaManager;
|
||||
_appHost = appHost;
|
||||
_imageProcessor = imageProcessor;
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_userDataManager = userDataManager;
|
||||
_localization = localization;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
|
||||
}
|
||||
|
||||
private async void OnDeviceDiscoveryDeviceDiscovered(object? sender, GenericEventArgs<UpnpDeviceInfo> e)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var info = e.Argument;
|
||||
|
||||
if (!info.Headers.TryGetValue("USN", out string? usn))
|
||||
{
|
||||
usn = string.Empty;
|
||||
}
|
||||
|
||||
if (!info.Headers.TryGetValue("NT", out string? nt))
|
||||
{
|
||||
nt = string.Empty;
|
||||
}
|
||||
|
||||
// It has to report that it's a media renderer
|
||||
if (!usn.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase)
|
||||
&& !nt.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var cancellationToken = _disposeCancellationTokenSource.Token;
|
||||
|
||||
await _sessionLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_sessionManager.Sessions.Any(i => usn.IndexOf(i.DeviceId, StringComparison.OrdinalIgnoreCase) != -1))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await AddDevice(info, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error creating PlayTo device.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sessionLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
internal static string GetUuid(string usn)
|
||||
{
|
||||
const string UuidStr = "uuid:";
|
||||
const string UuidColonStr = "::";
|
||||
|
||||
var index = usn.IndexOf(UuidStr, StringComparison.OrdinalIgnoreCase);
|
||||
if (index == -1)
|
||||
{
|
||||
return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
ReadOnlySpan<char> tmp = usn.AsSpan()[(index + UuidStr.Length)..];
|
||||
|
||||
index = tmp.IndexOf(UuidColonStr, StringComparison.OrdinalIgnoreCase);
|
||||
if (index != -1)
|
||||
{
|
||||
tmp = tmp[..index];
|
||||
}
|
||||
|
||||
index = tmp.IndexOf('{');
|
||||
if (index != -1)
|
||||
{
|
||||
int endIndex = tmp.IndexOf('}');
|
||||
if (endIndex != -1)
|
||||
{
|
||||
tmp = tmp[(index + 1)..endIndex];
|
||||
}
|
||||
}
|
||||
|
||||
return tmp.ToString();
|
||||
}
|
||||
|
||||
private async Task AddDevice(UpnpDeviceInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var uri = info.Location;
|
||||
_logger.LogDebug("Attempting to create PlayToController from location {0}", uri);
|
||||
|
||||
if (info.Headers.TryGetValue("USN", out string? uuid))
|
||||
{
|
||||
uuid = GetUuid(uuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
var sessionInfo = await _sessionManager
|
||||
.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault();
|
||||
|
||||
if (controller is null)
|
||||
{
|
||||
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
|
||||
if (device is null)
|
||||
{
|
||||
_logger.LogError("Ignoring device as xml response is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
string deviceName = device.Properties.Name;
|
||||
|
||||
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
|
||||
|
||||
string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIPAddress);
|
||||
|
||||
controller = new PlayToController(
|
||||
sessionInfo,
|
||||
_sessionManager,
|
||||
_libraryManager,
|
||||
_logger,
|
||||
_dlnaManager,
|
||||
_userManager,
|
||||
_imageProcessor,
|
||||
serverAddress,
|
||||
null,
|
||||
_deviceDiscovery,
|
||||
_userDataManager,
|
||||
_localization,
|
||||
_mediaSourceManager,
|
||||
_mediaEncoder,
|
||||
device);
|
||||
|
||||
sessionInfo.AddController(controller);
|
||||
|
||||
var profile = _dlnaManager.GetProfile(device.Properties.ToDeviceIdentification()) ??
|
||||
_dlnaManager.GetDefaultProfile();
|
||||
|
||||
_sessionManager.ReportCapabilities(sessionInfo.Id, new ClientCapabilities
|
||||
{
|
||||
PlayableMediaTypes = profile.GetSupportedMediaTypes(),
|
||||
|
||||
SupportedCommands = new[]
|
||||
{
|
||||
GeneralCommandType.VolumeDown,
|
||||
GeneralCommandType.VolumeUp,
|
||||
GeneralCommandType.Mute,
|
||||
GeneralCommandType.Unmute,
|
||||
GeneralCommandType.ToggleMute,
|
||||
GeneralCommandType.SetVolume,
|
||||
GeneralCommandType.SetAudioStreamIndex,
|
||||
GeneralCommandType.SetSubtitleStreamIndex,
|
||||
GeneralCommandType.PlayMediaSource
|
||||
},
|
||||
|
||||
SupportsMediaControl = true
|
||||
});
|
||||
|
||||
_logger.LogInformation("DLNA Session created for {0} - {1}", device.Properties.Name, device.Properties.ModelName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered;
|
||||
|
||||
try
|
||||
{
|
||||
_disposeCancellationTokenSource.Cancel();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Error while disposing PlayToManager");
|
||||
}
|
||||
|
||||
_sessionLock.Dispose();
|
||||
_disposeCancellationTokenSource.Dispose();
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class PlaybackProgressEventArgs : EventArgs
|
||||
{
|
||||
public PlaybackProgressEventArgs(UBaseObject mediaInfo)
|
||||
{
|
||||
MediaInfo = mediaInfo;
|
||||
}
|
||||
|
||||
public UBaseObject MediaInfo { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class PlaybackStartEventArgs : EventArgs
|
||||
{
|
||||
public PlaybackStartEventArgs(UBaseObject mediaInfo)
|
||||
{
|
||||
MediaInfo = mediaInfo;
|
||||
}
|
||||
|
||||
public UBaseObject MediaInfo { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class PlaybackStoppedEventArgs : EventArgs
|
||||
{
|
||||
public PlaybackStoppedEventArgs(UBaseObject mediaInfo)
|
||||
{
|
||||
MediaInfo = mediaInfo;
|
||||
}
|
||||
|
||||
public UBaseObject MediaInfo { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace Emby.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; }
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Session;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public static class PlaylistItemFactory
|
||||
{
|
||||
public static PlaylistItem Create(Photo item, DeviceProfile profile)
|
||||
{
|
||||
var playlistItem = new PlaylistItem
|
||||
{
|
||||
StreamInfo = new StreamInfo
|
||||
{
|
||||
ItemId = item.Id,
|
||||
MediaType = DlnaProfileType.Photo,
|
||||
DeviceProfile = profile
|
||||
},
|
||||
|
||||
Profile = profile
|
||||
};
|
||||
|
||||
var directPlay = profile.DirectPlayProfiles
|
||||
.FirstOrDefault(i => i.Type == DlnaProfileType.Photo && IsSupported(i, item));
|
||||
|
||||
if (directPlay is not 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 is not null)
|
||||
{
|
||||
playlistItem.StreamInfo.PlayMethod = PlayMethod.Transcode;
|
||||
playlistItem.StreamInfo.Container = "." + transcodingProfile.Container.TrimStart('.');
|
||||
}
|
||||
|
||||
return playlistItem;
|
||||
}
|
||||
|
||||
private static bool IsSupported(DirectPlayProfile profile, Photo item)
|
||||
{
|
||||
var mediaPath = item.Path;
|
||||
|
||||
if (profile.Container.Length > 0)
|
||||
{
|
||||
// Check container type
|
||||
var mediaContainer = (Path.GetExtension(mediaPath) ?? string.Empty).TrimStart('.');
|
||||
|
||||
if (!profile.SupportsContainer(mediaContainer))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,181 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Emby.Dlna.Common;
|
||||
using Emby.Dlna.Ssdp;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class TransportCommands
|
||||
{
|
||||
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>";
|
||||
|
||||
public List<StateVariable> StateVariables { get; } = new List<StateVariable>();
|
||||
|
||||
public List<ServiceAction> ServiceActions { get; } = new List<ServiceAction>();
|
||||
|
||||
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 is not null)
|
||||
{
|
||||
foreach (var container in stateValues.Elements(UPnpNamespaces.Svc + "stateVariable"))
|
||||
{
|
||||
command.StateVariables.Add(FromXml(container));
|
||||
}
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private static ServiceAction ServiceActionFromXml(XElement container)
|
||||
{
|
||||
var serviceAction = new ServiceAction
|
||||
{
|
||||
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
|
||||
};
|
||||
|
||||
var argumentList = serviceAction.ArgumentList;
|
||||
|
||||
foreach (var arg in container.Descendants(UPnpNamespaces.Svc + "argument"))
|
||||
{
|
||||
argumentList.Add(ArgumentFromXml(arg));
|
||||
}
|
||||
|
||||
return serviceAction;
|
||||
}
|
||||
|
||||
private static Argument ArgumentFromXml(XElement container)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
|
||||
return new Argument
|
||||
{
|
||||
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
|
||||
Direction = container.GetValue(UPnpNamespaces.Svc + "direction") ?? string.Empty,
|
||||
RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
private static StateVariable FromXml(XElement container)
|
||||
{
|
||||
var allowedValues = Array.Empty<string>();
|
||||
var element = container.Descendants(UPnpNamespaces.Svc + "allowedValueList")
|
||||
.FirstOrDefault();
|
||||
|
||||
if (element is not null)
|
||||
{
|
||||
var values = element.Descendants(UPnpNamespaces.Svc + "allowedValue");
|
||||
|
||||
allowedValues = values.Select(child => child.Value).ToArray();
|
||||
}
|
||||
|
||||
return new StateVariable
|
||||
{
|
||||
Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
|
||||
DataType = container.GetValue(UPnpNamespaces.Svc + "dataType") ?? string.Empty,
|
||||
AllowedValues = allowedValues
|
||||
};
|
||||
}
|
||||
|
||||
public string BuildPost(ServiceAction action, string xmlNamespace)
|
||||
{
|
||||
var stateString = string.Empty;
|
||||
|
||||
foreach (var arg in action.ArgumentList)
|
||||
{
|
||||
if (string.Equals(arg.Direction, "out", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal))
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, "0");
|
||||
}
|
||||
else
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, null);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
|
||||
}
|
||||
|
||||
public string BuildPost(ServiceAction action, string xmlNamespace, object value, string commandParameter = "")
|
||||
{
|
||||
var stateString = string.Empty;
|
||||
|
||||
foreach (var arg in action.ArgumentList)
|
||||
{
|
||||
if (string.Equals(arg.Direction, "out", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal))
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, "0");
|
||||
}
|
||||
else
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, value.ToString(), commandParameter);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
|
||||
}
|
||||
|
||||
public string BuildPost(ServiceAction action, string xmlNamespace, object value, Dictionary<string, string> dictionary)
|
||||
{
|
||||
var stateString = string.Empty;
|
||||
|
||||
foreach (var arg in action.ArgumentList)
|
||||
{
|
||||
if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal))
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, "0");
|
||||
}
|
||||
else if (dictionary.TryGetValue(arg.Name, out var argValue))
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, argValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
stateString += BuildArgumentXml(arg, value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, 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 is not null)
|
||||
{
|
||||
var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ??
|
||||
(state.AllowedValues.Count > 0 ? state.AllowedValues[0] : value);
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType, sendValue);
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, "<{0}>{1}</{0}>", argument.Name, value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
/// <summary>
|
||||
/// Core of the AVTransport service. It defines the conceptually top-
|
||||
/// level state of the transport, for example, whether it is playing, recording, etc.
|
||||
/// </summary>
|
||||
public enum TransportState
|
||||
{
|
||||
STOPPED,
|
||||
PLAYING,
|
||||
TRANSITIONING,
|
||||
PAUSED_PLAYBACK
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Xml.Linq;
|
||||
using Emby.Dlna.Ssdp;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class UpnpContainer : UBaseObject
|
||||
{
|
||||
public static UBaseObject Create(XElement container)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(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.Class)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Data.Enums;
|
||||
|
||||
namespace Emby.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 IReadOnlyList<string> ProtocolInfo { get; set; }
|
||||
|
||||
public string UpnpClass { get; set; }
|
||||
|
||||
public string MediaType
|
||||
{
|
||||
get
|
||||
{
|
||||
var classType = UpnpClass ?? string.Empty;
|
||||
|
||||
if (classType.Contains("Audio", StringComparison.Ordinal))
|
||||
{
|
||||
return "Audio";
|
||||
}
|
||||
|
||||
if (classType.Contains("Video", StringComparison.Ordinal))
|
||||
{
|
||||
return "Video";
|
||||
}
|
||||
|
||||
if (classType.Contains("image", StringComparison.Ordinal))
|
||||
{
|
||||
return "Photo";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Equals(UBaseObject obj)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(obj);
|
||||
|
||||
return string.Equals(Id, obj.Id, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public static class UPnpNamespaces
|
||||
{
|
||||
public static XNamespace Dc { get; } = "http://purl.org/dc/elements/1.1/";
|
||||
|
||||
public static XNamespace Ns { get; } = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
|
||||
|
||||
public static XNamespace Svc { get; } = "urn:schemas-upnp-org:service-1-0";
|
||||
|
||||
public static XNamespace Ud { get; } = "urn:schemas-upnp-org:device-1-0";
|
||||
|
||||
public static XNamespace UPnp { get; } = "urn:schemas-upnp-org:metadata-1-0/upnp/";
|
||||
|
||||
public static XNamespace RenderingControl { get; } = "urn:schemas-upnp-org:service:RenderingControl:1";
|
||||
|
||||
public static XNamespace AvTransport { get; } = "urn:schemas-upnp-org:service:AVTransport:1";
|
||||
|
||||
public static XNamespace ContentDirectory { get; } = "urn:schemas-upnp-org:service:ContentDirectory:1";
|
||||
|
||||
public static XName Containers { get; } = Ns + "container";
|
||||
|
||||
public static XName Items { get; } = Ns + "item";
|
||||
|
||||
public static XName Title { get; } = Dc + "title";
|
||||
|
||||
public static XName Creator { get; } = Dc + "creator";
|
||||
|
||||
public static XName Artist { get; } = UPnp + "artist";
|
||||
|
||||
public static XName Id { get; } = "id";
|
||||
|
||||
public static XName ParentId { get; } = "parentID";
|
||||
|
||||
public static XName Class { get; } = UPnp + "class";
|
||||
|
||||
public static XName Artwork { get; } = UPnp + "albumArtURI";
|
||||
|
||||
public static XName Description { get; } = Dc + "description";
|
||||
|
||||
public static XName LongDescription { get; } = UPnp + "longDescription";
|
||||
|
||||
public static XName Album { get; } = UPnp + "album";
|
||||
|
||||
public static XName Author { get; } = UPnp + "author";
|
||||
|
||||
public static XName Director { get; } = UPnp + "director";
|
||||
|
||||
public static XName PlayCount { get; } = UPnp + "playbackCount";
|
||||
|
||||
public static XName Tracknumber { get; } = UPnp + "originalTrackNumber";
|
||||
|
||||
public static XName Res { get; } = Ns + "res";
|
||||
|
||||
public static XName Duration { get; } = "duration";
|
||||
|
||||
public static XName ProtocolInfo { get; } = "protocolInfo";
|
||||
|
||||
public static XName ServiceStateTable { get; } = Svc + "serviceStateTable";
|
||||
|
||||
public static XName StateVariable { get; } = Svc + "stateVariable";
|
||||
}
|
||||
}
|
|
@ -1,179 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace Emby.Dlna.Profiles
|
||||
{
|
||||
[System.Xml.Serialization.XmlRoot("Profile")]
|
||||
public class DefaultProfile : DeviceProfile
|
||||
{
|
||||
public DefaultProfile()
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||
Name = "Generic Device";
|
||||
|
||||
ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*";
|
||||
|
||||
Manufacturer = "Jellyfin";
|
||||
ModelDescription = "UPnP/AV 1.0 Compliant Media Server";
|
||||
ModelName = "Jellyfin Server";
|
||||
ModelNumber = "01";
|
||||
ModelUrl = "https://github.com/jellyfin/jellyfin";
|
||||
ManufacturerUrl = "https://github.com/jellyfin/jellyfin";
|
||||
|
||||
AlbumArtPn = "JPEG_SM";
|
||||
|
||||
MaxAlbumArtHeight = 480;
|
||||
MaxAlbumArtWidth = 480;
|
||||
|
||||
MaxIconWidth = 48;
|
||||
MaxIconHeight = 48;
|
||||
|
||||
MaxStreamingBitrate = 140000000;
|
||||
MaxStaticBitrate = 140000000;
|
||||
MusicStreamingTranscodingBitrate = 192000;
|
||||
|
||||
EnableAlbumArtInDidl = false;
|
||||
|
||||
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
|
||||
{
|
||||
// play all
|
||||
Container = string.Empty,
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
|
||||
new DirectPlayProfile
|
||||
{
|
||||
// play all
|
||||
Container = string.Empty,
|
||||
Type = DlnaProfileType.Audio
|
||||
}
|
||||
};
|
||||
|
||||
SubtitleProfiles = new[]
|
||||
{
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "srt",
|
||||
Method = SubtitleDeliveryMethod.External,
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "sub",
|
||||
Method = SubtitleDeliveryMethod.External,
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "sup",
|
||||
Method = SubtitleDeliveryMethod.External
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "srt",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "ass",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "ssa",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "smi",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "dvdsub",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "pgs",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "pgssub",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "sub",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "sup",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "subrip",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
},
|
||||
|
||||
new SubtitleProfile
|
||||
{
|
||||
Format = "vtt",
|
||||
Method = SubtitleDeliveryMethod.Embed
|
||||
}
|
||||
};
|
||||
|
||||
ResponseProfiles = new[]
|
||||
{
|
||||
new ResponseProfile
|
||||
{
|
||||
Container = "m4v",
|
||||
Type = DlnaProfileType.Video,
|
||||
MimeType = "video/mp4"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Generic Device</Name>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="" type="Video" />
|
||||
<DirectPlayProfile container="" type="Audio" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles />
|
||||
<CodecProfiles />
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="External" />
|
||||
<SubtitleProfile format="sub" method="External" />
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
<SubtitleProfile format="ass" method="Embed" />
|
||||
<SubtitleProfile format="ssa" method="Embed" />
|
||||
<SubtitleProfile format="smi" method="Embed" />
|
||||
<SubtitleProfile format="dvdsub" method="Embed" />
|
||||
<SubtitleProfile format="pgs" method="Embed" />
|
||||
<SubtitleProfile format="pgssub" method="Embed" />
|
||||
<SubtitleProfile format="sub" method="Embed" />
|
||||
<SubtitleProfile format="subrip" method="Embed" />
|
||||
<SubtitleProfile format="vtt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,68 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Denon AVR</Name>
|
||||
<Identification>
|
||||
<FriendlyName>Denon:\[AVR:.*</FriendlyName>
|
||||
<Manufacturer>Denon</Manufacturer>
|
||||
<Headers />
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="mp3,flac,m4a,wma" type="Audio" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles />
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Audio" container="flac">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioSampleRate" value="96000" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles />
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="External" />
|
||||
<SubtitleProfile format="sub" method="External" />
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
<SubtitleProfile format="ass" method="Embed" />
|
||||
<SubtitleProfile format="ssa" method="Embed" />
|
||||
<SubtitleProfile format="smi" method="Embed" />
|
||||
<SubtitleProfile format="dvdsub" method="Embed" />
|
||||
<SubtitleProfile format="pgs" method="Embed" />
|
||||
<SubtitleProfile format="pgssub" method="Embed" />
|
||||
<SubtitleProfile format="sub" method="Embed" />
|
||||
<SubtitleProfile format="subrip" method="Embed" />
|
||||
<SubtitleProfile format="vtt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,67 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>DirecTV HD-DVR</Name>
|
||||
<Identification>
|
||||
<FriendlyName>^DIRECTV.*$</FriendlyName>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="User-Agent" value="DIRECTV" match="Substring" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>10</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>true</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>true</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="jpeg,jpg" type="Photo" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mpeg" type="Video" videoCodec="mpeg2video" audioCodec="mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles />
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video" codec="mpeg2video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="8192000" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Audio" codec="mp2">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles />
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,96 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Dish Hopper-Joey</Name>
|
||||
<Identification>
|
||||
<Manufacturer>Echostar Technologies LLC</Manufacturer>
|
||||
<ManufacturerUrl>http://www.echostar.com</ManufacturerUrl>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="User-Agent" value="Zip_" match="Substring" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mp2t:http-get:*:video/mpeg:*,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:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="mp4,mkv,mpeg,ts" audioCodec="mp3,ac3,aac,he-aac,pcm" videoCodec="h264,mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="mp3,alac,flac" type="Audio" />
|
||||
<DirectPlayProfile container="jpeg" type="Photo" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="mp4" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles />
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video" codec="h264">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="ac3,he-aac">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="aac">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="Equals" property="IsSecondaryAudio" value="false" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="mkv,ts,mpegts" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,92 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>LG Smart TV</Name>
|
||||
<Identification>
|
||||
<FriendlyName>LG.*</FriendlyName>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="User-Agent" value="LG" match="Substring" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>10</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="ts,mpegts,avi,mkv,m2ts" audioCodec="aac,ac3,eac3,mp3,dca,dts" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="mp4,m4v" audioCodec="aac,ac3,eac3,mp3,dca,dts" videoCodec="h264,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="mp3" type="Audio" />
|
||||
<DirectPlayProfile container="jpeg" type="Photo" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles>
|
||||
<ContainerProfile type="Photo">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
</Conditions>
|
||||
</ContainerProfile>
|
||||
</ContainerProfiles>
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video" codec="mpeg4">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video" codec="h264">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="ac3,eac3,aac,mp3">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" type="Video" mimeType="video/mpeg">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
<SubtitleProfile format="srt" method="External" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,54 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Linksys DMA2100</Name>
|
||||
<Identification>
|
||||
<ModelName>DMA2100us</ModelName>
|
||||
<Headers />
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="mp3,flac,m4a,wma" type="Audio" />
|
||||
<DirectPlayProfile container="avi,mp4,mkv,ts,mpegts,m4v" type="Video" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles />
|
||||
<CodecProfiles />
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,62 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Marantz</Name>
|
||||
<Identification>
|
||||
<Manufacturer>Marantz</Manufacturer>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="User-Agent" value="Marantz" match="Substring" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="aac,mp3,wav,wma,flac" type="Audio" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles />
|
||||
<CodecProfiles />
|
||||
<ResponseProfiles />
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="External" />
|
||||
<SubtitleProfile format="sub" method="External" />
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
<SubtitleProfile format="ass" method="Embed" />
|
||||
<SubtitleProfile format="ssa" method="Embed" />
|
||||
<SubtitleProfile format="smi" method="Embed" />
|
||||
<SubtitleProfile format="dvdsub" method="Embed" />
|
||||
<SubtitleProfile format="pgs" method="Embed" />
|
||||
<SubtitleProfile format="pgssub" method="Embed" />
|
||||
<SubtitleProfile format="sub" method="Embed" />
|
||||
<SubtitleProfile format="subrip" method="Embed" />
|
||||
<SubtitleProfile format="vtt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,62 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>MediaMonkey</Name>
|
||||
<Identification>
|
||||
<FriendlyName>MediaMonkey</FriendlyName>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="User-Agent" value="MediaMonkey" match="Substring" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="aac,mp3,mpa,wav,wma,mp2,ogg,oga,webma,ape,opus,flac,m4a" type="Audio" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles />
|
||||
<CodecProfiles />
|
||||
<ResponseProfiles />
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="External" />
|
||||
<SubtitleProfile format="sub" method="External" />
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
<SubtitleProfile format="ass" method="Embed" />
|
||||
<SubtitleProfile format="ssa" method="Embed" />
|
||||
<SubtitleProfile format="smi" method="Embed" />
|
||||
<SubtitleProfile format="dvdsub" method="Embed" />
|
||||
<SubtitleProfile format="pgs" method="Embed" />
|
||||
<SubtitleProfile format="pgssub" method="Embed" />
|
||||
<SubtitleProfile format="sub" method="Embed" />
|
||||
<SubtitleProfile format="subrip" method="Embed" />
|
||||
<SubtitleProfile format="vtt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,87 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Panasonic Viera</Name>
|
||||
<Identification>
|
||||
<FriendlyName>VIERA</FriendlyName>
|
||||
<Manufacturer>Panasonic</Manufacturer>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="User-Agent" value="Panasonic MIL DLNA" match="Substring" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>10</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes>
|
||||
<XmlAttribute name="xmlns:pv" value="http://www.pv.com/pvns/" />
|
||||
</XmlRootAttributes>
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="mpeg,mpg" audioCodec="ac3,mp3,pcm_dvd" videoCodec="mpeg2video,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="mkv" audioCodec="aac,ac3,dca,mp3,mp2,pcm,dts" videoCodec="h264,mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="ts,mpegts" audioCodec="aac,mp3,mp2" videoCodec="h264,mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="mp4,m4v" audioCodec="aac,ac3,mp3,pcm" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="mov" audioCodec="aac,pcm" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="avi" audioCodec="pcm" videoCodec="mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="flv" audioCodec="aac" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" />
|
||||
<DirectPlayProfile container="mp4" audioCodec="aac" type="Audio" />
|
||||
<DirectPlayProfile container="jpeg" type="Photo" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles>
|
||||
<ContainerProfile type="Photo">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
</Conditions>
|
||||
</ContainerProfile>
|
||||
</ContainerProfiles>
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitDepth" value="8" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="ts,mpegts" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
<SubtitleProfile format="srt" method="External" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,92 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Popcorn Hour</Name>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="mp4,mov,m4v" audioCodec="aac" videoCodec="h264,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,eac3,mp3,mp2,pcm" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="asf,wmv" audioCodec="wmav2,wmapro" videoCodec="wmv3,vc1" type="Video" />
|
||||
<DirectPlayProfile container="avi" audioCodec="mp3,ac3,eac3,mp2,pcm" videoCodec="mpeg4,msmpeg4" type="Video" />
|
||||
<DirectPlayProfile container="mkv" audioCodec="aac,mp3,ac3,eac3,mp2,pcm" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="aac,mp3,flac,ogg,wma,wav" type="Audio" />
|
||||
<DirectPlayProfile container="jpeg,gif,bmp,png" type="Photo" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="mp4" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles />
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video" codec="h264">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="EqualsAny" property="VideoProfile" value="baseline|constrained baseline" isRequired="false" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="NotEquals" property="IsAnamorphic" value="true" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="NotEquals" property="IsAnamorphic" value="true" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="aac">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Audio" codec="aac">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Audio" codec="mp3">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="false" />
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioBitrate" value="320000" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,128 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Samsung Smart TV</Name>
|
||||
<Identification>
|
||||
<ModelUrl>samsung.com</ModelUrl>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="User-Agent" value="SEC_" match="Substring" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>true</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes>
|
||||
<XmlAttribute name="xmlns:sec" value="http://www.sec.co.kr/" />
|
||||
</XmlRootAttributes>
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="asf" audioCodec="mp3,ac3,wmav2,wmapro,wmavoice" videoCodec="h264,mpeg4,mjpeg" type="Video" />
|
||||
<DirectPlayProfile container="avi" audioCodec="mp3,ac3,dca,dts" videoCodec="h264,mpeg4,mjpeg" type="Video" />
|
||||
<DirectPlayProfile container="mkv" audioCodec="mp3,ac3,dca,aac,dts" videoCodec="h264,mpeg4,mjpeg4" type="Video" />
|
||||
<DirectPlayProfile container="mp4,m4v" audioCodec="mp3,aac" videoCodec="h264,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="3gp" audioCodec="aac,he-aac" videoCodec="h264,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="mpg,mpeg" audioCodec="ac3,mp2,mp3,aac" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
|
||||
<DirectPlayProfile container="vro,vob" audioCodec="ac3,mp2,mp3" videoCodec="mpeg1video,mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="ts" audioCodec="ac3,aac,mp3,eac3" videoCodec="mpeg2video,h264,vc1" type="Video" />
|
||||
<DirectPlayProfile container="asf" audioCodec="wmav2,wmavoice" videoCodec="wmv2,wmv3" type="Video" />
|
||||
<DirectPlayProfile container="mp3,flac" type="Audio" />
|
||||
<DirectPlayProfile container="jpeg" type="Photo" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles>
|
||||
<ContainerProfile type="Photo">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
</Conditions>
|
||||
</ContainerProfile>
|
||||
</ContainerProfiles>
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video" codec="mpeg2video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="30720000" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video" codec="mpeg4">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="8192000" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video" codec="h264">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="37500000" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video" codec="wmv2,wmv3,vc1">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="25600000" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="wmav2,dca,aac,mp3,dts">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="avi" type="Video" mimeType="video/x-msvideo">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="mkv" type="Video" mimeType="video/x-mkv">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="flac" type="Audio" mimeType="audio/x-flac">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
<SubtitleProfile format="srt" method="External" didlMode="CaptionInfoEx" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,60 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Sharp Smart TV</Name>
|
||||
<Identification>
|
||||
<Manufacturer>Sharp</Manufacturer>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="User-Agent" value="Sharp" match="Substring" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>true</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>true</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="m4v,mkv,avi,mov,mp4" audioCodec="aac,mp3,ac3,dts,dca" videoCodec="h264,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="asf,wmv" type="Video" />
|
||||
<DirectPlayProfile container="mpg,mpeg" audioCodec="mp3,aac" videoCodec="mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="flv" audioCodec="mp3,aac" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="mp3,wav" type="Audio" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3,dts,dca" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles />
|
||||
<CodecProfiles />
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
<SubtitleProfile format="srt" method="External" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,133 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Sony Bravia (2010)</Name>
|
||||
<Identification>
|
||||
<FriendlyName>KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*</FriendlyName>
|
||||
<Manufacturer>Sony</Manufacturer>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[EHLNPB]X[0-9][01][0-9].*" match="Regex" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||
<ManufacturerUrl>http://www.microsoft.com/</ManufacturerUrl>
|
||||
<ModelName>Windows Media Player Sharing</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>3.0</ModelNumber>
|
||||
<ModelUrl>http://www.microsoft.com/</ModelUrl>
|
||||
<EnableAlbumArtInDidl>true</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_TN</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<SonyAggregationFlags>10</SonyAggregationFlags>
|
||||
<ProtocolInfo>http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=81500000000000000000000000000000,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;DLNA.ORG_OP=00;DLNA.ORG_FLAGS=00D00000000000000000000000000000,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_PAL;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=81500000000000000000000000000000</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes>
|
||||
<XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
|
||||
</XmlRootAttributes>
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="ts,mpegts" audioCodec="mp3,mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="mpeg" audioCodec="mp3,mp2" videoCodec="mpeg2video,mpeg1video" type="Video" />
|
||||
<DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles>
|
||||
<ContainerProfile type="Photo">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
</Conditions>
|
||||
</ContainerProfile>
|
||||
</ContainerProfiles>
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video" codec="h264">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video" codec="mpeg2video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="ac3">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="aac">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
|
||||
<ProfileCondition condition="NotEquals" property="AudioProfile" value="he-aac" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="mp3,mp2">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="Equals" property="PacketLength" value="192" isRequired="true" />
|
||||
<ProfileCondition condition="Equals" property="VideoTimestamp" value="Valid" isRequired="true" />
|
||||
</Conditions>
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO" mimeType="video/mpeg">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="Equals" property="PacketLength" value="188" isRequired="true" />
|
||||
</Conditions>
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" videoCodec="mpeg2video" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="mpeg" videoCodec="mpeg1video,mpeg2video" type="Video" orgPn="MPEG_PS_NTSC,MPEG_PS_PAL" mimeType="video/mpeg">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,139 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Sony Bravia (2011)</Name>
|
||||
<Identification>
|
||||
<FriendlyName>KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*</FriendlyName>
|
||||
<Manufacturer>Sony</Manufacturer>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}([A-Z]X[0-9]2[0-9]|CX400).*" match="Regex" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||
<ManufacturerUrl>http://www.microsoft.com/</ManufacturerUrl>
|
||||
<ModelName>Windows Media Player Sharing</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>3.0</ModelNumber>
|
||||
<ModelUrl>http://www.microsoft.com/</ModelUrl>
|
||||
<EnableAlbumArtInDidl>true</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_TN</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<SonyAggregationFlags>10</SonyAggregationFlags>
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes>
|
||||
<XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
|
||||
</XmlRootAttributes>
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="ts,mpegts" audioCodec="mp3" videoCodec="mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="mp4,m4v" audioCodec="ac3,aac,mp3" videoCodec="h264,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="mpeg" audioCodec="mp3" videoCodec="mpeg2video,mpeg1video" type="Video" />
|
||||
<DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" videoCodec="wmv2,wmv3,vc1" type="Video" />
|
||||
<DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" />
|
||||
<DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles>
|
||||
<ContainerProfile type="Photo">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
</Conditions>
|
||||
</ContainerProfile>
|
||||
</ContainerProfiles>
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video" codec="h264">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video" codec="mpeg2video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="20000000" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="ac3">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="aac">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
|
||||
<ProfileCondition condition="NotEquals" property="AudioProfile" value="he-aac" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="mp3,mp2">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="Equals" property="PacketLength" value="192" isRequired="true" />
|
||||
<ProfileCondition condition="Equals" property="VideoTimestamp" value="Valid" isRequired="true" />
|
||||
</Conditions>
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO" mimeType="video/mpeg">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="Equals" property="PacketLength" value="188" isRequired="true" />
|
||||
</Conditions>
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" videoCodec="mpeg2video" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="mpeg" videoCodec="mpeg1video,mpeg2video" type="Video" orgPn="MPEG_PS_NTSC,MPEG_PS_PAL" mimeType="video/mpeg">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,115 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Sony Bravia (2012)</Name>
|
||||
<Identification>
|
||||
<FriendlyName>KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*</FriendlyName>
|
||||
<Manufacturer>Sony</Manufacturer>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[A-Z]X[0-9]5([0-9]|G).*" match="Regex" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||
<ManufacturerUrl>http://www.microsoft.com/</ManufacturerUrl>
|
||||
<ModelName>Windows Media Player Sharing</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>3.0</ModelNumber>
|
||||
<ModelUrl>http://www.microsoft.com/</ModelUrl>
|
||||
<EnableAlbumArtInDidl>true</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_TN</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<SonyAggregationFlags>10</SonyAggregationFlags>
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes>
|
||||
<XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
|
||||
</XmlRootAttributes>
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="ts,mpegts" audioCodec="mp3,mp2" videoCodec="mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="mp4,m4v" audioCodec="ac3,aac,mp3,mp2" videoCodec="h264,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="avi" audioCodec="ac3,mp3" videoCodec="mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="mpeg" audioCodec="mp3,mp2" videoCodec="mpeg2video,mpeg1video" type="Video" />
|
||||
<DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" videoCodec="wmv2,wmv3,vc1" type="Video" />
|
||||
<DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" />
|
||||
<DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" />
|
||||
<DirectPlayProfile container="jpeg" type="Photo" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles>
|
||||
<ContainerProfile type="Photo">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
</Conditions>
|
||||
</ContainerProfile>
|
||||
</ContainerProfiles>
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="ac3">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="mp3,mp2">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="Equals" property="PacketLength" value="192" isRequired="true" />
|
||||
<ProfileCondition condition="Equals" property="VideoTimestamp" value="Valid" isRequired="true" />
|
||||
</Conditions>
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO" mimeType="video/mpeg">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="Equals" property="PacketLength" value="188" isRequired="true" />
|
||||
</Conditions>
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" videoCodec="mpeg2video" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="mpeg" videoCodec="mpeg1video,mpeg2video" type="Video" orgPn="MPEG_PS_NTSC,MPEG_PS_PAL" mimeType="video/mpeg">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,114 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Sony Bravia (2013)</Name>
|
||||
<Identification>
|
||||
<FriendlyName>KDL-[0-9]{2}[WR][5689][0-9]{2}A.*</FriendlyName>
|
||||
<Manufacturer>Sony</Manufacturer>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="X-AV-Client-Info" value=".*KDL-[0-9]{2}[WR][5689][0-9]{2}A.*" match="Regex" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||
<ManufacturerUrl>http://www.microsoft.com/</ManufacturerUrl>
|
||||
<ModelName>Windows Media Player Sharing</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>3.0</ModelNumber>
|
||||
<ModelUrl>http://www.microsoft.com/</ModelUrl>
|
||||
<EnableAlbumArtInDidl>true</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_TN</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<SonyAggregationFlags>10</SonyAggregationFlags>
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes>
|
||||
<XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
|
||||
</XmlRootAttributes>
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="ts,mpegts" audioCodec="ac3,eac3,aac,mp3" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="ts,mpegts" audioCodec="mp3,mp2" videoCodec="mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="mp4,m4v" audioCodec="ac3,eac3,aac,mp3,mp2" videoCodec="h264,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="mov" audioCodec="ac3,eac3,aac,mp3,mp2" videoCodec="h264,mpeg4,mjpeg" type="Video" />
|
||||
<DirectPlayProfile container="mkv" audioCodec="ac3,eac3,aac,mp3,mp2,pcm,vorbis" videoCodec="h264,mpeg4,vp8" type="Video" />
|
||||
<DirectPlayProfile container="avi" audioCodec="ac3,eac3,mp3" videoCodec="mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="avi" audioCodec="pcm" videoCodec="mjpeg" type="Video" />
|
||||
<DirectPlayProfile container="mpeg" audioCodec="mp3,mp2" videoCodec="mpeg2video,mpeg1video" type="Video" />
|
||||
<DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" videoCodec="wmv2,wmv3,vc1" type="Video" />
|
||||
<DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" />
|
||||
<DirectPlayProfile container="mp4" audioCodec="aac" type="Audio" />
|
||||
<DirectPlayProfile container="wav" audioCodec="pcm" type="Audio" />
|
||||
<DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" />
|
||||
<DirectPlayProfile container="jpeg" type="Photo" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles>
|
||||
<ContainerProfile type="Photo">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
</Conditions>
|
||||
</ContainerProfile>
|
||||
</ContainerProfiles>
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="mp3,mp2">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="Equals" property="PacketLength" value="192" isRequired="true" />
|
||||
<ProfileCondition condition="Equals" property="VideoTimestamp" value="Valid" isRequired="true" />
|
||||
</Conditions>
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO" mimeType="video/mpeg">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="Equals" property="PacketLength" value="188" isRequired="true" />
|
||||
</Conditions>
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" videoCodec="mpeg2video" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="mpeg" videoCodec="mpeg1video,mpeg2video" type="Video" orgPn="MPEG_PS_NTSC,MPEG_PS_PAL" mimeType="video/mpeg">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,114 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Sony Bravia (2014)</Name>
|
||||
<Identification>
|
||||
<FriendlyName>(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*</FriendlyName>
|
||||
<Manufacturer>Sony</Manufacturer>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="X-AV-Client-Info" value=".*(KDL-[0-9]{2}W[5-9][0-9]{2}B|KDL-[0-9]{2}R480|XBR-[0-9]{2}X[89][0-9]{2}B|KD-[0-9]{2}[SX][89][0-9]{3}B).*" match="Regex" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Microsoft Corporation</Manufacturer>
|
||||
<ManufacturerUrl>http://www.microsoft.com/</ManufacturerUrl>
|
||||
<ModelName>Windows Media Player Sharing</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>3.0</ModelNumber>
|
||||
<ModelUrl>http://www.microsoft.com/</ModelUrl>
|
||||
<EnableAlbumArtInDidl>true</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_TN</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<SonyAggregationFlags>10</SonyAggregationFlags>
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes>
|
||||
<XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
|
||||
</XmlRootAttributes>
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="ts,mpegts" audioCodec="ac3,eac3,aac,mp3" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="ts,mpegts" audioCodec="mp3,mp2" videoCodec="mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="mp4,m4v" audioCodec="ac3,eac3,aac,mp3,mp2" videoCodec="h264,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="mov" audioCodec="ac3,eac3,aac,mp3,mp2" videoCodec="h264,mpeg4,mjpeg" type="Video" />
|
||||
<DirectPlayProfile container="mkv" audioCodec="ac3,eac3,aac,mp3,mp2,pcm,vorbis" videoCodec="h264,mpeg4,vp8" type="Video" />
|
||||
<DirectPlayProfile container="avi" audioCodec="ac3,eac3,mp3" videoCodec="mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="avi" audioCodec="pcm" videoCodec="mjpeg" type="Video" />
|
||||
<DirectPlayProfile container="mpeg" audioCodec="mp3,mp2" videoCodec="mpeg2video,mpeg1video" type="Video" />
|
||||
<DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" videoCodec="wmv2,wmv3,vc1" type="Video" />
|
||||
<DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" />
|
||||
<DirectPlayProfile container="mp4" audioCodec="aac" type="Audio" />
|
||||
<DirectPlayProfile container="wav" audioCodec="pcm" type="Audio" />
|
||||
<DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" />
|
||||
<DirectPlayProfile container="jpeg" type="Photo" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles>
|
||||
<ContainerProfile type="Photo">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
</Conditions>
|
||||
</ContainerProfile>
|
||||
</ContainerProfiles>
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="mp3,mp2">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="Equals" property="PacketLength" value="192" isRequired="true" />
|
||||
<ProfileCondition condition="Equals" property="VideoTimestamp" value="Valid" isRequired="true" />
|
||||
</Conditions>
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO" mimeType="video/mpeg">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="Equals" property="PacketLength" value="188" isRequired="true" />
|
||||
</Conditions>
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264" type="Video" orgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="ts,mpegts" videoCodec="mpeg2video" type="Video" orgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO" mimeType="video/vnd.dlna.mpeg-tts">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="mpeg" videoCodec="mpeg1video,mpeg2video" type="Video" orgPn="MPEG_PS_NTSC,MPEG_PS_PAL" mimeType="video/mpeg">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,105 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Sony PlayStation 3</Name>
|
||||
<Identification>
|
||||
<FriendlyName>PLAYSTATION 3</FriendlyName>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="User-Agent" value="PLAYSTATION 3" match="Substring" />
|
||||
<HttpHeaderInfo name="X-AV-Client-Info" value="PLAYSTATION 3" match="Substring" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_TN</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<SonyAggregationFlags>10</SonyAggregationFlags>
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
|
||||
<DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="mp4" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="aac,mp3,wav" type="Audio" />
|
||||
<DirectPlayProfile container="jpeg,png,gif,bmp,tiff" type="Photo" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles>
|
||||
<ContainerProfile type="Photo">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
</Conditions>
|
||||
</ContainerProfile>
|
||||
</ContainerProfiles>
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video" codec="h264">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="15360000" isRequired="false" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="ac3">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="false" />
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioBitrate" value="640000" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="wmapro">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="aac">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="NotEquals" property="AudioProfile" value="he-aac" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="mp4,mov" audioCodec="aac" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="avi" type="Video" orgPn="AVI" mimeType="video/divx">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="wav" type="Audio" mimeType="audio/wav">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,108 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Sony PlayStation 4</Name>
|
||||
<Identification>
|
||||
<FriendlyName>PLAYSTATION 4</FriendlyName>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="User-Agent" value="PLAYSTATION 4" match="Substring" />
|
||||
<HttpHeaderInfo name="X-AV-Client-Info" value="PLAYSTATION 4" match="Substring" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>true</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_TN</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<SonyAggregationFlags>10</SonyAggregationFlags>
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
|
||||
<DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="mp4,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="aac,mp3,wav" type="Audio" />
|
||||
<DirectPlayProfile container="jpeg,png,gif,bmp,tiff" type="Photo" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Bytes" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles>
|
||||
<ContainerProfile type="Photo">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
</Conditions>
|
||||
</ContainerProfile>
|
||||
</ContainerProfiles>
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video" codec="h264">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="15360000" isRequired="false" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="ac3">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="false" />
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioBitrate" value="640000" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="wmapro">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="aac">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="NotEquals" property="AudioProfile" value="he-aac" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="mp4,mov" audioCodec="aac" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="avi" type="Video" orgPn="AVI" mimeType="video/divx">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="wav" type="Audio" mimeType="audio/wav">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,94 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>WDTV Live</Name>
|
||||
<Identification>
|
||||
<ModelName>WD TV</ModelName>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="User-Agent" value="alphanetworks" match="Substring" />
|
||||
<HttpHeaderInfo name="User-Agent" value="ALPHA Networks" match="Substring" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>5</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>true</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="avi" audioCodec="ac3,eac3,dca,mp2,mp3,pcm,dts" videoCodec="mpeg1video,mpeg2video,mpeg4,h264,vc1" type="Video" />
|
||||
<DirectPlayProfile container="mpeg" audioCodec="ac3,eac3,dca,mp2,mp3,pcm,dts" videoCodec="mpeg1video,mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="mkv" audioCodec="ac3,eac3,dca,aac,mp2,mp3,pcm,dts" videoCodec="mpeg1video,mpeg2video,mpeg4,h264,vc1" type="Video" />
|
||||
<DirectPlayProfile container="ts,m2ts,mpegts" audioCodec="ac3,eac3,dca,mp2,mp3,aac,dts" videoCodec="mpeg1video,mpeg2video,h264,vc1" type="Video" />
|
||||
<DirectPlayProfile container="mp4,mov,m4v" audioCodec="ac3,eac3,aac,mp2,mp3,dca,dts" videoCodec="h264,mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="asf" audioCodec="wmav2,wmapro" videoCodec="vc1" type="Video" />
|
||||
<DirectPlayProfile container="asf" audioCodec="mp2,ac3" videoCodec="mpeg2video" type="Video" />
|
||||
<DirectPlayProfile container="mp3" audioCodec="mp2,mp3" type="Audio" />
|
||||
<DirectPlayProfile container="mp4" audioCodec="mp4" type="Audio" />
|
||||
<DirectPlayProfile container="flac" type="Audio" />
|
||||
<DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" />
|
||||
<DirectPlayProfile container="ogg" audioCodec="vorbis" type="Audio" />
|
||||
<DirectPlayProfile container="jpeg,png,gif,bmp,tiff" type="Photo" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles>
|
||||
<ContainerProfile type="Photo">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
</Conditions>
|
||||
</ContainerProfile>
|
||||
</ContainerProfiles>
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video" codec="h264">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="aac">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="true" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="ts,mpegts" type="Video" orgPn="MPEG_TS_SD_NA">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="External" />
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
<SubtitleProfile format="sub" method="Embed" />
|
||||
<SubtitleProfile format="subrip" method="Embed" />
|
||||
<SubtitleProfile format="idx" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,126 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>Xbox One</Name>
|
||||
<Identification>
|
||||
<ModelName>Xbox One</ModelName>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="FriendlyName.DLNA.ORG" value="XboxOne" match="Substring" />
|
||||
<HttpHeaderInfo name="User-Agent" value="NSPlayer/12" match="Substring" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio,Photo,Video</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>40</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="ts,mpegts" audioCodec="ac3,aac,mp3" videoCodec="h264,mpeg2video,hevc" type="Video" />
|
||||
<DirectPlayProfile container="avi" audioCodec="ac3,mp3" videoCodec="mpeg4" type="Video" />
|
||||
<DirectPlayProfile container="avi" audioCodec="aac" videoCodec="h264" type="Video" />
|
||||
<DirectPlayProfile container="mp4,mov,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4,mpeg2video,hevc" type="Video" />
|
||||
<DirectPlayProfile container="asf" audioCodec="wmav2,wmapro" videoCodec="wmv2,wmv3,vc1" type="Video" />
|
||||
<DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" />
|
||||
<DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" />
|
||||
<DirectPlayProfile container="jpeg" type="Photo" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" videoCodec="jpeg" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles>
|
||||
<ContainerProfile type="Video" container="mp4,mov">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="Equals" property="Has64BitOffsets" value="false" isRequired="false" />
|
||||
</Conditions>
|
||||
</ContainerProfile>
|
||||
</ContainerProfiles>
|
||||
<CodecProfiles>
|
||||
<CodecProfile type="Video" codec="mpeg4">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="NotEquals" property="IsAnamorphic" value="true" isRequired="false" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitDepth" value="8" isRequired="false" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="5120000" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video" codec="h264">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="NotEquals" property="IsAnamorphic" value="true" isRequired="false" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitDepth" value="8" isRequired="false" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoLevel" value="41" isRequired="false" />
|
||||
<ProfileCondition condition="EqualsAny" property="VideoProfile" value="high|main|baseline|constrained baseline" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video" codec="wmv2,wmv3,vc1">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="NotEquals" property="IsAnamorphic" value="true" isRequired="false" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitDepth" value="8" isRequired="false" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Width" value="1920" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="Height" value="1080" isRequired="true" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoFramerate" value="30" isRequired="false" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitrate" value="15360000" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="Video">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="NotEquals" property="IsAnamorphic" value="true" isRequired="false" />
|
||||
<ProfileCondition condition="LessThanEqual" property="VideoBitDepth" value="8" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="ac3,wmav2,wmapro">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="6" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
<CodecProfile type="VideoAudio" codec="aac">
|
||||
<Conditions>
|
||||
<ProfileCondition condition="LessThanEqual" property="AudioChannels" value="2" isRequired="false" />
|
||||
<ProfileCondition condition="Equals" property="AudioProfile" value="lc" isRequired="false" />
|
||||
</Conditions>
|
||||
<ApplyConditions />
|
||||
</CodecProfile>
|
||||
</CodecProfiles>
|
||||
<ResponseProfiles>
|
||||
<ResponseProfile container="avi" type="Video" mimeType="video/avi">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
<ResponseProfile container="m4v" type="Video" mimeType="video/mp4">
|
||||
<Conditions />
|
||||
</ResponseProfile>
|
||||
</ResponseProfiles>
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,67 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Name>foobar2000</Name>
|
||||
<Identification>
|
||||
<FriendlyName>foobar</FriendlyName>
|
||||
<Headers>
|
||||
<HttpHeaderInfo name="User-Agent" value="foobar" match="Substring" />
|
||||
</Headers>
|
||||
</Identification>
|
||||
<Manufacturer>Jellyfin</Manufacturer>
|
||||
<ManufacturerUrl>https://github.com/jellyfin/jellyfin</ManufacturerUrl>
|
||||
<ModelName>Jellyfin Server</ModelName>
|
||||
<ModelDescription>UPnP/AV 1.0 Compliant Media Server</ModelDescription>
|
||||
<ModelNumber>01</ModelNumber>
|
||||
<ModelUrl>https://github.com/jellyfin/jellyfin</ModelUrl>
|
||||
<EnableAlbumArtInDidl>false</EnableAlbumArtInDidl>
|
||||
<EnableSingleAlbumArtLimit>false</EnableSingleAlbumArtLimit>
|
||||
<EnableSingleSubtitleLimit>false</EnableSingleSubtitleLimit>
|
||||
<SupportedMediaTypes>Audio</SupportedMediaTypes>
|
||||
<AlbumArtPn>JPEG_SM</AlbumArtPn>
|
||||
<MaxAlbumArtWidth>480</MaxAlbumArtWidth>
|
||||
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
|
||||
<MaxIconWidth>48</MaxIconWidth>
|
||||
<MaxIconHeight>48</MaxIconHeight>
|
||||
<MaxStreamingBitrate>140000000</MaxStreamingBitrate>
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
|
||||
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
|
||||
<XmlRootAttributes />
|
||||
<DirectPlayProfiles>
|
||||
<DirectPlayProfile container="mp3" audioCodec="mp2,mp3" type="Audio" />
|
||||
<DirectPlayProfile container="mp4" audioCodec="mp4" type="Audio" />
|
||||
<DirectPlayProfile container="aac,wav" type="Audio" />
|
||||
<DirectPlayProfile container="flac" audioCodec="flac" type="Audio" />
|
||||
<DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" />
|
||||
<DirectPlayProfile container="ogg" audioCodec="vorbis" type="Audio" />
|
||||
</DirectPlayProfiles>
|
||||
<TranscodingProfiles>
|
||||
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||
</TranscodingProfiles>
|
||||
<ContainerProfiles />
|
||||
<CodecProfiles />
|
||||
<ResponseProfiles />
|
||||
<SubtitleProfiles>
|
||||
<SubtitleProfile format="srt" method="External" />
|
||||
<SubtitleProfile format="sub" method="External" />
|
||||
<SubtitleProfile format="srt" method="Embed" />
|
||||
<SubtitleProfile format="ass" method="Embed" />
|
||||
<SubtitleProfile format="ssa" method="Embed" />
|
||||
<SubtitleProfile format="smi" method="Embed" />
|
||||
<SubtitleProfile format="dvdsub" method="Embed" />
|
||||
<SubtitleProfile format="pgs" method="Embed" />
|
||||
<SubtitleProfile format="pgssub" method="Embed" />
|
||||
<SubtitleProfile format="sub" method="Embed" />
|
||||
<SubtitleProfile format="subrip" method="Embed" />
|
||||
<SubtitleProfile format="vtt" method="Embed" />
|
||||
</SubtitleProfiles>
|
||||
</Profile>
|
|
@ -1,28 +0,0 @@
|
|||
using System.Reflection;
|
||||
using System.Resources;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("Emby.Dlna")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("Jellyfin Project")]
|
||||
[assembly: AssemblyProduct("Jellyfin Server")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
[assembly: NeutralResourcesLanguage("en")]
|
||||
[assembly: InternalsVisibleTo("Jellyfin.Dlna.Tests")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
|
@ -1,358 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using Emby.Dlna.Common;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace Emby.Dlna.Server
|
||||
{
|
||||
public class DescriptionXmlBuilder
|
||||
{
|
||||
private readonly DeviceProfile _profile;
|
||||
|
||||
private readonly string _serverUdn;
|
||||
private readonly string _serverAddress;
|
||||
private readonly string _serverName;
|
||||
private readonly string _serverId;
|
||||
|
||||
public DescriptionXmlBuilder(DeviceProfile profile, string serverUdn, string serverAddress, string serverName, string serverId)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(serverUdn);
|
||||
ArgumentException.ThrowIfNullOrEmpty(serverAddress);
|
||||
|
||||
_profile = profile;
|
||||
_serverUdn = serverUdn;
|
||||
_serverAddress = serverAddress;
|
||||
_serverName = serverName;
|
||||
_serverId = serverId;
|
||||
}
|
||||
|
||||
public string GetXml()
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.Append("<?xml version=\"1.0\"?>");
|
||||
|
||||
builder.Append("<root");
|
||||
|
||||
var attributes = _profile.XmlRootAttributes.ToList();
|
||||
|
||||
attributes.Insert(0, new XmlAttribute
|
||||
{
|
||||
Name = "xmlns:dlna",
|
||||
Value = "urn:schemas-dlna-org:device-1-0"
|
||||
});
|
||||
attributes.Insert(0, new XmlAttribute
|
||||
{
|
||||
Name = "xmlns",
|
||||
Value = "urn:schemas-upnp-org:device-1-0"
|
||||
});
|
||||
|
||||
foreach (var att in attributes)
|
||||
{
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, " {0}=\"{1}\"", att.Name, att.Value);
|
||||
}
|
||||
|
||||
builder.Append('>');
|
||||
|
||||
builder.Append("<specVersion>");
|
||||
builder.Append("<major>1</major>");
|
||||
builder.Append("<minor>0</minor>");
|
||||
builder.Append("</specVersion>");
|
||||
|
||||
AppendDeviceInfo(builder);
|
||||
|
||||
builder.Append("</root>");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private void AppendDeviceInfo(StringBuilder builder)
|
||||
{
|
||||
builder.Append("<device>");
|
||||
AppendDeviceProperties(builder);
|
||||
|
||||
AppendIconList(builder);
|
||||
|
||||
builder.Append("<presentationURL>")
|
||||
.Append(SecurityElement.Escape(_serverAddress))
|
||||
.Append("/web/index.html</presentationURL>");
|
||||
|
||||
AppendServiceList(builder);
|
||||
builder.Append("</device>");
|
||||
}
|
||||
|
||||
private void AppendDeviceProperties(StringBuilder builder)
|
||||
{
|
||||
builder.Append("<dlna:X_DLNACAP/>");
|
||||
|
||||
builder.Append("<dlna:X_DLNADOC xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">DMS-1.50</dlna:X_DLNADOC>");
|
||||
builder.Append("<dlna:X_DLNADOC xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">M-DMS-1.50</dlna:X_DLNADOC>");
|
||||
|
||||
builder.Append("<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>");
|
||||
|
||||
builder.Append("<friendlyName>")
|
||||
.Append(SecurityElement.Escape(GetFriendlyName()))
|
||||
.Append("</friendlyName>");
|
||||
builder.Append("<manufacturer>")
|
||||
.Append(SecurityElement.Escape(_profile.Manufacturer ?? string.Empty))
|
||||
.Append("</manufacturer>");
|
||||
builder.Append("<manufacturerURL>")
|
||||
.Append(SecurityElement.Escape(_profile.ManufacturerUrl ?? string.Empty))
|
||||
.Append("</manufacturerURL>");
|
||||
|
||||
builder.Append("<modelDescription>")
|
||||
.Append(SecurityElement.Escape(_profile.ModelDescription ?? string.Empty))
|
||||
.Append("</modelDescription>");
|
||||
builder.Append("<modelName>")
|
||||
.Append(SecurityElement.Escape(_profile.ModelName ?? string.Empty))
|
||||
.Append("</modelName>");
|
||||
|
||||
builder.Append("<modelNumber>")
|
||||
.Append(SecurityElement.Escape(_profile.ModelNumber ?? string.Empty))
|
||||
.Append("</modelNumber>");
|
||||
builder.Append("<modelURL>")
|
||||
.Append(SecurityElement.Escape(_profile.ModelUrl ?? string.Empty))
|
||||
.Append("</modelURL>");
|
||||
|
||||
if (string.IsNullOrEmpty(_profile.SerialNumber))
|
||||
{
|
||||
builder.Append("<serialNumber>")
|
||||
.Append(SecurityElement.Escape(_serverId))
|
||||
.Append("</serialNumber>");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append("<serialNumber>")
|
||||
.Append(SecurityElement.Escape(_profile.SerialNumber))
|
||||
.Append("</serialNumber>");
|
||||
}
|
||||
|
||||
builder.Append("<UPC/>");
|
||||
|
||||
builder.Append("<UDN>uuid:")
|
||||
.Append(SecurityElement.Escape(_serverUdn))
|
||||
.Append("</UDN>");
|
||||
|
||||
if (!string.IsNullOrEmpty(_profile.SonyAggregationFlags))
|
||||
{
|
||||
builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">")
|
||||
.Append(SecurityElement.Escape(_profile.SonyAggregationFlags))
|
||||
.Append("</av:aggregationFlags>");
|
||||
}
|
||||
}
|
||||
|
||||
internal string GetFriendlyName()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_profile.FriendlyName))
|
||||
{
|
||||
return _serverName;
|
||||
}
|
||||
|
||||
if (!_profile.FriendlyName.Contains("${HostName}", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return _profile.FriendlyName;
|
||||
}
|
||||
|
||||
var characterList = new List<char>();
|
||||
|
||||
foreach (var c in _serverName)
|
||||
{
|
||||
if (char.IsLetterOrDigit(c) || c == '-')
|
||||
{
|
||||
characterList.Add(c);
|
||||
}
|
||||
}
|
||||
|
||||
var serverName = string.Create(
|
||||
characterList.Count,
|
||||
characterList,
|
||||
(dest, source) =>
|
||||
{
|
||||
for (int i = 0; i < dest.Length; i++)
|
||||
{
|
||||
dest[i] = source[i];
|
||||
}
|
||||
});
|
||||
|
||||
return _profile.FriendlyName.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void AppendIconList(StringBuilder builder)
|
||||
{
|
||||
builder.Append("<iconList>");
|
||||
|
||||
foreach (var icon in GetIcons())
|
||||
{
|
||||
builder.Append("<icon>");
|
||||
|
||||
builder.Append("<mimetype>")
|
||||
.Append(SecurityElement.Escape(icon.MimeType))
|
||||
.Append("</mimetype>");
|
||||
builder.Append("<width>")
|
||||
.Append(SecurityElement.Escape(icon.Width.ToString(CultureInfo.InvariantCulture)))
|
||||
.Append("</width>");
|
||||
builder.Append("<height>")
|
||||
.Append(SecurityElement.Escape(icon.Height.ToString(CultureInfo.InvariantCulture)))
|
||||
.Append("</height>");
|
||||
builder.Append("<depth>")
|
||||
.Append(SecurityElement.Escape(icon.Depth))
|
||||
.Append("</depth>");
|
||||
builder.Append("<url>")
|
||||
.Append(BuildUrl(icon.Url))
|
||||
.Append("</url>");
|
||||
|
||||
builder.Append("</icon>");
|
||||
}
|
||||
|
||||
builder.Append("</iconList>");
|
||||
}
|
||||
|
||||
private void AppendServiceList(StringBuilder builder)
|
||||
{
|
||||
builder.Append("<serviceList>");
|
||||
|
||||
foreach (var service in GetServices())
|
||||
{
|
||||
builder.Append("<service>");
|
||||
|
||||
builder.Append("<serviceType>")
|
||||
.Append(SecurityElement.Escape(service.ServiceType))
|
||||
.Append("</serviceType>");
|
||||
builder.Append("<serviceId>")
|
||||
.Append(SecurityElement.Escape(service.ServiceId))
|
||||
.Append("</serviceId>");
|
||||
builder.Append("<SCPDURL>")
|
||||
.Append(BuildUrl(service.ScpdUrl))
|
||||
.Append("</SCPDURL>");
|
||||
builder.Append("<controlURL>")
|
||||
.Append(BuildUrl(service.ControlUrl))
|
||||
.Append("</controlURL>");
|
||||
builder.Append("<eventSubURL>")
|
||||
.Append(BuildUrl(service.EventSubUrl))
|
||||
.Append("</eventSubURL>");
|
||||
|
||||
builder.Append("</service>");
|
||||
}
|
||||
|
||||
builder.Append("</serviceList>");
|
||||
}
|
||||
|
||||
private string BuildUrl(string url)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
|
||||
|
||||
return SecurityElement.Escape(url);
|
||||
}
|
||||
|
||||
private IEnumerable<DeviceIcon> GetIcons()
|
||||
=> new[]
|
||||
{
|
||||
new DeviceIcon
|
||||
{
|
||||
MimeType = "image/png",
|
||||
Depth = "24",
|
||||
Width = 240,
|
||||
Height = 240,
|
||||
Url = "icons/logo240.png"
|
||||
},
|
||||
|
||||
new DeviceIcon
|
||||
{
|
||||
MimeType = "image/jpeg",
|
||||
Depth = "24",
|
||||
Width = 240,
|
||||
Height = 240,
|
||||
Url = "icons/logo240.jpg"
|
||||
},
|
||||
|
||||
new DeviceIcon
|
||||
{
|
||||
MimeType = "image/png",
|
||||
Depth = "24",
|
||||
Width = 120,
|
||||
Height = 120,
|
||||
Url = "icons/logo120.png"
|
||||
},
|
||||
|
||||
new DeviceIcon
|
||||
{
|
||||
MimeType = "image/jpeg",
|
||||
Depth = "24",
|
||||
Width = 120,
|
||||
Height = 120,
|
||||
Url = "icons/logo120.jpg"
|
||||
},
|
||||
|
||||
new DeviceIcon
|
||||
{
|
||||
MimeType = "image/png",
|
||||
Depth = "24",
|
||||
Width = 48,
|
||||
Height = 48,
|
||||
Url = "icons/logo48.png"
|
||||
},
|
||||
|
||||
new DeviceIcon
|
||||
{
|
||||
MimeType = "image/jpeg",
|
||||
Depth = "24",
|
||||
Width = 48,
|
||||
Height = 48,
|
||||
Url = "icons/logo48.jpg"
|
||||
}
|
||||
};
|
||||
|
||||
private IEnumerable<DeviceService> GetServices()
|
||||
{
|
||||
var list = new List<DeviceService>();
|
||||
|
||||
list.Add(new DeviceService
|
||||
{
|
||||
ServiceType = "urn:schemas-upnp-org:service:ContentDirectory:1",
|
||||
ServiceId = "urn:upnp-org:serviceId:ContentDirectory",
|
||||
ScpdUrl = "contentdirectory/contentdirectory.xml",
|
||||
ControlUrl = "contentdirectory/control",
|
||||
EventSubUrl = "contentdirectory/events"
|
||||
});
|
||||
|
||||
list.Add(new DeviceService
|
||||
{
|
||||
ServiceType = "urn:schemas-upnp-org:service:ConnectionManager:1",
|
||||
ServiceId = "urn:upnp-org:serviceId:ConnectionManager",
|
||||
ScpdUrl = "connectionmanager/connectionmanager.xml",
|
||||
ControlUrl = "connectionmanager/control",
|
||||
EventSubUrl = "connectionmanager/events"
|
||||
});
|
||||
|
||||
if (_profile.EnableMSMediaReceiverRegistrar)
|
||||
{
|
||||
list.Add(new DeviceService
|
||||
{
|
||||
ServiceType = "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1",
|
||||
ServiceId = "urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar",
|
||||
ScpdUrl = "mediareceiverregistrar/mediareceiverregistrar.xml",
|
||||
ControlUrl = "mediareceiverregistrar/control",
|
||||
EventSubUrl = "mediareceiverregistrar/events"
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return GetXml();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,242 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Emby.Dlna.Didl;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.Service
|
||||
{
|
||||
public abstract class BaseControlHandler
|
||||
{
|
||||
private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/";
|
||||
|
||||
protected BaseControlHandler(IServerConfigurationManager config, ILogger logger)
|
||||
{
|
||||
Config = config;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
protected IServerConfigurationManager Config { get; }
|
||||
|
||||
protected ILogger Logger { get; }
|
||||
|
||||
public async Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogRequest(request);
|
||||
|
||||
var response = await ProcessControlRequestInternalAsync(request).ConfigureAwait(false);
|
||||
LogResponse(response);
|
||||
return response;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error processing control request");
|
||||
|
||||
return ControlErrorHandler.GetResponse(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request)
|
||||
{
|
||||
ControlRequestInfo requestInfo;
|
||||
|
||||
using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8))
|
||||
{
|
||||
var readerSettings = new XmlReaderSettings()
|
||||
{
|
||||
ValidationType = ValidationType.None,
|
||||
CheckCharacters = false,
|
||||
IgnoreProcessingInstructions = true,
|
||||
IgnoreComments = true,
|
||||
Async = true
|
||||
};
|
||||
|
||||
using var reader = XmlReader.Create(streamReader, readerSettings);
|
||||
requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Logger.LogDebug("Received control request {LocalName}, params: {@Headers}", requestInfo.LocalName, requestInfo.Headers);
|
||||
|
||||
return CreateControlResponse(requestInfo);
|
||||
}
|
||||
|
||||
private ControlResponse CreateControlResponse(ControlRequestInfo requestInfo)
|
||||
{
|
||||
var settings = new XmlWriterSettings
|
||||
{
|
||||
Encoding = Encoding.UTF8,
|
||||
CloseOutput = false
|
||||
};
|
||||
|
||||
StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8);
|
||||
|
||||
using (var writer = XmlWriter.Create(builder, settings))
|
||||
{
|
||||
writer.WriteStartDocument(true);
|
||||
|
||||
writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv);
|
||||
writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/");
|
||||
|
||||
writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv);
|
||||
writer.WriteStartElement("u", requestInfo.LocalName + "Response", requestInfo.NamespaceURI);
|
||||
|
||||
WriteResult(requestInfo.LocalName, requestInfo.Headers, writer);
|
||||
|
||||
writer.WriteFullEndElement();
|
||||
writer.WriteFullEndElement();
|
||||
|
||||
writer.WriteFullEndElement();
|
||||
writer.WriteEndDocument();
|
||||
}
|
||||
|
||||
var xml = builder.ToString().Replace("xmlns:m=", "xmlns:u=", StringComparison.Ordinal);
|
||||
|
||||
var controlResponse = new ControlResponse(xml, true);
|
||||
|
||||
controlResponse.Headers.Add("EXT", string.Empty);
|
||||
|
||||
return controlResponse;
|
||||
}
|
||||
|
||||
private async Task<ControlRequestInfo> ParseRequestAsync(XmlReader reader)
|
||||
{
|
||||
await reader.MoveToContentAsync().ConfigureAwait(false);
|
||||
await reader.ReadAsync().ConfigureAwait(false);
|
||||
|
||||
// Loop through each element
|
||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
if (string.Equals(reader.LocalName, "Body", StringComparison.Ordinal))
|
||||
{
|
||||
if (reader.IsEmptyElement)
|
||||
{
|
||||
await reader.ReadAsync().ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
using var subReader = reader.ReadSubtree();
|
||||
return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await reader.SkipAsync().ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await reader.ReadAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
throw new EndOfStreamException("Stream ended but no body tag found.");
|
||||
}
|
||||
|
||||
private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
|
||||
{
|
||||
string? namespaceURI = null, localName = null;
|
||||
|
||||
await reader.MoveToContentAsync().ConfigureAwait(false);
|
||||
await reader.ReadAsync().ConfigureAwait(false);
|
||||
|
||||
// Loop through each element
|
||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
localName = reader.LocalName;
|
||||
namespaceURI = reader.NamespaceURI;
|
||||
|
||||
if (reader.IsEmptyElement)
|
||||
{
|
||||
await reader.ReadAsync().ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = new ControlRequestInfo(localName, namespaceURI);
|
||||
using var subReader = reader.ReadSubtree();
|
||||
await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await reader.ReadAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (localName is not null && namespaceURI is not null)
|
||||
{
|
||||
return new ControlRequestInfo(localName, namespaceURI);
|
||||
}
|
||||
|
||||
throw new EndOfStreamException("Stream ended but no control found.");
|
||||
}
|
||||
|
||||
private async Task ParseFirstBodyChildAsync(XmlReader reader, IDictionary<string, string> headers)
|
||||
{
|
||||
await reader.MoveToContentAsync().ConfigureAwait(false);
|
||||
await reader.ReadAsync().ConfigureAwait(false);
|
||||
|
||||
// Loop through each element
|
||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
// TODO: Should we be doing this here, or should it be handled earlier when decoding the request?
|
||||
headers[reader.LocalName.RemoveDiacritics()] = await reader.ReadElementContentAsStringAsync().ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await reader.ReadAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void WriteResult(string methodName, IReadOnlyDictionary<string, string> methodParams, XmlWriter xmlWriter);
|
||||
|
||||
private void LogRequest(ControlRequest request)
|
||||
{
|
||||
if (!Config.GetDlnaConfiguration().EnableDebugLog)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogDebug("Control request. Headers: {@Headers}", request.Headers);
|
||||
}
|
||||
|
||||
private void LogResponse(ControlResponse response)
|
||||
{
|
||||
if (!Config.GetDlnaConfiguration().EnableDebugLog)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogDebug("Control response. Headers: {@Headers}\n{Xml}", response.Headers, response.Xml);
|
||||
}
|
||||
|
||||
private class ControlRequestInfo
|
||||
{
|
||||
public ControlRequestInfo(string localName, string namespaceUri)
|
||||
{
|
||||
LocalName = localName;
|
||||
NamespaceURI = namespaceUri;
|
||||
Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public string LocalName { get; set; }
|
||||
|
||||
public string NamespaceURI { get; set; }
|
||||
|
||||
public Dictionary<string, string> Headers { get; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Net.Http;
|
||||
using Emby.Dlna.Eventing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.Service
|
||||
{
|
||||
public class BaseService : IDlnaEventManager
|
||||
{
|
||||
protected BaseService(ILogger<BaseService> logger, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
Logger = logger;
|
||||
EventManager = new DlnaEventManager(logger, httpClientFactory);
|
||||
}
|
||||
|
||||
protected IDlnaEventManager EventManager { get; }
|
||||
|
||||
protected ILogger Logger { get; }
|
||||
|
||||
public EventSubscriptionResponse CancelEventSubscription(string subscriptionId)
|
||||
{
|
||||
return EventManager.CancelEventSubscription(subscriptionId);
|
||||
}
|
||||
|
||||
public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl)
|
||||
{
|
||||
return EventManager.RenewEventSubscription(subscriptionId, notificationType, requestedTimeoutString, callbackUrl);
|
||||
}
|
||||
|
||||
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
|
||||
{
|
||||
return EventManager.CreateEventSubscription(notificationType, requestedTimeoutString, callbackUrl);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using Emby.Dlna.Didl;
|
||||
|
||||
namespace Emby.Dlna.Service
|
||||
{
|
||||
public static class ControlErrorHandler
|
||||
{
|
||||
private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/";
|
||||
|
||||
public static ControlResponse GetResponse(Exception ex)
|
||||
{
|
||||
var settings = new XmlWriterSettings
|
||||
{
|
||||
Encoding = Encoding.UTF8,
|
||||
CloseOutput = false
|
||||
};
|
||||
|
||||
StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8);
|
||||
|
||||
using (var writer = XmlWriter.Create(builder, settings))
|
||||
{
|
||||
writer.WriteStartDocument(true);
|
||||
|
||||
writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv);
|
||||
writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/");
|
||||
|
||||
writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv);
|
||||
writer.WriteStartElement("SOAP-ENV", "Fault", NsSoapEnv);
|
||||
|
||||
writer.WriteElementString("faultcode", "500");
|
||||
writer.WriteElementString("faultstring", ex.Message);
|
||||
|
||||
writer.WriteStartElement("detail");
|
||||
writer.WriteRaw("<UPnPError xmlns=\"urn:schemas-upnp-org:control-1-0\"><errorCode>401</errorCode><errorDescription>Invalid Action</errorDescription></UPnPError>");
|
||||
writer.WriteFullEndElement();
|
||||
|
||||
writer.WriteFullEndElement();
|
||||
writer.WriteFullEndElement();
|
||||
|
||||
writer.WriteFullEndElement();
|
||||
writer.WriteEndDocument();
|
||||
}
|
||||
|
||||
return new ControlResponse(builder.ToString(), false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using Emby.Dlna.Common;
|
||||
|
||||
namespace Emby.Dlna.Service
|
||||
{
|
||||
public class ServiceXmlBuilder
|
||||
{
|
||||
public string GetXml(IEnumerable<ServiceAction> actions, IEnumerable<StateVariable> stateVariables)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.Append("<?xml version=\"1.0\"?>");
|
||||
builder.Append("<scpd xmlns=\"urn:schemas-upnp-org:service-1-0\">");
|
||||
|
||||
builder.Append("<specVersion>");
|
||||
builder.Append("<major>1</major>");
|
||||
builder.Append("<minor>0</minor>");
|
||||
builder.Append("</specVersion>");
|
||||
|
||||
AppendActionList(builder, actions);
|
||||
AppendServiceStateTable(builder, stateVariables);
|
||||
|
||||
builder.Append("</scpd>");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static void AppendActionList(StringBuilder builder, IEnumerable<ServiceAction> actions)
|
||||
{
|
||||
builder.Append("<actionList>");
|
||||
|
||||
foreach (var item in actions)
|
||||
{
|
||||
builder.Append("<action>");
|
||||
|
||||
builder.Append("<name>")
|
||||
.Append(SecurityElement.Escape(item.Name))
|
||||
.Append("</name>");
|
||||
|
||||
builder.Append("<argumentList>");
|
||||
|
||||
foreach (var argument in item.ArgumentList)
|
||||
{
|
||||
builder.Append("<argument>");
|
||||
|
||||
builder.Append("<name>")
|
||||
.Append(SecurityElement.Escape(argument.Name))
|
||||
.Append("</name>");
|
||||
builder.Append("<direction>")
|
||||
.Append(SecurityElement.Escape(argument.Direction))
|
||||
.Append("</direction>");
|
||||
builder.Append("<relatedStateVariable>")
|
||||
.Append(SecurityElement.Escape(argument.RelatedStateVariable))
|
||||
.Append("</relatedStateVariable>");
|
||||
|
||||
builder.Append("</argument>");
|
||||
}
|
||||
|
||||
builder.Append("</argumentList>");
|
||||
|
||||
builder.Append("</action>");
|
||||
}
|
||||
|
||||
builder.Append("</actionList>");
|
||||
}
|
||||
|
||||
private static void AppendServiceStateTable(StringBuilder builder, IEnumerable<StateVariable> stateVariables)
|
||||
{
|
||||
builder.Append("<serviceStateTable>");
|
||||
|
||||
foreach (var item in stateVariables)
|
||||
{
|
||||
var sendEvents = item.SendsEvents ? "yes" : "no";
|
||||
|
||||
builder.Append("<stateVariable sendEvents=\"")
|
||||
.Append(sendEvents)
|
||||
.Append("\">");
|
||||
|
||||
builder.Append("<name>")
|
||||
.Append(SecurityElement.Escape(item.Name))
|
||||
.Append("</name>");
|
||||
builder.Append("<dataType>")
|
||||
.Append(SecurityElement.Escape(item.DataType))
|
||||
.Append("</dataType>");
|
||||
|
||||
if (item.AllowedValues.Count > 0)
|
||||
{
|
||||
builder.Append("<allowedValueList>");
|
||||
foreach (var allowedValue in item.AllowedValues)
|
||||
{
|
||||
builder.Append("<allowedValue>")
|
||||
.Append(SecurityElement.Escape(allowedValue))
|
||||
.Append("</allowedValue>");
|
||||
}
|
||||
|
||||
builder.Append("</allowedValueList>");
|
||||
}
|
||||
|
||||
builder.Append("</stateVariable>");
|
||||
}
|
||||
|
||||
builder.Append("</serviceStateTable>");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,151 +0,0 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using Rssdp;
|
||||
using Rssdp.Infrastructure;
|
||||
|
||||
namespace Emby.Dlna.Ssdp
|
||||
{
|
||||
public sealed class DeviceDiscovery : IDeviceDiscovery, IDisposable
|
||||
{
|
||||
private readonly object _syncLock = new object();
|
||||
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
||||
private SsdpDeviceLocator _deviceLocator;
|
||||
private ISsdpCommunicationsServer _commsServer;
|
||||
|
||||
private int _listenerCount;
|
||||
private bool _disposed;
|
||||
|
||||
public DeviceDiscovery(IServerConfigurationManager config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
private event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscoveredInternal;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscovered
|
||||
{
|
||||
add
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
_listenerCount++;
|
||||
DeviceDiscoveredInternal += value;
|
||||
}
|
||||
|
||||
StartInternal();
|
||||
}
|
||||
|
||||
remove
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
_listenerCount--;
|
||||
DeviceDiscoveredInternal -= value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft;
|
||||
|
||||
// Call this method from somewhere in your code to start the search.
|
||||
public void Start(ISsdpCommunicationsServer communicationsServer)
|
||||
{
|
||||
_commsServer = communicationsServer;
|
||||
|
||||
StartInternal();
|
||||
}
|
||||
|
||||
private void StartInternal()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_listenerCount > 0 && _deviceLocator is null && _commsServer is not null)
|
||||
{
|
||||
_deviceLocator = new SsdpDeviceLocator(
|
||||
_commsServer,
|
||||
Environment.OSVersion.Platform.ToString(),
|
||||
// Can not use VersionString here since that includes OS and version
|
||||
Environment.OSVersion.Version.ToString());
|
||||
|
||||
// (Optional) Set the filter so we only see notifications for devices we care about
|
||||
// (can be any search target value i.e device type, uuid value etc - any value that appears in the
|
||||
// DiscoverdSsdpDevice.NotificationType property or that is used with the searchTarget parameter of the Search method).
|
||||
// _DeviceLocator.NotificationFilter = "upnp:rootdevice";
|
||||
|
||||
// Connect our event handler so we process devices as they are found
|
||||
_deviceLocator.DeviceAvailable += OnDeviceLocatorDeviceAvailable;
|
||||
_deviceLocator.DeviceUnavailable += OnDeviceLocatorDeviceUnavailable;
|
||||
|
||||
var dueTime = TimeSpan.FromSeconds(5);
|
||||
var interval = TimeSpan.FromSeconds(_config.GetDlnaConfiguration().ClientDiscoveryIntervalSeconds);
|
||||
|
||||
_deviceLocator.RestartBroadcastTimer(dueTime, interval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process each found device in the event handler
|
||||
private void OnDeviceLocatorDeviceAvailable(object sender, DeviceAvailableEventArgs e)
|
||||
{
|
||||
var originalHeaders = e.DiscoveredDevice.ResponseHeaders;
|
||||
|
||||
var headerDict = originalHeaders is null ? new Dictionary<string, KeyValuePair<string, IEnumerable<string>>>() : originalHeaders.ToDictionary(i => i.Key, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var args = new GenericEventArgs<UpnpDeviceInfo>(
|
||||
new UpnpDeviceInfo
|
||||
{
|
||||
Location = e.DiscoveredDevice.DescriptionLocation,
|
||||
Headers = headers,
|
||||
RemoteIPAddress = e.RemoteIPAddress
|
||||
});
|
||||
|
||||
DeviceDiscoveredInternal?.Invoke(this, args);
|
||||
}
|
||||
|
||||
private void OnDeviceLocatorDeviceUnavailable(object sender, DeviceUnavailableEventArgs e)
|
||||
{
|
||||
var originalHeaders = e.DiscoveredDevice.ResponseHeaders;
|
||||
|
||||
var headerDict = originalHeaders is null ? new Dictionary<string, KeyValuePair<string, IEnumerable<string>>>() : originalHeaders.ToDictionary(i => i.Key, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var headers = headerDict.ToDictionary(i => i.Key, i => i.Value.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var args = new GenericEventArgs<UpnpDeviceInfo>(
|
||||
new UpnpDeviceInfo
|
||||
{
|
||||
Location = e.DiscoveredDevice.DescriptionLocation,
|
||||
Headers = headers
|
||||
});
|
||||
|
||||
DeviceLeft?.Invoke(this, args);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_disposed = true;
|
||||
if (_deviceLocator is not null)
|
||||
{
|
||||
_deviceLocator.Dispose();
|
||||
_deviceLocator = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|