Device.cs 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207
  1. #pragma warning disable CS1591
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Globalization;
  5. using System.Linq;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. using System.Xml;
  9. using System.Xml.Linq;
  10. using Emby.Dlna.Common;
  11. using Emby.Dlna.Server;
  12. using Emby.Dlna.Ssdp;
  13. using MediaBrowser.Common.Net;
  14. using MediaBrowser.Controller.Configuration;
  15. using Microsoft.Extensions.Logging;
  16. namespace Emby.Dlna.PlayTo
  17. {
  18. public class Device : IDisposable
  19. {
  20. #region Fields & Properties
  21. private Timer _timer;
  22. public DeviceInfo Properties { get; set; }
  23. private int _muteVol;
  24. public bool IsMuted { get; set; }
  25. private int _volume;
  26. public int Volume
  27. {
  28. get
  29. {
  30. RefreshVolumeIfNeeded();
  31. return _volume;
  32. }
  33. set => _volume = value;
  34. }
  35. public TimeSpan? Duration { get; set; }
  36. public TimeSpan Position { get; set; } = TimeSpan.FromSeconds(0);
  37. public TRANSPORTSTATE TransportState { get; private set; }
  38. public bool IsPlaying => TransportState == TRANSPORTSTATE.PLAYING;
  39. public bool IsPaused => TransportState == TRANSPORTSTATE.PAUSED || TransportState == TRANSPORTSTATE.PAUSED_PLAYBACK;
  40. public bool IsStopped => TransportState == TRANSPORTSTATE.STOPPED;
  41. #endregion
  42. private readonly IHttpClient _httpClient;
  43. private readonly ILogger _logger;
  44. private readonly IServerConfigurationManager _config;
  45. public Action OnDeviceUnavailable { get; set; }
  46. public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config)
  47. {
  48. Properties = deviceProperties;
  49. _httpClient = httpClient;
  50. _logger = logger;
  51. _config = config;
  52. }
  53. public void Start()
  54. {
  55. _logger.LogDebug("Dlna Device.Start");
  56. _timer = new Timer(TimerCallback, null, 1000, Timeout.Infinite);
  57. }
  58. private DateTime _lastVolumeRefresh;
  59. private bool _volumeRefreshActive;
  60. private void RefreshVolumeIfNeeded()
  61. {
  62. if (!_volumeRefreshActive)
  63. {
  64. return;
  65. }
  66. if (DateTime.UtcNow >= _lastVolumeRefresh.AddSeconds(5))
  67. {
  68. _lastVolumeRefresh = DateTime.UtcNow;
  69. RefreshVolume(CancellationToken.None);
  70. }
  71. }
  72. private async void RefreshVolume(CancellationToken cancellationToken)
  73. {
  74. if (_disposed)
  75. return;
  76. try
  77. {
  78. await GetVolume(cancellationToken).ConfigureAwait(false);
  79. await GetMute(cancellationToken).ConfigureAwait(false);
  80. }
  81. catch (Exception ex)
  82. {
  83. _logger.LogError(ex, "Error updating device volume info for {DeviceName}", Properties.Name);
  84. }
  85. }
  86. private readonly object _timerLock = new object();
  87. private void RestartTimer(bool immediate = false)
  88. {
  89. lock (_timerLock)
  90. {
  91. if (_disposed)
  92. {
  93. return;
  94. }
  95. _volumeRefreshActive = true;
  96. var time = immediate ? 100 : 10000;
  97. _timer.Change(time, Timeout.Infinite);
  98. }
  99. }
  100. /// <summary>
  101. /// Restarts the timer in inactive mode.
  102. /// </summary>
  103. private void RestartTimerInactive()
  104. {
  105. lock (_timerLock)
  106. {
  107. if (_disposed)
  108. {
  109. return;
  110. }
  111. _volumeRefreshActive = false;
  112. _timer.Change(Timeout.Infinite, Timeout.Infinite);
  113. }
  114. }
  115. #region Commanding
  116. public Task VolumeDown(CancellationToken cancellationToken)
  117. {
  118. var sendVolume = Math.Max(Volume - 5, 0);
  119. return SetVolume(sendVolume, cancellationToken);
  120. }
  121. public Task VolumeUp(CancellationToken cancellationToken)
  122. {
  123. var sendVolume = Math.Min(Volume + 5, 100);
  124. return SetVolume(sendVolume, cancellationToken);
  125. }
  126. public Task ToggleMute(CancellationToken cancellationToken)
  127. {
  128. if (IsMuted)
  129. {
  130. return Unmute(cancellationToken);
  131. }
  132. return Mute(cancellationToken);
  133. }
  134. public async Task Mute(CancellationToken cancellationToken)
  135. {
  136. var success = await SetMute(true, cancellationToken).ConfigureAwait(true);
  137. if (!success)
  138. {
  139. await SetVolume(0, cancellationToken).ConfigureAwait(false);
  140. }
  141. }
  142. public async Task Unmute(CancellationToken cancellationToken)
  143. {
  144. var success = await SetMute(false, cancellationToken).ConfigureAwait(true);
  145. if (!success)
  146. {
  147. var sendVolume = _muteVol <= 0 ? 20 : _muteVol;
  148. await SetVolume(sendVolume, cancellationToken).ConfigureAwait(false);
  149. }
  150. }
  151. private DeviceService GetServiceRenderingControl()
  152. {
  153. var services = Properties.Services;
  154. return services.FirstOrDefault(s => string.Equals(s.ServiceType, "urn:schemas-upnp-org:service:RenderingControl:1", StringComparison.OrdinalIgnoreCase)) ??
  155. services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:RenderingControl", StringComparison.OrdinalIgnoreCase));
  156. }
  157. private DeviceService GetAvTransportService()
  158. {
  159. var services = Properties.Services;
  160. return services.FirstOrDefault(s => string.Equals(s.ServiceType, "urn:schemas-upnp-org:service:AVTransport:1", StringComparison.OrdinalIgnoreCase)) ??
  161. services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:AVTransport", StringComparison.OrdinalIgnoreCase));
  162. }
  163. private async Task<bool> SetMute(bool mute, CancellationToken cancellationToken)
  164. {
  165. var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
  166. var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
  167. if (command == null)
  168. return false;
  169. var service = GetServiceRenderingControl();
  170. if (service == null)
  171. {
  172. return false;
  173. }
  174. _logger.LogDebug("Setting mute");
  175. var value = mute ? 1 : 0;
  176. await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
  177. .ConfigureAwait(false);
  178. IsMuted = mute;
  179. return true;
  180. }
  181. /// <summary>
  182. /// Sets volume on a scale of 0-100
  183. /// </summary>
  184. public async Task SetVolume(int value, CancellationToken cancellationToken)
  185. {
  186. var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
  187. var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
  188. if (command == null)
  189. return;
  190. var service = GetServiceRenderingControl();
  191. if (service == null)
  192. {
  193. throw new InvalidOperationException("Unable to find service");
  194. }
  195. // Set it early and assume it will succeed
  196. // Remote control will perform better
  197. Volume = value;
  198. await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType, value))
  199. .ConfigureAwait(false);
  200. }
  201. public async Task Seek(TimeSpan value, CancellationToken cancellationToken)
  202. {
  203. var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
  204. var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
  205. if (command == null)
  206. return;
  207. var service = GetAvTransportService();
  208. if (service == null)
  209. {
  210. throw new InvalidOperationException("Unable to find service");
  211. }
  212. 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"))
  213. .ConfigureAwait(false);
  214. RestartTimer(true);
  215. }
  216. public async Task SetAvTransport(string url, string header, string metaData, CancellationToken cancellationToken)
  217. {
  218. var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
  219. url = url.Replace("&", "&amp;");
  220. _logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
  221. var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
  222. if (command == null)
  223. return;
  224. var dictionary = new Dictionary<string, string>
  225. {
  226. {"CurrentURI", url},
  227. {"CurrentURIMetaData", CreateDidlMeta(metaData)}
  228. };
  229. var service = GetAvTransportService();
  230. if (service == null)
  231. {
  232. throw new InvalidOperationException("Unable to find service");
  233. }
  234. var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
  235. await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header)
  236. .ConfigureAwait(false);
  237. await Task.Delay(50).ConfigureAwait(false);
  238. try
  239. {
  240. await SetPlay(avCommands, CancellationToken.None).ConfigureAwait(false);
  241. }
  242. catch
  243. {
  244. // Some devices will throw an error if you tell it to play when it's already playing
  245. // Others won't
  246. }
  247. RestartTimer(true);
  248. }
  249. private string CreateDidlMeta(string value)
  250. {
  251. if (string.IsNullOrEmpty(value))
  252. {
  253. return string.Empty;
  254. }
  255. return DescriptionXmlBuilder.Escape(value);
  256. }
  257. private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken)
  258. {
  259. var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Play");
  260. if (command == null)
  261. {
  262. return Task.CompletedTask;
  263. }
  264. var service = GetAvTransportService();
  265. if (service == null)
  266. {
  267. throw new InvalidOperationException("Unable to find service");
  268. }
  269. return new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1));
  270. }
  271. public async Task SetPlay(CancellationToken cancellationToken)
  272. {
  273. var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
  274. await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
  275. RestartTimer(true);
  276. }
  277. public async Task SetStop(CancellationToken cancellationToken)
  278. {
  279. var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
  280. var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
  281. if (command == null)
  282. {
  283. return;
  284. }
  285. var service = GetAvTransportService();
  286. await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
  287. .ConfigureAwait(false);
  288. RestartTimer(true);
  289. }
  290. public async Task SetPause(CancellationToken cancellationToken)
  291. {
  292. var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
  293. var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
  294. if (command == null)
  295. {
  296. return;
  297. }
  298. var service = GetAvTransportService();
  299. await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
  300. .ConfigureAwait(false);
  301. TransportState = TRANSPORTSTATE.PAUSED;
  302. RestartTimer(true);
  303. }
  304. #endregion
  305. #region Get data
  306. private int _connectFailureCount;
  307. private async void TimerCallback(object sender)
  308. {
  309. if (_disposed)
  310. {
  311. return;
  312. }
  313. try
  314. {
  315. var cancellationToken = CancellationToken.None;
  316. var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
  317. if (avCommands == null)
  318. {
  319. return;
  320. }
  321. var transportState = await GetTransportInfo(avCommands, cancellationToken).ConfigureAwait(false);
  322. if (_disposed)
  323. {
  324. return;
  325. }
  326. if (transportState.HasValue)
  327. {
  328. // If we're not playing anything no need to get additional data
  329. if (transportState.Value == TRANSPORTSTATE.STOPPED)
  330. {
  331. UpdateMediaInfo(null, transportState.Value);
  332. }
  333. else
  334. {
  335. var tuple = await GetPositionInfo(avCommands, cancellationToken).ConfigureAwait(false);
  336. var currentObject = tuple.Item2;
  337. if (tuple.Item1 && currentObject == null)
  338. {
  339. currentObject = await GetMediaInfo(avCommands, cancellationToken).ConfigureAwait(false);
  340. }
  341. if (currentObject != null)
  342. {
  343. UpdateMediaInfo(currentObject, transportState.Value);
  344. }
  345. }
  346. _connectFailureCount = 0;
  347. if (_disposed)
  348. return;
  349. // If we're not playing anything make sure we don't get data more often than neccessry to keep the Session alive
  350. if (transportState.Value == TRANSPORTSTATE.STOPPED)
  351. {
  352. RestartTimerInactive();
  353. }
  354. else
  355. {
  356. RestartTimer();
  357. }
  358. }
  359. else
  360. {
  361. RestartTimerInactive();
  362. }
  363. }
  364. catch (Exception ex)
  365. {
  366. if (_disposed)
  367. return;
  368. _logger.LogError(ex, "Error updating device info for {DeviceName}", Properties.Name);
  369. _connectFailureCount++;
  370. if (_connectFailureCount >= 3)
  371. {
  372. var action = OnDeviceUnavailable;
  373. if (action != null)
  374. {
  375. _logger.LogDebug("Disposing device due to loss of connection");
  376. action();
  377. return;
  378. }
  379. }
  380. RestartTimerInactive();
  381. }
  382. }
  383. private async Task GetVolume(CancellationToken cancellationToken)
  384. {
  385. if (_disposed)
  386. {
  387. return;
  388. }
  389. var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
  390. var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
  391. if (command == null)
  392. {
  393. return;
  394. }
  395. var service = GetServiceRenderingControl();
  396. if (service == null)
  397. {
  398. return;
  399. }
  400. var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), true)
  401. .ConfigureAwait(false);
  402. if (result == null || result.Document == null)
  403. {
  404. return;
  405. }
  406. var volume = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null);
  407. var volumeValue = volume?.Value;
  408. if (string.IsNullOrWhiteSpace(volumeValue))
  409. {
  410. return;
  411. }
  412. Volume = int.Parse(volumeValue, UsCulture);
  413. if (Volume > 0)
  414. {
  415. _muteVol = Volume;
  416. }
  417. }
  418. private async Task GetMute(CancellationToken cancellationToken)
  419. {
  420. if (_disposed)
  421. {
  422. return;
  423. }
  424. var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
  425. var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
  426. if (command == null)
  427. {
  428. return;
  429. }
  430. var service = GetServiceRenderingControl();
  431. if (service == null)
  432. {
  433. return;
  434. }
  435. var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), true)
  436. .ConfigureAwait(false);
  437. if (result == null || result.Document == null)
  438. return;
  439. var valueNode = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetMuteResponse")
  440. .Select(i => i.Element("CurrentMute"))
  441. .FirstOrDefault(i => i != null);
  442. IsMuted = string.Equals(valueNode?.Value, "1", StringComparison.OrdinalIgnoreCase);
  443. }
  444. private async Task<TRANSPORTSTATE?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken)
  445. {
  446. var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetTransportInfo");
  447. if (command == null)
  448. {
  449. return null;
  450. }
  451. var service = GetAvTransportService();
  452. if (service == null)
  453. {
  454. return null;
  455. }
  456. var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType), false)
  457. .ConfigureAwait(false);
  458. if (result == null || result.Document == null)
  459. {
  460. return null;
  461. }
  462. var transportState =
  463. result.Document.Descendants(uPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null);
  464. var transportStateValue = transportState == null ? null : transportState.Value;
  465. if (transportStateValue != null
  466. && Enum.TryParse(transportStateValue, true, out TRANSPORTSTATE state))
  467. {
  468. return state;
  469. }
  470. return null;
  471. }
  472. private async Task<uBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
  473. {
  474. var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo");
  475. if (command == null)
  476. {
  477. return null;
  478. }
  479. var service = GetAvTransportService();
  480. if (service == null)
  481. {
  482. throw new InvalidOperationException("Unable to find service");
  483. }
  484. var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
  485. var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), false)
  486. .ConfigureAwait(false);
  487. if (result == null || result.Document == null)
  488. {
  489. return null;
  490. }
  491. var track = result.Document.Descendants("CurrentURIMetaData").FirstOrDefault();
  492. if (track == null)
  493. {
  494. return null;
  495. }
  496. var e = track.Element(uPnpNamespaces.items) ?? track;
  497. var elementString = (string)e;
  498. if (!string.IsNullOrWhiteSpace(elementString))
  499. {
  500. return UpnpContainer.Create(e);
  501. }
  502. track = result.Document.Descendants("CurrentURI").FirstOrDefault();
  503. if (track == null)
  504. {
  505. return null;
  506. }
  507. e = track.Element(uPnpNamespaces.items) ?? track;
  508. elementString = (string)e;
  509. if (!string.IsNullOrWhiteSpace(elementString))
  510. {
  511. return new uBaseObject
  512. {
  513. Url = elementString
  514. };
  515. }
  516. return null;
  517. }
  518. private async Task<(bool, uBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
  519. {
  520. var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
  521. if (command == null)
  522. {
  523. return (false, null);
  524. }
  525. var service = GetAvTransportService();
  526. if (service == null)
  527. {
  528. throw new InvalidOperationException("Unable to find service");
  529. }
  530. var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
  531. var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), false)
  532. .ConfigureAwait(false);
  533. if (result == null || result.Document == null)
  534. {
  535. return (false, null);
  536. }
  537. var trackUriElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null);
  538. var trackUri = trackUriElem == null ? null : trackUriElem.Value;
  539. var durationElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null);
  540. var duration = durationElem == null ? null : durationElem.Value;
  541. if (!string.IsNullOrWhiteSpace(duration)
  542. && !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
  543. {
  544. Duration = TimeSpan.Parse(duration, UsCulture);
  545. }
  546. else
  547. {
  548. Duration = null;
  549. }
  550. var positionElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null);
  551. var position = positionElem == null ? null : positionElem.Value;
  552. if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
  553. {
  554. Position = TimeSpan.Parse(position, UsCulture);
  555. }
  556. var track = result.Document.Descendants("TrackMetaData").FirstOrDefault();
  557. if (track == null)
  558. {
  559. //If track is null, some vendors do this, use GetMediaInfo instead
  560. return (true, null);
  561. }
  562. var trackString = (string)track;
  563. if (string.IsNullOrWhiteSpace(trackString) || string.Equals(trackString, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
  564. {
  565. return (true, null);
  566. }
  567. XElement uPnpResponse = null;
  568. try
  569. {
  570. uPnpResponse = ParseResponse(trackString);
  571. }
  572. catch (Exception ex)
  573. {
  574. _logger.LogError(ex, "Uncaught exception while parsing xml");
  575. }
  576. if (uPnpResponse == null)
  577. {
  578. _logger.LogError("Failed to parse xml: \n {Xml}", trackString);
  579. return (true, null);
  580. }
  581. var e = uPnpResponse.Element(uPnpNamespaces.items);
  582. var uTrack = CreateUBaseObject(e, trackUri);
  583. return (true, uTrack);
  584. }
  585. private XElement ParseResponse(string xml)
  586. {
  587. // Handle different variations sent back by devices
  588. try
  589. {
  590. return XElement.Parse(xml);
  591. }
  592. catch (XmlException)
  593. {
  594. }
  595. // first try to add a root node with a dlna namesapce
  596. try
  597. {
  598. return XElement.Parse("<data xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">" + xml + "</data>")
  599. .Descendants()
  600. .First();
  601. }
  602. catch (XmlException)
  603. {
  604. }
  605. // some devices send back invalid xml
  606. try
  607. {
  608. return XElement.Parse(xml.Replace("&", "&amp;"));
  609. }
  610. catch (XmlException)
  611. {
  612. }
  613. return null;
  614. }
  615. private static uBaseObject CreateUBaseObject(XElement container, string trackUri)
  616. {
  617. if (container == null)
  618. {
  619. throw new ArgumentNullException(nameof(container));
  620. }
  621. var url = container.GetValue(uPnpNamespaces.Res);
  622. if (string.IsNullOrWhiteSpace(url))
  623. {
  624. url = trackUri;
  625. }
  626. return new uBaseObject
  627. {
  628. Id = container.GetAttributeValue(uPnpNamespaces.Id),
  629. ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId),
  630. Title = container.GetValue(uPnpNamespaces.title),
  631. IconUrl = container.GetValue(uPnpNamespaces.Artwork),
  632. SecondText = "",
  633. Url = url,
  634. ProtocolInfo = GetProtocolInfo(container),
  635. MetaData = container.ToString()
  636. };
  637. }
  638. private static string[] GetProtocolInfo(XElement container)
  639. {
  640. if (container == null)
  641. {
  642. throw new ArgumentNullException(nameof(container));
  643. }
  644. var resElement = container.Element(uPnpNamespaces.Res);
  645. if (resElement != null)
  646. {
  647. var info = resElement.Attribute(uPnpNamespaces.ProtocolInfo);
  648. if (info != null && !string.IsNullOrWhiteSpace(info.Value))
  649. {
  650. return info.Value.Split(':');
  651. }
  652. }
  653. return new string[4];
  654. }
  655. #endregion
  656. #region From XML
  657. private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken)
  658. {
  659. if (AvCommands != null)
  660. {
  661. return AvCommands;
  662. }
  663. if (_disposed)
  664. {
  665. throw new ObjectDisposedException(GetType().Name);
  666. }
  667. var avService = GetAvTransportService();
  668. if (avService == null)
  669. {
  670. return null;
  671. }
  672. string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
  673. var httpClient = new SsdpHttpClient(_httpClient);
  674. var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
  675. AvCommands = TransportCommands.Create(document);
  676. return AvCommands;
  677. }
  678. private async Task<TransportCommands> GetRenderingProtocolAsync(CancellationToken cancellationToken)
  679. {
  680. if (RendererCommands != null)
  681. {
  682. return RendererCommands;
  683. }
  684. if (_disposed)
  685. {
  686. throw new ObjectDisposedException(GetType().Name);
  687. }
  688. var avService = GetServiceRenderingControl();
  689. if (avService == null)
  690. {
  691. throw new ArgumentException("Device AvService is null");
  692. }
  693. string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
  694. var httpClient = new SsdpHttpClient(_httpClient);
  695. _logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
  696. var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
  697. RendererCommands = TransportCommands.Create(document);
  698. return RendererCommands;
  699. }
  700. private string NormalizeUrl(string baseUrl, string url)
  701. {
  702. // If it's already a complete url, don't stick anything onto the front of it
  703. if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
  704. {
  705. return url;
  706. }
  707. if (!url.Contains("/"))
  708. {
  709. url = "/dmr/" + url;
  710. }
  711. if (!url.StartsWith("/"))
  712. {
  713. url = "/" + url;
  714. }
  715. return baseUrl + url;
  716. }
  717. private TransportCommands AvCommands { get; set; }
  718. private TransportCommands RendererCommands { get; set; }
  719. public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, IServerConfigurationManager config, ILogger logger, CancellationToken cancellationToken)
  720. {
  721. var ssdpHttpClient = new SsdpHttpClient(httpClient);
  722. var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
  723. var friendlyNames = new List<string>();
  724. var name = document.Descendants(uPnpNamespaces.ud.GetName("friendlyName")).FirstOrDefault();
  725. if (name != null && !string.IsNullOrWhiteSpace(name.Value))
  726. {
  727. friendlyNames.Add(name.Value);
  728. }
  729. var room = document.Descendants(uPnpNamespaces.ud.GetName("roomName")).FirstOrDefault();
  730. if (room != null && !string.IsNullOrWhiteSpace(room.Value))
  731. {
  732. friendlyNames.Add(room.Value);
  733. }
  734. var deviceProperties = new DeviceInfo()
  735. {
  736. Name = string.Join(" ", friendlyNames),
  737. BaseUrl = string.Format("http://{0}:{1}", url.Host, url.Port)
  738. };
  739. var model = document.Descendants(uPnpNamespaces.ud.GetName("modelName")).FirstOrDefault();
  740. if (model != null)
  741. {
  742. deviceProperties.ModelName = model.Value;
  743. }
  744. var modelNumber = document.Descendants(uPnpNamespaces.ud.GetName("modelNumber")).FirstOrDefault();
  745. if (modelNumber != null)
  746. {
  747. deviceProperties.ModelNumber = modelNumber.Value;
  748. }
  749. var uuid = document.Descendants(uPnpNamespaces.ud.GetName("UDN")).FirstOrDefault();
  750. if (uuid != null)
  751. {
  752. deviceProperties.UUID = uuid.Value;
  753. }
  754. var manufacturer = document.Descendants(uPnpNamespaces.ud.GetName("manufacturer")).FirstOrDefault();
  755. if (manufacturer != null)
  756. {
  757. deviceProperties.Manufacturer = manufacturer.Value;
  758. }
  759. var manufacturerUrl = document.Descendants(uPnpNamespaces.ud.GetName("manufacturerURL")).FirstOrDefault();
  760. if (manufacturerUrl != null)
  761. {
  762. deviceProperties.ManufacturerUrl = manufacturerUrl.Value;
  763. }
  764. var presentationUrl = document.Descendants(uPnpNamespaces.ud.GetName("presentationURL")).FirstOrDefault();
  765. if (presentationUrl != null)
  766. {
  767. deviceProperties.PresentationUrl = presentationUrl.Value;
  768. }
  769. var modelUrl = document.Descendants(uPnpNamespaces.ud.GetName("modelURL")).FirstOrDefault();
  770. if (modelUrl != null)
  771. {
  772. deviceProperties.ModelUrl = modelUrl.Value;
  773. }
  774. var serialNumber = document.Descendants(uPnpNamespaces.ud.GetName("serialNumber")).FirstOrDefault();
  775. if (serialNumber != null)
  776. {
  777. deviceProperties.SerialNumber = serialNumber.Value;
  778. }
  779. var modelDescription = document.Descendants(uPnpNamespaces.ud.GetName("modelDescription")).FirstOrDefault();
  780. if (modelDescription != null)
  781. {
  782. deviceProperties.ModelDescription = modelDescription.Value;
  783. }
  784. var icon = document.Descendants(uPnpNamespaces.ud.GetName("icon")).FirstOrDefault();
  785. if (icon != null)
  786. {
  787. deviceProperties.Icon = CreateIcon(icon);
  788. }
  789. foreach (var services in document.Descendants(uPnpNamespaces.ud.GetName("serviceList")))
  790. {
  791. if (services == null)
  792. {
  793. continue;
  794. }
  795. var servicesList = services.Descendants(uPnpNamespaces.ud.GetName("service"));
  796. if (servicesList == null)
  797. {
  798. continue;
  799. }
  800. foreach (var element in servicesList)
  801. {
  802. var service = Create(element);
  803. if (service != null)
  804. {
  805. deviceProperties.Services.Add(service);
  806. }
  807. }
  808. }
  809. return new Device(deviceProperties, httpClient, logger, config);
  810. }
  811. #endregion
  812. private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
  813. private static DeviceIcon CreateIcon(XElement element)
  814. {
  815. if (element == null)
  816. {
  817. throw new ArgumentNullException(nameof(element));
  818. }
  819. var mimeType = element.GetDescendantValue(uPnpNamespaces.ud.GetName("mimetype"));
  820. var width = element.GetDescendantValue(uPnpNamespaces.ud.GetName("width"));
  821. var height = element.GetDescendantValue(uPnpNamespaces.ud.GetName("height"));
  822. var depth = element.GetDescendantValue(uPnpNamespaces.ud.GetName("depth"));
  823. var url = element.GetDescendantValue(uPnpNamespaces.ud.GetName("url"));
  824. var widthValue = int.Parse(width, NumberStyles.Integer, UsCulture);
  825. var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture);
  826. return new DeviceIcon
  827. {
  828. Depth = depth,
  829. Height = heightValue,
  830. MimeType = mimeType,
  831. Url = url,
  832. Width = widthValue
  833. };
  834. }
  835. private static DeviceService Create(XElement element)
  836. {
  837. var type = element.GetDescendantValue(uPnpNamespaces.ud.GetName("serviceType"));
  838. var id = element.GetDescendantValue(uPnpNamespaces.ud.GetName("serviceId"));
  839. var scpdUrl = element.GetDescendantValue(uPnpNamespaces.ud.GetName("SCPDURL"));
  840. var controlURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("controlURL"));
  841. var eventSubURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("eventSubURL"));
  842. return new DeviceService
  843. {
  844. ControlUrl = controlURL,
  845. EventSubUrl = eventSubURL,
  846. ScpdUrl = scpdUrl,
  847. ServiceId = id,
  848. ServiceType = type
  849. };
  850. }
  851. public event EventHandler<PlaybackStartEventArgs> PlaybackStart;
  852. public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
  853. public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped;
  854. public event EventHandler<MediaChangedEventArgs> MediaChanged;
  855. public uBaseObject CurrentMediaInfo { get; private set; }
  856. private void UpdateMediaInfo(uBaseObject mediaInfo, TRANSPORTSTATE state)
  857. {
  858. TransportState = state;
  859. var previousMediaInfo = CurrentMediaInfo;
  860. CurrentMediaInfo = mediaInfo;
  861. if (previousMediaInfo == null && mediaInfo != null)
  862. {
  863. if (state != TRANSPORTSTATE.STOPPED)
  864. {
  865. OnPlaybackStart(mediaInfo);
  866. }
  867. }
  868. else if (mediaInfo != null && previousMediaInfo != null && !mediaInfo.Equals(previousMediaInfo))
  869. {
  870. OnMediaChanged(previousMediaInfo, mediaInfo);
  871. }
  872. else if (mediaInfo == null && previousMediaInfo != null)
  873. {
  874. OnPlaybackStop(previousMediaInfo);
  875. }
  876. else if (mediaInfo != null && mediaInfo.Equals(previousMediaInfo))
  877. {
  878. OnPlaybackProgress(mediaInfo);
  879. }
  880. }
  881. private void OnPlaybackStart(uBaseObject mediaInfo)
  882. {
  883. if (string.IsNullOrWhiteSpace(mediaInfo.Url))
  884. {
  885. return;
  886. }
  887. PlaybackStart?.Invoke(this, new PlaybackStartEventArgs
  888. {
  889. MediaInfo = mediaInfo
  890. });
  891. }
  892. private void OnPlaybackProgress(uBaseObject mediaInfo)
  893. {
  894. if (string.IsNullOrWhiteSpace(mediaInfo.Url))
  895. {
  896. return;
  897. }
  898. PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs
  899. {
  900. MediaInfo = mediaInfo
  901. });
  902. }
  903. private void OnPlaybackStop(uBaseObject mediaInfo)
  904. {
  905. PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs
  906. {
  907. MediaInfo = mediaInfo
  908. });
  909. }
  910. private void OnMediaChanged(uBaseObject old, uBaseObject newMedia)
  911. {
  912. MediaChanged?.Invoke(this, new MediaChangedEventArgs
  913. {
  914. OldMediaInfo = old,
  915. NewMediaInfo = newMedia
  916. });
  917. }
  918. #region IDisposable
  919. bool _disposed;
  920. public void Dispose()
  921. {
  922. Dispose(true);
  923. GC.SuppressFinalize(this);
  924. }
  925. protected virtual void Dispose(bool disposing)
  926. {
  927. if (_disposed)
  928. {
  929. return;
  930. }
  931. if (disposing)
  932. {
  933. _timer?.Dispose();
  934. }
  935. _timer = null;
  936. Properties = null;
  937. _disposed = true;
  938. }
  939. #endregion
  940. public override string ToString()
  941. {
  942. return string.Format("{0} - {1}", Properties.Name, Properties.BaseUrl);
  943. }
  944. }
  945. }