HdHomerunHost.cs 28 KB

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