HdHomerunHost.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. using MediaBrowser.Common.Configuration;
  2. using MediaBrowser.Common.Net;
  3. using MediaBrowser.Controller.LiveTv;
  4. using MediaBrowser.Model.Dto;
  5. using MediaBrowser.Model.Entities;
  6. using MediaBrowser.Model.LiveTv;
  7. using MediaBrowser.Model.Logging;
  8. using MediaBrowser.Model.MediaInfo;
  9. using MediaBrowser.Model.Serialization;
  10. using System;
  11. using System.Collections.Generic;
  12. using System.IO;
  13. using System.Linq;
  14. using System.Threading;
  15. using System.Threading.Tasks;
  16. using MediaBrowser.Model.IO;
  17. using MediaBrowser.Common.Extensions;
  18. using MediaBrowser.Controller;
  19. using MediaBrowser.Controller.Configuration;
  20. using MediaBrowser.Controller.MediaEncoding;
  21. using MediaBrowser.Model.Configuration;
  22. using MediaBrowser.Model.Net;
  23. using MediaBrowser.Model.System;
  24. namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
  25. {
  26. public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
  27. {
  28. private readonly IHttpClient _httpClient;
  29. private readonly IServerApplicationHost _appHost;
  30. private readonly ISocketFactory _socketFactory;
  31. private readonly INetworkManager _networkManager;
  32. private readonly IEnvironmentInfo _environment;
  33. public HdHomerunHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient, IServerApplicationHost appHost, ISocketFactory socketFactory, INetworkManager networkManager, IEnvironmentInfo environment) : base(config, logger, jsonSerializer, mediaEncoder, fileSystem)
  34. {
  35. _httpClient = httpClient;
  36. _appHost = appHost;
  37. _socketFactory = socketFactory;
  38. _networkManager = networkManager;
  39. _environment = environment;
  40. }
  41. public string Name
  42. {
  43. get { return "HD Homerun"; }
  44. }
  45. public override string Type
  46. {
  47. get { return DeviceType; }
  48. }
  49. public static string DeviceType
  50. {
  51. get { return "hdhomerun"; }
  52. }
  53. protected override string ChannelIdPrefix
  54. {
  55. get
  56. {
  57. return "hdhr_";
  58. }
  59. }
  60. private string GetChannelId(TunerHostInfo info, Channels i)
  61. {
  62. var id = ChannelIdPrefix + i.GuideNumber;
  63. if (!info.EnableNewHdhrChannelIds)
  64. {
  65. id += '_' + (i.GuideName ?? string.Empty).GetMD5().ToString("N");
  66. }
  67. return id;
  68. }
  69. private async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
  70. {
  71. var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
  72. var options = new HttpRequestOptions
  73. {
  74. Url = model.LineupURL,
  75. CancellationToken = cancellationToken,
  76. BufferContent = false
  77. };
  78. using (var stream = await _httpClient.Get(options).ConfigureAwait(false))
  79. {
  80. var lineup = JsonSerializer.DeserializeFromStream<List<Channels>>(stream) ?? new List<Channels>();
  81. if (info.ImportFavoritesOnly)
  82. {
  83. lineup = lineup.Where(i => i.Favorite).ToList();
  84. }
  85. return lineup.Where(i => !i.DRM).ToList();
  86. }
  87. }
  88. private class HdHomerunChannelInfo : ChannelInfo
  89. {
  90. public bool IsLegacyTuner { get; set; }
  91. public string Url { get; set; }
  92. }
  93. protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
  94. {
  95. var lineup = await GetLineup(info, cancellationToken).ConfigureAwait(false);
  96. return lineup.Select(i => new HdHomerunChannelInfo
  97. {
  98. Name = i.GuideName,
  99. Number = i.GuideNumber,
  100. Id = GetChannelId(info, i),
  101. IsFavorite = i.Favorite,
  102. TunerHostId = info.Id,
  103. IsHD = i.HD == 1,
  104. AudioCodec = i.AudioCodec,
  105. VideoCodec = i.VideoCodec,
  106. ChannelType = ChannelType.TV,
  107. IsLegacyTuner = (i.URL ?? string.Empty).StartsWith("hdhomerun", StringComparison.OrdinalIgnoreCase),
  108. Url = i.URL
  109. }).Cast<ChannelInfo>().ToList();
  110. }
  111. private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
  112. private async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken)
  113. {
  114. lock (_modelCache)
  115. {
  116. DiscoverResponse response;
  117. if (_modelCache.TryGetValue(info.Url, out response))
  118. {
  119. if ((DateTime.UtcNow - response.DateQueried).TotalHours <= 12)
  120. {
  121. return response;
  122. }
  123. }
  124. }
  125. try
  126. {
  127. using (var stream = await _httpClient.Get(new HttpRequestOptions()
  128. {
  129. Url = string.Format("{0}/discover.json", GetApiUrl(info, false)),
  130. CancellationToken = cancellationToken,
  131. TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds),
  132. BufferContent = false
  133. }).ConfigureAwait(false))
  134. {
  135. var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream);
  136. if (!string.IsNullOrWhiteSpace(info.Id))
  137. {
  138. lock (_modelCache)
  139. {
  140. _modelCache[info.Id] = response;
  141. }
  142. }
  143. return response;
  144. }
  145. }
  146. catch (HttpException ex)
  147. {
  148. if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
  149. {
  150. var defaultValue = "HDHR";
  151. var response = new DiscoverResponse
  152. {
  153. ModelNumber = defaultValue
  154. };
  155. if (!string.IsNullOrWhiteSpace(info.Id))
  156. {
  157. // HDHR4 doesn't have this api
  158. lock (_modelCache)
  159. {
  160. _modelCache[info.Id] = response;
  161. }
  162. }
  163. return response;
  164. }
  165. throw;
  166. }
  167. }
  168. private async Task<List<LiveTvTunerInfo>> GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken)
  169. {
  170. var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
  171. var tuners = new List<LiveTvTunerInfo>();
  172. var uri = new Uri(GetApiUrl(info, false));
  173. using (var manager = new HdHomerunManager(_socketFactory))
  174. {
  175. // Legacy HdHomeruns are IPv4 only
  176. var ipInfo = _networkManager.ParseIpAddress(uri.Host);
  177. for (int i = 0; i < model.TunerCount; ++i)
  178. {
  179. var name = String.Format("Tuner {0}", i + 1);
  180. var currentChannel = "none"; /// @todo Get current channel and map back to Station Id
  181. var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false);
  182. LiveTvTunerStatus status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv;
  183. tuners.Add(new LiveTvTunerInfo
  184. {
  185. Name = name,
  186. SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
  187. ProgramName = currentChannel,
  188. Status = status
  189. });
  190. }
  191. }
  192. return tuners;
  193. }
  194. public async Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
  195. {
  196. var list = new List<LiveTvTunerInfo>();
  197. foreach (var host in GetConfiguration().TunerHosts
  198. .Where(i => string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase)))
  199. {
  200. try
  201. {
  202. list.AddRange(await GetTunerInfos(host, cancellationToken).ConfigureAwait(false));
  203. }
  204. catch (Exception ex)
  205. {
  206. Logger.ErrorException("Error getting tuner info", ex);
  207. }
  208. }
  209. return list;
  210. }
  211. private string GetApiUrl(TunerHostInfo info, bool isPlayback)
  212. {
  213. var url = info.Url;
  214. if (string.IsNullOrWhiteSpace(url))
  215. {
  216. throw new ArgumentException("Invalid tuner info");
  217. }
  218. if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
  219. {
  220. url = "http://" + url;
  221. }
  222. var uri = new Uri(url);
  223. if (isPlayback)
  224. {
  225. var builder = new UriBuilder(uri);
  226. builder.Port = 5004;
  227. uri = builder.Uri;
  228. }
  229. return uri.AbsoluteUri.TrimEnd('/');
  230. }
  231. private class Channels
  232. {
  233. public string GuideNumber { get; set; }
  234. public string GuideName { get; set; }
  235. public string VideoCodec { get; set; }
  236. public string AudioCodec { get; set; }
  237. public string URL { get; set; }
  238. public bool Favorite { get; set; }
  239. public bool DRM { get; set; }
  240. public int HD { get; set; }
  241. }
  242. protected EncodingOptions GetEncodingOptions()
  243. {
  244. return Config.GetConfiguration<EncodingOptions>("encoding");
  245. }
  246. private string GetHdHrIdFromChannelId(string channelId)
  247. {
  248. return channelId.Split('_')[1];
  249. }
  250. private MediaSourceInfo GetMediaSource(TunerHostInfo info, string channelId, ChannelInfo channelInfo, string profile)
  251. {
  252. int? width = null;
  253. int? height = null;
  254. bool isInterlaced = true;
  255. string videoCodec = null;
  256. string audioCodec = null;
  257. int? videoBitrate = null;
  258. int? audioBitrate = null;
  259. if (string.Equals(profile, "mobile", StringComparison.OrdinalIgnoreCase))
  260. {
  261. width = 1280;
  262. height = 720;
  263. isInterlaced = false;
  264. videoCodec = "h264";
  265. videoBitrate = 2000000;
  266. }
  267. else if (string.Equals(profile, "heavy", StringComparison.OrdinalIgnoreCase))
  268. {
  269. width = 1920;
  270. height = 1080;
  271. isInterlaced = false;
  272. videoCodec = "h264";
  273. videoBitrate = 15000000;
  274. }
  275. else if (string.Equals(profile, "internet540", StringComparison.OrdinalIgnoreCase))
  276. {
  277. width = 960;
  278. height = 546;
  279. isInterlaced = false;
  280. videoCodec = "h264";
  281. videoBitrate = 2500000;
  282. }
  283. else if (string.Equals(profile, "internet480", StringComparison.OrdinalIgnoreCase))
  284. {
  285. width = 848;
  286. height = 480;
  287. isInterlaced = false;
  288. videoCodec = "h264";
  289. videoBitrate = 2000000;
  290. }
  291. else if (string.Equals(profile, "internet360", StringComparison.OrdinalIgnoreCase))
  292. {
  293. width = 640;
  294. height = 360;
  295. isInterlaced = false;
  296. videoCodec = "h264";
  297. videoBitrate = 1500000;
  298. }
  299. else if (string.Equals(profile, "internet240", StringComparison.OrdinalIgnoreCase))
  300. {
  301. width = 432;
  302. height = 240;
  303. isInterlaced = false;
  304. videoCodec = "h264";
  305. videoBitrate = 1000000;
  306. }
  307. else
  308. {
  309. // This is for android tv's 1200 condition. Remove once not needed anymore so that we can avoid possible side effects of dummying up this data
  310. if ((channelInfo.IsHD ?? true))
  311. {
  312. width = 1920;
  313. height = 1080;
  314. }
  315. }
  316. if (channelInfo != null)
  317. {
  318. if (string.IsNullOrWhiteSpace(videoCodec))
  319. {
  320. videoCodec = channelInfo.VideoCodec;
  321. }
  322. audioCodec = channelInfo.AudioCodec;
  323. if (!videoBitrate.HasValue)
  324. {
  325. videoBitrate = (channelInfo.IsHD ?? true) ? 15000000 : 2000000;
  326. }
  327. audioBitrate = (channelInfo.IsHD ?? true) ? 448000 : 192000;
  328. }
  329. // normalize
  330. if (string.Equals(videoCodec, "mpeg2", StringComparison.OrdinalIgnoreCase))
  331. {
  332. videoCodec = "mpeg2video";
  333. }
  334. string nal = null;
  335. if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
  336. {
  337. nal = "0";
  338. }
  339. var url = GetApiUrl(info, false);
  340. var id = profile;
  341. if (string.IsNullOrWhiteSpace(id))
  342. {
  343. id = "native";
  344. }
  345. id += "_" + channelId.GetMD5().ToString("N") + "_" + url.GetMD5().ToString("N");
  346. var mediaSource = new MediaSourceInfo
  347. {
  348. Path = url,
  349. Protocol = MediaProtocol.Udp,
  350. MediaStreams = new List<MediaStream>
  351. {
  352. new MediaStream
  353. {
  354. Type = MediaStreamType.Video,
  355. // Set the index to -1 because we don't know the exact index of the video stream within the container
  356. Index = -1,
  357. IsInterlaced = isInterlaced,
  358. Codec = videoCodec,
  359. Width = width,
  360. Height = height,
  361. BitRate = videoBitrate,
  362. NalLengthSize = nal
  363. },
  364. new MediaStream
  365. {
  366. Type = MediaStreamType.Audio,
  367. // Set the index to -1 because we don't know the exact index of the audio stream within the container
  368. Index = -1,
  369. Codec = audioCodec,
  370. BitRate = audioBitrate
  371. }
  372. },
  373. RequiresOpening = true,
  374. RequiresClosing = true,
  375. BufferMs = 0,
  376. Container = "ts",
  377. Id = id,
  378. SupportsDirectPlay = false,
  379. SupportsDirectStream = true,
  380. SupportsTranscoding = true,
  381. IsInfiniteStream = true,
  382. IgnoreDts = true,
  383. SupportsProbing = false,
  384. //AnalyzeDurationMs = 2000000
  385. //IgnoreIndex = true,
  386. //ReadAtNativeFramerate = true
  387. };
  388. mediaSource.InferTotalBitrate();
  389. return mediaSource;
  390. }
  391. protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, string channelId, CancellationToken cancellationToken)
  392. {
  393. var list = new List<MediaSourceInfo>();
  394. if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase))
  395. {
  396. return list;
  397. }
  398. var hdhrId = GetHdHrIdFromChannelId(channelId);
  399. var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false);
  400. var channelInfo = channels.FirstOrDefault(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase));
  401. var hdHomerunChannelInfo = channelInfo as HdHomerunChannelInfo;
  402. var isLegacyTuner = hdHomerunChannelInfo != null && hdHomerunChannelInfo.IsLegacyTuner;
  403. if (isLegacyTuner)
  404. {
  405. list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
  406. }
  407. else
  408. {
  409. try
  410. {
  411. var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
  412. var model = modelInfo == null ? string.Empty : (modelInfo.ModelNumber ?? string.Empty);
  413. if ((model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1))
  414. {
  415. list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
  416. if (info.AllowHWTranscoding)
  417. {
  418. list.Add(GetMediaSource(info, hdhrId, channelInfo, "heavy"));
  419. list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet540"));
  420. list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet480"));
  421. list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet360"));
  422. list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet240"));
  423. list.Add(GetMediaSource(info, hdhrId, channelInfo, "mobile"));
  424. }
  425. }
  426. }
  427. catch
  428. {
  429. }
  430. if (list.Count == 0)
  431. {
  432. list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
  433. }
  434. }
  435. return list;
  436. }
  437. protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
  438. {
  439. var profile = streamId.Split('_')[0];
  440. Logger.Info("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelId, streamId, profile);
  441. var hdhrId = GetHdHrIdFromChannelId(channelId);
  442. var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false);
  443. var channelInfo = channels.FirstOrDefault(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase));
  444. var hdhomerunChannel = channelInfo as HdHomerunChannelInfo;
  445. var mediaSource = GetMediaSource(info, hdhrId, channelInfo, profile);
  446. var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
  447. if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner)
  448. {
  449. return new HdHomerunUdpStream(mediaSource, streamId, new LegacyHdHomerunChannelCommands(hdhomerunChannel.Url), modelInfo.TunerCount, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager, _environment);
  450. }
  451. // The UDP method is not working reliably on OSX, and on BSD it hasn't been tested yet
  452. var enableHttpStream = _environment.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.OSX
  453. || _environment.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.BSD;
  454. enableHttpStream = true;
  455. if (enableHttpStream)
  456. {
  457. mediaSource.Protocol = MediaProtocol.Http;
  458. var httpUrl = GetApiUrl(info, true) + "/auto/v" + hdhrId;
  459. // If raw was used, the tuner doesn't support params
  460. if (!string.IsNullOrWhiteSpace(profile) && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase))
  461. {
  462. httpUrl += "?transcode=" + profile;
  463. }
  464. mediaSource.Path = httpUrl;
  465. return new HdHomerunHttpStream(mediaSource, streamId, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _environment);
  466. }
  467. return new HdHomerunUdpStream(mediaSource, streamId, new HdHomerunChannelCommands(hdhomerunChannel.Number, profile), modelInfo.TunerCount, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager, _environment);
  468. }
  469. public async Task Validate(TunerHostInfo info)
  470. {
  471. lock (_modelCache)
  472. {
  473. _modelCache.Clear();
  474. }
  475. try
  476. {
  477. // Test it by pulling down the lineup
  478. var modelInfo = await GetModelInfo(info, true, CancellationToken.None).ConfigureAwait(false);
  479. info.DeviceId = modelInfo.DeviceID;
  480. }
  481. catch (HttpException ex)
  482. {
  483. if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
  484. {
  485. // HDHR4 doesn't have this api
  486. return;
  487. }
  488. throw;
  489. }
  490. }
  491. protected override async Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
  492. {
  493. var info = await GetTunerInfos(tuner, cancellationToken).ConfigureAwait(false);
  494. return info.Any(i => i.Status == LiveTvTunerStatus.Available);
  495. }
  496. public class DiscoverResponse
  497. {
  498. public string FriendlyName { get; set; }
  499. public string ModelNumber { get; set; }
  500. public string FirmwareName { get; set; }
  501. public string FirmwareVersion { get; set; }
  502. public string DeviceID { get; set; }
  503. public string DeviceAuth { get; set; }
  504. public string BaseURL { get; set; }
  505. public string LineupURL { get; set; }
  506. public int TunerCount { get; set; }
  507. public DateTime DateQueried { get; set; }
  508. public DiscoverResponse()
  509. {
  510. DateQueried = DateTime.UtcNow;
  511. }
  512. }
  513. public async Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken)
  514. {
  515. cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(new CancellationTokenSource(discoveryDurationMs).Token, cancellationToken).Token;
  516. var list = new List<TunerHostInfo>();
  517. // Create udp broadcast discovery message
  518. byte[] discBytes = { 0, 2, 0, 12, 1, 4, 255, 255, 255, 255, 2, 4, 255, 255, 255, 255, 115, 204, 125, 143 };
  519. using (var udpClient = _socketFactory.CreateUdpBroadcastSocket(0))
  520. {
  521. // Need a way to set the Receive timeout on the socket otherwise this might never timeout?
  522. try
  523. {
  524. await udpClient.SendToAsync(discBytes, 0, discBytes.Length, new IpEndPointInfo(new IpAddressInfo("255.255.255.255", IpAddressFamily.InterNetwork), 65001), cancellationToken);
  525. var receiveBuffer = new byte[8192];
  526. while (!cancellationToken.IsCancellationRequested)
  527. {
  528. var response = await udpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false);
  529. var deviceIp = response.RemoteEndPoint.IpAddress.Address;
  530. // check to make sure we have enough bytes received to be a valid message and make sure the 2nd byte is the discover reply byte
  531. if (response.ReceivedBytes > 13 && response.Buffer[1] == 3)
  532. {
  533. var deviceAddress = "http://" + deviceIp;
  534. var info = await TryGetTunerHostInfo(deviceAddress, cancellationToken).ConfigureAwait(false);
  535. if (info != null)
  536. {
  537. list.Add(info);
  538. }
  539. }
  540. }
  541. }
  542. catch (OperationCanceledException)
  543. {
  544. }
  545. catch
  546. {
  547. // Socket timeout indicates all messages have been received.
  548. }
  549. }
  550. return list;
  551. }
  552. private async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken)
  553. {
  554. var hostInfo = new TunerHostInfo
  555. {
  556. Type = Type,
  557. Url = url
  558. };
  559. try
  560. {
  561. var modelInfo = await GetModelInfo(hostInfo, false, cancellationToken).ConfigureAwait(false);
  562. hostInfo.DeviceId = modelInfo.DeviceID;
  563. hostInfo.FriendlyName = modelInfo.FriendlyName;
  564. return hostInfo;
  565. }
  566. catch
  567. {
  568. // logged at lower levels
  569. }
  570. return null;
  571. }
  572. }
  573. }