HdHomerunHost.cs 27 KB

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