diff options
Diffstat (limited to 'Emby.Dlna/PlayTo/Device.cs')
| -rw-r--r-- | Emby.Dlna/PlayTo/Device.cs | 1092 |
1 files changed, 1092 insertions, 0 deletions
diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs new file mode 100644 index 0000000000..00bb28cda0 --- /dev/null +++ b/Emby.Dlna/PlayTo/Device.cs @@ -0,0 +1,1092 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Dlna.Common; +using MediaBrowser.Dlna.Ssdp; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Security; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using MediaBrowser.Dlna.Server; + +namespace MediaBrowser.Dlna.PlayTo +{ + public class Device : IDisposable + { + const string ServiceAvtransportType = "urn:schemas-upnp-org:service:AVTransport:1"; + const string ServiceRenderingType = "urn:schemas-upnp-org:service:RenderingControl:1"; + + #region Fields & Properties + + private Timer _timer; + + public DeviceInfo Properties { get; set; } + + private int _muteVol; + public bool IsMuted { get; set; } + + private int _volume; + + public int Volume + { + get + { + RefreshVolumeIfNeeded(); + return _volume; + } + set + { + _volume = value; + } + } + + public TimeSpan? Duration { get; set; } + + private TimeSpan _position = TimeSpan.FromSeconds(0); + public TimeSpan Position + { + get + { + return _position; + } + set + { + _position = value; + } + } + + public TRANSPORTSTATE TransportState { get; private set; } + + public bool IsPlaying + { + get + { + return TransportState == TRANSPORTSTATE.PLAYING; + } + } + + public bool IsPaused + { + get + { + return TransportState == TRANSPORTSTATE.PAUSED || TransportState == TRANSPORTSTATE.PAUSED_PLAYBACK; + } + } + + public bool IsStopped + { + get + { + return TransportState == TRANSPORTSTATE.STOPPED; + } + } + + #endregion + + private readonly IHttpClient _httpClient; + private readonly ILogger _logger; + private readonly IServerConfigurationManager _config; + + public DateTime DateLastActivity { get; private set; } + public Action OnDeviceUnavailable { get; set; } + + public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config) + { + Properties = deviceProperties; + _httpClient = httpClient; + _logger = logger; + _config = config; + } + + private int GetPlaybackTimerIntervalMs() + { + return 1000; + } + + private int GetInactiveTimerIntervalMs() + { + return 20000; + } + + public void Start() + { + _timer = new Timer(TimerCallback, null, GetPlaybackTimerIntervalMs(), GetInactiveTimerIntervalMs()); + + _timerActive = false; + } + + private DateTime _lastVolumeRefresh; + private void RefreshVolumeIfNeeded() + { + if (!_timerActive) + { + return; + } + + if (DateTime.UtcNow >= _lastVolumeRefresh.AddSeconds(5)) + { + _lastVolumeRefresh = DateTime.UtcNow; + RefreshVolume(); + } + } + + private async void RefreshVolume() + { + if (_disposed) + return; + + try + { + await GetVolume().ConfigureAwait(false); + await GetMute().ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error updating device volume info for {0}", ex, Properties.Name); + } + } + + private readonly object _timerLock = new object(); + private bool _timerActive; + private void RestartTimer() + { + if (_disposed) + return; + + if (!_timerActive) + { + lock (_timerLock) + { + if (!_timerActive) + { + _logger.Debug("RestartTimer"); + _timer.Change(10, GetPlaybackTimerIntervalMs()); + } + + _timerActive = true; + } + } + } + + /// <summary> + /// Restarts the timer in inactive mode. + /// </summary> + private void RestartTimerInactive() + { + if (_disposed) + return; + + if (_timerActive) + { + lock (_timerLock) + { + if (_timerActive) + { + _logger.Debug("RestartTimerInactive"); + var interval = GetInactiveTimerIntervalMs(); + + if (_timer != null) + { + _timer.Change(interval, interval); + } + } + + _timerActive = false; + } + } + } + + #region Commanding + + public Task VolumeDown() + { + var sendVolume = Math.Max(Volume - 5, 0); + + return SetVolume(sendVolume); + } + + public Task VolumeUp() + { + var sendVolume = Math.Min(Volume + 5, 100); + + return SetVolume(sendVolume); + } + + public Task ToggleMute() + { + if (IsMuted) + { + return Unmute(); + } + + return Mute(); + } + + public async Task Mute() + { + var success = await SetMute(true).ConfigureAwait(true); + + if (!success) + { + await SetVolume(0).ConfigureAwait(false); + } + } + + public async Task Unmute() + { + var success = await SetMute(false).ConfigureAwait(true); + + if (!success) + { + var sendVolume = _muteVol <= 0 ? 20 : _muteVol; + + await SetVolume(sendVolume).ConfigureAwait(false); + } + } + + private async Task<bool> SetMute(bool mute) + { + var command = RendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute"); + if (command == null) + return false; + + var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceRenderingType); + + if (service == null) + { + return false; + } + + _logger.Debug("Setting mute"); + var value = mute ? 1 : 0; + + await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType, value)) + .ConfigureAwait(false); + + IsMuted = mute; + + return true; + } + + /// <summary> + /// Sets volume on a scale of 0-100 + /// </summary> + public async Task SetVolume(int value) + { + var command = RendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume"); + if (command == null) + return; + + var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceRenderingType); + + 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(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType, value)) + .ConfigureAwait(false); + } + + public async Task Seek(TimeSpan value) + { + var command = AvCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek"); + if (command == null) + return; + + var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceAvtransportType); + + if (service == null) + { + throw new InvalidOperationException("Unable to find service"); + } + + await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType, String.Format("{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME")) + .ConfigureAwait(false); + } + + public async Task SetAvTransport(string url, string header, string metaData) + { + _logger.Debug("{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)} + }; + + var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceAvtransportType); + + if (service == null) + { + throw new InvalidOperationException("Unable to find service"); + } + + var post = AvCommands.BuildPost(command, service.ServiceType, url, dictionary); + await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header) + .ConfigureAwait(false); + + await Task.Delay(50).ConfigureAwait(false); + + try + { + await SetPlay().ConfigureAwait(false); + } + catch + { + // Some devices will throw an error if you tell it to play when it's already playing + // Others won't + } + + RestartTimer(); + } + + private string CreateDidlMeta(string value) + { + if (string.IsNullOrEmpty(value)) + return String.Empty; + + return DescriptionXmlBuilder.Escape(value); + } + + public async Task SetPlay() + { + var command = AvCommands.ServiceActions.FirstOrDefault(c => c.Name == "Play"); + if (command == null) + return; + + var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceAvtransportType); + + if (service == null) + { + throw new InvalidOperationException("Unable to find service"); + } + + await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType, 1)) + .ConfigureAwait(false); + } + + public async Task SetStop() + { + var command = AvCommands.ServiceActions.FirstOrDefault(c => c.Name == "Stop"); + if (command == null) + return; + + var service = Properties.Services.First(s => s.ServiceType == ServiceAvtransportType); + + await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType, 1)) + .ConfigureAwait(false); + } + + public async Task SetPause() + { + var command = AvCommands.ServiceActions.FirstOrDefault(c => c.Name == "Pause"); + if (command == null) + return; + + var service = Properties.Services.First(s => s.ServiceType == ServiceAvtransportType); + + await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType, 1)) + .ConfigureAwait(false); + + TransportState = TRANSPORTSTATE.PAUSED; + } + + #endregion + + #region Get data + + private int _successiveStopCount; + private int _connectFailureCount; + private async void TimerCallback(object sender) + { + if (_disposed) + return; + + const int maxSuccessiveStopReturns = 5; + + try + { + var transportState = await GetTransportInfo().ConfigureAwait(false); + + DateLastActivity = DateTime.UtcNow; + + 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().ConfigureAwait(false); + + var currentObject = tuple.Item2; + + if (tuple.Item1 && currentObject == null) + { + currentObject = await GetMediaInfo().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 neccessry to keep the Session alive + if (transportState.Value == TRANSPORTSTATE.STOPPED) + { + _successiveStopCount++; + + if (_successiveStopCount >= maxSuccessiveStopReturns) + { + RestartTimerInactive(); + } + } + else + { + _successiveStopCount = 0; + RestartTimer(); + } + } + } + catch (HttpException ex) + { + if (_disposed) + return; + + //_logger.ErrorException("Error updating device info for {0}", ex, Properties.Name); + + _successiveStopCount++; + _connectFailureCount++; + + if (_connectFailureCount >= 3) + { + if (OnDeviceUnavailable != null) + { + _logger.Debug("Disposing device due to loss of connection"); + OnDeviceUnavailable(); + return; + } + } + if (_successiveStopCount >= maxSuccessiveStopReturns) + { + RestartTimerInactive(); + } + } + catch (Exception ex) + { + if (_disposed) + return; + + _logger.ErrorException("Error updating device info for {0}", ex, Properties.Name); + + _successiveStopCount++; + + if (_successiveStopCount >= maxSuccessiveStopReturns) + { + RestartTimerInactive(); + } + } + } + + private async Task GetVolume() + { + var command = RendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume"); + if (command == null) + return; + + var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceRenderingType); + + if (service == null) + { + return; + } + + var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType), true) + .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 == null ? null : volume.Value; + + if (string.IsNullOrWhiteSpace(volumeValue)) + return; + + Volume = int.Parse(volumeValue, UsCulture); + + if (Volume > 0) + { + _muteVol = Volume; + } + } + + private async Task GetMute() + { + var command = RendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMute"); + if (command == null) + return; + + var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceRenderingType); + + if (service == null) + { + return; + } + + var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType), true) + .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); + var value = valueNode == null ? null : valueNode.Value; + + IsMuted = string.Equals(value, "1", StringComparison.OrdinalIgnoreCase); + } + + private async Task<TRANSPORTSTATE?> GetTransportInfo() + { + var command = AvCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetTransportInfo"); + if (command == null) + return null; + + var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceAvtransportType); + if (service == null) + return null; + + var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType), false) + .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 == null ? null : transportState.Value; + + if (transportStateValue != null) + { + TRANSPORTSTATE state; + + if (Enum.TryParse(transportStateValue, true, out state)) + { + return state; + } + } + + return null; + } + + private async Task<uBaseObject> GetMediaInfo() + { + var command = AvCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo"); + if (command == null) + return null; + + var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceAvtransportType); + + if (service == null) + { + throw new InvalidOperationException("Unable to find service"); + } + + var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType), false) + .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; + + return UpnpContainer.Create(e); + } + + private async Task<Tuple<bool, uBaseObject>> GetPositionInfo() + { + var command = AvCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo"); + if (command == null) + return new Tuple<bool, uBaseObject>(false, null); + + var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceAvtransportType); + + if (service == null) + { + throw new InvalidOperationException("Unable to find service"); + } + + var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType), false) + .ConfigureAwait(false); + + if (result == null || result.Document == null) + return new Tuple<bool, uBaseObject>(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 durationElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null); + var duration = durationElem == null ? null : 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 == null ? null : 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 new Tuple<bool, uBaseObject>(true, null); + } + + var trackString = (string)track; + + if (string.IsNullOrWhiteSpace(trackString) || string.Equals(trackString, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) + { + return new Tuple<bool, uBaseObject>(false, null); + } + + XElement uPnpResponse; + + // Handle different variations sent back by devices + try + { + uPnpResponse = XElement.Parse(trackString); + } + catch (Exception) + { + // first try to add a root node with a dlna namesapce + try + { + uPnpResponse = XElement.Parse("<data xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">" + trackString + "</data>"); + uPnpResponse = uPnpResponse.Descendants().First(); + } + catch (Exception ex) + { + _logger.ErrorException("Unable to parse xml {0}", ex, trackString); + return new Tuple<bool, uBaseObject>(true, null); + } + } + + var e = uPnpResponse.Element(uPnpNamespaces.items); + + var uTrack = CreateUBaseObject(e, trackUri); + + return new Tuple<bool, uBaseObject>(true, uTrack); + } + + private static uBaseObject CreateUBaseObject(XElement container, string trackUri) + { + if (container == null) + { + throw new ArgumentNullException("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 = "", + Url = url, + ProtocolInfo = GetProtocolInfo(container), + MetaData = container.ToString() + }; + } + + private static string[] GetProtocolInfo(XElement container) + { + if (container == null) + { + throw new ArgumentNullException("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]; + } + + #endregion + + #region From XML + + private async Task GetAVProtocolAsync() + { + var avService = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceAvtransportType); + if (avService == null) + return; + + string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); + + var httpClient = new SsdpHttpClient(_httpClient, _config); + var document = await httpClient.GetDataAsync(url); + + AvCommands = TransportCommands.Create(document); + } + + private async Task GetRenderingProtocolAsync() + { + var avService = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceRenderingType); + + if (avService == null) + return; + string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); + + var httpClient = new SsdpHttpClient(_httpClient, _config); + var document = await httpClient.GetDataAsync(url); + + RendererCommands = TransportCommands.Create(document); + } + + 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("/")) + url = "/dmr/" + url; + if (!url.StartsWith("/")) + url = "/" + url; + + return baseUrl + url; + } + + private TransportCommands AvCommands + { + get; + set; + } + + internal TransportCommands RendererCommands + { + get; + set; + } + + public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, IServerConfigurationManager config, ILogger logger) + { + var ssdpHttpClient = new SsdpHttpClient(httpClient, config); + + var document = await ssdpHttpClient.GetDataAsync(url.ToString()).ConfigureAwait(false); + + var deviceProperties = new DeviceInfo(); + + var friendlyNames = new List<string>(); + + 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); + + deviceProperties.Name = string.Join(" ", friendlyNames.ToArray()); + + 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; + + deviceProperties.BaseUrl = String.Format("http://{0}:{1}", url.Host, url.Port); + + var icon = document.Descendants(uPnpNamespaces.ud.GetName("icon")).FirstOrDefault(); + + if (icon != null) + { + deviceProperties.Icon = CreateIcon(icon); + } + + var isRenderer = false; + + 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); + if (service.ServiceType == ServiceAvtransportType) + { + isRenderer = true; + } + } + } + } + + var device = new Device(deviceProperties, httpClient, logger, config); + + if (isRenderer) + { + await device.GetRenderingProtocolAsync().ConfigureAwait(false); + await device.GetAVProtocolAsync().ConfigureAwait(false); + } + + return device; + } + + #endregion + + private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + private static DeviceIcon CreateIcon(XElement element) + { + if (element == null) + { + throw new ArgumentNullException("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.Any, UsCulture); + var heightValue = int.Parse(height, NumberStyles.Any, 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 + }; + } + + 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) + { + 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 (PlaybackStart != null) + { + PlaybackStart.Invoke(this, new PlaybackStartEventArgs + { + MediaInfo = mediaInfo + }); + } + } + + private void OnPlaybackProgress(uBaseObject mediaInfo) + { + if (PlaybackProgress != null) + { + PlaybackProgress.Invoke(this, new PlaybackProgressEventArgs + { + MediaInfo = mediaInfo + }); + } + } + + private void OnPlaybackStop(uBaseObject mediaInfo) + { + if (PlaybackStopped != null) + { + PlaybackStopped.Invoke(this, new PlaybackStoppedEventArgs + { + MediaInfo = mediaInfo + }); + } + } + + private void OnMediaChanged(uBaseObject old, uBaseObject newMedia) + { + if (MediaChanged != null) + { + MediaChanged.Invoke(this, new MediaChangedEventArgs + { + OldMediaInfo = old, + NewMediaInfo = newMedia + }); + } + } + + #region IDisposable + + bool _disposed; + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + + DisposeTimer(); + } + } + + private void DisposeTimer() + { + if (_timer != null) + { + _timer.Dispose(); + _timer = null; + } + } + + #endregion + + public override string ToString() + { + return String.Format("{0} - {1}", Properties.Name, Properties.BaseUrl); + } + } +} |
