diff options
Diffstat (limited to 'Jellyfin.Api')
| -rw-r--r-- | Jellyfin.Api/Controllers/DynamicHlsController.cs | 9 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/ItemUpdateController.cs | 5 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/LocalizationController.cs | 11 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs | 60 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/RequestHelpers.cs | 11 |
5 files changed, 86 insertions, 10 deletions
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 4cac8ed67..2614fe995 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -46,6 +46,7 @@ public class DynamicHlsController : BaseJellyfinApiController private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0); private readonly Version _minFFmpegX265BframeInFmp4 = new Version(7, 0, 1); + private readonly Version _minFFmpegHlsSegmentOptions = new Version(5, 0); private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; @@ -1606,6 +1607,7 @@ public class DynamicHlsController : BaseJellyfinApiController var segmentFormat = string.Empty; var segmentContainer = outputExtension.TrimStart('.'); var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions, segmentContainer); + var hlsArguments = $"-hls_playlist_type {(isEventPlaylist ? "event" : "vod")} -hls_list_size 0"; if (string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase)) { @@ -1621,6 +1623,11 @@ public class DynamicHlsController : BaseJellyfinApiController false => " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\"" }; + var useLegacySegmentOption = _mediaEncoder.EncoderVersion < _minFFmpegHlsSegmentOptions; + + // fMP4 needs this flag to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT + hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont"; + segmentFormat = "fmp4" + outputFmp4HeaderArg; } else @@ -1642,8 +1649,6 @@ public class DynamicHlsController : BaseJellyfinApiController Path.GetFileNameWithoutExtension(outputPath)); } - var hlsArguments = $"-hls_playlist_type {(isEventPlaylist ? "event" : "vod")} -hls_list_size 0"; - return string.Format( CultureInfo.InvariantCulture, "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9}{10} -hls_segment_filename \"{11}\" {12} -y \"{13}\"", diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 50eeaeac6..e1d9b6bba 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -158,7 +158,10 @@ public class ItemUpdateController : BaseJellyfinApiController ParentalRatingOptions = _localizationManager.GetParentalRatings().ToList(), ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(), Countries = _localizationManager.GetCountries().ToArray(), - Cultures = _localizationManager.GetCultures().ToArray() + Cultures = _localizationManager.GetCultures() + .DistinctBy(c => c.DisplayName, StringComparer.OrdinalIgnoreCase) + .OrderBy(c => c.DisplayName) + .ToArray() }; if (!item.IsVirtualItem diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs index bbce5a9e1..dd8f935dc 100644 --- a/Jellyfin.Api/Controllers/LocalizationController.cs +++ b/Jellyfin.Api/Controllers/LocalizationController.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.Linq; using MediaBrowser.Common.Api; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; @@ -34,7 +36,14 @@ public class LocalizationController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<IEnumerable<CultureDto>> GetCultures() { - return Ok(_localization.GetCultures()); + var allCultures = _localization.GetCultures(); + + var distinctCultures = allCultures + .DistinctBy(c => c.DisplayName, StringComparer.OrdinalIgnoreCase) + .OrderBy(c => c.DisplayName) + .AsEnumerable(); + + return Ok(distinctCultures); } /// <summary> diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index 0690f0c8d..4034a8088 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); + } + + // 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); + } } - // 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; + // 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); - httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none"; + // 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); } diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index e10e940f2..5072f902d 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -111,7 +111,16 @@ public static class RequestHelpers return user.EnableUserPreferenceAccess; } - internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext, Guid? userId = null) + /// <summary> + /// Get the session based on http request. + /// </summary> + /// <param name="sessionManager">The session manager.</param> + /// <param name="userManager">The user manager.</param> + /// <param name="httpContext">The http context.</param> + /// <param name="userId">The optional userid.</param> + /// <returns>The session.</returns> + /// <exception cref="ResourceNotFoundException">Session not found.</exception> + public static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext, Guid? userId = null) { userId ??= httpContext.User.GetUserId(); User? user = null; |
