#nullable disable #pragma warning disable CS1591 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.Ssdp; using Microsoft.Extensions.Logging; namespace Emby.Dlna.PlayTo { public class Device : IDisposable { 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 PlaybackStart; public event EventHandler PlaybackProgress; public event EventHandler PlaybackStopped; public event EventHandler MediaChanged; public DeviceInfo Properties { get; set; } public bool IsMuted { get; set; } public int Volume { get { RefreshVolumeIfNeeded().GetAwaiter().GetResult(); return _volume; } set => _volume = value; } public TimeSpan? Duration { get; set; } public TimeSpan Position { get; set; } = TimeSpan.FromSeconds(0); public TransportState TransportState { get; private set; } public bool IsPlaying => TransportState == TransportState.Playing; public bool IsPaused => TransportState == TransportState.Paused || TransportState == TransportState.PausedPlayback; public bool IsStopped => TransportState == TransportState.Stopped; public Action OnDeviceUnavailable { get; set; } private TransportCommands AvCommands { get; set; } private TransportCommands RendererCommands { get; set; } public UBaseObject CurrentMediaInfo { get; private set; } public void Start() { _logger.LogDebug("Dlna Device.Start"); _timer = new Timer(TimerCallback, null, 1000, Timeout.Infinite); } private Task RefreshVolumeIfNeeded() { if (_volumeRefreshActive && DateTime.UtcNow >= _lastVolumeRefresh.AddSeconds(5)) { _lastVolumeRefresh = DateTime.UtcNow; return RefreshVolume(); } return Task.CompletedTask; } private async Task RefreshVolume(CancellationToken cancellationToken = default) { if (_disposed) { return; } try { await GetVolume(cancellationToken).ConfigureAwait(false); await GetMute(cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "Error updating device volume info for {DeviceName}", Properties.Name); } } private void RestartTimer(bool immediate = false) { lock (_timerLock) { if (_disposed) { return; } _volumeRefreshActive = true; var time = immediate ? 100 : 10000; _timer.Change(time, Timeout.Infinite); } } /// /// Restarts the timer in inactive mode. /// private void RestartTimerInactive() { lock (_timerLock) { if (_disposed) { return; } _volumeRefreshActive = false; _timer.Change(Timeout.Infinite, Timeout.Infinite); } } public Task VolumeDown(CancellationToken cancellationToken) { var sendVolume = Math.Max(Volume - 5, 0); return SetVolume(sendVolume, cancellationToken); } public Task VolumeUp(CancellationToken cancellationToken) { var sendVolume = Math.Min(Volume + 5, 100); return SetVolume(sendVolume, cancellationToken); } public Task ToggleMute(CancellationToken cancellationToken) { if (IsMuted) { return Unmute(cancellationToken); } return Mute(cancellationToken); } public async Task Mute(CancellationToken cancellationToken) { var success = await SetMute(true, cancellationToken).ConfigureAwait(true); if (!success) { await SetVolume(0, cancellationToken).ConfigureAwait(false); } } public async Task Unmute(CancellationToken cancellationToken) { var success = await SetMute(false, cancellationToken).ConfigureAwait(true); if (!success) { var sendVolume = _muteVol <= 0 ? 20 : _muteVol; await SetVolume(sendVolume, cancellationToken).ConfigureAwait(false); } } private DeviceService GetServiceRenderingControl() { var services = Properties.Services; return services.FirstOrDefault(s => string.Equals(s.ServiceType, "urn:schemas-upnp-org:service:RenderingControl:1", StringComparison.OrdinalIgnoreCase)) ?? services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:RenderingControl", StringComparison.OrdinalIgnoreCase)); } private DeviceService GetAvTransportService() { var services = Properties.Services; return services.FirstOrDefault(s => string.Equals(s.ServiceType, "urn:schemas-upnp-org:service:AVTransport:1", StringComparison.OrdinalIgnoreCase)) ?? services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:AVTransport", StringComparison.OrdinalIgnoreCase)); } private async Task SetMute(bool mute, CancellationToken cancellationToken) { var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetMute"); if (command == null) { return false; } var service = GetServiceRenderingControl(); if (service == null) { return false; } _logger.LogDebug("Setting mute"); var value = mute ? 1 : 0; await new SsdpHttpClient(_httpClientFactory) .SendCommandAsync( Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value), cancellationToken: cancellationToken) .ConfigureAwait(false); IsMuted = mute; return true; } /// /// Sets volume on a scale of 0-100. /// /// The volume on a scale of 0-100. /// The cancellation token to cancel operation. /// A representing the asynchronous operation. 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(); if (service == null) { throw new InvalidOperationException("Unable to find service"); } // Set it early and assume it will succeed // Remote control will perform better Volume = value; await new SsdpHttpClient(_httpClientFactory) .SendCommandAsync( Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value), cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task Seek(TimeSpan value, CancellationToken cancellationToken) { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Seek"); if (command == null) { return; } var service = GetAvTransportService(); if (service == null) { throw new InvalidOperationException("Unable to find service"); } 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"), cancellationToken: cancellationToken) .ConfigureAwait(false); RestartTimer(true); } public async Task SetAvTransport(string url, string header, string metaData, CancellationToken cancellationToken) { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); 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 { { "CurrentURI", url }, { "CurrentURIMetaData", CreateDidlMeta(metaData) } }; var service = GetAvTransportService(); if (service == null) { throw new InvalidOperationException("Unable to find service"); } var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary); await new SsdpHttpClient(_httpClientFactory) .SendCommandAsync( Properties.BaseUrl, service, command.Name, post, header: header, cancellationToken: cancellationToken) .ConfigureAwait(false); await Task.Delay(50, cancellationToken).ConfigureAwait(false); try { await SetPlay(avCommands, cancellationToken).ConfigureAwait(false); } catch { // Some devices will throw an error if you tell it to play when it's already playing // Others won't } RestartTimer(true); } /* * SetNextAvTransport is used to specify to the DLNA device what is the next track to play. * Without that information, the next track command on the device does not work. */ public async Task SetNextAvTransport(string url, string header, string metaData, CancellationToken cancellationToken = default) { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); url = url.Replace("&", "&", StringComparison.Ordinal); _logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header); var command = avCommands.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase)); if (command == null) { return; } var dictionary = new Dictionary { { "NextURI", url }, { "NextURIMetaData", CreateDidlMeta(metaData) } }; var service = GetAvTransportService(); if (service == null) { throw new InvalidOperationException("Unable to find service"); } var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary); await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header, cancellationToken) .ConfigureAwait(false); } private static string CreateDidlMeta(string value) { if (string.IsNullOrEmpty(value)) { return string.Empty; } return SecurityElement.Escape(value); } private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken) { var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Play"); if (command == null) { return Task.CompletedTask; } var service = GetAvTransportService(); if (service == null) { throw new InvalidOperationException("Unable to find service"); } return new SsdpHttpClient(_httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1), cancellationToken: cancellationToken); } public async Task SetPlay(CancellationToken cancellationToken) { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); if (avCommands == null) { return; } await SetPlay(avCommands, cancellationToken).ConfigureAwait(false); RestartTimer(true); } public async Task SetStop(CancellationToken cancellationToken) { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Stop"); if (command == null) { return; } var service = GetAvTransportService(); await new SsdpHttpClient(_httpClientFactory) .SendCommandAsync( Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1), cancellationToken: cancellationToken) .ConfigureAwait(false); RestartTimer(true); } public async Task SetPause(CancellationToken cancellationToken) { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Pause"); if (command == null) { return; } var service = GetAvTransportService(); await new SsdpHttpClient(_httpClientFactory) .SendCommandAsync( Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1), cancellationToken: cancellationToken) .ConfigureAwait(false); TransportState = TransportState.Paused; RestartTimer(true); } private async void TimerCallback(object sender) { if (_disposed) { return; } try { var cancellationToken = CancellationToken.None; var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); if (avCommands == null) { return; } var transportState = await GetTransportInfo(avCommands, cancellationToken).ConfigureAwait(false); if (_disposed) { return; } if (transportState.HasValue) { // If we're not playing anything no need to get additional data if (transportState.Value == TransportState.Stopped) { UpdateMediaInfo(null, transportState.Value); } else { var tuple = await GetPositionInfo(avCommands, cancellationToken).ConfigureAwait(false); var currentObject = tuple.Item2; if (tuple.Item1 && currentObject == null) { currentObject = await GetMediaInfo(avCommands, cancellationToken).ConfigureAwait(false); } if (currentObject != null) { UpdateMediaInfo(currentObject, transportState.Value); } } _connectFailureCount = 0; if (_disposed) { return; } // If we're not playing anything make sure we don't get data more often than necessary to keep the Session alive if (transportState.Value == TransportState.Stopped) { RestartTimerInactive(); } else { RestartTimer(); } } else { RestartTimerInactive(); } } catch (Exception ex) { if (_disposed) { return; } _logger.LogError(ex, "Error updating device info for {DeviceName}", Properties.Name); _connectFailureCount++; if (_connectFailureCount >= 3) { var action = OnDeviceUnavailable; if (action != null) { _logger.LogDebug("Disposing device due to loss of connection"); action(); return; } } RestartTimerInactive(); } } private async Task GetVolume(CancellationToken cancellationToken) { if (_disposed) { return; } var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume"); if (command == null) { return; } var service = GetServiceRenderingControl(); if (service == null) { return; } var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), cancellationToken: cancellationToken).ConfigureAwait(false); if (result == null || result.Document == null) { return; } 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)) { return; } Volume = int.Parse(volumeValue, UsCulture); if (Volume > 0) { _muteVol = Volume; } } private async Task GetMute(CancellationToken cancellationToken) { if (_disposed) { return; } var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetMute"); if (command == null) { return; } var service = GetServiceRenderingControl(); if (service == null) { return; } var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), cancellationToken: cancellationToken).ConfigureAwait(false); if (result == null || result.Document == null) { return; } 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 GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken) { var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetTransportInfo"); if (command == null) { return null; } var service = GetAvTransportService(); if (service == null) { return null; } var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType), cancellationToken: cancellationToken).ConfigureAwait(false); if (result == null || result.Document == null) { return null; } var transportState = 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)) { return state; } return null; } private async Task GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken) { var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo"); if (command == null) { return null; } var service = GetAvTransportService(); if (service == null) { throw new InvalidOperationException("Unable to find service"); } var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); if (rendererCommands == null) { return null; } var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), cancellationToken: cancellationToken).ConfigureAwait(false); if (result == null || result.Document == null) { return null; } var track = result.Document.Descendants("CurrentURIMetaData").FirstOrDefault(); if (track == null) { return null; } var e = track.Element(UPnpNamespaces.Items) ?? track; var elementString = (string)e; if (!string.IsNullOrWhiteSpace(elementString)) { return UpnpContainer.Create(e); } track = result.Document.Descendants("CurrentURI").FirstOrDefault(); if (track == null) { return null; } e = track.Element(UPnpNamespaces.Items) ?? track; elementString = (string)e; if (!string.IsNullOrWhiteSpace(elementString)) { return new UBaseObject { Url = elementString }; } return null; } private async Task<(bool, UBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken) { var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo"); if (command == null) { return (false, null); } var service = GetAvTransportService(); if (service == null) { throw new InvalidOperationException("Unable to find service"); } var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); if (rendererCommands == null) { return (false, null); } var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), cancellationToken: cancellationToken).ConfigureAwait(false); if (result == null || result.Document == null) { return (false, null); } 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?.Value; if (!string.IsNullOrWhiteSpace(duration) && !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) { Duration = TimeSpan.Parse(duration, UsCulture); } else { Duration = null; } 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)) { Position = TimeSpan.Parse(position, UsCulture); } var track = result.Document.Descendants("TrackMetaData").FirstOrDefault(); if (track == null) { // If track is null, some vendors do this, use GetMediaInfo instead. return (true, null); } var trackString = (string)track; if (string.IsNullOrWhiteSpace(trackString) || string.Equals(trackString, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) { return (true, null); } XElement uPnpResponse = null; try { uPnpResponse = ParseResponse(trackString); } catch (Exception ex) { _logger.LogError(ex, "Uncaught exception while parsing xml"); } if (uPnpResponse == null) { _logger.LogError("Failed to parse xml: \n {Xml}", trackString); return (true, null); } var e = uPnpResponse.Element(UPnpNamespaces.Items); var uTrack = CreateUBaseObject(e, trackUri); return (true, uTrack); } private XElement ParseResponse(string xml) { // Handle different variations sent back by devices. try { return XElement.Parse(xml); } catch (XmlException) { } // first try to add a root node with a dlna namespace. try { return XElement.Parse("" + xml + "") .Descendants() .First(); } catch (XmlException) { } // some devices send back invalid xml try { return XElement.Parse(xml.Replace("&", "&", StringComparison.Ordinal)); } catch (XmlException) { } return null; } private static UBaseObject CreateUBaseObject(XElement container, string trackUri) { if (container == null) { throw new ArgumentNullException(nameof(container)); } var url = container.GetValue(UPnpNamespaces.Res); if (string.IsNullOrWhiteSpace(url)) { url = trackUri; } return new UBaseObject { 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() }; } private static string[] GetProtocolInfo(XElement container) { if (container == null) { throw new ArgumentNullException(nameof(container)); } var resElement = container.Element(UPnpNamespaces.Res); if (resElement != null) { var info = resElement.Attribute(UPnpNamespaces.ProtocolInfo); if (info != null && !string.IsNullOrWhiteSpace(info.Value)) { return info.Value.Split(':'); } } return new string[4]; } private async Task GetAVProtocolAsync(CancellationToken cancellationToken) { if (AvCommands != null) { return AvCommands; } if (_disposed) { throw new ObjectDisposedException(GetType().Name); } var avService = GetAvTransportService(); if (avService == null) { return null; } string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); var httpClient = new SsdpHttpClient(_httpClientFactory); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); if (document == null) { return null; } AvCommands = TransportCommands.Create(document); return AvCommands; } private async Task GetRenderingProtocolAsync(CancellationToken cancellationToken) { if (RendererCommands != null) { return RendererCommands; } if (_disposed) { throw new ObjectDisposedException(GetType().Name); } var avService = GetServiceRenderingControl(); if (avService == null) { throw new ArgumentException("Device AvService is null"); } string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); var httpClient = new SsdpHttpClient(_httpClientFactory); _logger.LogDebug("Dlna Device.GetRenderingProtocolAsync"); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); if (document == null) { return null; } RendererCommands = TransportCommands.Create(document); return RendererCommands; } private string NormalizeUrl(string baseUrl, string url) { // If it's already a complete url, don't stick anything onto the front of it if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { return url; } if (!url.Contains('/', StringComparison.Ordinal)) { url = "/dmr/" + url; } if (!url.StartsWith('/')) { url = "/" + url; } return baseUrl + url; } public static async Task CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken) { var ssdpHttpClient = new SsdpHttpClient(httpClientFactory); var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false); if (document == null) { return null; } var friendlyNames = new List(); 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(); if (room != null && !string.IsNullOrWhiteSpace(room.Value)) { friendlyNames.Add(room.Value); } var deviceProperties = new DeviceInfo() { Name = string.Join(' ', friendlyNames), BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port) }; 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(); if (modelNumber != null) { deviceProperties.ModelNumber = modelNumber.Value; } 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(); if (manufacturer != null) { deviceProperties.Manufacturer = manufacturer.Value; } 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(); if (presentationUrl != null) { deviceProperties.PresentationUrl = presentationUrl.Value; } 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(); if (serialNumber != null) { deviceProperties.SerialNumber = serialNumber.Value; } 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(); if (icon != null) { deviceProperties.Icon = CreateIcon(icon); } foreach (var services in document.Descendants(UPnpNamespaces.Ud.GetName("serviceList"))) { if (services == null) { continue; } var servicesList = services.Descendants(UPnpNamespaces.Ud.GetName("service")); if (servicesList == null) { continue; } foreach (var element in servicesList) { var service = Create(element); if (service != null) { deviceProperties.Services.Add(service); } } } return new Device(deviceProperties, httpClientFactory, logger); } private static DeviceIcon CreateIcon(XElement element) { if (element == null) { 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 widthValue = int.Parse(width, NumberStyles.Integer, UsCulture); var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture); return new DeviceIcon { Depth = depth, Height = heightValue, MimeType = mimeType, Url = url, Width = widthValue }; } 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")); return new DeviceService { ControlUrl = controlURL, EventSubUrl = eventSubURL, ScpdUrl = scpdUrl, ServiceId = id, ServiceType = type }; } private void UpdateMediaInfo(UBaseObject mediaInfo, TransportState state) { TransportState = state; var previousMediaInfo = CurrentMediaInfo; CurrentMediaInfo = mediaInfo; if (previousMediaInfo == null && mediaInfo != null) { if (state != TransportState.Stopped) { OnPlaybackStart(mediaInfo); } } else if (mediaInfo != null && previousMediaInfo != null && !mediaInfo.Equals(previousMediaInfo)) { OnMediaChanged(previousMediaInfo, mediaInfo); } else if (mediaInfo == null && previousMediaInfo != null) { OnPlaybackStop(previousMediaInfo); } else if (mediaInfo != null && mediaInfo.Equals(previousMediaInfo)) { OnPlaybackProgress(mediaInfo); } } private void OnPlaybackStart(UBaseObject mediaInfo) { if (string.IsNullOrWhiteSpace(mediaInfo.Url)) { return; } PlaybackStart?.Invoke(this, new PlaybackStartEventArgs(mediaInfo)); } private void OnPlaybackProgress(UBaseObject mediaInfo) { if (string.IsNullOrWhiteSpace(mediaInfo.Url)) { return; } PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs(mediaInfo)); } private void OnPlaybackStop(UBaseObject mediaInfo) { PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs(mediaInfo)); } private void OnMediaChanged(UBaseObject old, UBaseObject newMedia) { MediaChanged?.Invoke(this, new MediaChangedEventArgs(old, newMedia)); } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases unmanaged and optionally managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { if (_disposed) { return; } if (disposing) { _timer?.Dispose(); } _timer = null; Properties = null; _disposed = true; } /// public override string ToString() { return string.Format(CultureInfo.InvariantCulture, "{0} - {1}", Properties.Name, Properties.BaseUrl); } } }