BaseTunerHost.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. using MediaBrowser.Common.Configuration;
  2. using MediaBrowser.Controller.LiveTv;
  3. using MediaBrowser.Model.Dto;
  4. using MediaBrowser.Model.LiveTv;
  5. using MediaBrowser.Model.Logging;
  6. using System;
  7. using System.Collections.Concurrent;
  8. using System.Collections.Generic;
  9. using System.IO;
  10. using System.Linq;
  11. using System.Threading;
  12. using System.Threading.Tasks;
  13. using MediaBrowser.Controller.MediaEncoding;
  14. using MediaBrowser.Model.Dlna;
  15. using MediaBrowser.Model.Serialization;
  16. namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
  17. {
  18. public abstract class BaseTunerHost
  19. {
  20. protected readonly IConfigurationManager Config;
  21. protected readonly ILogger Logger;
  22. protected IJsonSerializer JsonSerializer;
  23. protected readonly IMediaEncoder MediaEncoder;
  24. private readonly ConcurrentDictionary<string, ChannelCache> _channelCache =
  25. new ConcurrentDictionary<string, ChannelCache>(StringComparer.OrdinalIgnoreCase);
  26. protected BaseTunerHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder)
  27. {
  28. Config = config;
  29. Logger = logger;
  30. JsonSerializer = jsonSerializer;
  31. MediaEncoder = mediaEncoder;
  32. }
  33. protected abstract Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
  34. public abstract string Type { get; }
  35. public async Task<IEnumerable<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
  36. {
  37. ChannelCache cache = null;
  38. var key = tuner.Id;
  39. if (enableCache && !string.IsNullOrWhiteSpace(key) && _channelCache.TryGetValue(key, out cache))
  40. {
  41. if (DateTime.UtcNow - cache.Date < TimeSpan.FromMinutes(60))
  42. {
  43. return cache.Channels.ToList();
  44. }
  45. }
  46. var result = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false);
  47. var list = result.ToList();
  48. Logger.Debug("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list));
  49. if (!string.IsNullOrWhiteSpace(key) && list.Count > 0)
  50. {
  51. cache = cache ?? new ChannelCache();
  52. cache.Date = DateTime.UtcNow;
  53. cache.Channels = list;
  54. _channelCache.AddOrUpdate(key, cache, (k, v) => cache);
  55. }
  56. return list;
  57. }
  58. protected virtual List<TunerHostInfo> GetTunerHosts()
  59. {
  60. return GetConfiguration().TunerHosts
  61. .Where(i => i.IsEnabled && string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))
  62. .ToList();
  63. }
  64. public async Task<IEnumerable<ChannelInfo>> GetChannels(CancellationToken cancellationToken)
  65. {
  66. var list = new List<ChannelInfo>();
  67. var hosts = GetTunerHosts();
  68. foreach (var host in hosts)
  69. {
  70. try
  71. {
  72. var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false);
  73. var newChannels = channels.Where(i => !list.Any(l => string.Equals(i.Id, l.Id, StringComparison.OrdinalIgnoreCase))).ToList();
  74. list.AddRange(newChannels);
  75. }
  76. catch (Exception ex)
  77. {
  78. Logger.ErrorException("Error getting channel list", ex);
  79. }
  80. }
  81. return list;
  82. }
  83. protected abstract Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken);
  84. public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
  85. {
  86. if (IsValidChannelId(channelId))
  87. {
  88. var hosts = GetTunerHosts();
  89. var hostsWithChannel = new List<TunerHostInfo>();
  90. foreach (var host in hosts)
  91. {
  92. try
  93. {
  94. var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false);
  95. if (channels.Any(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)))
  96. {
  97. hostsWithChannel.Add(host);
  98. }
  99. }
  100. catch (Exception ex)
  101. {
  102. Logger.Error("Error getting channels", ex);
  103. }
  104. }
  105. foreach (var host in hostsWithChannel)
  106. {
  107. var resourcePool = GetLock(host.Url);
  108. Logger.Debug("GetChannelStreamMediaSources - Waiting on tuner resource pool");
  109. await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
  110. Logger.Debug("GetChannelStreamMediaSources - Unlocked resource pool");
  111. try
  112. {
  113. // Check to make sure the tuner is available
  114. // If there's only one tuner, don't bother with the check and just let the tuner be the one to throw an error
  115. if (hostsWithChannel.Count > 1 &&
  116. !await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false))
  117. {
  118. Logger.Error("Tuner is not currently available");
  119. continue;
  120. }
  121. var mediaSources = await GetChannelStreamMediaSources(host, channelId, cancellationToken).ConfigureAwait(false);
  122. // Prefix the id with the host Id so that we can easily find it
  123. foreach (var mediaSource in mediaSources)
  124. {
  125. mediaSource.Id = host.Id + mediaSource.Id;
  126. }
  127. return mediaSources;
  128. }
  129. catch (Exception ex)
  130. {
  131. Logger.Error("Error opening tuner", ex);
  132. }
  133. finally
  134. {
  135. resourcePool.Release();
  136. }
  137. }
  138. }
  139. return new List<MediaSourceInfo>();
  140. }
  141. protected abstract Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken);
  142. public async Task<Tuple<MediaSourceInfo, SemaphoreSlim>> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
  143. {
  144. if (IsValidChannelId(channelId))
  145. {
  146. var hosts = GetTunerHosts();
  147. var hostsWithChannel = new List<TunerHostInfo>();
  148. foreach (var host in hosts)
  149. {
  150. if (string.IsNullOrWhiteSpace(streamId))
  151. {
  152. try
  153. {
  154. var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false);
  155. if (channels.Any(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)))
  156. {
  157. hostsWithChannel.Add(host);
  158. }
  159. }
  160. catch (Exception ex)
  161. {
  162. Logger.Error("Error getting channels", ex);
  163. }
  164. }
  165. else if (streamId.StartsWith(host.Id, StringComparison.OrdinalIgnoreCase))
  166. {
  167. hostsWithChannel = new List<TunerHostInfo> {host};
  168. streamId = streamId.Substring(host.Id.Length);
  169. break;
  170. }
  171. }
  172. foreach (var host in hostsWithChannel)
  173. {
  174. var resourcePool = GetLock(host.Url);
  175. Logger.Debug("GetChannelStream - Waiting on tuner resource pool");
  176. await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
  177. Logger.Debug("GetChannelStream - Unlocked resource pool");
  178. try
  179. {
  180. // Check to make sure the tuner is available
  181. // If there's only one tuner, don't bother with the check and just let the tuner be the one to throw an error
  182. // If a streamId is specified then availibility has already been checked in GetChannelStreamMediaSources
  183. if (string.IsNullOrWhiteSpace(streamId) && hostsWithChannel.Count > 1)
  184. {
  185. if (!await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false))
  186. {
  187. Logger.Error("Tuner is not currently available");
  188. resourcePool.Release();
  189. continue;
  190. }
  191. }
  192. var stream =
  193. await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false);
  194. if (EnableMediaProbing)
  195. {
  196. await AddMediaInfo(stream, false, resourcePool, cancellationToken).ConfigureAwait(false);
  197. }
  198. return new Tuple<MediaSourceInfo, SemaphoreSlim>(stream, resourcePool);
  199. }
  200. catch (Exception ex)
  201. {
  202. Logger.Error("Error opening tuner", ex);
  203. resourcePool.Release();
  204. }
  205. }
  206. }
  207. else
  208. {
  209. throw new FileNotFoundException();
  210. }
  211. throw new LiveTvConflictException();
  212. }
  213. protected virtual bool EnableMediaProbing
  214. {
  215. get { return false; }
  216. }
  217. protected async Task<bool> IsAvailable(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
  218. {
  219. try
  220. {
  221. return await IsAvailableInternal(tuner, channelId, cancellationToken).ConfigureAwait(false);
  222. }
  223. catch (Exception ex)
  224. {
  225. Logger.ErrorException("Error checking tuner availability", ex);
  226. return false;
  227. }
  228. }
  229. protected abstract Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken);
  230. /// <summary>
  231. /// The _semaphoreLocks
  232. /// </summary>
  233. private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks = new ConcurrentDictionary<string, SemaphoreSlim>(StringComparer.OrdinalIgnoreCase);
  234. /// <summary>
  235. /// Gets the lock.
  236. /// </summary>
  237. /// <param name="url">The filename.</param>
  238. /// <returns>System.Object.</returns>
  239. private SemaphoreSlim GetLock(string url)
  240. {
  241. return _semaphoreLocks.GetOrAdd(url, key => new SemaphoreSlim(1, 1));
  242. }
  243. private async Task AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
  244. {
  245. await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
  246. try
  247. {
  248. await AddMediaInfoInternal(mediaSource, isAudio, cancellationToken).ConfigureAwait(false);
  249. // Leave the resource locked. it will be released upstream
  250. }
  251. catch (Exception)
  252. {
  253. // Release the resource if there's some kind of failure.
  254. resourcePool.Release();
  255. throw;
  256. }
  257. }
  258. private async Task AddMediaInfoInternal(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken)
  259. {
  260. var originalRuntime = mediaSource.RunTimeTicks;
  261. var info = await MediaEncoder.GetMediaInfo(new MediaInfoRequest
  262. {
  263. InputPath = mediaSource.Path,
  264. Protocol = mediaSource.Protocol,
  265. MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
  266. ExtractChapters = false
  267. }, cancellationToken).ConfigureAwait(false);
  268. mediaSource.Bitrate = info.Bitrate;
  269. mediaSource.Container = info.Container;
  270. mediaSource.Formats = info.Formats;
  271. mediaSource.MediaStreams = info.MediaStreams;
  272. mediaSource.RunTimeTicks = info.RunTimeTicks;
  273. mediaSource.Size = info.Size;
  274. mediaSource.Timestamp = info.Timestamp;
  275. mediaSource.Video3DFormat = info.Video3DFormat;
  276. mediaSource.VideoType = info.VideoType;
  277. mediaSource.DefaultSubtitleStreamIndex = null;
  278. // Null this out so that it will be treated like a live stream
  279. if (!originalRuntime.HasValue)
  280. {
  281. mediaSource.RunTimeTicks = null;
  282. }
  283. var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Audio);
  284. if (audioStream == null || audioStream.Index == -1)
  285. {
  286. mediaSource.DefaultAudioStreamIndex = null;
  287. }
  288. else
  289. {
  290. mediaSource.DefaultAudioStreamIndex = audioStream.Index;
  291. }
  292. var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Video);
  293. if (videoStream != null)
  294. {
  295. if (!videoStream.BitRate.HasValue)
  296. {
  297. var width = videoStream.Width ?? 1920;
  298. if (width >= 1900)
  299. {
  300. videoStream.BitRate = 8000000;
  301. }
  302. else if (width >= 1260)
  303. {
  304. videoStream.BitRate = 3000000;
  305. }
  306. else if (width >= 700)
  307. {
  308. videoStream.BitRate = 1000000;
  309. }
  310. }
  311. }
  312. // Try to estimate this
  313. if (!mediaSource.Bitrate.HasValue)
  314. {
  315. var total = mediaSource.MediaStreams.Select(i => i.BitRate ?? 0).Sum();
  316. if (total > 0)
  317. {
  318. mediaSource.Bitrate = total;
  319. }
  320. }
  321. }
  322. protected abstract bool IsValidChannelId(string channelId);
  323. protected LiveTvOptions GetConfiguration()
  324. {
  325. return Config.GetConfiguration<LiveTvOptions>("livetv");
  326. }
  327. private class ChannelCache
  328. {
  329. public DateTime Date;
  330. public List<ChannelInfo> Channels;
  331. }
  332. }
  333. }