BaseProgressiveStreamingService.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.IO;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. using MediaBrowser.Common.Net;
  8. using MediaBrowser.Controller.Configuration;
  9. using MediaBrowser.Controller.Devices;
  10. using MediaBrowser.Controller.Dlna;
  11. using MediaBrowser.Controller.Library;
  12. using MediaBrowser.Controller.MediaEncoding;
  13. using MediaBrowser.Controller.Net;
  14. using MediaBrowser.Model.IO;
  15. using MediaBrowser.Model.MediaInfo;
  16. using MediaBrowser.Model.Serialization;
  17. using MediaBrowser.Model.Services;
  18. using Microsoft.Extensions.Logging;
  19. using Microsoft.Net.Http.Headers;
  20. namespace MediaBrowser.Api.Playback.Progressive
  21. {
  22. /// <summary>
  23. /// Class BaseProgressiveStreamingService
  24. /// </summary>
  25. public abstract class BaseProgressiveStreamingService : BaseStreamingService
  26. {
  27. protected IHttpClient HttpClient { get; private set; }
  28. public BaseProgressiveStreamingService(
  29. ILogger<BaseProgressiveStreamingService> logger,
  30. IServerConfigurationManager serverConfigurationManager,
  31. IHttpResultFactory httpResultFactory,
  32. IHttpClient httpClient,
  33. IUserManager userManager,
  34. ILibraryManager libraryManager,
  35. IIsoManager isoManager,
  36. IMediaEncoder mediaEncoder,
  37. IFileSystem fileSystem,
  38. IDlnaManager dlnaManager,
  39. IDeviceManager deviceManager,
  40. IMediaSourceManager mediaSourceManager,
  41. IJsonSerializer jsonSerializer,
  42. IAuthorizationContext authorizationContext,
  43. EncodingHelper encodingHelper)
  44. : base(
  45. logger,
  46. serverConfigurationManager,
  47. httpResultFactory,
  48. userManager,
  49. libraryManager,
  50. isoManager,
  51. mediaEncoder,
  52. fileSystem,
  53. dlnaManager,
  54. deviceManager,
  55. mediaSourceManager,
  56. jsonSerializer,
  57. authorizationContext,
  58. encodingHelper)
  59. {
  60. HttpClient = httpClient;
  61. }
  62. /// <summary>
  63. /// Gets the output file extension.
  64. /// </summary>
  65. /// <param name="state">The state.</param>
  66. /// <returns>System.String.</returns>
  67. protected override string GetOutputFileExtension(StreamState state)
  68. {
  69. var ext = base.GetOutputFileExtension(state);
  70. if (!string.IsNullOrEmpty(ext))
  71. {
  72. return ext;
  73. }
  74. var isVideoRequest = state.VideoRequest != null;
  75. // Try to infer based on the desired video codec
  76. if (isVideoRequest)
  77. {
  78. var videoCodec = state.VideoRequest.VideoCodec;
  79. if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
  80. string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase))
  81. {
  82. return ".ts";
  83. }
  84. if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
  85. {
  86. return ".ogv";
  87. }
  88. if (string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
  89. {
  90. return ".webm";
  91. }
  92. if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
  93. {
  94. return ".asf";
  95. }
  96. }
  97. // Try to infer based on the desired audio codec
  98. if (!isVideoRequest)
  99. {
  100. var audioCodec = state.Request.AudioCodec;
  101. if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
  102. {
  103. return ".aac";
  104. }
  105. if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase))
  106. {
  107. return ".mp3";
  108. }
  109. if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase))
  110. {
  111. return ".ogg";
  112. }
  113. if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase))
  114. {
  115. return ".wma";
  116. }
  117. }
  118. return null;
  119. }
  120. /// <summary>
  121. /// Gets the type of the transcoding job.
  122. /// </summary>
  123. /// <value>The type of the transcoding job.</value>
  124. protected override TranscodingJobType TranscodingJobType => TranscodingJobType.Progressive;
  125. /// <summary>
  126. /// Processes the request.
  127. /// </summary>
  128. /// <param name="request">The request.</param>
  129. /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
  130. /// <returns>Task.</returns>
  131. protected async Task<object> ProcessRequest(StreamRequest request, bool isHeadRequest)
  132. {
  133. var cancellationTokenSource = new CancellationTokenSource();
  134. var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false);
  135. var responseHeaders = new Dictionary<string, string>();
  136. if (request.Static && state.DirectStreamProvider != null)
  137. {
  138. AddDlnaHeaders(state, responseHeaders, true);
  139. using (state)
  140. {
  141. var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  142. // TODO: Don't hardcode this
  143. outputHeaders[HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType("file.ts");
  144. return new ProgressiveFileCopier(state.DirectStreamProvider, outputHeaders, null, Logger, CancellationToken.None)
  145. {
  146. AllowEndOfFile = false
  147. };
  148. }
  149. }
  150. // Static remote stream
  151. if (request.Static && state.InputProtocol == MediaProtocol.Http)
  152. {
  153. AddDlnaHeaders(state, responseHeaders, true);
  154. using (state)
  155. {
  156. return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
  157. }
  158. }
  159. if (request.Static && state.InputProtocol != MediaProtocol.File)
  160. {
  161. throw new ArgumentException(string.Format("Input protocol {0} cannot be streamed statically.", state.InputProtocol));
  162. }
  163. var outputPath = state.OutputFilePath;
  164. var outputPathExists = File.Exists(outputPath);
  165. var transcodingJob = ApiEntryPoint.Instance.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
  166. var isTranscodeCached = outputPathExists && transcodingJob != null;
  167. AddDlnaHeaders(state, responseHeaders, request.Static || isTranscodeCached);
  168. // Static stream
  169. if (request.Static)
  170. {
  171. var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
  172. using (state)
  173. {
  174. if (state.MediaSource.IsInfiniteStream)
  175. {
  176. var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
  177. {
  178. [HeaderNames.ContentType] = contentType
  179. };
  180. return new ProgressiveFileCopier(FileSystem, state.MediaPath, outputHeaders, null, Logger, CancellationToken.None)
  181. {
  182. AllowEndOfFile = false
  183. };
  184. }
  185. TimeSpan? cacheDuration = null;
  186. if (!string.IsNullOrEmpty(request.Tag))
  187. {
  188. cacheDuration = TimeSpan.FromDays(365);
  189. }
  190. return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
  191. {
  192. ResponseHeaders = responseHeaders,
  193. ContentType = contentType,
  194. IsHeadRequest = isHeadRequest,
  195. Path = state.MediaPath,
  196. CacheDuration = cacheDuration
  197. }).ConfigureAwait(false);
  198. }
  199. }
  200. //// Not static but transcode cache file exists
  201. // if (isTranscodeCached && state.VideoRequest == null)
  202. //{
  203. // var contentType = state.GetMimeType(outputPath);
  204. // try
  205. // {
  206. // if (transcodingJob != null)
  207. // {
  208. // ApiEntryPoint.Instance.OnTranscodeBeginRequest(transcodingJob);
  209. // }
  210. // return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
  211. // {
  212. // ResponseHeaders = responseHeaders,
  213. // ContentType = contentType,
  214. // IsHeadRequest = isHeadRequest,
  215. // Path = outputPath,
  216. // FileShare = FileShare.ReadWrite,
  217. // OnComplete = () =>
  218. // {
  219. // if (transcodingJob != null)
  220. // {
  221. // ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
  222. // }
  223. // }
  224. // }).ConfigureAwait(false);
  225. // }
  226. // finally
  227. // {
  228. // state.Dispose();
  229. // }
  230. //}
  231. // Need to start ffmpeg
  232. try
  233. {
  234. return await GetStreamResult(request, state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
  235. }
  236. catch
  237. {
  238. state.Dispose();
  239. throw;
  240. }
  241. }
  242. /// <summary>
  243. /// Gets the static remote stream result.
  244. /// </summary>
  245. /// <param name="state">The state.</param>
  246. /// <param name="responseHeaders">The response headers.</param>
  247. /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
  248. /// <param name="cancellationTokenSource">The cancellation token source.</param>
  249. /// <returns>Task{System.Object}.</returns>
  250. private async Task<object> GetStaticRemoteStreamResult(
  251. StreamState state,
  252. Dictionary<string, string> responseHeaders,
  253. bool isHeadRequest,
  254. CancellationTokenSource cancellationTokenSource)
  255. {
  256. var options = new HttpRequestOptions
  257. {
  258. Url = state.MediaPath,
  259. BufferContent = false,
  260. CancellationToken = cancellationTokenSource.Token
  261. };
  262. if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
  263. {
  264. options.UserAgent = useragent;
  265. }
  266. var response = await HttpClient.GetResponse(options).ConfigureAwait(false);
  267. responseHeaders[HeaderNames.AcceptRanges] = "none";
  268. // Seeing cases of -1 here
  269. if (response.ContentLength.HasValue && response.ContentLength.Value >= 0)
  270. {
  271. responseHeaders[HeaderNames.ContentLength] = response.ContentLength.Value.ToString(CultureInfo.InvariantCulture);
  272. }
  273. if (isHeadRequest)
  274. {
  275. using (response)
  276. {
  277. return ResultFactory.GetResult(null, Array.Empty<byte>(), response.ContentType, responseHeaders);
  278. }
  279. }
  280. var result = new StaticRemoteStreamWriter(response);
  281. result.Headers[HeaderNames.ContentType] = response.ContentType;
  282. // Add the response headers to the result object
  283. foreach (var header in responseHeaders)
  284. {
  285. result.Headers[header.Key] = header.Value;
  286. }
  287. return result;
  288. }
  289. /// <summary>
  290. /// Gets the stream result.
  291. /// </summary>
  292. /// <param name="state">The state.</param>
  293. /// <param name="responseHeaders">The response headers.</param>
  294. /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
  295. /// <param name="cancellationTokenSource">The cancellation token source.</param>
  296. /// <returns>Task{System.Object}.</returns>
  297. private async Task<object> GetStreamResult(StreamRequest request, StreamState state, IDictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource)
  298. {
  299. // Use the command line args with a dummy playlist path
  300. var outputPath = state.OutputFilePath;
  301. responseHeaders[HeaderNames.AcceptRanges] = "none";
  302. var contentType = state.GetMimeType(outputPath);
  303. // TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response
  304. var contentLength = state.EstimateContentLength || isHeadRequest ? GetEstimatedContentLength(state) : null;
  305. if (contentLength.HasValue)
  306. {
  307. responseHeaders[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
  308. }
  309. // Headers only
  310. if (isHeadRequest)
  311. {
  312. var streamResult = ResultFactory.GetResult(null, Array.Empty<byte>(), contentType, responseHeaders);
  313. if (streamResult is IHasHeaders hasHeaders)
  314. {
  315. if (contentLength.HasValue)
  316. {
  317. hasHeaders.Headers[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
  318. }
  319. else
  320. {
  321. hasHeaders.Headers.Remove(HeaderNames.ContentLength);
  322. }
  323. }
  324. return streamResult;
  325. }
  326. var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(outputPath);
  327. await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
  328. try
  329. {
  330. TranscodingJob job;
  331. if (!File.Exists(outputPath))
  332. {
  333. job = await StartFfMpeg(state, outputPath, cancellationTokenSource).ConfigureAwait(false);
  334. }
  335. else
  336. {
  337. job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
  338. state.Dispose();
  339. }
  340. var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
  341. {
  342. [HeaderNames.ContentType] = contentType
  343. };
  344. // Add the response headers to the result object
  345. foreach (var item in responseHeaders)
  346. {
  347. outputHeaders[item.Key] = item.Value;
  348. }
  349. return new ProgressiveFileCopier(FileSystem, outputPath, outputHeaders, job, Logger, CancellationToken.None);
  350. }
  351. finally
  352. {
  353. transcodingLock.Release();
  354. }
  355. }
  356. /// <summary>
  357. /// Gets the length of the estimated content.
  358. /// </summary>
  359. /// <param name="state">The state.</param>
  360. /// <returns>System.Nullable{System.Int64}.</returns>
  361. private long? GetEstimatedContentLength(StreamState state)
  362. {
  363. var totalBitrate = state.TotalOutputBitrate ?? 0;
  364. if (totalBitrate > 0 && state.RunTimeTicks.HasValue)
  365. {
  366. return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds / 8);
  367. }
  368. return null;
  369. }
  370. }
  371. }