HdHomerunHost.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. using MediaBrowser.Common.Configuration;
  2. using MediaBrowser.Common.Net;
  3. using MediaBrowser.Controller.LiveTv;
  4. using MediaBrowser.Model.Dto;
  5. using MediaBrowser.Model.Entities;
  6. using MediaBrowser.Model.LiveTv;
  7. using MediaBrowser.Model.Logging;
  8. using MediaBrowser.Model.MediaInfo;
  9. using MediaBrowser.Model.Serialization;
  10. using System;
  11. using System.Collections.Generic;
  12. using System.IO;
  13. using System.Linq;
  14. using System.Threading;
  15. using System.Threading.Tasks;
  16. using MediaBrowser.Model.IO;
  17. using MediaBrowser.Common.Extensions;
  18. using MediaBrowser.Controller;
  19. using MediaBrowser.Controller.Configuration;
  20. using MediaBrowser.Controller.MediaEncoding;
  21. using MediaBrowser.Model.Configuration;
  22. using MediaBrowser.Model.Net;
  23. namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
  24. {
  25. public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
  26. {
  27. private readonly IHttpClient _httpClient;
  28. private readonly IFileSystem _fileSystem;
  29. private readonly IServerApplicationHost _appHost;
  30. private readonly ISocketFactory _socketFactory;
  31. public HdHomerunHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient, IFileSystem fileSystem, IServerApplicationHost appHost, ISocketFactory socketFactory)
  32. : base(config, logger, jsonSerializer, mediaEncoder)
  33. {
  34. _httpClient = httpClient;
  35. _fileSystem = fileSystem;
  36. _appHost = appHost;
  37. _socketFactory = socketFactory;
  38. }
  39. public string Name
  40. {
  41. get { return "HD Homerun"; }
  42. }
  43. public override string Type
  44. {
  45. get { return DeviceType; }
  46. }
  47. public static string DeviceType
  48. {
  49. get { return "hdhomerun"; }
  50. }
  51. private const string ChannelIdPrefix = "hdhr_";
  52. private string GetChannelId(TunerHostInfo info, Channels i)
  53. {
  54. var id = ChannelIdPrefix + i.GuideNumber;
  55. id += '_' + (i.GuideName ?? string.Empty).GetMD5().ToString("N");
  56. return id;
  57. }
  58. private async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
  59. {
  60. var options = new HttpRequestOptions
  61. {
  62. Url = string.Format("{0}/lineup.json", GetApiUrl(info, false)),
  63. CancellationToken = cancellationToken,
  64. BufferContent = false
  65. };
  66. using (var stream = await _httpClient.Get(options).ConfigureAwait(false))
  67. {
  68. var lineup = JsonSerializer.DeserializeFromStream<List<Channels>>(stream) ?? new List<Channels>();
  69. if (info.ImportFavoritesOnly)
  70. {
  71. lineup = lineup.Where(i => i.Favorite).ToList();
  72. }
  73. return lineup.Where(i => !i.DRM).ToList();
  74. }
  75. }
  76. private class HdHomerunChannelInfo : ChannelInfo
  77. {
  78. public string Url { 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. Url = info.Url
  95. }).Cast<ChannelInfo>().ToList();
  96. }
  97. private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
  98. private async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken)
  99. {
  100. lock (_modelCache)
  101. {
  102. DiscoverResponse response;
  103. if (_modelCache.TryGetValue(info.Url, out response))
  104. {
  105. return response;
  106. }
  107. }
  108. try
  109. {
  110. using (var stream = await _httpClient.Get(new HttpRequestOptions()
  111. {
  112. Url = string.Format("{0}/discover.json", GetApiUrl(info, false)),
  113. CancellationToken = cancellationToken,
  114. CacheLength = TimeSpan.FromDays(1),
  115. CacheMode = CacheMode.Unconditional,
  116. TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds),
  117. BufferContent = false
  118. }).ConfigureAwait(false))
  119. {
  120. var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream);
  121. lock (_modelCache)
  122. {
  123. _modelCache[info.Id] = response;
  124. }
  125. return response;
  126. }
  127. }
  128. catch (HttpException ex)
  129. {
  130. if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
  131. {
  132. var defaultValue = "HDHR";
  133. var response = new DiscoverResponse
  134. {
  135. ModelNumber = defaultValue
  136. };
  137. // HDHR4 doesn't have this api
  138. lock (_modelCache)
  139. {
  140. _modelCache[info.Id] = response;
  141. }
  142. return response;
  143. }
  144. throw;
  145. }
  146. }
  147. private async Task<List<LiveTvTunerInfo>> GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken)
  148. {
  149. var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
  150. var tuners = new List<LiveTvTunerInfo>();
  151. var uri = new Uri(info.Url);
  152. using (var manager = new HdHomerunManager(_socketFactory))
  153. {
  154. // Legacy HdHomeruns are IPv4 only
  155. var ipInfo = new IpAddressInfo(uri.Host, IpAddressFamily.InterNetwork);
  156. for (int i = 0; i < model.TunerCount; ++i)
  157. {
  158. var name = String.Format("Tuner {0}", i + 1);
  159. var currentChannel = "none"; /// @todo Get current channel and map back to Station Id
  160. var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false);
  161. LiveTvTunerStatus status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv;
  162. tuners.Add(new LiveTvTunerInfo
  163. {
  164. Name = name,
  165. SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
  166. ProgramName = currentChannel,
  167. Status = status
  168. });
  169. }
  170. }
  171. return tuners;
  172. }
  173. public async Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
  174. {
  175. var list = new List<LiveTvTunerInfo>();
  176. foreach (var host in GetConfiguration().TunerHosts
  177. .Where(i => i.IsEnabled && string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase)))
  178. {
  179. try
  180. {
  181. list.AddRange(await GetTunerInfos(host, cancellationToken).ConfigureAwait(false));
  182. }
  183. catch (Exception ex)
  184. {
  185. Logger.ErrorException("Error getting tuner info", ex);
  186. }
  187. }
  188. return list;
  189. }
  190. private string GetApiUrl(TunerHostInfo info, bool isPlayback)
  191. {
  192. var url = info.Url;
  193. if (string.IsNullOrWhiteSpace(url))
  194. {
  195. throw new ArgumentException("Invalid tuner info");
  196. }
  197. if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
  198. {
  199. url = "http://" + url;
  200. }
  201. var uri = new Uri(url);
  202. if (isPlayback)
  203. {
  204. var builder = new UriBuilder(uri);
  205. builder.Port = 5004;
  206. uri = builder.Uri;
  207. }
  208. return uri.AbsoluteUri.TrimEnd('/');
  209. }
  210. private class Channels
  211. {
  212. public string GuideNumber { get; set; }
  213. public string GuideName { get; set; }
  214. public string VideoCodec { get; set; }
  215. public string AudioCodec { get; set; }
  216. public string URL { get; set; }
  217. public bool Favorite { get; set; }
  218. public bool DRM { get; set; }
  219. public int HD { get; set; }
  220. }
  221. private MediaSourceInfo GetMediaSource(TunerHostInfo info, string channelId, ChannelInfo channelInfo, string profile)
  222. {
  223. int? width = null;
  224. int? height = null;
  225. bool isInterlaced = true;
  226. string videoCodec = null;
  227. string audioCodec = null;
  228. int? videoBitrate = null;
  229. int? audioBitrate = null;
  230. if (string.Equals(profile, "mobile", StringComparison.OrdinalIgnoreCase))
  231. {
  232. width = 1280;
  233. height = 720;
  234. isInterlaced = false;
  235. videoCodec = "h264";
  236. videoBitrate = 2000000;
  237. }
  238. else if (string.Equals(profile, "heavy", StringComparison.OrdinalIgnoreCase))
  239. {
  240. width = 1920;
  241. height = 1080;
  242. isInterlaced = false;
  243. videoCodec = "h264";
  244. videoBitrate = 15000000;
  245. }
  246. else if (string.Equals(profile, "internet540", StringComparison.OrdinalIgnoreCase))
  247. {
  248. width = 960;
  249. height = 546;
  250. isInterlaced = false;
  251. videoCodec = "h264";
  252. videoBitrate = 2500000;
  253. }
  254. else if (string.Equals(profile, "internet480", StringComparison.OrdinalIgnoreCase))
  255. {
  256. width = 848;
  257. height = 480;
  258. isInterlaced = false;
  259. videoCodec = "h264";
  260. videoBitrate = 2000000;
  261. }
  262. else if (string.Equals(profile, "internet360", StringComparison.OrdinalIgnoreCase))
  263. {
  264. width = 640;
  265. height = 360;
  266. isInterlaced = false;
  267. videoCodec = "h264";
  268. videoBitrate = 1500000;
  269. }
  270. else if (string.Equals(profile, "internet240", StringComparison.OrdinalIgnoreCase))
  271. {
  272. width = 432;
  273. height = 240;
  274. isInterlaced = false;
  275. videoCodec = "h264";
  276. videoBitrate = 1000000;
  277. }
  278. if (channelInfo != null)
  279. {
  280. if (string.IsNullOrWhiteSpace(videoCodec))
  281. {
  282. videoCodec = channelInfo.VideoCodec;
  283. }
  284. audioCodec = channelInfo.AudioCodec;
  285. if (!videoBitrate.HasValue)
  286. {
  287. videoBitrate = (channelInfo.IsHD ?? true) ? 15000000 : 2000000;
  288. }
  289. audioBitrate = (channelInfo.IsHD ?? true) ? 448000 : 192000;
  290. }
  291. // normalize
  292. if (string.Equals(videoCodec, "mpeg2", StringComparison.OrdinalIgnoreCase))
  293. {
  294. videoCodec = "mpeg2video";
  295. }
  296. string nal = null;
  297. if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
  298. {
  299. nal = "0";
  300. }
  301. var url = GetApiUrl(info, true) + "/auto/v" + channelId;
  302. // If raw was used, the tuner doesn't support params
  303. if (!string.IsNullOrWhiteSpace(profile)
  304. && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase))
  305. {
  306. url += "?transcode=" + profile;
  307. }
  308. var id = profile;
  309. if (string.IsNullOrWhiteSpace(id))
  310. {
  311. id = "native";
  312. }
  313. id += "_" + url.GetMD5().ToString("N");
  314. var mediaSource = new MediaSourceInfo
  315. {
  316. Path = url,
  317. Protocol = MediaProtocol.Http,
  318. MediaStreams = new List<MediaStream>
  319. {
  320. new MediaStream
  321. {
  322. Type = MediaStreamType.Video,
  323. // Set the index to -1 because we don't know the exact index of the video stream within the container
  324. Index = -1,
  325. IsInterlaced = isInterlaced,
  326. Codec = videoCodec,
  327. Width = width,
  328. Height = height,
  329. BitRate = videoBitrate,
  330. NalLengthSize = nal
  331. },
  332. new MediaStream
  333. {
  334. Type = MediaStreamType.Audio,
  335. // Set the index to -1 because we don't know the exact index of the audio stream within the container
  336. Index = -1,
  337. Codec = audioCodec,
  338. BitRate = audioBitrate
  339. }
  340. },
  341. RequiresOpening = true,
  342. RequiresClosing = false,
  343. BufferMs = 0,
  344. Container = "ts",
  345. Id = id,
  346. SupportsDirectPlay = false,
  347. SupportsDirectStream = true,
  348. SupportsTranscoding = true,
  349. IsInfiniteStream = true
  350. };
  351. mediaSource.InferTotalBitrate();
  352. return mediaSource;
  353. }
  354. protected EncodingOptions GetEncodingOptions()
  355. {
  356. return Config.GetConfiguration<EncodingOptions>("encoding");
  357. }
  358. private string GetHdHrIdFromChannelId(string channelId)
  359. {
  360. return channelId.Split('_')[1];
  361. }
  362. private MediaSourceInfo GetLegacyMediaSource(TunerHostInfo info, string channelId, ChannelInfo channel)
  363. {
  364. int? width = null;
  365. int? height = null;
  366. bool isInterlaced = true;
  367. string videoCodec = null;
  368. string audioCodec = null;
  369. int? videoBitrate = null;
  370. int? audioBitrate = null;
  371. if (channel != null)
  372. {
  373. if (string.IsNullOrWhiteSpace(videoCodec))
  374. {
  375. videoCodec = channel.VideoCodec;
  376. }
  377. audioCodec = channel.AudioCodec;
  378. }
  379. // normalize
  380. if (string.Equals(videoCodec, "mpeg2", StringComparison.OrdinalIgnoreCase))
  381. {
  382. videoCodec = "mpeg2video";
  383. }
  384. string nal = null;
  385. var url = info.Url;
  386. var id = channelId;
  387. id += "_" + url.GetMD5().ToString("N");
  388. var mediaSource = new MediaSourceInfo
  389. {
  390. Path = url,
  391. Protocol = MediaProtocol.Udp,
  392. MediaStreams = new List<MediaStream>
  393. {
  394. new MediaStream
  395. {
  396. Type = MediaStreamType.Video,
  397. // Set the index to -1 because we don't know the exact index of the video stream within the container
  398. Index = -1,
  399. IsInterlaced = isInterlaced,
  400. Codec = videoCodec,
  401. Width = width,
  402. Height = height,
  403. BitRate = videoBitrate,
  404. NalLengthSize = nal
  405. },
  406. new MediaStream
  407. {
  408. Type = MediaStreamType.Audio,
  409. // Set the index to -1 because we don't know the exact index of the audio stream within the container
  410. Index = -1,
  411. Codec = audioCodec,
  412. BitRate = audioBitrate
  413. }
  414. },
  415. RequiresOpening = true,
  416. RequiresClosing = true,
  417. BufferMs = 0,
  418. Container = "ts",
  419. Id = id,
  420. SupportsDirectPlay = false,
  421. SupportsDirectStream = true,
  422. SupportsTranscoding = true,
  423. IsInfiniteStream = true
  424. };
  425. mediaSource.InferTotalBitrate();
  426. return mediaSource;
  427. }
  428. protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, string channelId, CancellationToken cancellationToken)
  429. {
  430. var list = new List<MediaSourceInfo>();
  431. if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase))
  432. {
  433. return list;
  434. }
  435. var hdhrId = GetHdHrIdFromChannelId(channelId);
  436. var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false);
  437. var channelInfo = channels.FirstOrDefault(i => string.Equals(i.Number, channelId, StringComparison.OrdinalIgnoreCase));
  438. var hdHomerunChannelInfo = channelInfo as HdHomerunChannelInfo;
  439. var isLegacyTuner = hdHomerunChannelInfo != null && hdHomerunChannelInfo.Url.StartsWith("hdhomerun:", StringComparison.OrdinalIgnoreCase);
  440. if (isLegacyTuner)
  441. {
  442. list.Add(GetLegacyMediaSource(info, hdhrId, channelInfo));
  443. }
  444. else
  445. {
  446. try
  447. {
  448. var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
  449. var model = modelInfo == null ? string.Empty : (modelInfo.ModelNumber ?? string.Empty);
  450. if ((model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1))
  451. {
  452. list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
  453. if (info.AllowHWTranscoding)
  454. {
  455. list.Add(GetMediaSource(info, hdhrId, channelInfo, "heavy"));
  456. list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet540"));
  457. list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet480"));
  458. list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet360"));
  459. list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet240"));
  460. list.Add(GetMediaSource(info, hdhrId, channelInfo, "mobile"));
  461. }
  462. }
  463. }
  464. catch
  465. {
  466. }
  467. if (list.Count == 0)
  468. {
  469. list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
  470. }
  471. }
  472. return list;
  473. }
  474. protected override bool IsValidChannelId(string channelId)
  475. {
  476. if (string.IsNullOrWhiteSpace(channelId))
  477. {
  478. throw new ArgumentNullException("channelId");
  479. }
  480. return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
  481. }
  482. protected override async Task<LiveStream> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
  483. {
  484. var profile = streamId.Split('_')[0];
  485. Logger.Info("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelId, streamId, profile);
  486. if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase))
  487. {
  488. throw new ArgumentException("Channel not found");
  489. }
  490. var hdhrId = GetHdHrIdFromChannelId(channelId);
  491. var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false);
  492. var channelInfo = channels.FirstOrDefault(i => string.Equals(i.Number, channelId, StringComparison.OrdinalIgnoreCase));
  493. var mediaSource = GetMediaSource(info, hdhrId, channelInfo, profile);
  494. var liveStream = new HdHomerunLiveStream(mediaSource, streamId, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost);
  495. liveStream.EnableStreamSharing = true;
  496. return liveStream;
  497. }
  498. public async Task Validate(TunerHostInfo info)
  499. {
  500. if (!info.IsEnabled)
  501. {
  502. return;
  503. }
  504. lock (_modelCache)
  505. {
  506. _modelCache.Clear();
  507. }
  508. try
  509. {
  510. // Test it by pulling down the lineup
  511. var modelInfo = await GetModelInfo(info, true, CancellationToken.None).ConfigureAwait(false);
  512. info.DeviceId = modelInfo.DeviceID;
  513. }
  514. catch (HttpException ex)
  515. {
  516. if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
  517. {
  518. // HDHR4 doesn't have this api
  519. return;
  520. }
  521. throw;
  522. }
  523. }
  524. protected override async Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
  525. {
  526. var info = await GetTunerInfos(tuner, cancellationToken).ConfigureAwait(false);
  527. return info.Any(i => i.Status == LiveTvTunerStatus.Available);
  528. }
  529. public class DiscoverResponse
  530. {
  531. public string FriendlyName { get; set; }
  532. public string ModelNumber { get; set; }
  533. public string FirmwareName { get; set; }
  534. public string FirmwareVersion { get; set; }
  535. public string DeviceID { get; set; }
  536. public string DeviceAuth { get; set; }
  537. public string BaseURL { get; set; }
  538. public string LineupURL { get; set; }
  539. public int TunerCount { get; set; }
  540. }
  541. }
  542. }