using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Dlna.Common; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; 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 { return _muteVol > 0; } } private string _currentId = String.Empty; public string CurrentId { get { return _currentId; } set { if (_currentId == value) return; _currentId = value; NotifyCurrentIdChanged(value); } } public int Volume { get; set; } public TimeSpan Duration { get; set; } private TimeSpan _position = TimeSpan.FromSeconds(0); public TimeSpan Position { get { return _position; } set { _position = value; } } private string _transportState = String.Empty; public string TransportState { get { return _transportState; } set { if (_transportState == value) return; _transportState = value; if (value == TRANSPORTSTATE.PLAYING || value == TRANSPORTSTATE.STOPPED) NotifyPlaybackChanged(value == TRANSPORTSTATE.STOPPED); } } public bool IsPlaying { get { return TransportState == TRANSPORTSTATE.PLAYING; } } public bool IsTransitioning { get { return (TransportState == TRANSPORTSTATE.TRANSITIONING); } } public bool IsPaused { get { return TransportState == TRANSPORTSTATE.PAUSED || TransportState == TRANSPORTSTATE.PAUSED_PLAYBACK; } } public bool IsStopped { get { return TransportState == TRANSPORTSTATE.STOPPED; } } public DateTime UpdateTime { get; private set; } #endregion private readonly IHttpClient _httpClient; private readonly ILogger _logger; private readonly IServerConfigurationManager _config; public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config) { Properties = deviceProperties; _httpClient = httpClient; _logger = logger; _config = config; } private int GetPlaybackTimerIntervalMs() { return 2000; } private int GetInactiveTimerIntervalMs() { return 20000; } public void Start() { UpdateTime = DateTime.UtcNow; var interval = GetPlaybackTimerIntervalMs(); _timer = new Timer(TimerCallback, null, interval, interval); } private void RestartTimer() { var interval = GetPlaybackTimerIntervalMs(); _timer.Change(interval, interval); } /// /// Restarts the timer in inactive mode. /// private void RestartTimerInactive() { var interval = GetInactiveTimerIntervalMs(); _timer.Change(interval, interval); } private void StopTimer() { _timer.Change(Timeout.Infinite, Timeout.Infinite); } #region Commanding public Task VolumeDown(bool mute = false) { var sendVolume = (Volume - 5) > 0 ? Volume - 5 : 0; if (mute && _muteVol == 0) { sendVolume = 0; _muteVol = Volume; } return SetVolume(sendVolume); } public Task VolumeUp(bool unmute = false) { var sendVolume = (Volume + 5) < 100 ? Volume + 5 : 100; if (unmute && _muteVol > 0) sendVolume = _muteVol; _muteVol = 0; return SetVolume(sendVolume); } public Task ToggleMute() { if (_muteVol == 0) { _muteVol = Volume; return SetVolume(0); } var tmp = _muteVol; _muteVol = 0; return SetVolume(tmp); } /// /// Sets volume on a scale of 0-100 /// public async Task SetVolume(int value) { var command = RendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume"); if (command == null) return true; var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceRenderingType); 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, value)) .ConfigureAwait(false); Volume = value; return true; } public async Task Seek(TimeSpan value) { var command = AvCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek"); if (command == null) return value; 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, AvCommands.BuildPost(command, service.ServiceType, String.Format("{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME")) .ConfigureAwait(false); return value; } public async Task SetAvTransport(string url, string header, string metaData) { StopTimer(); await SetStop().ConfigureAwait(false); CurrentId = null; var command = AvCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI"); if (command == null) return false; var dictionary = new Dictionary { {"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 result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType, url, dictionary), header) .ConfigureAwait(false); await Task.Delay(50).ConfigureAwait(false); await SetPlay().ConfigureAwait(false); _lapsCount = GetLapsCount(); RestartTimer(); return true; } private string CreateDidlMeta(string value) { if (value == null) return String.Empty; var escapedData = value.Replace("<", "<").Replace(">", ">"); return String.Format(BaseDidl, escapedData.Replace("\r\n", "")); } private const string BaseDidl = "<DIDL-Lite xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" xmlns:dlna=\"urn:schemas-dlna-org:metadata-1-0/\">{0}</DIDL-Lite>"; public async Task SetNextAvTransport(string value, string header, string metaData) { var command = AvCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetNextAVTransportURI"); if (command == null) return false; var dictionary = new Dictionary { {"NextURI", value}, {"NextURIMetaData", CreateDidlMeta(metaData)} }; 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, AvCommands.BuildPost(command, service.ServiceType, value, dictionary), header) .ConfigureAwait(false); await Task.Delay(100).ConfigureAwait(false); return true; } public async Task SetPlay() { var command = AvCommands.ServiceActions.FirstOrDefault(c => c.Name == "Play"); if (command == null) return false; 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, AvCommands.BuildPost(command, service.ServiceType, 1)) .ConfigureAwait(false); _lapsCount = GetLapsCount(); return true; } public async Task SetStop() { var command = AvCommands.ServiceActions.FirstOrDefault(c => c.Name == "Stop"); if (command == null) return false; var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceAvtransportType); var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType, 1)) .ConfigureAwait(false); await Task.Delay(50).ConfigureAwait(false); return true; } public async Task SetPause() { var command = AvCommands.ServiceActions.FirstOrDefault(c => c.Name == "Pause"); if (command == null) return false; var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceAvtransportType); var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType, 1)) .ConfigureAwait(false); await Task.Delay(50).ConfigureAwait(false); TransportState = "PAUSED_PLAYBACK"; return true; } #endregion #region Get data private int GetLapsCount() { // No need to get all data every lap, just every X time. return 10; } int _lapsCount = 0; private async void TimerCallback(object sender) { if (_disposed) return; StopTimer(); try { await GetTransportInfo().ConfigureAwait(false); //If we're not playing anything no need to get additional data if (TransportState != TRANSPORTSTATE.STOPPED) { var hasTrack = await GetPositionInfo().ConfigureAwait(false); // TODO: Why make these requests if hasTrack==false? // TODO ANSWER Some vendors don't include track in GetPositionInfo, use GetMediaInfo instead. if (_lapsCount > GetLapsCount()) { if (!hasTrack) { await GetMediaInfo().ConfigureAwait(false); } await GetVolume().ConfigureAwait(false); _lapsCount = 0; } } } catch (Exception ex) { _logger.ErrorException("Error updating device info", ex); } _lapsCount++; 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 != TRANSPORTSTATE.STOPPED) RestartTimer(); else 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) { 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)) .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); //Reset the Mute value if Volume is bigger than zero if (Volume > 0 && _muteVol > 0) { _muteVol = 0; } } private async Task GetTransportInfo() { var command = AvCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetTransportInfo"); if (command == null) return; var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceAvtransportType); if (service == null) return; var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType)) .ConfigureAwait(false); if (result == null || result.Document == null) return; 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 = transportStateValue; UpdateTime = DateTime.UtcNow; } private async Task GetMediaInfo() { var command = AvCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo"); if (command == null) return; 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)) .ConfigureAwait(false); if (result == null || result.Document == null) return; var track = result.Document.Descendants("CurrentURIMetaData").FirstOrDefault(); if (track == null) { CurrentId = null; return; } var e = track.Element(uPnpNamespaces.items) ?? track; var uTrack = uParser.CreateObjectFromXML(new uParserObject { Type = e.GetValue(uPnpNamespaces.uClass), Element = e }); if (uTrack != null) CurrentId = uTrack.Id; } private async Task GetPositionInfo() { var command = AvCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo"); if (command == null) return true; 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)) .ConfigureAwait(false); if (result == null || result.Document == null) return true; 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); } 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 false; } var trackString = (string)track; if (string.IsNullOrWhiteSpace(trackString) || string.Equals(trackString, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) { return false; } XElement uPnpResponse; try { uPnpResponse = XElement.Parse(trackString); } catch { _logger.Error("Unable to parse xml {0}", trackString); return false; } var e = uPnpResponse.Element(uPnpNamespaces.items); var uTrack = CreateUBaseObject(e); if (uTrack == null) return true; CurrentId = uTrack.Id; return true; } private static uBaseObject CreateUBaseObject(XElement container) { if (container == null) { throw new ArgumentNullException("container"); } return new uBaseObject { Id = container.GetAttributeValue(uPnpNamespaces.Id), ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId), Title = container.GetValue(uPnpNamespaces.title), IconUrl = container.GetValue(uPnpNamespaces.Artwork), SecondText = "", Url = container.GetValue(uPnpNamespaces.Res), 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; var url = avService.ScpdUrl; if (!url.Contains("/")) url = "/dmr/" + url; if (!url.StartsWith("/")) url = "/" + url; var httpClient = new SsdpHttpClient(_httpClient, _config); var document = await httpClient.GetDataAsync(Properties.BaseUrl + url); AvCommands = TransportCommands.Create(document); } private async Task GetRenderingProtocolAsync() { var avService = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceRenderingType); if (avService == null) return; string url = avService.ScpdUrl; if (!url.Contains("/")) url = "/dmr/" + url; if (!url.StartsWith("/")) url = "/" + url; var httpClient = new SsdpHttpClient(_httpClient, _config); var document = await httpClient.GetDataAsync(Properties.BaseUrl + url); RendererCommands = TransportCommands.Create(document); } private TransportCommands AvCommands { get; set; } internal TransportCommands RendererCommands { get; set; } public static async Task 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 name = document.Descendants(uPnpNamespaces.ud.GetName("friendlyName")).FirstOrDefault(); if (name != null) deviceProperties.Name = name.Value; var name2 = document.Descendants(uPnpNamespaces.ud.GetName("roomName")).FirstOrDefault(); if (name2 != null) deviceProperties.Name = name2.Value; 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) return null; var servicesList = services.Descendants(uPnpNamespaces.ud.GetName("service")); if (servicesList == null) return null; foreach (var element in servicesList) { var service = Create(element); if (service != null) { deviceProperties.Services.Add(service); if (service.ServiceType == ServiceAvtransportType) { isRenderer = true; } } } } if (isRenderer) { var device = new Device(deviceProperties, httpClient, logger, config); await device.GetRenderingProtocolAsync().ConfigureAwait(false); await device.GetAVProtocolAsync().ConfigureAwait(false); return device; } return null; } #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 }; } #region Events public event EventHandler PlaybackChanged; public event EventHandler CurrentIdChanged; private void NotifyPlaybackChanged(bool value) { if (PlaybackChanged != null) { PlaybackChanged.Invoke(this, new TransportStateEventArgs { Stopped = IsStopped }); } } private void NotifyCurrentIdChanged(string value) { if (CurrentIdChanged != null) CurrentIdChanged.Invoke(this, new CurrentIdEventArgs { Id = value }); } #endregion #region IDisposable bool _disposed; public void Dispose() { if (!_disposed) { _disposed = true; _timer.Dispose(); } } #endregion public override string ToString() { return String.Format("{0} - {1}", Properties.Name, Properties.BaseUrl); } private class TRANSPORTSTATE { public const string STOPPED = "STOPPED"; public const string PLAYING = "PLAYING"; public const string TRANSITIONING = "TRANSITIONING"; public const string PAUSED_PLAYBACK = "PAUSED_PLAYBACK"; public const string PAUSED = "PAUSED"; } } }