diff options
Diffstat (limited to 'Jellyfin.Api')
| -rw-r--r-- | Jellyfin.Api/Controllers/AudioController.cs | 14 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/DynamicHlsController.cs | 63 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/LibraryController.cs | 38 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/LiveTvController.cs | 14 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/UniversalAudioController.cs | 23 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/VideosController.cs | 14 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 23 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs | 2 | ||||
| -rw-r--r-- | Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs | 62 | ||||
| -rw-r--r-- | Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs | 43 |
10 files changed, 219 insertions, 77 deletions
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 72be55513..8954c8ef5 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -83,6 +83,7 @@ public class AudioController : BaseJellyfinApiController /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> /// <param name="streamOptions">Optional. The streaming options.</param> + /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param> /// <response code="200">Audio stream returned.</response> /// <returns>A <see cref="FileResult"/> containing the audio file.</returns> [HttpGet("{itemId}/stream", Name = "GetAudioStream")] @@ -138,7 +139,8 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, - [FromQuery] Dictionary<string, string>? streamOptions) + [FromQuery] Dictionary<string, string>? streamOptions, + [FromQuery] bool enableAudioVbrEncoding = true) { StreamingRequestDto streamingRequest = new StreamingRequestDto { @@ -189,7 +191,8 @@ public class AudioController : BaseJellyfinApiController AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, Context = context ?? EncodingContext.Static, - StreamOptions = streamOptions + StreamOptions = streamOptions, + EnableAudioVbrEncoding = enableAudioVbrEncoding }; return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false); @@ -247,6 +250,7 @@ public class AudioController : BaseJellyfinApiController /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> /// <param name="streamOptions">Optional. The streaming options.</param> + /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param> /// <response code="200">Audio stream returned.</response> /// <returns>A <see cref="FileResult"/> containing the audio file.</returns> [HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")] @@ -302,7 +306,8 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, - [FromQuery] Dictionary<string, string>? streamOptions) + [FromQuery] Dictionary<string, string>? streamOptions, + [FromQuery] bool enableAudioVbrEncoding = true) { StreamingRequestDto streamingRequest = new StreamingRequestDto { @@ -353,7 +358,8 @@ public class AudioController : BaseJellyfinApiController AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, Context = context ?? EncodingContext.Static, - StreamOptions = streamOptions + StreamOptions = streamOptions, + EnableAudioVbrEncoding = enableAudioVbrEncoding }; return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false); diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 68602c80d..329dd2c4c 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -156,6 +156,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// <param name="maxWidth">Optional. The max width.</param> /// <param name="maxHeight">Optional. The max height.</param> /// <param name="enableSubtitlesInManifest">Optional. Whether to enable subtitles in the manifest.</param> + /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param> /// <response code="200">Hls live stream retrieved.</response> /// <returns>A <see cref="FileResult"/> containing the hls file.</returns> [HttpGet("Videos/{itemId}/live.m3u8")] @@ -213,7 +214,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] Dictionary<string, string> streamOptions, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, - [FromQuery] bool? enableSubtitlesInManifest) + [FromQuery] bool? enableSubtitlesInManifest, + [FromQuery] bool enableAudioVbrEncoding = true) { VideoRequestDto streamingRequest = new VideoRequestDto { @@ -267,7 +269,8 @@ public class DynamicHlsController : BaseJellyfinApiController StreamOptions = streamOptions, MaxHeight = maxHeight, MaxWidth = maxWidth, - EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true + EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true, + EnableAudioVbrEncoding = enableAudioVbrEncoding }; // CTS lifecycle is managed internally. @@ -393,6 +396,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// <param name="streamOptions">Optional. The streaming options.</param> /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param> /// <param name="enableTrickplay">Enable trickplay image playlists being added to master playlist.</param> + /// <param name="enableAudioVbrEncoding">Whether to enable Audio Encoding.</param> /// <response code="200">Video stream returned.</response> /// <returns>A <see cref="FileResult"/> containing the playlist file.</returns> [HttpGet("Videos/{itemId}/master.m3u8")] @@ -451,7 +455,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions, [FromQuery] bool enableAdaptiveBitrateStreaming = true, - [FromQuery] bool enableTrickplay = true) + [FromQuery] bool enableTrickplay = true, + [FromQuery] bool enableAudioVbrEncoding = true) { var streamingRequest = new HlsVideoRequestDto { @@ -505,7 +510,8 @@ public class DynamicHlsController : BaseJellyfinApiController Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming, - EnableTrickplay = enableTrickplay + EnableTrickplay = enableTrickplay, + EnableAudioVbrEncoding = enableAudioVbrEncoding }; return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); @@ -564,6 +570,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> /// <param name="streamOptions">Optional. The streaming options.</param> /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param> + /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param> /// <response code="200">Audio stream returned.</response> /// <returns>A <see cref="FileResult"/> containing the playlist file.</returns> [HttpGet("Audio/{itemId}/master.m3u8")] @@ -620,7 +627,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions, - [FromQuery] bool enableAdaptiveBitrateStreaming = true) + [FromQuery] bool enableAdaptiveBitrateStreaming = true, + [FromQuery] bool enableAudioVbrEncoding = true) { var streamingRequest = new HlsAudioRequestDto { @@ -671,7 +679,8 @@ public class DynamicHlsController : BaseJellyfinApiController VideoStreamIndex = videoStreamIndex, Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, - EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming + EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming, + EnableAudioVbrEncoding = enableAudioVbrEncoding }; return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); @@ -730,6 +739,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> /// <param name="streamOptions">Optional. The streaming options.</param> + /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param> /// <response code="200">Video stream returned.</response> /// <returns>A <see cref="FileResult"/> containing the audio file.</returns> [HttpGet("Videos/{itemId}/main.m3u8")] @@ -785,7 +795,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, - [FromQuery] Dictionary<string, string> streamOptions) + [FromQuery] Dictionary<string, string> streamOptions, + [FromQuery] bool enableAudioVbrEncoding = true) { using var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new VideoRequestDto @@ -838,7 +849,8 @@ public class DynamicHlsController : BaseJellyfinApiController AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, Context = context ?? EncodingContext.Streaming, - StreamOptions = streamOptions + StreamOptions = streamOptions, + EnableAudioVbrEncoding = enableAudioVbrEncoding }; return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource) @@ -897,6 +909,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> /// <param name="streamOptions">Optional. The streaming options.</param> + /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param> /// <response code="200">Audio stream returned.</response> /// <returns>A <see cref="FileResult"/> containing the audio file.</returns> [HttpGet("Audio/{itemId}/main.m3u8")] @@ -951,7 +964,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, - [FromQuery] Dictionary<string, string> streamOptions) + [FromQuery] Dictionary<string, string> streamOptions, + [FromQuery] bool enableAudioVbrEncoding = true) { using var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new StreamingRequestDto @@ -1002,7 +1016,8 @@ public class DynamicHlsController : BaseJellyfinApiController AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, Context = context ?? EncodingContext.Streaming, - StreamOptions = streamOptions + StreamOptions = streamOptions, + EnableAudioVbrEncoding = enableAudioVbrEncoding }; return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource) @@ -1067,6 +1082,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> /// <param name="streamOptions">Optional. The streaming options.</param> + /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param> /// <response code="200">Video stream returned.</response> /// <returns>A <see cref="FileResult"/> containing the audio file.</returns> [HttpGet("Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")] @@ -1128,7 +1144,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, - [FromQuery] Dictionary<string, string> streamOptions) + [FromQuery] Dictionary<string, string> streamOptions, + [FromQuery] bool enableAudioVbrEncoding = true) { var streamingRequest = new VideoRequestDto { @@ -1183,7 +1200,8 @@ public class DynamicHlsController : BaseJellyfinApiController AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, Context = context ?? EncodingContext.Streaming, - StreamOptions = streamOptions + StreamOptions = streamOptions, + EnableAudioVbrEncoding = enableAudioVbrEncoding }; return await GetDynamicSegment(streamingRequest, segmentId) @@ -1247,6 +1265,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> /// <param name="streamOptions">Optional. The streaming options.</param> + /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param> /// <response code="200">Video stream returned.</response> /// <returns>A <see cref="FileResult"/> containing the audio file.</returns> [HttpGet("Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")] @@ -1307,7 +1326,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, - [FromQuery] Dictionary<string, string> streamOptions) + [FromQuery] Dictionary<string, string> streamOptions, + [FromQuery] bool enableAudioVbrEncoding = true) { var streamingRequest = new StreamingRequestDto { @@ -1360,7 +1380,8 @@ public class DynamicHlsController : BaseJellyfinApiController AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, Context = context ?? EncodingContext.Streaming, - StreamOptions = streamOptions + StreamOptions = streamOptions, + EnableAudioVbrEncoding = enableAudioVbrEncoding }; return await GetDynamicSegment(streamingRequest, segmentId) @@ -1671,8 +1692,8 @@ public class DynamicHlsController : BaseJellyfinApiController if (audioBitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(state.ActualOutputAudioCodec, StringComparison.OrdinalIgnoreCase)) { - var vbrParam = _encodingHelper.GetAudioVbrModeParam(audioCodec, audioBitrate.Value / (audioChannels ?? 2)); - if (_encodingOptions.EnableAudioVbr && vbrParam is not null) + var vbrParam = _encodingHelper.GetAudioVbrModeParam(audioCodec, audioBitrate.Value, audioChannels ?? 2); + if (_encodingOptions.EnableAudioVbr && state.EnableAudioVbrEncoding && vbrParam is not null) { audioTranscodeParams += vbrParam; } @@ -1724,8 +1745,8 @@ public class DynamicHlsController : BaseJellyfinApiController var bitrate = state.OutputAudioBitrate; if (bitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(actualOutputAudioCodec, StringComparison.OrdinalIgnoreCase)) { - var vbrParam = _encodingHelper.GetAudioVbrModeParam(audioCodec, bitrate.Value / (channels ?? 2)); - if (_encodingOptions.EnableAudioVbr && vbrParam is not null) + var vbrParam = _encodingHelper.GetAudioVbrModeParam(audioCodec, bitrate.Value, channels ?? 2); + if (_encodingOptions.EnableAudioVbr && state.EnableAudioVbrEncoding && vbrParam is not null) { args += vbrParam; } @@ -1739,6 +1760,12 @@ public class DynamicHlsController : BaseJellyfinApiController { args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); } + else if (state.AudioStream?.CodecTag is not null && state.AudioStream.CodecTag.Equals("ac-4", StringComparison.Ordinal)) + { + // ac-4 audio tends to hava a super weird sample rate that will fail most encoders + // force resample it to 48KHz + args += " -ar 48000"; + } args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions); diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 64df4c4f0..62cb59335 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -131,6 +131,8 @@ public class LibraryController : BaseJellyfinApiController /// <param name="itemId">The item id.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param> /// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param> + /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param> + /// <param name="sortOrder">Optional. Sort Order - Ascending, Descending.</param> /// <response code="200">Theme songs returned.</response> /// <response code="404">Item not found.</response> /// <returns>The item theme songs.</returns> @@ -141,7 +143,9 @@ public class LibraryController : BaseJellyfinApiController public ActionResult<ThemeMediaResult> GetThemeSongs( [FromRoute, Required] Guid itemId, [FromQuery] Guid? userId, - [FromQuery] bool inheritFromParent = false) + [FromQuery] bool inheritFromParent = false, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[]? sortBy = null, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[]? sortOrder = null) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.IsNullOrEmpty() @@ -158,11 +162,15 @@ public class LibraryController : BaseJellyfinApiController return NotFound(); } + sortOrder ??= []; + sortBy ??= []; + var orderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder); + IReadOnlyList<BaseItem> themeItems; while (true) { - themeItems = item.GetThemeSongs(); + themeItems = item.GetThemeSongs(user, orderBy); if (themeItems.Count > 0 || !inheritFromParent) { @@ -197,6 +205,8 @@ public class LibraryController : BaseJellyfinApiController /// <param name="itemId">The item id.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param> /// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param> + /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param> + /// <param name="sortOrder">Optional. Sort Order - Ascending, Descending.</param> /// <response code="200">Theme videos returned.</response> /// <response code="404">Item not found.</response> /// <returns>The item theme videos.</returns> @@ -207,7 +217,9 @@ public class LibraryController : BaseJellyfinApiController public ActionResult<ThemeMediaResult> GetThemeVideos( [FromRoute, Required] Guid itemId, [FromQuery] Guid? userId, - [FromQuery] bool inheritFromParent = false) + [FromQuery] bool inheritFromParent = false, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[]? sortBy = null, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[]? sortOrder = null) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.IsNullOrEmpty() @@ -223,11 +235,15 @@ public class LibraryController : BaseJellyfinApiController return NotFound(); } + sortOrder ??= []; + sortBy ??= []; + var orderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder); + IEnumerable<BaseItem> themeItems; while (true) { - themeItems = item.GetThemeVideos(); + themeItems = item.GetThemeVideos(user, orderBy); if (themeItems.Any() || !inheritFromParent) { @@ -262,6 +278,8 @@ public class LibraryController : BaseJellyfinApiController /// <param name="itemId">The item id.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param> /// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param> + /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param> + /// <param name="sortOrder">Optional. Sort Order - Ascending, Descending.</param> /// <response code="200">Theme songs and videos returned.</response> /// <response code="404">Item not found.</response> /// <returns>The item theme videos.</returns> @@ -271,17 +289,23 @@ public class LibraryController : BaseJellyfinApiController public ActionResult<AllThemeMediaResult> GetThemeMedia( [FromRoute, Required] Guid itemId, [FromQuery] Guid? userId, - [FromQuery] bool inheritFromParent = false) + [FromQuery] bool inheritFromParent = false, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[]? sortBy = null, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[]? sortOrder = null) { var themeSongs = GetThemeSongs( itemId, userId, - inheritFromParent); + inheritFromParent, + sortBy, + sortOrder); var themeVideos = GetThemeVideos( itemId, userId, - inheritFromParent); + inheritFromParent, + sortBy, + sortOrder); if (themeSongs.Result is StatusCodeResult { StatusCode: StatusCodes.Status404NotFound } || themeVideos.Result is StatusCodeResult { StatusCode: StatusCodes.Status404NotFound }) diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 2b26c01f8..0cf36f57e 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -656,7 +656,7 @@ public class LiveTvController : BaseJellyfinApiController var query = new InternalItemsQuery(user) { - ChannelIds = body.ChannelIds, + ChannelIds = body.ChannelIds ?? [], HasAired = body.HasAired, IsAiring = body.IsAiring, EnableTotalRecordCount = body.EnableTotalRecordCount, @@ -666,22 +666,22 @@ public class LiveTvController : BaseJellyfinApiController MaxEndDate = body.MaxEndDate, StartIndex = body.StartIndex, Limit = body.Limit, - OrderBy = RequestHelpers.GetOrderBy(body.SortBy, body.SortOrder), + OrderBy = RequestHelpers.GetOrderBy(body.SortBy ?? [], body.SortOrder ?? []), IsNews = body.IsNews, IsMovie = body.IsMovie, IsSeries = body.IsSeries, IsKids = body.IsKids, IsSports = body.IsSports, SeriesTimerId = body.SeriesTimerId, - Genres = body.Genres, - GenreIds = body.GenreIds + Genres = body.Genres ?? [], + GenreIds = body.GenreIds ?? [] }; - if (!body.LibrarySeriesId.IsEmpty()) + if (!body.LibrarySeriesId.IsNullOrEmpty()) { query.IsSeries = true; - var series = _libraryManager.GetItemById<Series>(body.LibrarySeriesId); + var series = _libraryManager.GetItemById<Series>(body.LibrarySeriesId.Value); if (series is not null) { query.Name = series.Name; @@ -690,7 +690,7 @@ public class LiveTvController : BaseJellyfinApiController var dtoOptions = new DtoOptions { Fields = body.Fields } .AddClientFields(User) - .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes); + .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes ?? []); return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); } diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 1d4adae06..fe7353496 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -17,6 +17,7 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Streaming; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -81,6 +82,7 @@ public class UniversalAudioController : BaseJellyfinApiController /// <param name="maxAudioSampleRate">Optional. The maximum audio sample rate.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="enableRemoteMedia">Optional. Whether to enable remote media.</param> + /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param> /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> /// <param name="enableRedirection">Whether to enable redirection. Defaults to true.</param> /// <response code="200">Audio stream returned.</response> @@ -111,6 +113,7 @@ public class UniversalAudioController : BaseJellyfinApiController [FromQuery] int? maxAudioSampleRate, [FromQuery] int? maxAudioBitDepth, [FromQuery] bool? enableRemoteMedia, + [FromQuery] bool enableAudioVbrEncoding = true, [FromQuery] bool breakOnNonKeyFrames = false, [FromQuery] bool enableRedirection = true) { @@ -137,6 +140,8 @@ public class UniversalAudioController : BaseJellyfinApiController // set device specific data foreach (var sourceInfo in info.MediaSources) { + sourceInfo.TranscodingContainer = transcodingContainer; + sourceInfo.TranscodingSubProtocol = transcodingProtocol ?? sourceInfo.TranscodingSubProtocol; _mediaInfoHelper.SetDeviceSpecificData( item, sourceInfo, @@ -171,6 +176,8 @@ public class UniversalAudioController : BaseJellyfinApiController return Redirect(mediaSource.Path); } + // This one is currently very misleading as the SupportsDirectStream actually means "can direct play" + // The definition of DirectStream also seems changed during development var isStatic = mediaSource.SupportsDirectStream; if (!isStatic && mediaSource.TranscodingSubProtocol == MediaStreamProtocol.hls) { @@ -178,20 +185,25 @@ public class UniversalAudioController : BaseJellyfinApiController // ffmpeg option -> file extension // mpegts -> ts // fmp4 -> mp4 - // TODO: remove this when we switch back to the segment muxer var supportedHlsContainers = new[] { "ts", "mp4" }; + // fallback to mpegts if device reports some weird value unsupported by hls + var requestedSegmentContainer = Array.Exists( + supportedHlsContainers, + element => string.Equals(element, transcodingContainer, StringComparison.OrdinalIgnoreCase)) ? transcodingContainer : "ts"; + var segmentContainer = Array.Exists( + supportedHlsContainers, + element => string.Equals(element, mediaSource.TranscodingContainer, StringComparison.OrdinalIgnoreCase)) ? mediaSource.TranscodingContainer : requestedSegmentContainer; var dynamicHlsRequestDto = new HlsAudioRequestDto { Id = itemId, Container = ".m3u8", Static = isStatic, PlaySessionId = info.PlaySessionId, - // fallback to mpegts if device reports some weird value unsupported by hls - SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "ts", + SegmentContainer = segmentContainer, MediaSourceId = mediaSourceId, DeviceId = deviceId, - AudioCodec = audioCodec, + AudioCodec = mediaSource.TranscodeReasons == TranscodeReason.ContainerNotSupported ? "copy" : audioCodec, EnableAutoStreamCopy = true, AllowAudioStreamCopy = true, AllowVideoStreamCopy = true, @@ -209,7 +221,8 @@ public class UniversalAudioController : BaseJellyfinApiController TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(), Context = EncodingContext.Static, StreamOptions = new Dictionary<string, string>(), - EnableAdaptiveBitrateStreaming = true + EnableAdaptiveBitrateStreaming = true, + EnableAudioVbrEncoding = enableAudioVbrEncoding }; return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType.Hls, dynamicHlsRequestDto, true) diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index a9e1d4484..7f9608378 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -306,6 +306,7 @@ public class VideosController : BaseJellyfinApiController /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> /// <param name="streamOptions">Optional. The streaming options.</param> + /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param> /// <response code="200">Video stream returned.</response> /// <returns>A <see cref="FileResult"/> containing the audio file.</returns> [HttpGet("{itemId}/stream")] @@ -363,7 +364,8 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, - [FromQuery] Dictionary<string, string> streamOptions) + [FromQuery] Dictionary<string, string> streamOptions, + [FromQuery] bool enableAudioVbrEncoding = true) { var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head; // CTS lifecycle is managed internally. @@ -419,7 +421,8 @@ public class VideosController : BaseJellyfinApiController AudioStreamIndex = audioStreamIndex, VideoStreamIndex = videoStreamIndex, Context = context ?? EncodingContext.Streaming, - StreamOptions = streamOptions + StreamOptions = streamOptions, + EnableAudioVbrEncoding = enableAudioVbrEncoding }; var state = await StreamingHelpers.GetStreamingState( @@ -544,6 +547,7 @@ public class VideosController : BaseJellyfinApiController /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> /// <param name="streamOptions">Optional. The streaming options.</param> + /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param> /// <response code="200">Video stream returned.</response> /// <returns>A <see cref="FileResult"/> containing the audio file.</returns> [HttpGet("{itemId}/stream.{container}")] @@ -601,7 +605,8 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, - [FromQuery] Dictionary<string, string> streamOptions) + [FromQuery] Dictionary<string, string> streamOptions, + [FromQuery] bool enableAudioVbrEncoding = true) { return GetVideoStream( itemId, @@ -654,6 +659,7 @@ public class VideosController : BaseJellyfinApiController audioStreamIndex, videoStreamIndex, context, - streamOptions); + streamOptions, + enableAudioVbrEncoding); } } diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index f8d89119a..6f040cfae 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)) @@ -714,6 +722,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/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index cb178a61d..0690f0c8d 100644 --- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs +++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs @@ -38,7 +38,7 @@ public static class FileStreamResponseHelpers } // 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 response = await httpClient.GetAsync(new Uri(state.MediaPath), HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Text.Plain; httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none"; diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs index ec67b4c1b..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> diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs index 8482b1cf1..7210cc8f7 100644 --- a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs +++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json.Converters; @@ -17,7 +18,7 @@ public class GetProgramsDto /// Gets or sets the channels to return guide information for. /// </summary> [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList<Guid> ChannelIds { get; set; } = Array.Empty<Guid>(); + public IReadOnlyList<Guid>? ChannelIds { get; set; } /// <summary> /// Gets or sets optional. Filter by user id. @@ -26,153 +27,133 @@ public class GetProgramsDto /// <summary> /// Gets or sets the minimum premiere start date. - /// Optional. /// </summary> public DateTime? MinStartDate { get; set; } /// <summary> /// Gets or sets filter by programs that have completed airing, or not. - /// Optional. /// </summary> public bool? HasAired { get; set; } /// <summary> /// Gets or sets filter by programs that are currently airing, or not. - /// Optional. /// </summary> public bool? IsAiring { get; set; } /// <summary> /// Gets or sets the maximum premiere start date. - /// Optional. /// </summary> public DateTime? MaxStartDate { get; set; } /// <summary> /// Gets or sets the minimum premiere end date. - /// Optional. /// </summary> public DateTime? MinEndDate { get; set; } /// <summary> /// Gets or sets the maximum premiere end date. - /// Optional. /// </summary> public DateTime? MaxEndDate { get; set; } /// <summary> /// Gets or sets filter for movies. - /// Optional. /// </summary> public bool? IsMovie { get; set; } /// <summary> /// Gets or sets filter for series. - /// Optional. /// </summary> public bool? IsSeries { get; set; } /// <summary> /// Gets or sets filter for news. - /// Optional. /// </summary> public bool? IsNews { get; set; } /// <summary> /// Gets or sets filter for kids. - /// Optional. /// </summary> public bool? IsKids { get; set; } /// <summary> /// Gets or sets filter for sports. - /// Optional. /// </summary> public bool? IsSports { get; set; } /// <summary> /// Gets or sets the record index to start at. All items with a lower index will be dropped from the results. - /// Optional. /// </summary> public int? StartIndex { get; set; } /// <summary> /// Gets or sets the maximum number of records to return. - /// Optional. /// </summary> public int? Limit { get; set; } /// <summary> /// Gets or sets specify one or more sort orders, comma delimited. Options: Name, StartDate. - /// Optional. /// </summary> [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList<ItemSortBy> SortBy { get; set; } = Array.Empty<ItemSortBy>(); + public IReadOnlyList<ItemSortBy>? SortBy { get; set; } /// <summary> - /// Gets or sets sort Order - Ascending,Descending. + /// Gets or sets sort order. /// </summary> [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList<SortOrder> SortOrder { get; set; } = Array.Empty<SortOrder>(); + public IReadOnlyList<SortOrder>? SortOrder { get; set; } /// <summary> /// Gets or sets the genres to return guide information for. /// </summary> [JsonConverter(typeof(JsonPipeDelimitedArrayConverterFactory))] - public IReadOnlyList<string> Genres { get; set; } = Array.Empty<string>(); + public IReadOnlyList<string>? Genres { get; set; } /// <summary> /// Gets or sets the genre ids to return guide information for. /// </summary> [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList<Guid> GenreIds { get; set; } = Array.Empty<Guid>(); + public IReadOnlyList<Guid>? GenreIds { get; set; } /// <summary> /// Gets or sets include image information in output. - /// Optional. /// </summary> public bool? EnableImages { get; set; } /// <summary> /// Gets or sets a value indicating whether retrieve total record count. /// </summary> + [DefaultValue(true)] public bool EnableTotalRecordCount { get; set; } = true; /// <summary> /// Gets or sets the max number of images to return, per image type. - /// Optional. /// </summary> public int? ImageTypeLimit { get; set; } /// <summary> /// Gets or sets the image types to include in the output. - /// Optional. /// </summary> [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList<ImageType> EnableImageTypes { get; set; } = Array.Empty<ImageType>(); + public IReadOnlyList<ImageType>? EnableImageTypes { get; set; } /// <summary> /// Gets or sets include user data. - /// Optional. /// </summary> public bool? EnableUserData { get; set; } /// <summary> /// Gets or sets filter by series timer id. - /// Optional. /// </summary> public string? SeriesTimerId { get; set; } /// <summary> /// Gets or sets filter by library series id. - /// Optional. /// </summary> - public Guid LibrarySeriesId { get; set; } + public Guid? LibrarySeriesId { get; set; } /// <summary> - /// Gets or sets specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. - /// Optional. + /// Gets or sets specify additional fields of information to return in the output. /// </summary> [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList<ItemFields> Fields { get; set; } = Array.Empty<ItemFields>(); + public IReadOnlyList<ItemFields>? Fields { get; set; } } |
