FileStreamResponseHelpers.cs 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. using System;
  2. using System.IO;
  3. using System.Net.Http;
  4. using System.Net.Mime;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. using Jellyfin.Api.Extensions;
  8. using MediaBrowser.Controller.MediaEncoding;
  9. using MediaBrowser.Controller.Streaming;
  10. using Microsoft.AspNetCore.Http;
  11. using Microsoft.AspNetCore.Mvc;
  12. using Microsoft.Net.Http.Headers;
  13. namespace Jellyfin.Api.Helpers;
  14. /// <summary>
  15. /// The stream response helpers.
  16. /// </summary>
  17. public static class FileStreamResponseHelpers
  18. {
  19. /// <summary>
  20. /// Returns a static file from a remote source.
  21. /// </summary>
  22. /// <param name="state">The current <see cref="StreamState"/>.</param>
  23. /// <param name="httpClient">The <see cref="HttpClient"/> making the remote request.</param>
  24. /// <param name="httpContext">The current http context.</param>
  25. /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
  26. /// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns>
  27. public static async Task<ActionResult> GetStaticRemoteStreamResult(
  28. StreamState state,
  29. HttpClient httpClient,
  30. HttpContext httpContext,
  31. CancellationToken cancellationToken = default)
  32. {
  33. using var requestMessage = new HttpRequestMessage(HttpMethod.Get, new Uri(state.MediaPath));
  34. // Forward User-Agent if provided
  35. if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
  36. {
  37. // Clear default and add specific one if exists, otherwise HttpClient default might be used
  38. requestMessage.Headers.UserAgent.Clear();
  39. requestMessage.Headers.TryAddWithoutValidation(HeaderNames.UserAgent, useragent);
  40. }
  41. // Forward Range header if present in the client request
  42. if (httpContext.Request.Headers.TryGetValue(HeaderNames.Range, out var rangeValue))
  43. {
  44. var rangeString = rangeValue.ToString();
  45. if (!string.IsNullOrEmpty(rangeString))
  46. {
  47. requestMessage.Headers.Range = System.Net.Http.Headers.RangeHeaderValue.Parse(rangeString);
  48. }
  49. }
  50. // Send the request to the upstream server
  51. // Use ResponseHeadersRead to avoid downloading the whole content immediately
  52. var response = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
  53. // Check if the upstream server supports range requests and acted upon our Range header
  54. bool upstreamSupportsRange = response.StatusCode == System.Net.HttpStatusCode.PartialContent;
  55. string acceptRangesValue = "none";
  56. if (response.Headers.TryGetValues(HeaderNames.AcceptRanges, out var acceptRangesHeaders))
  57. {
  58. // Prefer upstream server's Accept-Ranges header if available
  59. acceptRangesValue = string.Join(", ", acceptRangesHeaders);
  60. upstreamSupportsRange |= acceptRangesValue.Contains("bytes", StringComparison.OrdinalIgnoreCase);
  61. }
  62. else if (upstreamSupportsRange) // If we got 206 but no Accept-Ranges header, assume bytes
  63. {
  64. acceptRangesValue = "bytes";
  65. }
  66. // Set Accept-Ranges header for the client based on upstream support
  67. httpContext.Response.Headers[HeaderNames.AcceptRanges] = acceptRangesValue;
  68. // Set Content-Range header if upstream provided it (implies partial content)
  69. if (response.Content.Headers.ContentRange is not null)
  70. {
  71. httpContext.Response.Headers[HeaderNames.ContentRange] = response.Content.Headers.ContentRange.ToString();
  72. }
  73. // Set Content-Length header. For partial content, this is the length of the partial segment.
  74. if (response.Content.Headers.ContentLength.HasValue)
  75. {
  76. httpContext.Response.ContentLength = response.Content.Headers.ContentLength.Value;
  77. }
  78. // Set Content-Type header
  79. var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Application.Octet; // Use a more generic default
  80. // Set the status code for the client response (e.g., 200 OK or 206 Partial Content)
  81. httpContext.Response.StatusCode = (int)response.StatusCode;
  82. // Return the stream from the upstream server
  83. // IMPORTANT: Do not dispose the response stream here, FileStreamResult will handle it.
  84. return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType);
  85. }
  86. /// <summary>
  87. /// Returns a static file from the server.
  88. /// </summary>
  89. /// <param name="path">The path to the file.</param>
  90. /// <param name="contentType">The content type of the file.</param>
  91. /// <returns>An <see cref="ActionResult"/> the file.</returns>
  92. public static ActionResult GetStaticFileResult(
  93. string path,
  94. string contentType)
  95. {
  96. return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true };
  97. }
  98. /// <summary>
  99. /// Returns a transcoded file from the server.
  100. /// </summary>
  101. /// <param name="state">The current <see cref="StreamState"/>.</param>
  102. /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param>
  103. /// <param name="httpContext">The current http context.</param>
  104. /// <param name="transcodeManager">The <see cref="ITranscodeManager"/> singleton.</param>
  105. /// <param name="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</param>
  106. /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
  107. /// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param>
  108. /// <returns>A <see cref="Task{ActionResult}"/> containing the transcoded file.</returns>
  109. public static async Task<ActionResult> GetTranscodedFile(
  110. StreamState state,
  111. bool isHeadRequest,
  112. HttpContext httpContext,
  113. ITranscodeManager transcodeManager,
  114. string ffmpegCommandLineArguments,
  115. TranscodingJobType transcodingJobType,
  116. CancellationTokenSource cancellationTokenSource)
  117. {
  118. // Use the command line args with a dummy playlist path
  119. var outputPath = state.OutputFilePath;
  120. httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
  121. var contentType = state.GetMimeType(outputPath);
  122. // Headers only
  123. if (isHeadRequest)
  124. {
  125. httpContext.Response.Headers[HeaderNames.ContentType] = contentType;
  126. return new OkResult();
  127. }
  128. using (await transcodeManager.LockAsync(outputPath, cancellationTokenSource.Token).ConfigureAwait(false))
  129. {
  130. TranscodingJob? job;
  131. if (!File.Exists(outputPath))
  132. {
  133. job = await transcodeManager.StartFfMpeg(
  134. state,
  135. outputPath,
  136. ffmpegCommandLineArguments,
  137. httpContext.User.GetUserId(),
  138. transcodingJobType,
  139. cancellationTokenSource).ConfigureAwait(false);
  140. }
  141. else
  142. {
  143. job = transcodeManager.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
  144. state.Dispose();
  145. }
  146. var stream = new ProgressiveFileStream(outputPath, job, transcodeManager);
  147. return new FileStreamResult(stream, contentType);
  148. }
  149. }
  150. }