using System; using System.IO; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Controller.MediaEncoding; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; namespace Jellyfin.Api.Helpers { /// /// The stream response helpers. /// public static class FileStreamResponseHelpers { /// /// Returns a static file from a remote source. /// /// The current . /// Whether the current request is a HTTP HEAD request so only the headers get returned. /// The managing the response. /// The making the remote request. /// A containing the API response. public static async Task GetStaticRemoteStreamResult( StreamState state, bool isHeadRequest, ControllerBase controller, HttpClient httpClient) { if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent)) { httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, useragent); } // Can't dispose the response as it's required up the call chain. var response = await httpClient.GetAsync(state.MediaPath).ConfigureAwait(false); var contentType = response.Content.Headers.ContentType.ToString(); controller.Response.Headers[HeaderNames.AcceptRanges] = "none"; if (isHeadRequest) { return controller.File(Array.Empty(), contentType); } return controller.File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), contentType); } /// /// Returns a static file from the server. /// /// The path to the file. /// The content type of the file. /// Whether the current request is a HTTP HEAD request so only the headers get returned. /// The managing the response. /// An the file. public static ActionResult GetStaticFileResult( string path, string contentType, bool isHeadRequest, ControllerBase controller) { controller.Response.ContentType = contentType; // if the request is a head request, return a NoContent result with the same headers as it would with a GET request if (isHeadRequest) { return controller.NoContent(); } return controller.PhysicalFile(path, contentType); } /// /// Returns a transcoded file from the server. /// /// The current . /// Whether the current request is a HTTP HEAD request so only the headers get returned. /// The managing the response. /// The singleton. /// The command line arguments to start ffmpeg. /// The starting the transcoding. /// The . /// The . /// A containing the transcoded file. public static async Task GetTranscodedFile( StreamState state, bool isHeadRequest, ControllerBase controller, TranscodingJobHelper transcodingJobHelper, string ffmpegCommandLineArguments, HttpRequest request, TranscodingJobType transcodingJobType, CancellationTokenSource cancellationTokenSource) { // Use the command line args with a dummy playlist path var outputPath = state.OutputFilePath; controller.Response.Headers[HeaderNames.AcceptRanges] = "none"; var contentType = state.GetMimeType(outputPath); // Headers only if (isHeadRequest) { return controller.File(Array.Empty(), contentType); } var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath); await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); try { TranscodingJobDto? job; if (!File.Exists(outputPath)) { job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false); } else { job = transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive); state.Dispose(); } var memoryStream = new MemoryStream(); await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false); memoryStream.Position = 0; return controller.File(memoryStream, contentType); } finally { transcodingLock.Release(); } } } }