|
@@ -4,12 +4,13 @@ using System;
|
|
|
using System.Collections.Generic;
|
|
|
using System.Globalization;
|
|
|
using System.Linq;
|
|
|
+using System.Net.Http;
|
|
|
+using System.Security;
|
|
|
using System.Threading;
|
|
|
using System.Threading.Tasks;
|
|
|
using System.Xml;
|
|
|
using System.Xml.Linq;
|
|
|
using Emby.Dlna.Common;
|
|
|
-using Emby.Dlna.Server;
|
|
|
using Emby.Dlna.Ssdp;
|
|
|
using MediaBrowser.Common.Net;
|
|
|
using MediaBrowser.Controller.Configuration;
|
|
@@ -19,24 +20,48 @@ namespace Emby.Dlna.PlayTo
|
|
|
{
|
|
|
public class Device : IDisposable
|
|
|
{
|
|
|
- #region Fields & Properties
|
|
|
+ private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
|
|
+
|
|
|
+ private readonly IHttpClientFactory _httpClientFactory;
|
|
|
+
|
|
|
+ private readonly ILogger _logger;
|
|
|
|
|
|
+ private readonly object _timerLock = new object();
|
|
|
private Timer _timer;
|
|
|
+ private int _muteVol;
|
|
|
+ private int _volume;
|
|
|
+ private DateTime _lastVolumeRefresh;
|
|
|
+ private bool _volumeRefreshActive;
|
|
|
+ private int _connectFailureCount;
|
|
|
+ private bool _disposed;
|
|
|
+
|
|
|
+ public Device(DeviceInfo deviceProperties, IHttpClientFactory httpClientFactory, ILogger logger)
|
|
|
+ {
|
|
|
+ Properties = deviceProperties;
|
|
|
+ _httpClientFactory = httpClientFactory;
|
|
|
+ _logger = logger;
|
|
|
+ }
|
|
|
+
|
|
|
+ public event EventHandler<PlaybackStartEventArgs> PlaybackStart;
|
|
|
+
|
|
|
+ public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
|
|
|
+
|
|
|
+ public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped;
|
|
|
+
|
|
|
+ public event EventHandler<MediaChangedEventArgs> MediaChanged;
|
|
|
|
|
|
public DeviceInfo Properties { get; set; }
|
|
|
|
|
|
- private int _muteVol;
|
|
|
public bool IsMuted { get; set; }
|
|
|
|
|
|
- private int _volume;
|
|
|
-
|
|
|
public int Volume
|
|
|
{
|
|
|
get
|
|
|
{
|
|
|
- RefreshVolumeIfNeeded();
|
|
|
+ RefreshVolumeIfNeeded().GetAwaiter().GetResult();
|
|
|
return _volume;
|
|
|
}
|
|
|
+
|
|
|
set => _volume = value;
|
|
|
}
|
|
|
|
|
@@ -44,29 +69,21 @@ namespace Emby.Dlna.PlayTo
|
|
|
|
|
|
public TimeSpan Position { get; set; } = TimeSpan.FromSeconds(0);
|
|
|
|
|
|
- public TRANSPORTSTATE TransportState { get; private set; }
|
|
|
+ public TransportState TransportState { get; private set; }
|
|
|
|
|
|
- public bool IsPlaying => TransportState == TRANSPORTSTATE.PLAYING;
|
|
|
+ public bool IsPlaying => TransportState == TransportState.Playing;
|
|
|
|
|
|
- public bool IsPaused => TransportState == TRANSPORTSTATE.PAUSED || TransportState == TRANSPORTSTATE.PAUSED_PLAYBACK;
|
|
|
+ public bool IsPaused => TransportState == TransportState.Paused || TransportState == TransportState.PausedPlayback;
|
|
|
|
|
|
- public bool IsStopped => TransportState == TRANSPORTSTATE.STOPPED;
|
|
|
+ public bool IsStopped => TransportState == TransportState.Stopped;
|
|
|
|
|
|
- #endregion
|
|
|
+ public Action OnDeviceUnavailable { get; set; }
|
|
|
|
|
|
- private readonly IHttpClient _httpClient;
|
|
|
- private readonly ILogger _logger;
|
|
|
- private readonly IServerConfigurationManager _config;
|
|
|
+ private TransportCommands AvCommands { get; set; }
|
|
|
|
|
|
- public Action OnDeviceUnavailable { get; set; }
|
|
|
+ private TransportCommands RendererCommands { get; set; }
|
|
|
|
|
|
- public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config)
|
|
|
- {
|
|
|
- Properties = deviceProperties;
|
|
|
- _httpClient = httpClient;
|
|
|
- _logger = logger;
|
|
|
- _config = config;
|
|
|
- }
|
|
|
+ public UBaseObject CurrentMediaInfo { get; private set; }
|
|
|
|
|
|
public void Start()
|
|
|
{
|
|
@@ -74,26 +91,24 @@ namespace Emby.Dlna.PlayTo
|
|
|
_timer = new Timer(TimerCallback, null, 1000, Timeout.Infinite);
|
|
|
}
|
|
|
|
|
|
- private DateTime _lastVolumeRefresh;
|
|
|
- private bool _volumeRefreshActive;
|
|
|
- private void RefreshVolumeIfNeeded()
|
|
|
+ private Task RefreshVolumeIfNeeded()
|
|
|
{
|
|
|
- if (!_volumeRefreshActive)
|
|
|
- {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (DateTime.UtcNow >= _lastVolumeRefresh.AddSeconds(5))
|
|
|
+ if (_volumeRefreshActive
|
|
|
+ && DateTime.UtcNow >= _lastVolumeRefresh.AddSeconds(5))
|
|
|
{
|
|
|
_lastVolumeRefresh = DateTime.UtcNow;
|
|
|
- RefreshVolume(CancellationToken.None);
|
|
|
+ return RefreshVolume();
|
|
|
}
|
|
|
+
|
|
|
+ return Task.CompletedTask;
|
|
|
}
|
|
|
|
|
|
- private async void RefreshVolume(CancellationToken cancellationToken)
|
|
|
+ private async Task RefreshVolume(CancellationToken cancellationToken = default)
|
|
|
{
|
|
|
if (_disposed)
|
|
|
+ {
|
|
|
return;
|
|
|
+ }
|
|
|
|
|
|
try
|
|
|
{
|
|
@@ -106,7 +121,6 @@ namespace Emby.Dlna.PlayTo
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private readonly object _timerLock = new object();
|
|
|
private void RestartTimer(bool immediate = false)
|
|
|
{
|
|
|
lock (_timerLock)
|
|
@@ -141,8 +155,6 @@ namespace Emby.Dlna.PlayTo
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- #region Commanding
|
|
|
-
|
|
|
public Task VolumeDown(CancellationToken cancellationToken)
|
|
|
{
|
|
|
var sendVolume = Math.Max(Volume - 5, 0);
|
|
@@ -211,7 +223,9 @@ namespace Emby.Dlna.PlayTo
|
|
|
|
|
|
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
|
|
|
if (command == null)
|
|
|
+ {
|
|
|
return false;
|
|
|
+ }
|
|
|
|
|
|
var service = GetServiceRenderingControl();
|
|
|
|
|
@@ -223,7 +237,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
_logger.LogDebug("Setting mute");
|
|
|
var value = mute ? 1 : 0;
|
|
|
|
|
|
- await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
|
|
|
+ await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
|
IsMuted = mute;
|
|
@@ -232,15 +246,20 @@ namespace Emby.Dlna.PlayTo
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
- /// Sets volume on a scale of 0-100
|
|
|
+ /// Sets volume on a scale of 0-100.
|
|
|
/// </summary>
|
|
|
+ /// <param name="value">The volume on a scale of 0-100.</param>
|
|
|
+ /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
|
|
|
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
|
|
public async Task SetVolume(int value, CancellationToken cancellationToken)
|
|
|
{
|
|
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
|
|
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
|
|
|
if (command == null)
|
|
|
+ {
|
|
|
return;
|
|
|
+ }
|
|
|
|
|
|
var service = GetServiceRenderingControl();
|
|
|
|
|
@@ -253,7 +272,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
// Remote control will perform better
|
|
|
Volume = value;
|
|
|
|
|
|
- await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
|
|
|
+ await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
|
|
|
.ConfigureAwait(false);
|
|
|
}
|
|
|
|
|
@@ -263,7 +282,9 @@ namespace Emby.Dlna.PlayTo
|
|
|
|
|
|
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
|
|
|
if (command == null)
|
|
|
+ {
|
|
|
return;
|
|
|
+ }
|
|
|
|
|
|
var service = GetAvTransportService();
|
|
|
|
|
@@ -272,7 +293,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
throw new InvalidOperationException("Unable to find service");
|
|
|
}
|
|
|
|
|
|
- await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format("{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
|
|
|
+ await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
|
RestartTimer(true);
|
|
@@ -282,18 +303,20 @@ namespace Emby.Dlna.PlayTo
|
|
|
{
|
|
|
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
|
|
- url = url.Replace("&", "&");
|
|
|
+ url = url.Replace("&", "&", StringComparison.Ordinal);
|
|
|
|
|
|
_logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
|
|
|
|
|
|
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
|
|
|
if (command == null)
|
|
|
+ {
|
|
|
return;
|
|
|
+ }
|
|
|
|
|
|
var dictionary = new Dictionary<string, string>
|
|
|
{
|
|
|
- {"CurrentURI", url},
|
|
|
- {"CurrentURIMetaData", CreateDidlMeta(metaData)}
|
|
|
+ { "CurrentURI", url },
|
|
|
+ { "CurrentURIMetaData", CreateDidlMeta(metaData) }
|
|
|
};
|
|
|
|
|
|
var service = GetAvTransportService();
|
|
@@ -304,7 +327,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
}
|
|
|
|
|
|
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
|
|
|
- await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
|
|
|
+ await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
|
await Task.Delay(50).ConfigureAwait(false);
|
|
@@ -329,7 +352,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
return string.Empty;
|
|
|
}
|
|
|
|
|
|
- return DescriptionXmlBuilder.Escape(value);
|
|
|
+ return SecurityElement.Escape(value);
|
|
|
}
|
|
|
|
|
|
private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)
|
|
@@ -346,7 +369,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
throw new InvalidOperationException("Unable to find service");
|
|
|
}
|
|
|
|
|
|
- return new SsdpHttpClient(_httpClient).SendCommandAsync(
|
|
|
+ return new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
|
|
Properties.BaseUrl,
|
|
|
service,
|
|
|
command.Name,
|
|
@@ -375,7 +398,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
|
|
|
var service = GetAvTransportService();
|
|
|
|
|
|
- await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
|
|
|
+ await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
|
RestartTimer(true);
|
|
@@ -393,19 +416,14 @@ namespace Emby.Dlna.PlayTo
|
|
|
|
|
|
var service = GetAvTransportService();
|
|
|
|
|
|
- await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
|
|
|
+ await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
|
- TransportState = TRANSPORTSTATE.PAUSED;
|
|
|
+ TransportState = TransportState.Paused;
|
|
|
|
|
|
RestartTimer(true);
|
|
|
}
|
|
|
|
|
|
- #endregion
|
|
|
-
|
|
|
- #region Get data
|
|
|
-
|
|
|
- private int _connectFailureCount;
|
|
|
private async void TimerCallback(object sender)
|
|
|
{
|
|
|
if (_disposed)
|
|
@@ -434,7 +452,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
if (transportState.HasValue)
|
|
|
{
|
|
|
// If we're not playing anything no need to get additional data
|
|
|
- if (transportState.Value == TRANSPORTSTATE.STOPPED)
|
|
|
+ if (transportState.Value == TransportState.Stopped)
|
|
|
{
|
|
|
UpdateMediaInfo(null, transportState.Value);
|
|
|
}
|
|
@@ -458,10 +476,12 @@ namespace Emby.Dlna.PlayTo
|
|
|
_connectFailureCount = 0;
|
|
|
|
|
|
if (_disposed)
|
|
|
+ {
|
|
|
return;
|
|
|
+ }
|
|
|
|
|
|
// If we're not playing anything make sure we don't get data more often than neccessry to keep the Session alive
|
|
|
- if (transportState.Value == TRANSPORTSTATE.STOPPED)
|
|
|
+ if (transportState.Value == TransportState.Stopped)
|
|
|
{
|
|
|
RestartTimerInactive();
|
|
|
}
|
|
@@ -478,7 +498,9 @@ namespace Emby.Dlna.PlayTo
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
if (_disposed)
|
|
|
+ {
|
|
|
return;
|
|
|
+ }
|
|
|
|
|
|
_logger.LogError(ex, "Error updating device info for {DeviceName}", Properties.Name);
|
|
|
|
|
@@ -494,6 +516,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
RestartTimerInactive();
|
|
|
}
|
|
|
}
|
|
@@ -520,7 +543,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
|
|
|
+ var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
|
|
Properties.BaseUrl,
|
|
|
service,
|
|
|
command.Name,
|
|
@@ -532,7 +555,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- var volume = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null);
|
|
|
+ var volume = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null);
|
|
|
var volumeValue = volume?.Value;
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(volumeValue))
|
|
@@ -570,7 +593,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
|
|
|
+ var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
|
|
Properties.BaseUrl,
|
|
|
service,
|
|
|
command.Name,
|
|
@@ -578,16 +601,18 @@ namespace Emby.Dlna.PlayTo
|
|
|
cancellationToken: cancellationToken).ConfigureAwait(false);
|
|
|
|
|
|
if (result == null || result.Document == null)
|
|
|
+ {
|
|
|
return;
|
|
|
+ }
|
|
|
|
|
|
- var valueNode = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetMuteResponse")
|
|
|
+ var valueNode = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetMuteResponse")
|
|
|
.Select(i => i.Element("CurrentMute"))
|
|
|
.FirstOrDefault(i => i != null);
|
|
|
|
|
|
IsMuted = string.Equals(valueNode?.Value, "1", StringComparison.OrdinalIgnoreCase);
|
|
|
}
|
|
|
|
|
|
- private async Task<TRANSPORTSTATE?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
|
|
+ private async Task<TransportState?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
|
|
{
|
|
|
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetTransportInfo");
|
|
|
if (command == null)
|
|
@@ -601,7 +626,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
|
|
|
+ var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
|
|
Properties.BaseUrl,
|
|
|
service,
|
|
|
command.Name,
|
|
@@ -614,12 +639,12 @@ namespace Emby.Dlna.PlayTo
|
|
|
}
|
|
|
|
|
|
var transportState =
|
|
|
- result.Document.Descendants(uPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null);
|
|
|
+ result.Document.Descendants(UPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null);
|
|
|
|
|
|
var transportStateValue = transportState?.Value;
|
|
|
|
|
|
if (transportStateValue != null
|
|
|
- && Enum.TryParse(transportStateValue, true, out TRANSPORTSTATE state))
|
|
|
+ && Enum.TryParse(transportStateValue, true, out TransportState state))
|
|
|
{
|
|
|
return state;
|
|
|
}
|
|
@@ -627,7 +652,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- private async Task<uBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
|
|
+ private async Task<UBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
|
|
{
|
|
|
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo");
|
|
|
if (command == null)
|
|
@@ -643,7 +668,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
|
|
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
|
|
- var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
|
|
|
+ var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
|
|
Properties.BaseUrl,
|
|
|
service,
|
|
|
command.Name,
|
|
@@ -662,7 +687,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- var e = track.Element(uPnpNamespaces.items) ?? track;
|
|
|
+ var e = track.Element(UPnpNamespaces.Items) ?? track;
|
|
|
|
|
|
var elementString = (string)e;
|
|
|
|
|
@@ -678,13 +703,13 @@ namespace Emby.Dlna.PlayTo
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- e = track.Element(uPnpNamespaces.items) ?? track;
|
|
|
+ e = track.Element(UPnpNamespaces.Items) ?? track;
|
|
|
|
|
|
elementString = (string)e;
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(elementString))
|
|
|
{
|
|
|
- return new uBaseObject
|
|
|
+ return new UBaseObject
|
|
|
{
|
|
|
Url = elementString
|
|
|
};
|
|
@@ -693,7 +718,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- private async Task<(bool, uBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
|
|
+ private async Task<(bool, UBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
|
|
{
|
|
|
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
|
|
|
if (command == null)
|
|
@@ -710,7 +735,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
|
|
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
|
|
- var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(
|
|
|
+ var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
|
|
Properties.BaseUrl,
|
|
|
service,
|
|
|
command.Name,
|
|
@@ -722,11 +747,11 @@ namespace Emby.Dlna.PlayTo
|
|
|
return (false, null);
|
|
|
}
|
|
|
|
|
|
- var trackUriElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null);
|
|
|
- var trackUri = trackUriElem == null ? null : trackUriElem.Value;
|
|
|
+ var trackUriElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null);
|
|
|
+ var trackUri = trackUriElem?.Value;
|
|
|
|
|
|
- var durationElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null);
|
|
|
- var duration = durationElem == null ? null : durationElem.Value;
|
|
|
+ var durationElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null);
|
|
|
+ var duration = durationElem?.Value;
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(duration)
|
|
|
&& !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
|
|
@@ -738,8 +763,8 @@ namespace Emby.Dlna.PlayTo
|
|
|
Duration = null;
|
|
|
}
|
|
|
|
|
|
- var positionElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null);
|
|
|
- var position = positionElem == null ? null : positionElem.Value;
|
|
|
+ var positionElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null);
|
|
|
+ var position = positionElem?.Value;
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
|
|
|
{
|
|
@@ -750,7 +775,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
|
|
|
if (track == null)
|
|
|
{
|
|
|
- //If track is null, some vendors do this, use GetMediaInfo instead
|
|
|
+ // If track is null, some vendors do this, use GetMediaInfo instead
|
|
|
return (true, null);
|
|
|
}
|
|
|
|
|
@@ -778,7 +803,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
return (true, null);
|
|
|
}
|
|
|
|
|
|
- var e = uPnpResponse.Element(uPnpNamespaces.items);
|
|
|
+ var e = uPnpResponse.Element(UPnpNamespaces.Items);
|
|
|
|
|
|
var uTrack = CreateUBaseObject(e, trackUri);
|
|
|
|
|
@@ -794,7 +819,6 @@ namespace Emby.Dlna.PlayTo
|
|
|
}
|
|
|
catch (XmlException)
|
|
|
{
|
|
|
-
|
|
|
}
|
|
|
|
|
|
// first try to add a root node with a dlna namesapce
|
|
@@ -806,43 +830,41 @@ namespace Emby.Dlna.PlayTo
|
|
|
}
|
|
|
catch (XmlException)
|
|
|
{
|
|
|
-
|
|
|
}
|
|
|
|
|
|
// some devices send back invalid xml
|
|
|
try
|
|
|
{
|
|
|
- return XElement.Parse(xml.Replace("&", "&"));
|
|
|
+ return XElement.Parse(xml.Replace("&", "&", StringComparison.Ordinal));
|
|
|
}
|
|
|
catch (XmlException)
|
|
|
{
|
|
|
-
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- private static uBaseObject CreateUBaseObject(XElement container, string trackUri)
|
|
|
+ private static UBaseObject CreateUBaseObject(XElement container, string trackUri)
|
|
|
{
|
|
|
if (container == null)
|
|
|
{
|
|
|
throw new ArgumentNullException(nameof(container));
|
|
|
}
|
|
|
|
|
|
- var url = container.GetValue(uPnpNamespaces.Res);
|
|
|
+ var url = container.GetValue(UPnpNamespaces.Res);
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(url))
|
|
|
{
|
|
|
url = trackUri;
|
|
|
}
|
|
|
|
|
|
- return new uBaseObject
|
|
|
+ return new UBaseObject
|
|
|
{
|
|
|
- Id = container.GetAttributeValue(uPnpNamespaces.Id),
|
|
|
- ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId),
|
|
|
- Title = container.GetValue(uPnpNamespaces.title),
|
|
|
- IconUrl = container.GetValue(uPnpNamespaces.Artwork),
|
|
|
- SecondText = "",
|
|
|
+ Id = container.GetAttributeValue(UPnpNamespaces.Id),
|
|
|
+ ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId),
|
|
|
+ Title = container.GetValue(UPnpNamespaces.Title),
|
|
|
+ IconUrl = container.GetValue(UPnpNamespaces.Artwork),
|
|
|
+ SecondText = string.Empty,
|
|
|
Url = url,
|
|
|
ProtocolInfo = GetProtocolInfo(container),
|
|
|
MetaData = container.ToString()
|
|
@@ -856,11 +878,11 @@ namespace Emby.Dlna.PlayTo
|
|
|
throw new ArgumentNullException(nameof(container));
|
|
|
}
|
|
|
|
|
|
- var resElement = container.Element(uPnpNamespaces.Res);
|
|
|
+ var resElement = container.Element(UPnpNamespaces.Res);
|
|
|
|
|
|
if (resElement != null)
|
|
|
{
|
|
|
- var info = resElement.Attribute(uPnpNamespaces.ProtocolInfo);
|
|
|
+ var info = resElement.Attribute(UPnpNamespaces.ProtocolInfo);
|
|
|
|
|
|
if (info != null && !string.IsNullOrWhiteSpace(info.Value))
|
|
|
{
|
|
@@ -871,10 +893,6 @@ namespace Emby.Dlna.PlayTo
|
|
|
return new string[4];
|
|
|
}
|
|
|
|
|
|
- #endregion
|
|
|
-
|
|
|
- #region From XML
|
|
|
-
|
|
|
private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken)
|
|
|
{
|
|
|
if (AvCommands != null)
|
|
@@ -895,7 +913,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
|
|
|
string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
|
|
|
|
|
|
- var httpClient = new SsdpHttpClient(_httpClient);
|
|
|
+ var httpClient = new SsdpHttpClient(_httpClientFactory);
|
|
|
|
|
|
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
@@ -923,7 +941,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
|
|
|
string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
|
|
|
|
|
|
- var httpClient = new SsdpHttpClient(_httpClient);
|
|
|
+ var httpClient = new SsdpHttpClient(_httpClientFactory);
|
|
|
_logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
|
|
|
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
@@ -939,12 +957,12 @@ namespace Emby.Dlna.PlayTo
|
|
|
return url;
|
|
|
}
|
|
|
|
|
|
- if (!url.Contains("/"))
|
|
|
+ if (!url.Contains('/', StringComparison.Ordinal))
|
|
|
{
|
|
|
url = "/dmr/" + url;
|
|
|
}
|
|
|
|
|
|
- if (!url.StartsWith("/"))
|
|
|
+ if (!url.StartsWith("/", StringComparison.Ordinal))
|
|
|
{
|
|
|
url = "/" + url;
|
|
|
}
|
|
@@ -952,25 +970,21 @@ namespace Emby.Dlna.PlayTo
|
|
|
return baseUrl + url;
|
|
|
}
|
|
|
|
|
|
- private TransportCommands AvCommands { get; set; }
|
|
|
-
|
|
|
- private TransportCommands RendererCommands { get; set; }
|
|
|
-
|
|
|
- public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, IServerConfigurationManager config, ILogger logger, CancellationToken cancellationToken)
|
|
|
+ public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken)
|
|
|
{
|
|
|
- var ssdpHttpClient = new SsdpHttpClient(httpClient);
|
|
|
+ var ssdpHttpClient = new SsdpHttpClient(httpClientFactory);
|
|
|
|
|
|
var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
|
|
|
|
|
|
var friendlyNames = new List<string>();
|
|
|
|
|
|
- var name = document.Descendants(uPnpNamespaces.ud.GetName("friendlyName")).FirstOrDefault();
|
|
|
+ var name = document.Descendants(UPnpNamespaces.Ud.GetName("friendlyName")).FirstOrDefault();
|
|
|
if (name != null && !string.IsNullOrWhiteSpace(name.Value))
|
|
|
{
|
|
|
friendlyNames.Add(name.Value);
|
|
|
}
|
|
|
|
|
|
- var room = document.Descendants(uPnpNamespaces.ud.GetName("roomName")).FirstOrDefault();
|
|
|
+ var room = document.Descendants(UPnpNamespaces.Ud.GetName("roomName")).FirstOrDefault();
|
|
|
if (room != null && !string.IsNullOrWhiteSpace(room.Value))
|
|
|
{
|
|
|
friendlyNames.Add(room.Value);
|
|
@@ -979,77 +993,77 @@ namespace Emby.Dlna.PlayTo
|
|
|
var deviceProperties = new DeviceInfo()
|
|
|
{
|
|
|
Name = string.Join(" ", friendlyNames),
|
|
|
- BaseUrl = string.Format("http://{0}:{1}", url.Host, url.Port)
|
|
|
+ BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
|
|
|
};
|
|
|
|
|
|
- var model = document.Descendants(uPnpNamespaces.ud.GetName("modelName")).FirstOrDefault();
|
|
|
+ var model = document.Descendants(UPnpNamespaces.Ud.GetName("modelName")).FirstOrDefault();
|
|
|
if (model != null)
|
|
|
{
|
|
|
deviceProperties.ModelName = model.Value;
|
|
|
}
|
|
|
|
|
|
- var modelNumber = document.Descendants(uPnpNamespaces.ud.GetName("modelNumber")).FirstOrDefault();
|
|
|
+ var modelNumber = document.Descendants(UPnpNamespaces.Ud.GetName("modelNumber")).FirstOrDefault();
|
|
|
if (modelNumber != null)
|
|
|
{
|
|
|
deviceProperties.ModelNumber = modelNumber.Value;
|
|
|
}
|
|
|
|
|
|
- var uuid = document.Descendants(uPnpNamespaces.ud.GetName("UDN")).FirstOrDefault();
|
|
|
+ var uuid = document.Descendants(UPnpNamespaces.Ud.GetName("UDN")).FirstOrDefault();
|
|
|
if (uuid != null)
|
|
|
{
|
|
|
deviceProperties.UUID = uuid.Value;
|
|
|
}
|
|
|
|
|
|
- var manufacturer = document.Descendants(uPnpNamespaces.ud.GetName("manufacturer")).FirstOrDefault();
|
|
|
+ var manufacturer = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturer")).FirstOrDefault();
|
|
|
if (manufacturer != null)
|
|
|
{
|
|
|
deviceProperties.Manufacturer = manufacturer.Value;
|
|
|
}
|
|
|
|
|
|
- var manufacturerUrl = document.Descendants(uPnpNamespaces.ud.GetName("manufacturerURL")).FirstOrDefault();
|
|
|
+ var manufacturerUrl = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturerURL")).FirstOrDefault();
|
|
|
if (manufacturerUrl != null)
|
|
|
{
|
|
|
deviceProperties.ManufacturerUrl = manufacturerUrl.Value;
|
|
|
}
|
|
|
|
|
|
- var presentationUrl = document.Descendants(uPnpNamespaces.ud.GetName("presentationURL")).FirstOrDefault();
|
|
|
+ var presentationUrl = document.Descendants(UPnpNamespaces.Ud.GetName("presentationURL")).FirstOrDefault();
|
|
|
if (presentationUrl != null)
|
|
|
{
|
|
|
deviceProperties.PresentationUrl = presentationUrl.Value;
|
|
|
}
|
|
|
|
|
|
- var modelUrl = document.Descendants(uPnpNamespaces.ud.GetName("modelURL")).FirstOrDefault();
|
|
|
+ var modelUrl = document.Descendants(UPnpNamespaces.Ud.GetName("modelURL")).FirstOrDefault();
|
|
|
if (modelUrl != null)
|
|
|
{
|
|
|
deviceProperties.ModelUrl = modelUrl.Value;
|
|
|
}
|
|
|
|
|
|
- var serialNumber = document.Descendants(uPnpNamespaces.ud.GetName("serialNumber")).FirstOrDefault();
|
|
|
+ var serialNumber = document.Descendants(UPnpNamespaces.Ud.GetName("serialNumber")).FirstOrDefault();
|
|
|
if (serialNumber != null)
|
|
|
{
|
|
|
deviceProperties.SerialNumber = serialNumber.Value;
|
|
|
}
|
|
|
|
|
|
- var modelDescription = document.Descendants(uPnpNamespaces.ud.GetName("modelDescription")).FirstOrDefault();
|
|
|
+ var modelDescription = document.Descendants(UPnpNamespaces.Ud.GetName("modelDescription")).FirstOrDefault();
|
|
|
if (modelDescription != null)
|
|
|
{
|
|
|
deviceProperties.ModelDescription = modelDescription.Value;
|
|
|
}
|
|
|
|
|
|
- var icon = document.Descendants(uPnpNamespaces.ud.GetName("icon")).FirstOrDefault();
|
|
|
+ var icon = document.Descendants(UPnpNamespaces.Ud.GetName("icon")).FirstOrDefault();
|
|
|
if (icon != null)
|
|
|
{
|
|
|
deviceProperties.Icon = CreateIcon(icon);
|
|
|
}
|
|
|
|
|
|
- foreach (var services in document.Descendants(uPnpNamespaces.ud.GetName("serviceList")))
|
|
|
+ foreach (var services in document.Descendants(UPnpNamespaces.Ud.GetName("serviceList")))
|
|
|
{
|
|
|
if (services == null)
|
|
|
{
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- var servicesList = services.Descendants(uPnpNamespaces.ud.GetName("service"));
|
|
|
+ var servicesList = services.Descendants(UPnpNamespaces.Ud.GetName("service"));
|
|
|
if (servicesList == null)
|
|
|
{
|
|
|
continue;
|
|
@@ -1066,12 +1080,9 @@ namespace Emby.Dlna.PlayTo
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- return new Device(deviceProperties, httpClient, logger, config);
|
|
|
+ return new Device(deviceProperties, httpClientFactory, logger);
|
|
|
}
|
|
|
|
|
|
- #endregion
|
|
|
-
|
|
|
- private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
|
|
private static DeviceIcon CreateIcon(XElement element)
|
|
|
{
|
|
|
if (element == null)
|
|
@@ -1079,11 +1090,11 @@ namespace Emby.Dlna.PlayTo
|
|
|
throw new ArgumentNullException(nameof(element));
|
|
|
}
|
|
|
|
|
|
- var mimeType = element.GetDescendantValue(uPnpNamespaces.ud.GetName("mimetype"));
|
|
|
- var width = element.GetDescendantValue(uPnpNamespaces.ud.GetName("width"));
|
|
|
- var height = element.GetDescendantValue(uPnpNamespaces.ud.GetName("height"));
|
|
|
- var depth = element.GetDescendantValue(uPnpNamespaces.ud.GetName("depth"));
|
|
|
- var url = element.GetDescendantValue(uPnpNamespaces.ud.GetName("url"));
|
|
|
+ var mimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype"));
|
|
|
+ var width = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("width"));
|
|
|
+ var height = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("height"));
|
|
|
+ var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth"));
|
|
|
+ var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url"));
|
|
|
|
|
|
var widthValue = int.Parse(width, NumberStyles.Integer, UsCulture);
|
|
|
var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture);
|
|
@@ -1100,11 +1111,11 @@ namespace Emby.Dlna.PlayTo
|
|
|
|
|
|
private static DeviceService Create(XElement element)
|
|
|
{
|
|
|
- var type = element.GetDescendantValue(uPnpNamespaces.ud.GetName("serviceType"));
|
|
|
- var id = element.GetDescendantValue(uPnpNamespaces.ud.GetName("serviceId"));
|
|
|
- var scpdUrl = element.GetDescendantValue(uPnpNamespaces.ud.GetName("SCPDURL"));
|
|
|
- var controlURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("controlURL"));
|
|
|
- var eventSubURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("eventSubURL"));
|
|
|
+ var type = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType"));
|
|
|
+ var id = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId"));
|
|
|
+ var scpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL"));
|
|
|
+ var controlURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL"));
|
|
|
+ var eventSubURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL"));
|
|
|
|
|
|
return new DeviceService
|
|
|
{
|
|
@@ -1116,14 +1127,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
};
|
|
|
}
|
|
|
|
|
|
- public event EventHandler<PlaybackStartEventArgs> PlaybackStart;
|
|
|
- public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
|
|
|
- public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped;
|
|
|
- public event EventHandler<MediaChangedEventArgs> MediaChanged;
|
|
|
-
|
|
|
- public uBaseObject CurrentMediaInfo { get; private set; }
|
|
|
-
|
|
|
- private void UpdateMediaInfo(uBaseObject mediaInfo, TRANSPORTSTATE state)
|
|
|
+ private void UpdateMediaInfo(UBaseObject mediaInfo, TransportState state)
|
|
|
{
|
|
|
TransportState = state;
|
|
|
|
|
@@ -1132,7 +1136,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
|
|
|
if (previousMediaInfo == null && mediaInfo != null)
|
|
|
{
|
|
|
- if (state != TRANSPORTSTATE.STOPPED)
|
|
|
+ if (state != TransportState.Stopped)
|
|
|
{
|
|
|
OnPlaybackStart(mediaInfo);
|
|
|
}
|
|
@@ -1151,7 +1155,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private void OnPlaybackStart(uBaseObject mediaInfo)
|
|
|
+ private void OnPlaybackStart(UBaseObject mediaInfo)
|
|
|
{
|
|
|
if (string.IsNullOrWhiteSpace(mediaInfo.Url))
|
|
|
{
|
|
@@ -1164,7 +1168,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- private void OnPlaybackProgress(uBaseObject mediaInfo)
|
|
|
+ private void OnPlaybackProgress(UBaseObject mediaInfo)
|
|
|
{
|
|
|
if (string.IsNullOrWhiteSpace(mediaInfo.Url))
|
|
|
{
|
|
@@ -1177,7 +1181,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- private void OnPlaybackStop(uBaseObject mediaInfo)
|
|
|
+ private void OnPlaybackStop(UBaseObject mediaInfo)
|
|
|
{
|
|
|
PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs
|
|
|
{
|
|
@@ -1185,7 +1189,7 @@ namespace Emby.Dlna.PlayTo
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- private void OnMediaChanged(uBaseObject old, uBaseObject newMedia)
|
|
|
+ private void OnMediaChanged(UBaseObject old, UBaseObject newMedia)
|
|
|
{
|
|
|
MediaChanged?.Invoke(this, new MediaChangedEventArgs
|
|
|
{
|
|
@@ -1194,16 +1198,17 @@ namespace Emby.Dlna.PlayTo
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- #region IDisposable
|
|
|
-
|
|
|
- bool _disposed;
|
|
|
-
|
|
|
+ /// <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)
|
|
@@ -1222,11 +1227,10 @@ namespace Emby.Dlna.PlayTo
|
|
|
_disposed = true;
|
|
|
}
|
|
|
|
|
|
- #endregion
|
|
|
-
|
|
|
+ /// <inheritdoc />
|
|
|
public override string ToString()
|
|
|
{
|
|
|
- return string.Format("{0} - {1}", Properties.Name, Properties.BaseUrl);
|
|
|
+ return string.Format(CultureInfo.InvariantCulture, "{0} - {1}", Properties.Name, Properties.BaseUrl);
|
|
|
}
|
|
|
}
|
|
|
}
|