using System; using System.IO; using System.Net.Http; using System.Net.Mime; 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 . /// The making the remote request. /// The current http context. /// A cancellation token that can be used to cancel the operation. /// A containing the API response. public static async Task GetStaticRemoteStreamResult( StreamState state, HttpClient httpClient, HttpContext httpContext, CancellationToken cancellationToken = default) { 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(new Uri(state.MediaPath), cancellationToken).ConfigureAwait(false); var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Text.Plain; httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none"; return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType); } /// /// Returns a static file from the server. /// /// The path to the file. /// The content type of the file. /// The current http context. /// An the file. public static ActionResult GetStaticFileResult( string path, string contentType, HttpContext httpContext) { return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true }; } /// /// 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 current http context. /// The singleton. /// The command line arguments to start ffmpeg. /// The . /// The . /// A containing the transcoded file. public static async Task GetTranscodedFile( StreamState state, bool isHeadRequest, HttpContext httpContext, TranscodingJobHelper transcodingJobHelper, string ffmpegCommandLineArguments, TranscodingJobType transcodingJobType, CancellationTokenSource cancellationTokenSource) { // Use the command line args with a dummy playlist path var outputPath = state.OutputFilePath; httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none"; var contentType = state.GetMimeType(outputPath); // Headers only if (isHeadRequest) { httpContext.Response.Headers[HeaderNames.ContentType] = contentType; return new OkResult(); } 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, httpContext.Request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false); } else { job = transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive); state.Dispose(); } var stream = new ProgressiveFileStream(outputPath, job, transcodingJobHelper); return new FileStreamResult(stream, contentType); } finally { transcodingLock.Release(); } } } }