HdHomerunHost.cs 28 KB

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