using System; using System.IO; using System.Net.Http; using System.Net.Mime; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Extensions; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Streaming; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; namespace Jellyfin.Api.Helpers; /// <summary> /// The stream response helpers. /// </summary> public static class FileStreamResponseHelpers { /// <summary> /// Returns a static file from a remote source. /// </summary> /// <param name="state">The current <see cref="StreamState"/>.</param> /// <param name="httpClient">The <see cref="HttpClient"/> making the remote request.</param> /// <param name="httpContext">The current http context.</param> /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> /// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns> public static async Task<ActionResult> 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); } /// <summary> /// Returns a static file from the server. /// </summary> /// <param name="path">The path to the file.</param> /// <param name="contentType">The content type of the file.</param> /// <returns>An <see cref="ActionResult"/> the file.</returns> public static ActionResult GetStaticFileResult( string path, string contentType) { return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true }; } /// <summary> /// Returns a transcoded file from the server. /// </summary> /// <param name="state">The current <see cref="StreamState"/>.</param> /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param> /// <param name="httpContext">The current http context.</param> /// <param name="transcodeManager">The <see cref="ITranscodeManager"/> singleton.</param> /// <param name="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</param> /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param> /// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param> /// <returns>A <see cref="Task{ActionResult}"/> containing the transcoded file.</returns> public static async Task<ActionResult> GetTranscodedFile( StreamState state, bool isHeadRequest, HttpContext httpContext, ITranscodeManager transcodeManager, 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(); } using (await transcodeManager.LockAsync(outputPath, cancellationTokenSource.Token).ConfigureAwait(false)) { TranscodingJob? job; if (!File.Exists(outputPath)) { job = await transcodeManager.StartFfMpeg( state, outputPath, ffmpegCommandLineArguments, httpContext.User.GetUserId(), transcodingJobType, cancellationTokenSource).ConfigureAwait(false); } else { job = transcodeManager.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive); state.Dispose(); } var stream = new ProgressiveFileStream(outputPath, job, transcodeManager); return new FileStreamResult(stream, contentType); } } }