HdHomerunHost.cs 27 KB

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