FileStreamResponseHelpers.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. using System;
  2. using System.Globalization;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Net.Http;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. using Jellyfin.Api.Models.StreamingDtos;
  9. using MediaBrowser.Controller.MediaEncoding;
  10. using MediaBrowser.Model.IO;
  11. using Microsoft.AspNetCore.Http;
  12. using Microsoft.AspNetCore.Mvc;
  13. using Microsoft.Extensions.Primitives;
  14. using Microsoft.Net.Http.Headers;
  15. namespace Jellyfin.Api.Helpers
  16. {
  17. /// <summary>
  18. /// The stream response helpers.
  19. /// </summary>
  20. public static class FileStreamResponseHelpers
  21. {
  22. /// <summary>
  23. /// Returns a static file from a remote source.
  24. /// </summary>
  25. /// <param name="state">The current <see cref="StreamState"/>.</param>
  26. /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
  27. /// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
  28. /// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param>
  29. /// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns>
  30. public static async Task<ActionResult> GetStaticRemoteStreamResult(
  31. StreamState state,
  32. bool isHeadRequest,
  33. ControllerBase controller,
  34. CancellationTokenSource cancellationTokenSource)
  35. {
  36. HttpClient httpClient = new HttpClient();
  37. var responseHeaders = controller.Response.Headers;
  38. if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
  39. {
  40. httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, useragent);
  41. }
  42. var response = await httpClient.GetAsync(state.MediaPath).ConfigureAwait(false);
  43. var contentType = response.Content.Headers.ContentType.ToString();
  44. responseHeaders[HeaderNames.AcceptRanges] = "none";
  45. // Seeing cases of -1 here
  46. if (response.Content.Headers.ContentLength.HasValue && response.Content.Headers.ContentLength.Value >= 0)
  47. {
  48. responseHeaders[HeaderNames.ContentLength] = response.Content.Headers.ContentLength.Value.ToString(CultureInfo.InvariantCulture);
  49. }
  50. if (isHeadRequest)
  51. {
  52. using (response)
  53. {
  54. return controller.File(Array.Empty<byte>(), contentType);
  55. }
  56. }
  57. return controller.File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), contentType);
  58. }
  59. /// <summary>
  60. /// Returns a static file from the server.
  61. /// </summary>
  62. /// <param name="path">The path to the file.</param>
  63. /// <param name="contentType">The content type of the file.</param>
  64. /// <param name="dateLastModified">The <see cref="DateTime"/> of the last modification of the file.</param>
  65. /// <param name="cacheDuration">The cache duration of the file.</param>
  66. /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
  67. /// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
  68. /// <returns>An <see cref="ActionResult"/> the file.</returns>
  69. // TODO: caching doesn't work
  70. public static ActionResult GetStaticFileResult(
  71. string path,
  72. string contentType,
  73. DateTime dateLastModified,
  74. TimeSpan? cacheDuration,
  75. bool isHeadRequest,
  76. ControllerBase controller)
  77. {
  78. bool disableCaching = false;
  79. if (controller.Request.Headers.TryGetValue(HeaderNames.CacheControl, out StringValues headerValue))
  80. {
  81. disableCaching = headerValue.FirstOrDefault().Contains("no-cache", StringComparison.InvariantCulture);
  82. }
  83. bool parsingSuccessful = DateTime.TryParseExact(controller.Request.Headers[HeaderNames.IfModifiedSince], "ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false), DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime ifModifiedSinceHeader);
  84. // if the parsing of the IfModifiedSince header was not successfull, disable caching
  85. if (!parsingSuccessful)
  86. {
  87. disableCaching = true;
  88. }
  89. controller.Response.ContentType = contentType;
  90. controller.Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateLastModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
  91. controller.Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept);
  92. if (disableCaching)
  93. {
  94. controller.Response.Headers.Add(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
  95. controller.Response.Headers.Add(HeaderNames.Pragma, "no-cache, no-store, must-revalidate");
  96. }
  97. else
  98. {
  99. if (cacheDuration.HasValue)
  100. {
  101. controller.Response.Headers.Add(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds);
  102. }
  103. else
  104. {
  105. controller.Response.Headers.Add(HeaderNames.CacheControl, "public");
  106. }
  107. controller.Response.Headers.Add(HeaderNames.LastModified, dateLastModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false)));
  108. // if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified
  109. if (!(dateLastModified > ifModifiedSinceHeader))
  110. {
  111. if (ifModifiedSinceHeader.Add(cacheDuration!.Value) < DateTime.UtcNow)
  112. {
  113. controller.Response.StatusCode = StatusCodes.Status304NotModified;
  114. return new ContentResult();
  115. }
  116. }
  117. }
  118. // if the request is a head request, return a NoContent result with the same headers as it would with a GET request
  119. if (isHeadRequest)
  120. {
  121. return controller.NoContent();
  122. }
  123. var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
  124. return controller.File(stream, contentType);
  125. }
  126. /// <summary>
  127. /// Returns a transcoded file from the server.
  128. /// </summary>
  129. /// <param name="state">The current <see cref="StreamState"/>.</param>
  130. /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
  131. /// <param name="streamHelper">Instance of the <see cref="IStreamHelper"/> interface.</param>
  132. /// <param name="controller">The <see cref="ControllerBase"/> managing the response.</param>
  133. /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
  134. /// <param name="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</param>
  135. /// <param name="request">The <see cref="HttpRequest"/> starting the transcoding.</param>
  136. /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
  137. /// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param>
  138. /// <returns>A <see cref="Task{ActionResult}"/> containing the transcoded file.</returns>
  139. public static async Task<ActionResult> GetTranscodedFile(
  140. StreamState state,
  141. bool isHeadRequest,
  142. IStreamHelper streamHelper,
  143. ControllerBase controller,
  144. TranscodingJobHelper transcodingJobHelper,
  145. string ffmpegCommandLineArguments,
  146. HttpRequest request,
  147. TranscodingJobType transcodingJobType,
  148. CancellationTokenSource cancellationTokenSource)
  149. {
  150. IHeaderDictionary responseHeaders = controller.Response.Headers;
  151. // Use the command line args with a dummy playlist path
  152. var outputPath = state.OutputFilePath;
  153. responseHeaders[HeaderNames.AcceptRanges] = "none";
  154. var contentType = state.GetMimeType(outputPath);
  155. // TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response
  156. // TODO (from api-migration): Investigate if this is still neccessary as we migrated away from ServiceStack
  157. var contentLength = state.EstimateContentLength || isHeadRequest ? GetEstimatedContentLength(state) : null;
  158. if (contentLength.HasValue)
  159. {
  160. responseHeaders[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
  161. }
  162. else
  163. {
  164. responseHeaders.Remove(HeaderNames.ContentLength);
  165. }
  166. // Headers only
  167. if (isHeadRequest)
  168. {
  169. return controller.File(Array.Empty<byte>(), contentType);
  170. }
  171. var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);
  172. await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
  173. try
  174. {
  175. if (!File.Exists(outputPath))
  176. {
  177. await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false);
  178. }
  179. else
  180. {
  181. transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
  182. state.Dispose();
  183. }
  184. Stream stream = new MemoryStream();
  185. await new ProgressiveFileCopier(streamHelper, outputPath).WriteToAsync(stream, CancellationToken.None).ConfigureAwait(false);
  186. return controller.File(stream, contentType);
  187. }
  188. finally
  189. {
  190. transcodingLock.Release();
  191. }
  192. }
  193. /// <summary>
  194. /// Gets the length of the estimated content.
  195. /// </summary>
  196. /// <param name="state">The state.</param>
  197. /// <returns>System.Nullable{System.Int64}.</returns>
  198. private static long? GetEstimatedContentLength(StreamState state)
  199. {
  200. var totalBitrate = state.TotalOutputBitrate ?? 0;
  201. if (totalBitrate > 0 && state.RunTimeTicks.HasValue)
  202. {
  203. return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds / 8);
  204. }
  205. return null;
  206. }
  207. }
  208. }