From a7891b3f2ddadac2049619fb929b8da149fc3a40 Mon Sep 17 00:00:00 2001 From: sususu98 Date: Wed, 23 Apr 2025 15:37:58 +0800 Subject: [PATCH] Enhanced HTTP Range request support for. strm file Forward the Range, Accept-Ranges, and Content- Range headers, improve User-Agent handling, and adjust the default Content-Type. --- .../Helpers/FileStreamResponseHelpers.cs | 60 +++++++++++++++++-- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index 0690f0c8d7..4034a80887 100644 --- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs +++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs @@ -32,17 +32,67 @@ public static class FileStreamResponseHelpers HttpContext httpContext, CancellationToken cancellationToken = default) { + using var requestMessage = new HttpRequestMessage(HttpMethod.Get, new Uri(state.MediaPath)); + + // Forward User-Agent if provided if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent)) { - httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, useragent); + // Clear default and add specific one if exists, otherwise HttpClient default might be used + requestMessage.Headers.UserAgent.Clear(); + requestMessage.Headers.TryAddWithoutValidation(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), HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Text.Plain; + // Forward Range header if present in the client request + if (httpContext.Request.Headers.TryGetValue(HeaderNames.Range, out var rangeValue)) + { + var rangeString = rangeValue.ToString(); + if (!string.IsNullOrEmpty(rangeString)) + { + requestMessage.Headers.Range = System.Net.Http.Headers.RangeHeaderValue.Parse(rangeString); + } + } - httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none"; + // Send the request to the upstream server + // Use ResponseHeadersRead to avoid downloading the whole content immediately + var response = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + // Check if the upstream server supports range requests and acted upon our Range header + bool upstreamSupportsRange = response.StatusCode == System.Net.HttpStatusCode.PartialContent; + string acceptRangesValue = "none"; + if (response.Headers.TryGetValues(HeaderNames.AcceptRanges, out var acceptRangesHeaders)) + { + // Prefer upstream server's Accept-Ranges header if available + acceptRangesValue = string.Join(", ", acceptRangesHeaders); + upstreamSupportsRange |= acceptRangesValue.Contains("bytes", StringComparison.OrdinalIgnoreCase); + } + else if (upstreamSupportsRange) // If we got 206 but no Accept-Ranges header, assume bytes + { + acceptRangesValue = "bytes"; + } + + // Set Accept-Ranges header for the client based on upstream support + httpContext.Response.Headers[HeaderNames.AcceptRanges] = acceptRangesValue; + + // Set Content-Range header if upstream provided it (implies partial content) + if (response.Content.Headers.ContentRange is not null) + { + httpContext.Response.Headers[HeaderNames.ContentRange] = response.Content.Headers.ContentRange.ToString(); + } + + // Set Content-Length header. For partial content, this is the length of the partial segment. + if (response.Content.Headers.ContentLength.HasValue) + { + httpContext.Response.ContentLength = response.Content.Headers.ContentLength.Value; + } + + // Set Content-Type header + var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Application.Octet; // Use a more generic default + + // Set the status code for the client response (e.g., 200 OK or 206 Partial Content) + httpContext.Response.StatusCode = (int)response.StatusCode; + + // Return the stream from the upstream server + // IMPORTANT: Do not dispose the response stream here, FileStreamResult will handle it. return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType); }