HdHomerunHost.cs 21 KB

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