HdHomerunHost.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673
  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. var isHd = channelInfo.IsHD ?? true;
  260. if (string.Equals(profile, "mobile", StringComparison.OrdinalIgnoreCase))
  261. {
  262. width = 1280;
  263. height = 720;
  264. isInterlaced = false;
  265. videoCodec = "h264";
  266. videoBitrate = 2000000;
  267. }
  268. else if (string.Equals(profile, "heavy", StringComparison.OrdinalIgnoreCase))
  269. {
  270. width = 1920;
  271. height = 1080;
  272. isInterlaced = false;
  273. videoCodec = "h264";
  274. videoBitrate = 15000000;
  275. }
  276. else if (string.Equals(profile, "internet540", StringComparison.OrdinalIgnoreCase))
  277. {
  278. width = 960;
  279. height = 546;
  280. isInterlaced = false;
  281. videoCodec = "h264";
  282. videoBitrate = 2500000;
  283. }
  284. else if (string.Equals(profile, "internet480", StringComparison.OrdinalIgnoreCase))
  285. {
  286. width = 848;
  287. height = 480;
  288. isInterlaced = false;
  289. videoCodec = "h264";
  290. videoBitrate = 2000000;
  291. }
  292. else if (string.Equals(profile, "internet360", StringComparison.OrdinalIgnoreCase))
  293. {
  294. width = 640;
  295. height = 360;
  296. isInterlaced = false;
  297. videoCodec = "h264";
  298. videoBitrate = 1500000;
  299. }
  300. else if (string.Equals(profile, "internet240", StringComparison.OrdinalIgnoreCase))
  301. {
  302. width = 432;
  303. height = 240;
  304. isInterlaced = false;
  305. videoCodec = "h264";
  306. videoBitrate = 1000000;
  307. }
  308. else
  309. {
  310. // 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
  311. if (isHd)
  312. {
  313. width = 1920;
  314. height = 1080;
  315. }
  316. }
  317. if (channelInfo != null)
  318. {
  319. if (string.IsNullOrWhiteSpace(videoCodec))
  320. {
  321. videoCodec = channelInfo.VideoCodec;
  322. }
  323. audioCodec = channelInfo.AudioCodec;
  324. if (!videoBitrate.HasValue)
  325. {
  326. videoBitrate = isHd ? 15000000 : 2000000;
  327. }
  328. audioBitrate = isHd ? 448000 : 192000;
  329. }
  330. // normalize
  331. if (string.Equals(videoCodec, "mpeg2", StringComparison.OrdinalIgnoreCase))
  332. {
  333. videoCodec = "mpeg2video";
  334. }
  335. string nal = null;
  336. if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
  337. {
  338. nal = "0";
  339. }
  340. var url = GetApiUrl(info, false);
  341. var id = profile;
  342. if (string.IsNullOrWhiteSpace(id))
  343. {
  344. id = "native";
  345. }
  346. id += "_" + channelId.GetMD5().ToString("N") + "_" + url.GetMD5().ToString("N");
  347. var mediaSource = new MediaSourceInfo
  348. {
  349. Path = url,
  350. Protocol = MediaProtocol.Udp,
  351. MediaStreams = new List<MediaStream>
  352. {
  353. new MediaStream
  354. {
  355. Type = MediaStreamType.Video,
  356. // Set the index to -1 because we don't know the exact index of the video stream within the container
  357. Index = -1,
  358. IsInterlaced = isInterlaced,
  359. Codec = videoCodec,
  360. Width = width,
  361. Height = height,
  362. BitRate = videoBitrate,
  363. NalLengthSize = nal
  364. },
  365. new MediaStream
  366. {
  367. Type = MediaStreamType.Audio,
  368. // Set the index to -1 because we don't know the exact index of the audio stream within the container
  369. Index = -1,
  370. Codec = audioCodec,
  371. BitRate = audioBitrate
  372. }
  373. },
  374. RequiresOpening = true,
  375. RequiresClosing = true,
  376. BufferMs = 0,
  377. Container = "ts",
  378. Id = id,
  379. SupportsDirectPlay = false,
  380. SupportsDirectStream = true,
  381. SupportsTranscoding = true,
  382. IsInfiniteStream = true,
  383. IgnoreDts = true,
  384. SupportsProbing = false,
  385. //AnalyzeDurationMs = 2000000
  386. //IgnoreIndex = true,
  387. //ReadAtNativeFramerate = true
  388. };
  389. mediaSource.InferTotalBitrate();
  390. return mediaSource;
  391. }
  392. protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, string channelId, CancellationToken cancellationToken)
  393. {
  394. var list = new List<MediaSourceInfo>();
  395. if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase))
  396. {
  397. return list;
  398. }
  399. var hdhrId = GetHdHrIdFromChannelId(channelId);
  400. var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false);
  401. var channelInfo = channels.FirstOrDefault(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase));
  402. var hdHomerunChannelInfo = channelInfo as HdHomerunChannelInfo;
  403. var isLegacyTuner = hdHomerunChannelInfo != null && hdHomerunChannelInfo.IsLegacyTuner;
  404. if (isLegacyTuner)
  405. {
  406. list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
  407. }
  408. else
  409. {
  410. try
  411. {
  412. var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
  413. var model = modelInfo == null ? string.Empty : (modelInfo.ModelNumber ?? string.Empty);
  414. if ((model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1))
  415. {
  416. list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
  417. if (info.AllowHWTranscoding)
  418. {
  419. list.Add(GetMediaSource(info, hdhrId, channelInfo, "heavy"));
  420. list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet540"));
  421. list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet480"));
  422. list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet360"));
  423. list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet240"));
  424. list.Add(GetMediaSource(info, hdhrId, channelInfo, "mobile"));
  425. }
  426. }
  427. }
  428. catch
  429. {
  430. }
  431. if (list.Count == 0)
  432. {
  433. list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
  434. }
  435. }
  436. return list;
  437. }
  438. protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
  439. {
  440. var profile = streamId.Split('_')[0];
  441. Logger.Info("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelId, streamId, profile);
  442. var hdhrId = GetHdHrIdFromChannelId(channelId);
  443. var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false);
  444. var channelInfo = channels.FirstOrDefault(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase));
  445. var hdhomerunChannel = channelInfo as HdHomerunChannelInfo;
  446. var mediaSource = GetMediaSource(info, hdhrId, channelInfo, profile);
  447. var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
  448. if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner)
  449. {
  450. return new HdHomerunUdpStream(mediaSource, streamId, new LegacyHdHomerunChannelCommands(hdhomerunChannel.Url), modelInfo.TunerCount, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager, _environment);
  451. }
  452. // The UDP method is not working reliably on OSX, and on BSD it hasn't been tested yet
  453. var enableHttpStream = _environment.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.OSX
  454. || _environment.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.BSD;
  455. enableHttpStream = true;
  456. if (enableHttpStream)
  457. {
  458. mediaSource.Protocol = MediaProtocol.Http;
  459. var httpUrl = GetApiUrl(info, true) + "/auto/v" + hdhrId;
  460. // If raw was used, the tuner doesn't support params
  461. if (!string.IsNullOrWhiteSpace(profile) && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase))
  462. {
  463. httpUrl += "?transcode=" + profile;
  464. }
  465. mediaSource.Path = httpUrl;
  466. return new HdHomerunHttpStream(mediaSource, streamId, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _environment);
  467. }
  468. return new HdHomerunUdpStream(mediaSource, streamId, new HdHomerunChannelCommands(hdhomerunChannel.Number, profile), modelInfo.TunerCount, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager, _environment);
  469. }
  470. public async Task Validate(TunerHostInfo info)
  471. {
  472. lock (_modelCache)
  473. {
  474. _modelCache.Clear();
  475. }
  476. try
  477. {
  478. // Test it by pulling down the lineup
  479. var modelInfo = await GetModelInfo(info, true, CancellationToken.None).ConfigureAwait(false);
  480. info.DeviceId = modelInfo.DeviceID;
  481. }
  482. catch (HttpException ex)
  483. {
  484. if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
  485. {
  486. // HDHR4 doesn't have this api
  487. return;
  488. }
  489. throw;
  490. }
  491. }
  492. protected override async Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
  493. {
  494. var info = await GetTunerInfos(tuner, cancellationToken).ConfigureAwait(false);
  495. return info.Any(i => i.Status == LiveTvTunerStatus.Available);
  496. }
  497. public class DiscoverResponse
  498. {
  499. public string FriendlyName { get; set; }
  500. public string ModelNumber { get; set; }
  501. public string FirmwareName { get; set; }
  502. public string FirmwareVersion { get; set; }
  503. public string DeviceID { get; set; }
  504. public string DeviceAuth { get; set; }
  505. public string BaseURL { get; set; }
  506. public string LineupURL { get; set; }
  507. public int TunerCount { get; set; }
  508. public DateTime DateQueried { get; set; }
  509. public DiscoverResponse()
  510. {
  511. DateQueried = DateTime.UtcNow;
  512. }
  513. }
  514. public async Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken)
  515. {
  516. cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(new CancellationTokenSource(discoveryDurationMs).Token, cancellationToken).Token;
  517. var list = new List<TunerHostInfo>();
  518. // Create udp broadcast discovery message
  519. byte[] discBytes = { 0, 2, 0, 12, 1, 4, 255, 255, 255, 255, 2, 4, 255, 255, 255, 255, 115, 204, 125, 143 };
  520. using (var udpClient = _socketFactory.CreateUdpBroadcastSocket(0))
  521. {
  522. // Need a way to set the Receive timeout on the socket otherwise this might never timeout?
  523. try
  524. {
  525. await udpClient.SendToAsync(discBytes, 0, discBytes.Length, new IpEndPointInfo(new IpAddressInfo("255.255.255.255", IpAddressFamily.InterNetwork), 65001), cancellationToken);
  526. var receiveBuffer = new byte[8192];
  527. while (!cancellationToken.IsCancellationRequested)
  528. {
  529. var response = await udpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false);
  530. var deviceIp = response.RemoteEndPoint.IpAddress.Address;
  531. // 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
  532. if (response.ReceivedBytes > 13 && response.Buffer[1] == 3)
  533. {
  534. var deviceAddress = "http://" + deviceIp;
  535. var info = await TryGetTunerHostInfo(deviceAddress, cancellationToken).ConfigureAwait(false);
  536. if (info != null)
  537. {
  538. list.Add(info);
  539. }
  540. }
  541. }
  542. }
  543. catch (OperationCanceledException)
  544. {
  545. }
  546. catch
  547. {
  548. // Socket timeout indicates all messages have been received.
  549. }
  550. }
  551. return list;
  552. }
  553. private async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken)
  554. {
  555. var hostInfo = new TunerHostInfo
  556. {
  557. Type = Type,
  558. Url = url
  559. };
  560. try
  561. {
  562. var modelInfo = await GetModelInfo(hostInfo, false, cancellationToken).ConfigureAwait(false);
  563. hostInfo.DeviceId = modelInfo.DeviceID;
  564. hostInfo.FriendlyName = modelInfo.FriendlyName;
  565. return hostInfo;
  566. }
  567. catch
  568. {
  569. // logged at lower levels
  570. }
  571. return null;
  572. }
  573. }
  574. }