diff options
Diffstat (limited to 'Jellyfin.Api/Helpers')
| -rw-r--r-- | Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 23 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs | 70 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/MediaInfoHelper.cs | 16 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/RequestHelpers.cs | 11 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/StreamingHelpers.cs | 33 |
5 files changed, 131 insertions, 22 deletions
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index e5e4356f8..ba92d811c 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -151,6 +151,14 @@ public class DynamicHlsHelper var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString(); + // from universal audio service, need to override the AudioCodec when the actual request differs from original query + if (!string.Equals(state.OutputAudioCodec, _httpContextAccessor.HttpContext.Request.Query["AudioCodec"].ToString(), StringComparison.OrdinalIgnoreCase)) + { + var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(_httpContextAccessor.HttpContext.Request.QueryString.ToString()); + newQuery["AudioCodec"] = state.OutputAudioCodec; + queryString = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(string.Empty, newQuery); + } + // from universal audio service if (!string.IsNullOrWhiteSpace(state.Request.SegmentContainer) && !queryString.Contains("SegmentContainer", StringComparison.OrdinalIgnoreCase)) @@ -725,6 +733,21 @@ public class DynamicHlsHelper return HlsCodecStringHelpers.GetAv1String(profile, level, false, bitDepth); } + // VP9 HLS is for video remuxing only, everything is probed from the original video + if (string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase)) + { + var width = state.VideoStream.Width ?? 0; + var height = state.VideoStream.Height ?? 0; + var framerate = state.VideoStream.AverageFrameRate ?? 30; + var bitDepth = state.VideoStream.BitDepth ?? 8; + return HlsCodecStringHelpers.GetVp9String( + width, + height, + state.VideoStream.PixelFormat, + framerate, + bitDepth); + } + return string.Empty; } diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs index 5eec1b0ca..d0bfa1fbe 100644 --- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs @@ -183,6 +183,68 @@ public static class HlsCodecStringHelpers } /// <summary> + /// Gets a VP9 codec string. + /// </summary> + /// <param name="width">Video width.</param> + /// <param name="height">Video height.</param> + /// <param name="pixelFormat">Video pixel format.</param> + /// <param name="framerate">Video framerate.</param> + /// <param name="bitDepth">Video bitDepth.</param> + /// <returns>The VP9 codec string.</returns> + public static string GetVp9String(int width, int height, string pixelFormat, float framerate, int bitDepth) + { + // refer: https://www.webmproject.org/vp9/mp4/ + StringBuilder result = new StringBuilder("vp09", 13); + + var profileString = pixelFormat switch + { + "yuv420p" => "00", + "yuvj420p" => "00", + "yuv422p" => "01", + "yuv444p" => "01", + "yuv420p10le" => "02", + "yuv420p12le" => "02", + "yuv422p10le" => "03", + "yuv422p12le" => "03", + "yuv444p10le" => "03", + "yuv444p12le" => "03", + _ => "00" + }; + + var lumaPictureSize = width * height; + var lumaSampleRate = lumaPictureSize * framerate; + var levelString = lumaPictureSize switch + { + <= 0 => "00", + <= 36864 => "10", + <= 73728 => "11", + <= 122880 => "20", + <= 245760 => "21", + <= 552960 => "30", + <= 983040 => "31", + <= 2228224 => lumaSampleRate <= 83558400 ? "40" : "41", + <= 8912896 => lumaSampleRate <= 311951360 ? "50" : (lumaSampleRate <= 588251136 ? "51" : "52"), + <= 35651584 => lumaSampleRate <= 1176502272 ? "60" : (lumaSampleRate <= 4706009088 ? "61" : "62"), + _ => "00" // This should not happen + }; + + if (bitDepth != 8 + && bitDepth != 10 + && bitDepth != 12) + { + // Default to 8 bits + bitDepth = 8; + } + + result.Append('.').Append(profileString).Append('.').Append(levelString); + var bitDepthD2 = bitDepth.ToString("D2", CultureInfo.InvariantCulture); + result.Append('.') + .Append(bitDepthD2); + + return result.ToString(); + } + + /// <summary> /// Gets an AV1 codec string. /// </summary> /// <param name="profile">AV1 profile.</param> @@ -192,7 +254,7 @@ public static class HlsCodecStringHelpers /// <returns>The AV1 codec string.</returns> public static string GetAv1String(string? profile, int level, bool tierFlag, int bitDepth) { - // https://aomedia.org/av1/specification/annex-a/ + // https://aomediacodec.github.io/av1-isobmff/#codecsparam // FORMAT: [codecTag].[profile].[level][tier].[bitDepth] StringBuilder result = new StringBuilder("av01", 13); @@ -214,8 +276,7 @@ public static class HlsCodecStringHelpers result.Append(".0"); } - if (level <= 0 - || level > 31) + if (level is <= 0 or > 31) { // Default to the maximum defined level 6.3 level = 19; @@ -230,7 +291,8 @@ public static class HlsCodecStringHelpers } result.Append('.') - .Append(level) + // Needed to pad it double digits; otherwise, browsers will reject the stream. + .AppendFormat(CultureInfo.InvariantCulture, "{0:D2}", level) .Append(tierFlag ? 'H' : 'M'); string bitDepthD2 = bitDepth.ToString("D2", CultureInfo.InvariantCulture); diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 6a24ad32a..212d678a8 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -24,6 +24,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Helpers; @@ -76,21 +77,17 @@ public class MediaInfoHelper /// <summary> /// Get playback info. /// </summary> - /// <param name="id">Item id.</param> - /// <param name="userId">User Id.</param> + /// <param name="item">The item.</param> + /// <param name="user">The user.</param> /// <param name="mediaSourceId">Media source id.</param> /// <param name="liveStreamId">Live stream id.</param> /// <returns>A <see cref="Task"/> containing the <see cref="PlaybackInfoResponse"/>.</returns> public async Task<PlaybackInfoResponse> GetPlaybackInfo( - Guid id, - Guid? userId, + BaseItem item, + User? user, string? mediaSourceId = null, string? liveStreamId = null) { - var user = userId.IsNullOrEmpty() - ? null - : _userManager.GetUserById(userId.Value); - var item = _libraryManager.GetItemById(id); var result = new PlaybackInfoResponse(); MediaSourceInfo[] mediaSources; @@ -402,7 +399,8 @@ public class MediaInfoHelper if (profile is not null) { - var item = _libraryManager.GetItemById(request.ItemId); + var item = _libraryManager.GetItemById<BaseItem>(request.ItemId) + ?? throw new ResourceNotFoundException(); SetDeviceSpecificData( item, diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 429e97213..a3d7f471e 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -117,10 +117,15 @@ public static class RequestHelpers return user.EnableUserPreferenceAccess; } - internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext) + internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext, Guid? userId = null) { - var userId = httpContext.User.GetUserId(); - var user = userManager.GetUserById(userId); + userId ??= httpContext.User.GetUserId(); + User? user = null; + if (!userId.IsNullOrEmpty()) + { + user = userManager.GetUserById(userId.Value); + } + var session = await sessionManager.LogSessionActivity( httpContext.User.GetClient(), httpContext.User.GetVersion(), diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index bfe71fd87..535ef27c3 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -11,6 +11,7 @@ using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Streaming; @@ -18,6 +19,7 @@ using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.Net.Http.Headers; namespace Jellyfin.Api.Helpers; @@ -107,7 +109,8 @@ public static class StreamingHelpers ?? state.SupportedSubtitleCodecs.FirstOrDefault(); } - var item = libraryManager.GetItemById(streamingRequest.Id); + var item = libraryManager.GetItemById<BaseItem>(streamingRequest.Id) + ?? throw new ResourceNotFoundException(); state.IsInputVideo = item.MediaType == MediaType.Video; @@ -125,7 +128,7 @@ public static class StreamingHelpers if (mediaSource is null) { - var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById(streamingRequest.Id), null, false, false, cancellationToken).ConfigureAwait(false); + var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById<BaseItem>(streamingRequest.Id), null, false, false, cancellationToken).ConfigureAwait(false); mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId) ? mediaSources[0] @@ -139,6 +142,25 @@ public static class StreamingHelpers } else { + // Enforce more restrictive transcoding profile for LiveTV due to compatability reasons + // Cap the MaxStreamingBitrate to 30Mbps, because we are unable to reliably probe source bitrate, + // which will cause the client to request extremely high bitrate that may fail the player/encoder + streamingRequest.VideoBitRate = streamingRequest.VideoBitRate > 30000000 ? 30000000 : streamingRequest.VideoBitRate; + + if (streamingRequest.SegmentContainer is not null) + { + // Remove all fmp4 transcoding profiles, because it causes playback error and/or A/V sync issues + // Notably: Some channels won't play on FireFox and LG webOS + // Some channels from HDHomerun will experience A/V sync issues + streamingRequest.SegmentContainer = "ts"; + streamingRequest.VideoCodec = "h264"; + streamingRequest.AudioCodec = "aac"; + state.SupportedVideoCodecs = ["h264"]; + state.Request.VideoCodec = "h264"; + state.SupportedAudioCodecs = ["aac"]; + state.Request.AudioCodec = "aac"; + } + var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false); mediaSource = liveStreamInfo.Item1; state.DirectStreamProvider = liveStreamInfo.Item2; @@ -163,6 +185,9 @@ public static class StreamingHelpers } var outputAudioCodec = streamingRequest.AudioCodec; + state.OutputAudioCodec = outputAudioCodec; + state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.'); + state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec); if (EncodingHelper.LosslessAudioCodecs.Contains(outputAudioCodec)) { state.OutputAudioBitrate = state.AudioStream.BitRate ?? 0; @@ -177,10 +202,6 @@ public static class StreamingHelpers containerInternal = ".pcm"; } - state.OutputAudioCodec = outputAudioCodec; - state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.'); - state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec); - if (state.VideoRequest is not null) { state.OutputVideoCodec = state.Request.VideoCodec; |
