HdHomerunHost.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  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 IFileSystem _fileSystem;
  30. private readonly IServerApplicationHost _appHost;
  31. private readonly ISocketFactory _socketFactory;
  32. private readonly INetworkManager _networkManager;
  33. private readonly IEnvironmentInfo _environment;
  34. public HdHomerunHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient, IFileSystem fileSystem, IServerApplicationHost appHost, ISocketFactory socketFactory, INetworkManager networkManager, IEnvironmentInfo environment)
  35. : base(config, logger, jsonSerializer, mediaEncoder)
  36. {
  37. _httpClient = httpClient;
  38. _fileSystem = fileSystem;
  39. _appHost = appHost;
  40. _socketFactory = socketFactory;
  41. _networkManager = networkManager;
  42. _environment = environment;
  43. }
  44. public string Name
  45. {
  46. get { return "HD Homerun"; }
  47. }
  48. public override string Type
  49. {
  50. get { return DeviceType; }
  51. }
  52. public static string DeviceType
  53. {
  54. get { return "hdhomerun"; }
  55. }
  56. protected override string ChannelIdPrefix
  57. {
  58. get
  59. {
  60. return "hdhr_";
  61. }
  62. }
  63. private string GetChannelId(TunerHostInfo info, Channels i)
  64. {
  65. var id = ChannelIdPrefix + i.GuideNumber;
  66. id += '_' + (i.GuideName ?? string.Empty).GetMD5().ToString("N");
  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. if (channelInfo != null)
  308. {
  309. if (string.IsNullOrWhiteSpace(videoCodec))
  310. {
  311. videoCodec = channelInfo.VideoCodec;
  312. }
  313. audioCodec = channelInfo.AudioCodec;
  314. if (!videoBitrate.HasValue)
  315. {
  316. videoBitrate = (channelInfo.IsHD ?? true) ? 15000000 : 2000000;
  317. }
  318. audioBitrate = (channelInfo.IsHD ?? true) ? 448000 : 192000;
  319. }
  320. // normalize
  321. if (string.Equals(videoCodec, "mpeg2", StringComparison.OrdinalIgnoreCase))
  322. {
  323. videoCodec = "mpeg2video";
  324. }
  325. string nal = null;
  326. if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
  327. {
  328. nal = "0";
  329. }
  330. var url = GetApiUrl(info, false);
  331. var id = profile;
  332. if (string.IsNullOrWhiteSpace(id))
  333. {
  334. id = "native";
  335. }
  336. id += "_" + channelId.GetMD5().ToString("N") + "_" + url.GetMD5().ToString("N");
  337. var mediaSource = new MediaSourceInfo
  338. {
  339. Path = url,
  340. Protocol = MediaProtocol.Udp,
  341. MediaStreams = new List<MediaStream>
  342. {
  343. new MediaStream
  344. {
  345. Type = MediaStreamType.Video,
  346. // Set the index to -1 because we don't know the exact index of the video stream within the container
  347. Index = -1,
  348. IsInterlaced = isInterlaced,
  349. Codec = videoCodec,
  350. Width = width,
  351. Height = height,
  352. BitRate = videoBitrate,
  353. NalLengthSize = nal
  354. },
  355. new MediaStream
  356. {
  357. Type = MediaStreamType.Audio,
  358. // Set the index to -1 because we don't know the exact index of the audio stream within the container
  359. Index = -1,
  360. Codec = audioCodec,
  361. BitRate = audioBitrate
  362. }
  363. },
  364. RequiresOpening = true,
  365. RequiresClosing = true,
  366. BufferMs = 0,
  367. Container = "ts",
  368. Id = id,
  369. SupportsDirectPlay = false,
  370. SupportsDirectStream = true,
  371. SupportsTranscoding = true,
  372. IsInfiniteStream = true,
  373. IgnoreDts = true,
  374. IgnoreIndex = true,
  375. GenPtsInput = true
  376. };
  377. mediaSource.InferTotalBitrate();
  378. return mediaSource;
  379. }
  380. protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, string channelId, CancellationToken cancellationToken)
  381. {
  382. var list = new List<MediaSourceInfo>();
  383. if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase))
  384. {
  385. return list;
  386. }
  387. var hdhrId = GetHdHrIdFromChannelId(channelId);
  388. var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false);
  389. var channelInfo = channels.FirstOrDefault(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase));
  390. var hdHomerunChannelInfo = channelInfo as HdHomerunChannelInfo;
  391. var isLegacyTuner = hdHomerunChannelInfo != null && hdHomerunChannelInfo.IsLegacyTuner;
  392. if (isLegacyTuner)
  393. {
  394. list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
  395. }
  396. else
  397. {
  398. try
  399. {
  400. var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
  401. var model = modelInfo == null ? string.Empty : (modelInfo.ModelNumber ?? string.Empty);
  402. if ((model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1))
  403. {
  404. list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
  405. if (info.AllowHWTranscoding)
  406. {
  407. list.Add(GetMediaSource(info, hdhrId, channelInfo, "heavy"));
  408. list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet540"));
  409. list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet480"));
  410. list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet360"));
  411. list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet240"));
  412. list.Add(GetMediaSource(info, hdhrId, channelInfo, "mobile"));
  413. }
  414. }
  415. }
  416. catch
  417. {
  418. }
  419. if (list.Count == 0)
  420. {
  421. list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
  422. }
  423. }
  424. return list;
  425. }
  426. protected override async Task<LiveStream> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
  427. {
  428. var profile = streamId.Split('_')[0];
  429. Logger.Info("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelId, streamId, profile);
  430. var hdhrId = GetHdHrIdFromChannelId(channelId);
  431. var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false);
  432. var channelInfo = channels.FirstOrDefault(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase));
  433. var hdhomerunChannel = channelInfo as HdHomerunChannelInfo;
  434. var mediaSource = GetMediaSource(info, hdhrId, channelInfo, profile);
  435. var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
  436. if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner)
  437. {
  438. return new HdHomerunUdpStream(mediaSource, streamId, new LegacyHdHomerunChannelCommands(hdhomerunChannel.Url), modelInfo.TunerCount, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager);
  439. }
  440. // The UDP method is not working reliably on OSX, and on BSD it hasn't been tested yet
  441. var enableHttpStream = _environment.OperatingSystem == OperatingSystem.OSX ||
  442. _environment.OperatingSystem == OperatingSystem.BSD;
  443. enableHttpStream = true;
  444. if (enableHttpStream)
  445. {
  446. mediaSource.Protocol = MediaProtocol.Http;
  447. var httpUrl = GetApiUrl(info, true) + "/auto/v" + hdhrId;
  448. // If raw was used, the tuner doesn't support params
  449. if (!string.IsNullOrWhiteSpace(profile)
  450. && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase))
  451. {
  452. httpUrl += "?transcode=" + profile;
  453. }
  454. mediaSource.Path = httpUrl;
  455. return new HdHomerunHttpStream(mediaSource, streamId, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost);
  456. }
  457. return new HdHomerunUdpStream(mediaSource, streamId, new HdHomerunChannelCommands(hdhomerunChannel.Number), modelInfo.TunerCount, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager);
  458. }
  459. public async Task Validate(TunerHostInfo info)
  460. {
  461. lock (_modelCache)
  462. {
  463. _modelCache.Clear();
  464. }
  465. try
  466. {
  467. // Test it by pulling down the lineup
  468. var modelInfo = await GetModelInfo(info, true, CancellationToken.None).ConfigureAwait(false);
  469. info.DeviceId = modelInfo.DeviceID;
  470. }
  471. catch (HttpException ex)
  472. {
  473. if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
  474. {
  475. // HDHR4 doesn't have this api
  476. return;
  477. }
  478. throw;
  479. }
  480. }
  481. protected override async Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
  482. {
  483. var info = await GetTunerInfos(tuner, cancellationToken).ConfigureAwait(false);
  484. return info.Any(i => i.Status == LiveTvTunerStatus.Available);
  485. }
  486. public class DiscoverResponse
  487. {
  488. public string FriendlyName { get; set; }
  489. public string ModelNumber { get; set; }
  490. public string FirmwareName { get; set; }
  491. public string FirmwareVersion { get; set; }
  492. public string DeviceID { get; set; }
  493. public string DeviceAuth { get; set; }
  494. public string BaseURL { get; set; }
  495. public string LineupURL { get; set; }
  496. public int TunerCount { get; set; }
  497. public DateTime DateQueried { get; set; }
  498. public DiscoverResponse()
  499. {
  500. DateQueried = DateTime.UtcNow;
  501. }
  502. }
  503. public async Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken)
  504. {
  505. cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(new CancellationTokenSource(discoveryDurationMs).Token, cancellationToken).Token;
  506. var list = new List<TunerHostInfo>();
  507. // Create udp broadcast discovery message
  508. byte[] discBytes = { 0, 2, 0, 12, 1, 4, 255, 255, 255, 255, 2, 4, 255, 255, 255, 255, 115, 204, 125, 143 };
  509. using (var udpClient = _socketFactory.CreateUdpBroadcastSocket(0))
  510. {
  511. // Need a way to set the Receive timeout on the socket otherwise this might never timeout?
  512. try
  513. {
  514. await udpClient.SendAsync(discBytes, discBytes.Length, new IpEndPointInfo(new IpAddressInfo("255.255.255.255", IpAddressFamily.InterNetwork), 65001), cancellationToken);
  515. while (!cancellationToken.IsCancellationRequested)
  516. {
  517. var response = await udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false);
  518. var deviceIp = response.RemoteEndPoint.IpAddress.Address;
  519. // 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
  520. if (response.ReceivedBytes > 13 && response.Buffer[1] == 3)
  521. {
  522. var deviceAddress = "http://" + deviceIp;
  523. var info = await TryGetTunerHostInfo(deviceAddress, cancellationToken).ConfigureAwait(false);
  524. if (info != null)
  525. {
  526. list.Add(info);
  527. }
  528. }
  529. }
  530. }
  531. catch (OperationCanceledException)
  532. {
  533. }
  534. catch
  535. {
  536. // Socket timeout indicates all messages have been received.
  537. }
  538. }
  539. return list;
  540. }
  541. private async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken)
  542. {
  543. var hostInfo = new TunerHostInfo
  544. {
  545. Type = Type,
  546. Url = url
  547. };
  548. try
  549. {
  550. var modelInfo = await GetModelInfo(hostInfo, false, cancellationToken).ConfigureAwait(false);
  551. hostInfo.DeviceId = modelInfo.DeviceID;
  552. hostInfo.FriendlyName = modelInfo.FriendlyName;
  553. return hostInfo;
  554. }
  555. catch
  556. {
  557. // logged at lower levels
  558. }
  559. return null;
  560. }
  561. }
  562. }