diff options
Diffstat (limited to 'Jellyfin.Api/Helpers')
| -rw-r--r-- | Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 85 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs | 8 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/MediaInfoHelper.cs | 2 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/RequestHelpers.cs | 6 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/StreamingHelpers.cs | 17 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 2 |
6 files changed, 68 insertions, 52 deletions
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index fe602fba3..24082fcff 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Extensions; using Jellyfin.Api.Models.StreamingDtos; +using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; @@ -19,6 +20,7 @@ using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Net; @@ -46,6 +48,7 @@ public class DynamicHlsHelper private readonly ILogger<DynamicHlsHelper> _logger; private readonly IHttpContextAccessor _httpContextAccessor; private readonly EncodingHelper _encodingHelper; + private readonly ITrickplayManager _trickplayManager; /// <summary> /// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class. @@ -62,6 +65,7 @@ public class DynamicHlsHelper /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param> /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> + /// <param name="trickplayManager">Instance of <see cref="ITrickplayManager"/>.</param> public DynamicHlsHelper( ILibraryManager libraryManager, IUserManager userManager, @@ -74,7 +78,8 @@ public class DynamicHlsHelper INetworkManager networkManager, ILogger<DynamicHlsHelper> logger, IHttpContextAccessor httpContextAccessor, - EncodingHelper encodingHelper) + EncodingHelper encodingHelper, + ITrickplayManager trickplayManager) { _libraryManager = libraryManager; _userManager = userManager; @@ -88,6 +93,7 @@ public class DynamicHlsHelper _logger = logger; _httpContextAccessor = httpContextAccessor; _encodingHelper = encodingHelper; + _trickplayManager = trickplayManager; } /// <summary> @@ -200,13 +206,6 @@ public class DynamicHlsHelper if (state.VideoStream is not null && state.VideoRequest is not null) { - // Provide a workaround for the case issue between flac and fLaC. - var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString()); - if (!string.IsNullOrEmpty(flacWaPlaylist)) - { - builder.Append(flacWaPlaylist); - } - var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); // Provide SDR HEVC entrance for backward compatibility. @@ -236,14 +235,7 @@ public class DynamicHlsHelper } var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate; - var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup); - - // Provide a workaround for the case issue between flac and fLaC. - flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString()); - if (!string.IsNullOrEmpty(flacWaPlaylist)) - { - builder.Append(flacWaPlaylist); - } + AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup); // Restore the video codec state.OutputVideoCodec = "copy"; @@ -274,13 +266,6 @@ public class DynamicHlsHelper state.VideoStream.Level = originalLevel; var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField); builder.Append(newPlaylist); - - // Provide a workaround for the case issue between flac and fLaC. - flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist); - if (!string.IsNullOrEmpty(flacWaPlaylist)) - { - builder.Append(flacWaPlaylist); - } } } @@ -301,6 +286,13 @@ public class DynamicHlsHelper AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); } + if (!isLiveStream && (state.VideoRequest?.EnableTrickplay ?? false)) + { + var sourceId = Guid.Parse(state.Request.MediaSourceId); + var trickplayResolutions = await _trickplayManager.GetTrickplayResolutions(sourceId).ConfigureAwait(false); + AddTrickplay(state, trickplayResolutions, builder, _httpContextAccessor.HttpContext.User); + } + return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); } @@ -530,6 +522,41 @@ public class DynamicHlsHelper } /// <summary> + /// Appends EXT-X-IMAGE-STREAM-INF playlists for each available trickplay resolution. + /// </summary> + /// <param name="state">StreamState of the current stream.</param> + /// <param name="trickplayResolutions">Dictionary of widths to corresponding tiles info.</param> + /// <param name="builder">StringBuilder to append the field to.</param> + /// <param name="user">Http user context.</param> + private void AddTrickplay(StreamState state, Dictionary<int, TrickplayInfo> trickplayResolutions, StringBuilder builder, ClaimsPrincipal user) + { + const string playlistFormat = "#EXT-X-IMAGE-STREAM-INF:BANDWIDTH={0},RESOLUTION={1}x{2},CODECS=\"jpeg\",URI=\"{3}\""; + + foreach (var resolution in trickplayResolutions) + { + var width = resolution.Key; + var trickplayInfo = resolution.Value; + + var url = string.Format( + CultureInfo.InvariantCulture, + "Trickplay/{0}/tiles.m3u8?MediaSourceId={1}&api_key={2}", + width.ToString(CultureInfo.InvariantCulture), + state.Request.MediaSourceId, + user.GetToken()); + + builder.AppendFormat( + CultureInfo.InvariantCulture, + playlistFormat, + trickplayInfo.Bandwidth.ToString(CultureInfo.InvariantCulture), + trickplayInfo.Width.ToString(CultureInfo.InvariantCulture), + trickplayInfo.Height.ToString(CultureInfo.InvariantCulture), + url); + + builder.AppendLine(); + } + } + + /// <summary> /// Get the H.26X level of the output video stream. /// </summary> /// <param name="state">StreamState of the current stream.</param> @@ -767,16 +794,4 @@ public class DynamicHlsHelper newValue.ToString(), StringComparison.Ordinal); } - - private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist) - { - if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)) - { - return string.Empty; - } - - var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal); - - return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty; - } } diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs index 9a141a16d..5eec1b0ca 100644 --- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs @@ -5,7 +5,9 @@ using System.Text; namespace Jellyfin.Api.Helpers; /// <summary> -/// Hls Codec string helpers. +/// Helpers to generate HLS codec strings according to +/// <a href="https://datatracker.ietf.org/doc/html/rfc6381#section-3.3">RFC 6381 section 3.3</a> +/// and the <a href="https://mp4ra.org">MP4 Registration Authority</a>. /// </summary> public static class HlsCodecStringHelpers { @@ -27,7 +29,7 @@ public static class HlsCodecStringHelpers /// <summary> /// Codec name for FLAC. /// </summary> - public const string FLAC = "flac"; + public const string FLAC = "fLaC"; /// <summary> /// Codec name for ALAC. @@ -37,7 +39,7 @@ public static class HlsCodecStringHelpers /// <summary> /// Codec name for OPUS. /// </summary> - public const string OPUS = "opus"; + public const string OPUS = "Opus"; /// <summary> /// Gets a MP3 codec string. diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index a36028cfe..321987ca7 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -243,7 +243,7 @@ public class MediaInfoHelper } // Beginning of Playback Determination - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) + var streamInfo = item.MediaType == MediaType.Audio ? streamBuilder.GetOptimalAudioStream(options) : streamBuilder.GetOptimalVideoStream(options); diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index bc12ca388..be3d4dfb6 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -30,14 +30,14 @@ public static class RequestHelpers /// <param name="sortBy">Sort By. Comma delimited string.</param> /// <param name="requestedSortOrder">Sort Order. Comma delimited string.</param> /// <returns>Order By.</returns> - public static (string, SortOrder)[] GetOrderBy(IReadOnlyList<string> sortBy, IReadOnlyList<SortOrder> requestedSortOrder) + public static (ItemSortBy, SortOrder)[] GetOrderBy(IReadOnlyList<ItemSortBy> sortBy, IReadOnlyList<SortOrder> requestedSortOrder) { if (sortBy.Count == 0) { - return Array.Empty<(string, SortOrder)>(); + return Array.Empty<(ItemSortBy, SortOrder)>(); } - var result = new (string, SortOrder)[sortBy.Count]; + var result = new (ItemSortBy, SortOrder)[sortBy.Count]; var i = 0; // Add elements which have a SortOrder specified for (; i < requestedSortOrder.Count; i++) diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index e55420d11..6fbbceeab 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Extensions; using Jellyfin.Api.Models.StreamingDtos; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; @@ -128,7 +129,7 @@ public static class StreamingHelpers var item = libraryManager.GetItemById(streamingRequest.Id); - state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase); + state.IsInputVideo = item.MediaType == MediaType.Video; MediaSourceInfo? mediaSource = null; if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId)) @@ -248,7 +249,7 @@ public static class StreamingHelpers ? GetOutputFileExtension(state, mediaSource) : ("." + state.OutputContainer); - state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId); + state.OutputFilePath = GetOutputFilePath(state, ext, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId); return state; } @@ -421,10 +422,9 @@ public static class StreamingHelpers /// <param name="state">The state.</param> /// <param name="mediaSource">The mediaSource.</param> /// <returns>System.String.</returns> - private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource) + private static string GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource) { var ext = Path.GetExtension(state.RequestedUrl); - if (!string.IsNullOrEmpty(ext)) { return ext; @@ -463,10 +463,9 @@ public static class StreamingHelpers return ".asf"; } } - - // Try to infer based on the desired audio codec - if (!state.IsVideoRequest) + else { + // Try to infer based on the desired audio codec var audioCodec = state.Request.AudioCodec; if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase)) @@ -497,7 +496,7 @@ public static class StreamingHelpers return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim(); } - return null; + throw new InvalidOperationException("Failed to find an appropriate file extension"); } /// <summary> @@ -514,7 +513,7 @@ public static class StreamingHelpers var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}"; var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture); - var ext = outputFileExtension?.ToLowerInvariant(); + var ext = outputFileExtension.ToLowerInvariant(); var folder = serverConfigurationManager.GetTranscodePath(); return Path.Combine(folder, filename + ext); diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 73ebb396d..c16a586d6 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -538,7 +538,7 @@ public class TranscodingJobHelper : IDisposable await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false); } - if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase)) + if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", StringComparison.OrdinalIgnoreCase)) { string subtitlePath = state.SubtitleStream.Path; string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal)); |
