aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/ISSUE_TEMPLATE/issue report.yml2
-rw-r--r--.github/workflows/ci-codeql-analysis.yml6
-rw-r--r--Directory.Packages.props6
-rw-r--r--Emby.Photos/PhotoProvider.cs2
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs3
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs12
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs14
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs63
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs38
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs14
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs23
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs14
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs23
-rw-r--r--Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs2
-rw-r--r--Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs62
-rw-r--r--Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs43
-rw-r--r--Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs1
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs23
-rw-r--r--MediaBrowser.Controller/Entities/Movies/Movie.cs18
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs17
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs28
-rw-r--r--MediaBrowser.Controller/Entities/Trailer.cs17
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs4
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvProgram.cs20
-rw-r--r--MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs40
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs2
-rw-r--r--MediaBrowser.Controller/Providers/IExternalId.cs2
-rw-r--r--MediaBrowser.Controller/Providers/IExternalUrlProvider.cs22
-rw-r--r--MediaBrowser.Controller/Providers/IProviderManager.cs4
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs74
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs32
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs42
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs5
-rw-r--r--MediaBrowser.Model/Configuration/TrickplayOptions.cs6
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs104
-rw-r--r--MediaBrowser.Model/Dlna/StreamInfo.cs4
-rw-r--r--MediaBrowser.Model/Dlna/TranscodingProfile.cs4
-rw-r--r--MediaBrowser.Model/Providers/ExternalIdInfo.cs5
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs61
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs2
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs17
-rw-r--r--tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs6
45 files changed, 603 insertions, 290 deletions
diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml
index 31ae50263..85590c0b0 100644
--- a/.github/ISSUE_TEMPLATE/issue report.yml
+++ b/.github/ISSUE_TEMPLATE/issue report.yml
@@ -86,7 +86,7 @@ body:
label: Jellyfin Server version
description: What version of Jellyfin are you using?
options:
- - 10.9.7
+ - 10.9.8+
- Master
- Unstable
- Older*
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index c6ea1d7ca..ba66526e0 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -27,11 +27,11 @@ jobs:
dotnet-version: '8.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12
+ uses: github/codeql-action/init@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12
+ uses: github/codeql-action/autobuild@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12
+ uses: github/codeql-action/analyze@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 825301bfc..5242126a3 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,7 +4,7 @@
</PropertyGroup>
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
<ItemGroup Label="Package Dependencies">
- <PackageVersion Include="AsyncKeyedLock" Version="6.4.2" />
+ <PackageVersion Include="AsyncKeyedLock" Version="7.0.0" />
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
@@ -22,7 +22,7 @@
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
- <PackageVersion Include="libse" Version="4.0.5" />
+ <PackageVersion Include="libse" Version="4.0.7" />
<PackageVersion Include="LrcParser" Version="2023.524.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.7" />
@@ -72,7 +72,7 @@
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.8" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
- <PackageVersion Include="Svg.Skia" Version="1.0.0.18" />
+ <PackageVersion Include="Svg.Skia" Version="2.0.0" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" />
diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs
index e2f1ca813..ac6c41ca5 100644
--- a/Emby.Photos/PhotoProvider.cs
+++ b/Emby.Photos/PhotoProvider.cs
@@ -26,7 +26,7 @@ public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IH
private readonly ILogger<PhotoProvider> _logger;
private readonly IImageProcessor _imageProcessor;
- // These are causing taglib to hang
+ // Other extensions might cause taglib to hang
private readonly string[] _includeExtensions = [".jpg", ".jpeg", ".png", ".tiff", ".cr2", ".webp", ".avif"];
/// <summary>
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index c394b25bd..5bf9c4fc2 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -664,7 +664,8 @@ namespace Emby.Server.Implementations
GetExports<IMetadataService>(),
GetExports<IMetadataProvider>(),
GetExports<IMetadataSaver>(),
- GetExports<IExternalId>());
+ GetExports<IExternalId>(),
+ GetExports<IExternalUrlProvider>());
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
}
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index c2e3312ab..5094dcf0d 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -5694,13 +5694,17 @@ AND Type = @InternalPersonType)");
item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
- if (item.Type == MediaStreamType.Subtitle)
+ if (item.Type is MediaStreamType.Audio or MediaStreamType.Subtitle)
{
- item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
item.LocalizedDefault = _localization.GetLocalizedString("Default");
- item.LocalizedForced = _localization.GetLocalizedString("Forced");
item.LocalizedExternal = _localization.GetLocalizedString("External");
- item.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
+
+ if (item.Type is MediaStreamType.Subtitle)
+ {
+ item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
+ item.LocalizedForced = _localization.GetLocalizedString("Forced");
+ item.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
+ }
}
return item;
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; }
}
diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
index c14be032e..bb32b7c20 100644
--- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
+++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
@@ -168,6 +168,7 @@ public class TrickplayManager : ITrickplayManager
options.ProcessThreads,
options.Qscale,
options.ProcessPriority,
+ options.EnableKeyFrameOnlyExtraction,
_encodingHelper,
cancellationToken).ConfigureAwait(false);
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 8bd4fb4f3..7b6f364f7 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -2497,11 +2497,6 @@ namespace MediaBrowser.Controller.Entities
return new[] { Id };
}
- public virtual List<ExternalUrl> GetRelatedUrls()
- {
- return new List<ExternalUrl>();
- }
-
public virtual double? GetRefreshProgress()
{
return null;
@@ -2549,14 +2544,24 @@ namespace MediaBrowser.Controller.Entities
StringComparison.OrdinalIgnoreCase);
}
- public IReadOnlyList<BaseItem> GetThemeSongs()
+ public IReadOnlyList<BaseItem> GetThemeSongs(User user = null)
+ {
+ return GetThemeSongs(user, Array.Empty<(ItemSortBy, SortOrder)>());
+ }
+
+ public IReadOnlyList<BaseItem> GetThemeSongs(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> orderBy)
+ {
+ return LibraryManager.Sort(GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeSong), user, orderBy).ToArray();
+ }
+
+ public IReadOnlyList<BaseItem> GetThemeVideos(User user = null)
{
- return GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeSong).ToArray();
+ return GetThemeVideos(user, Array.Empty<(ItemSortBy, SortOrder)>());
}
- public IReadOnlyList<BaseItem> GetThemeVideos()
+ public IReadOnlyList<BaseItem> GetThemeVideos(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> orderBy)
{
- return GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo).ToArray();
+ return LibraryManager.Sort(GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo), user, orderBy).ToArray();
}
/// <summary>
diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs
index ede544eec..710b05e7f 100644
--- a/MediaBrowser.Controller/Entities/Movies/Movie.cs
+++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs
@@ -121,23 +121,5 @@ namespace MediaBrowser.Controller.Entities.Movies
return hasChanges;
}
-
- /// <inheritdoc />
- public override List<ExternalUrl> GetRelatedUrls()
- {
- var list = base.GetRelatedUrls();
-
- var imdbId = this.GetProviderId(MetadataProvider.Imdb);
- if (!string.IsNullOrEmpty(imdbId))
- {
- list.Add(new ExternalUrl
- {
- Name = "Trakt",
- Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/movies/{0}", imdbId)
- });
- }
-
- return list;
- }
}
}
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index 37e241414..5c54f014c 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -344,22 +344,5 @@ namespace MediaBrowser.Controller.Entities.TV
return hasChanges;
}
-
- public override List<ExternalUrl> GetRelatedUrls()
- {
- var list = base.GetRelatedUrls();
-
- var imdbId = this.GetProviderId(MetadataProvider.Imdb);
- if (!string.IsNullOrEmpty(imdbId))
- {
- list.Add(new ExternalUrl
- {
- Name = "Trakt",
- Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/episodes/{0}", imdbId)
- });
- }
-
- return list;
- }
}
}
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index 6297b67e4..a324f79ef 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -350,10 +350,17 @@ namespace MediaBrowser.Controller.Entities.TV
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
+ var queryFromSeries = ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons;
+
+ // add optimization when this setting is not enabled
+ var seriesKey = queryFromSeries ?
+ GetUniqueSeriesKey(this) :
+ GetUniqueSeriesKey(parentSeason);
+
var query = new InternalItemsQuery(user)
{
- AncestorWithPresentationUniqueKey = null,
- SeriesPresentationUniqueKey = GetUniqueSeriesKey(this),
+ AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey,
+ SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null,
IncludeItemTypes = new[] { BaseItemKind.Episode },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options
@@ -482,22 +489,5 @@ namespace MediaBrowser.Controller.Entities.TV
return hasChanges;
}
-
- public override List<ExternalUrl> GetRelatedUrls()
- {
- var list = base.GetRelatedUrls();
-
- var imdbId = this.GetProviderId(MetadataProvider.Imdb);
- if (!string.IsNullOrEmpty(imdbId))
- {
- list.Add(new ExternalUrl
- {
- Name = "Trakt",
- Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/shows/{0}", imdbId)
- });
- }
-
- return list;
- }
}
}
diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs
index 81d50bbc1..939709215 100644
--- a/MediaBrowser.Controller/Entities/Trailer.cs
+++ b/MediaBrowser.Controller/Entities/Trailer.cs
@@ -80,22 +80,5 @@ namespace MediaBrowser.Controller.Entities
return hasChanges;
}
-
- public override List<ExternalUrl> GetRelatedUrls()
- {
- var list = base.GetRelatedUrls();
-
- var imdbId = this.GetProviderId(MetadataProvider.Imdb);
- if (!string.IsNullOrEmpty(imdbId))
- {
- list.Add(new ExternalUrl
- {
- Name = "Trakt",
- Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/movies/{0}", imdbId)
- });
- }
-
- return list;
- }
}
}
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index 4af000557..3a1d0c070 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -744,7 +744,7 @@ namespace MediaBrowser.Controller.Entities
{
var filterValue = query.HasThemeSong.Value;
- var themeCount = item.GetThemeSongs().Count;
+ var themeCount = item.GetThemeSongs(user).Count;
var ok = filterValue ? themeCount > 0 : themeCount == 0;
if (!ok)
@@ -757,7 +757,7 @@ namespace MediaBrowser.Controller.Entities
{
var filterValue = query.HasThemeVideo.Value;
- var themeCount = item.GetThemeVideos().Count;
+ var themeCount = item.GetThemeVideos(user).Count;
var ok = filterValue ? themeCount > 0 : themeCount == 0;
if (!ok)
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
index 05540d490..2ac6f9963 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
@@ -254,25 +254,5 @@ namespace MediaBrowser.Controller.LiveTv
return name;
}
-
- public override List<ExternalUrl> GetRelatedUrls()
- {
- var list = base.GetRelatedUrls();
-
- var imdbId = this.GetProviderId(MetadataProvider.Imdb);
- if (!string.IsNullOrEmpty(imdbId))
- {
- if (IsMovie)
- {
- list.Add(new ExternalUrl
- {
- Name = "Trakt",
- Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/movies/{0}", imdbId)
- });
- }
- }
-
- return list;
- }
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
index 29dd190ab..03ec6c658 100644
--- a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
+++ b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
@@ -191,6 +191,8 @@ namespace MediaBrowser.Controller.MediaEncoding
public Dictionary<string, string> StreamOptions { get; set; }
+ public bool EnableAudioVbrEncoding { get; set; }
+
public string GetOption(string qualifier, string name)
{
var value = GetOption(qualifier + "-" + name);
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index b175dc403..42b09a29e 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -120,7 +120,8 @@ namespace MediaBrowser.Controller.MediaEncoding
private static readonly Dictionary<string, string> _mjpegCodecMap = new(StringComparer.OrdinalIgnoreCase)
{
{ "vaapi", _defaultMjpegEncoder + "_vaapi" },
- { "qsv", _defaultMjpegEncoder + "_qsv" }
+ { "qsv", _defaultMjpegEncoder + "_qsv" },
+ { "videotoolbox", _defaultMjpegEncoder + "_videotoolbox" }
};
public static readonly string[] LosslessAudioCodecs = new string[]
@@ -2605,8 +2606,9 @@ namespace MediaBrowser.Controller.MediaEncoding
return 128000 * (outputAudioChannels ?? audioStream.Channels ?? 2);
}
- public string GetAudioVbrModeParam(string encoder, int bitratePerChannel)
+ public string GetAudioVbrModeParam(string encoder, int bitrate, int channels)
{
+ var bitratePerChannel = bitrate / Math.Max(channels, 1);
if (string.Equals(encoder, "libfdk_aac", StringComparison.OrdinalIgnoreCase))
{
return " -vbr:a " + bitratePerChannel switch
@@ -2621,14 +2623,26 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(encoder, "libmp3lame", StringComparison.OrdinalIgnoreCase))
{
- return " -qscale:a " + bitratePerChannel switch
+ // lame's VBR is only good for a certain bitrate range
+ // For very low and very high bitrate, use abr mode
+ if (bitratePerChannel is < 122500 and > 48000)
{
- < 48000 => "8",
- < 64000 => "6",
- < 88000 => "4",
- < 112000 => "2",
- _ => "0"
- };
+ return " -qscale:a " + bitratePerChannel switch
+ {
+ < 64000 => "6",
+ < 88000 => "4",
+ < 112000 => "2",
+ _ => "0"
+ };
+ }
+
+ return " -abr:a 1" + " -b:a " + bitrate;
+ }
+
+ if (string.Equals(encoder, "aac_at", StringComparison.OrdinalIgnoreCase))
+ {
+ // aac_at's CVBR mode
+ return " -aac_at_mode:a 2" + " -b:a " + bitrate;
}
if (string.Equals(encoder, "libvorbis", StringComparison.OrdinalIgnoreCase))
@@ -7002,8 +7016,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var bitrate = state.OutputAudioBitrate;
if (bitrate.HasValue && !LosslessAudioCodecs.Contains(codec, StringComparison.OrdinalIgnoreCase))
{
- var vbrParam = GetAudioVbrModeParam(codec, bitrate.Value / (channels ?? 2));
- if (encodingOptions.EnableAudioVbr && vbrParam is not null)
+ var vbrParam = GetAudioVbrModeParam(codec, bitrate.Value, channels ?? 2);
+ if (encodingOptions.EnableAudioVbr && state.EnableAudioVbrEncoding && vbrParam is not null)
{
args += vbrParam;
}
@@ -7033,8 +7047,8 @@ namespace MediaBrowser.Controller.MediaEncoding
if (bitrate.HasValue && !LosslessAudioCodecs.Contains(outputCodec, StringComparison.OrdinalIgnoreCase))
{
- var vbrParam = GetAudioVbrModeParam(GetAudioEncoder(state), bitrate.Value / (channels ?? 2));
- if (encodingOptions.EnableAudioVbr && vbrParam is not null)
+ var vbrParam = GetAudioVbrModeParam(GetAudioEncoder(state), bitrate.Value, channels ?? 2);
+ if (encodingOptions.EnableAudioVbr && state.EnableAudioVbrEncoding && vbrParam is not null)
{
audioTranscodeParams.Add(vbrParam);
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index f2a0b906d..72df7151d 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -508,6 +508,8 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
+ public bool EnableAudioVbrEncoding => BaseRequest.EnableAudioVbrEncoding;
+
public int HlsListSize => 0;
public bool EnableBreakOnNonKeyFrames(string videoCodec)
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index 26c353a54..038c6c7f6 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -153,6 +153,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <param name="threads">The input/output thread count for ffmpeg.</param>
/// <param name="qualityScale">The qscale value for ffmpeg.</param>
/// <param name="priority">The process priority for the ffmpeg process.</param>
+ /// <param name="enableKeyFrameOnlyExtraction">Whether to only extract key frames.</param>
/// <param name="encodingHelper">EncodingHelper instance.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Directory where images where extracted. A given image made before another will always be named with a lower number.</returns>
@@ -168,6 +169,7 @@ namespace MediaBrowser.Controller.MediaEncoding
int? threads,
int? qualityScale,
ProcessPriorityClass? priority,
+ bool enableKeyFrameOnlyExtraction,
EncodingHelper encodingHelper,
CancellationToken cancellationToken);
diff --git a/MediaBrowser.Controller/Providers/IExternalId.cs b/MediaBrowser.Controller/Providers/IExternalId.cs
index 0d847520d..f451eac6d 100644
--- a/MediaBrowser.Controller/Providers/IExternalId.cs
+++ b/MediaBrowser.Controller/Providers/IExternalId.cs
@@ -1,3 +1,4 @@
+using System;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
@@ -33,6 +34,7 @@ namespace MediaBrowser.Controller.Providers
/// <summary>
/// Gets the URL format string for this id.
/// </summary>
+ [Obsolete("Obsolete in 10.10, to be removed in 10.11")]
string? UrlFormatString { get; }
/// <summary>
diff --git a/MediaBrowser.Controller/Providers/IExternalUrlProvider.cs b/MediaBrowser.Controller/Providers/IExternalUrlProvider.cs
new file mode 100644
index 000000000..86a180627
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/IExternalUrlProvider.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Providers;
+
+/// <summary>
+/// Interface to include related urls for an item.
+/// </summary>
+public interface IExternalUrlProvider
+{
+ /// <summary>
+ /// Gets the external service name.
+ /// </summary>
+ string Name { get; }
+
+ /// <summary>
+ /// Get the list of external urls.
+ /// </summary>
+ /// <param name="item">The item to get external urls for.</param>
+ /// <returns>The list of external urls.</returns>
+ IEnumerable<string> GetExternalUrls(BaseItem item);
+}
diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs
index b52f16edc..38fc5f2cc 100644
--- a/MediaBrowser.Controller/Providers/IProviderManager.cs
+++ b/MediaBrowser.Controller/Providers/IProviderManager.cs
@@ -99,12 +99,14 @@ namespace MediaBrowser.Controller.Providers
/// <param name="metadataProviders">Metadata providers to use.</param>
/// <param name="metadataSavers">Metadata savers to use.</param>
/// <param name="externalIds">External IDs to use.</param>
+ /// <param name="externalUrlProviders">The list of external url providers.</param>
void AddParts(
IEnumerable<IImageProvider> imageProviders,
IEnumerable<IMetadataService> metadataServices,
IEnumerable<IMetadataProvider> metadataProviders,
IEnumerable<IMetadataSaver> metadataSavers,
- IEnumerable<IExternalId> externalIds);
+ IEnumerable<IExternalId> externalIds,
+ IEnumerable<IExternalUrlProvider> externalUrlProviders);
/// <summary>
/// Gets the available remote images.
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs
index 5bae4fbd5..6ca994fb7 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using BDInfo;
+using Jellyfin.Extensions;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
@@ -60,21 +62,20 @@ public class BdInfoExaminer : IBlurayExaminer
var sortedStreams = playlist.SortedStreams;
var mediaStreams = new List<MediaStream>(sortedStreams.Count);
- foreach (var stream in sortedStreams)
+ for (int i = 0; i < sortedStreams.Count; i++)
{
+ var stream = sortedStreams[i];
switch (stream)
{
case TSVideoStream videoStream:
- AddVideoStream(mediaStreams, videoStream);
+ AddVideoStream(mediaStreams, i, videoStream);
break;
case TSAudioStream audioStream:
- AddAudioStream(mediaStreams, audioStream);
+ AddAudioStream(mediaStreams, i, audioStream);
break;
- case TSTextStream textStream:
- AddSubtitleStream(mediaStreams, textStream);
- break;
- case TSGraphicsStream graphicStream:
- AddSubtitleStream(mediaStreams, graphicStream);
+ case TSTextStream:
+ case TSGraphicsStream:
+ AddSubtitleStream(mediaStreams, i, stream);
break;
}
}
@@ -96,18 +97,19 @@ public class BdInfoExaminer : IBlurayExaminer
/// Adds the video stream.
/// </summary>
/// <param name="streams">The streams.</param>
+ /// <param name="index">The stream index.</param>
/// <param name="videoStream">The video stream.</param>
- private void AddVideoStream(List<MediaStream> streams, TSVideoStream videoStream)
+ private void AddVideoStream(List<MediaStream> streams, int index, TSVideoStream videoStream)
{
var mediaStream = new MediaStream
{
BitRate = Convert.ToInt32(videoStream.BitRate),
Width = videoStream.Width,
Height = videoStream.Height,
- Codec = videoStream.CodecShortName,
+ Codec = GetNormalizedCodec(videoStream),
IsInterlaced = videoStream.IsInterlaced,
Type = MediaStreamType.Video,
- Index = streams.Count
+ Index = index
};
if (videoStream.FrameRateDenominator > 0)
@@ -125,17 +127,19 @@ public class BdInfoExaminer : IBlurayExaminer
/// Adds the audio stream.
/// </summary>
/// <param name="streams">The streams.</param>
+ /// <param name="index">The stream index.</param>
/// <param name="audioStream">The audio stream.</param>
- private void AddAudioStream(List<MediaStream> streams, TSAudioStream audioStream)
+ private void AddAudioStream(List<MediaStream> streams, int index, TSAudioStream audioStream)
{
var stream = new MediaStream
{
- Codec = audioStream.CodecShortName,
+ Codec = GetNormalizedCodec(audioStream),
Language = audioStream.LanguageCode,
- Channels = audioStream.ChannelCount,
+ ChannelLayout = string.Format(CultureInfo.InvariantCulture, "{0:D}.{1:D}", audioStream.ChannelCount, audioStream.LFE),
+ Channels = audioStream.ChannelCount + audioStream.LFE,
SampleRate = audioStream.SampleRate,
Type = MediaStreamType.Audio,
- Index = streams.Count
+ Index = index
};
var bitrate = Convert.ToInt32(audioStream.BitRate);
@@ -145,11 +149,6 @@ public class BdInfoExaminer : IBlurayExaminer
stream.BitRate = bitrate;
}
- if (audioStream.LFE > 0)
- {
- stream.Channels = audioStream.ChannelCount + 1;
- }
-
streams.Add(stream);
}
@@ -157,31 +156,28 @@ public class BdInfoExaminer : IBlurayExaminer
/// Adds the subtitle stream.
/// </summary>
/// <param name="streams">The streams.</param>
- /// <param name="textStream">The text stream.</param>
- private void AddSubtitleStream(List<MediaStream> streams, TSTextStream textStream)
+ /// <param name="index">The stream index.</param>
+ /// <param name="stream">The stream.</param>
+ private void AddSubtitleStream(List<MediaStream> streams, int index, TSStream stream)
{
streams.Add(new MediaStream
{
- Language = textStream.LanguageCode,
- Codec = textStream.CodecShortName,
+ Language = stream.LanguageCode,
+ Codec = GetNormalizedCodec(stream),
Type = MediaStreamType.Subtitle,
- Index = streams.Count
+ Index = index
});
}
- /// <summary>
- /// Adds the subtitle stream.
- /// </summary>
- /// <param name="streams">The streams.</param>
- /// <param name="textStream">The text stream.</param>
- private void AddSubtitleStream(List<MediaStream> streams, TSGraphicsStream textStream)
- {
- streams.Add(new MediaStream
+ private string GetNormalizedCodec(TSStream stream)
+ => stream.StreamType switch
{
- Language = textStream.LanguageCode,
- Codec = textStream.CodecShortName,
- Type = MediaStreamType.Subtitle,
- Index = streams.Count
- });
- }
+ TSStreamType.MPEG1_VIDEO => "mpeg1video",
+ TSStreamType.MPEG2_VIDEO => "mpeg2video",
+ TSStreamType.VC1_VIDEO => "vc1",
+ TSStreamType.AC3_PLUS_AUDIO or TSStreamType.AC3_PLUS_SECONDARY_AUDIO => "eac3",
+ TSStreamType.DTS_AUDIO or TSStreamType.DTS_HD_AUDIO or TSStreamType.DTS_HD_MASTER_AUDIO or TSStreamType.DTS_HD_SECONDARY_AUDIO => "dts",
+ TSStreamType.PRESENTATION_GRAPHICS => "pgssub",
+ _ => stream.CodecShortName
+ };
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 30bb21dcb..a865b0e4c 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -27,6 +27,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
"msmpeg4",
"dca",
"ac3",
+ "ac4",
"aac",
"mp3",
"flac",
@@ -94,6 +95,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
"h264_v4l2m2m",
"h264_videotoolbox",
"hevc_videotoolbox",
+ "mjpeg_videotoolbox",
"h264_rkmpp",
"hevc_rkmpp"
};
@@ -499,6 +501,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
return output.Contains(keyDesc, StringComparison.Ordinal);
}
+ public bool CheckSupportedHwaccelFlag(string flag)
+ {
+ return !string.IsNullOrEmpty(flag) && GetProcessExitCode(_encoderPath, $"-loglevel quiet -hwaccel_flags +{flag} -hide_banner -f lavfi -i nullsrc=s=1x1:d=100 -f null -");
+ }
+
private IEnumerable<string> GetCodecs(Codec codec)
{
string codecstr = codec == Codec.Encoder ? "encoders" : "decoders";
@@ -604,6 +611,31 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
+ private bool GetProcessExitCode(string path, string arguments)
+ {
+ using var process = new Process();
+ process.StartInfo = new ProcessStartInfo(path, arguments)
+ {
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ };
+ _logger.LogDebug("Running {Path} {Arguments}", path, arguments);
+
+ try
+ {
+ process.Start();
+ process.WaitForExit();
+ return process.ExitCode == 0;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError("Running {Path} {Arguments} failed with exception {Exception}", path, arguments, ex.Message);
+ return false;
+ }
+ }
+
[GeneratedRegex("^\\s\\S{6}\\s(?<codec>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)]
private static partial Regex CodecRegex();
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index d2aaba906..5cfead502 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -74,6 +74,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private IDictionary<int, bool> _filtersWithOption = new Dictionary<int, bool>();
private bool _isPkeyPauseSupported = false;
+ private bool _isLowPriorityHwDecodeSupported = false;
private bool _isVaapiDeviceAmd = false;
private bool _isVaapiDeviceInteliHD = false;
@@ -194,6 +195,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_threads = EncodingHelper.GetNumberOfThreads(null, options, null);
_isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding");
+ _isLowPriorityHwDecodeSupported = validator.CheckSupportedHwaccelFlag("low_priority");
// Check the Vaapi device vendor
if (OperatingSystem.IsLinux()
@@ -813,12 +815,28 @@ namespace MediaBrowser.MediaEncoding.Encoder
int? threads,
int? qualityScale,
ProcessPriorityClass? priority,
+ bool enableKeyFrameOnlyExtraction,
EncodingHelper encodingHelper,
CancellationToken cancellationToken)
{
var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions();
threads ??= _threads;
+ if (allowHwAccel && enableKeyFrameOnlyExtraction)
+ {
+ var supportsKeyFrameOnly = (string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && options.EnableEnhancedNvdecDecoder)
+ || (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && OperatingSystem.IsWindows())
+ || (string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && options.PreferSystemNativeHwDecoder)
+ || string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase);
+ if (!supportsKeyFrameOnly)
+ {
+ // Disable hardware acceleration when the hardware decoder does not support keyframe only mode.
+ allowHwAccel = false;
+ options = new EncodingOptions();
+ }
+ }
+
// A new EncodingOptions instance must be used as to not disable HW acceleration for all of Jellyfin.
// Additionally, we must set a few fields without defaults to prevent null pointer exceptions.
if (!allowHwAccel)
@@ -868,6 +886,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
inputArg = "-threads " + threads + " " + inputArg; // HW accel args set a different input thread count, only set if disabled
}
+ if (options.HardwareAccelerationType.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase) && _isLowPriorityHwDecodeSupported)
+ {
+ // VideoToolbox supports low priority decoding, which is useful for trickplay
+ inputArg = "-hwaccel_flags +low_priority " + inputArg;
+ }
+
+ if (enableKeyFrameOnlyExtraction)
+ {
+ inputArg = "-skip_frame nokey " + inputArg;
+ }
+
var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, vidEncoder).Trim();
if (string.IsNullOrWhiteSpace(filterParam))
{
@@ -900,6 +929,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
encoderQuality = (100 - ((qualityScale - 1) * (100 / 30))) / 118;
}
+ if (vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase))
+ {
+ // videotoolbox's mjpeg encoder uses jpeg quality scaled to QP2LAMBDA (118) instead of ffmpeg defined qscale
+ // ffmpeg qscale is a value from 1-31, with 1 being best quality and 31 being worst
+ // jpeg quality is a value from 0-100, with 0 being worst quality and 100 being best
+ encoderQuality = 118 - ((qualityScale - 1) * (118 / 30));
+ }
+
// Output arguments
var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(targetDirectory);
@@ -908,12 +945,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
// Final command arguments
var args = string.Format(
CultureInfo.InvariantCulture,
- "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}-f {5} \"{6}\"",
+ "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}{5}-f {6} \"{7}\"",
inputArg,
filterParam,
outputThreads.GetValueOrDefault(_threads),
vidEncoder,
qualityScale.HasValue ? "-qscale:v " + encoderQuality.Value.ToString(CultureInfo.InvariantCulture) + " " : string.Empty,
+ vidEncoder.Contains("videotoolbox", StringComparison.InvariantCultureIgnoreCase) ? "-allow_sw 1 " : string.Empty, // allow_sw fallback for some intel macs
"image2",
outputPath);
@@ -1212,7 +1250,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
// Add file path stanza to concat configuration
- sw.WriteLine("file '{0}'", path);
+ sw.WriteLine("file '{0}'", path.Replace("'", @"'\''", StringComparison.Ordinal));
// Add duration stanza to concat configuration
sw.WriteLine("duration {0}", duration);
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 3aafb733d..5a5eb6e61 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -721,6 +721,8 @@ namespace MediaBrowser.MediaEncoding.Probing
if (streamInfo.CodecType == CodecType.Audio)
{
stream.Type = MediaStreamType.Audio;
+ stream.LocalizedDefault = _localization.GetLocalizedString("Default");
+ stream.LocalizedExternal = _localization.GetLocalizedString("External");
stream.Channels = streamInfo.Channels;
diff --git a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
index 0b09e57b5..67a2dddb8 100644
--- a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
+++ b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
@@ -470,6 +470,11 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
: "FFmpeg.DirectStream-";
}
+ if (state.VideoRequest is null && EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
+ {
+ logFilePrefix = "FFmpeg.Remux-";
+ }
+
var logFilePath = Path.Combine(
_serverConfigurationManager.ApplicationPaths.LogDirectoryPath,
$"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log");
diff --git a/MediaBrowser.Model/Configuration/TrickplayOptions.cs b/MediaBrowser.Model/Configuration/TrickplayOptions.cs
index a151d3429..578bb306a 100644
--- a/MediaBrowser.Model/Configuration/TrickplayOptions.cs
+++ b/MediaBrowser.Model/Configuration/TrickplayOptions.cs
@@ -19,6 +19,12 @@ public class TrickplayOptions
public bool EnableHwEncoding { get; set; } = false;
/// <summary>
+ /// Gets or sets a value indicating whether to only extract key frames.
+ /// Significantly faster, but is not compatible with all decoders and/or video files.
+ /// </summary>
+ public bool EnableKeyFrameOnlyExtraction { get; set; } = false;
+
+ /// <summary>
/// Gets or sets the behavior used by trickplay provider on library scan/update.
/// </summary>
public TrickplayScanBehavior ScanBehavior { get; set; } = TrickplayScanBehavior.NonBlocking;
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index 55d1c3d51..4815dcc04 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -25,7 +25,7 @@ namespace MediaBrowser.Model.Dlna
private readonly ILogger _logger;
private readonly ITranscoderSupport _transcoderSupport;
- private static readonly string[] _supportedHlsVideoCodecs = new string[] { "h264", "hevc", "av1" };
+ private static readonly string[] _supportedHlsVideoCodecs = new string[] { "h264", "hevc", "vp9", "av1" };
private static readonly string[] _supportedHlsAudioCodecsTs = new string[] { "aac", "ac3", "eac3", "mp3" };
private static readonly string[] _supportedHlsAudioCodecsMp4 = new string[] { "aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd" };
@@ -108,7 +108,7 @@ namespace MediaBrowser.Model.Dlna
var inputAudioSampleRate = audioStream?.SampleRate;
var inputAudioBitDepth = audioStream?.BitDepth;
- if (directPlayMethod.HasValue)
+ if (directPlayMethod is PlayMethod.DirectPlay)
{
var profile = options.Profile;
var audioFailureConditions = GetProfileConditionsForAudio(profile.CodecProfiles, item.Container, audioStream?.Codec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, true);
@@ -124,6 +124,46 @@ namespace MediaBrowser.Model.Dlna
}
}
+ if (directPlayMethod is PlayMethod.DirectStream)
+ {
+ var remuxContainer = item.TranscodingContainer ?? "ts";
+ var supportedHlsContainers = new[] { "ts", "mp4" };
+ // If the container specified for the profile is an HLS supported container, use that container instead, overriding the preference
+ // The client should be responsible to ensure this container is compatible
+ remuxContainer = Array.Exists(supportedHlsContainers, element => string.Equals(element, directPlayInfo.Profile?.Container, StringComparison.OrdinalIgnoreCase)) ? directPlayInfo.Profile?.Container : remuxContainer;
+ bool codeIsSupported;
+ if (item.TranscodingSubProtocol == MediaStreamProtocol.hls)
+ {
+ // Enforce HLS audio codec restrictions
+ if (string.Equals(remuxContainer, "mp4", StringComparison.OrdinalIgnoreCase))
+ {
+ codeIsSupported = _supportedHlsAudioCodecsMp4.Contains(directPlayInfo.Profile?.AudioCodec ?? directPlayInfo.Profile?.Container);
+ }
+ else
+ {
+ codeIsSupported = _supportedHlsAudioCodecsTs.Contains(directPlayInfo.Profile?.AudioCodec ?? directPlayInfo.Profile?.Container);
+ }
+ }
+ else
+ {
+ // Let's assume the client has given a correct container for http
+ codeIsSupported = true;
+ }
+
+ if (codeIsSupported)
+ {
+ playlistItem.PlayMethod = directPlayMethod.Value;
+ playlistItem.Container = remuxContainer;
+ playlistItem.TranscodeReasons = transcodeReasons;
+ playlistItem.SubProtocol = item.TranscodingSubProtocol;
+ item.TranscodingContainer = remuxContainer;
+ return playlistItem;
+ }
+
+ transcodeReasons |= TranscodeReason.AudioCodecNotSupported;
+ playlistItem.TranscodeReasons = transcodeReasons;
+ }
+
TranscodingProfile? transcodingProfile = null;
foreach (var tcProfile in options.Profile.TranscodingProfiles)
{
@@ -379,6 +419,7 @@ namespace MediaBrowser.Model.Dlna
var directPlayProfile = options.Profile.DirectPlayProfiles
.FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream));
+ TranscodeReason transcodeReasons = 0;
if (directPlayProfile is null)
{
_logger.LogDebug(
@@ -387,14 +428,25 @@ namespace MediaBrowser.Model.Dlna
item.Path ?? "Unknown path",
audioStream.Codec ?? "Unknown codec");
- return (null, null, GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles));
- }
+ var directStreamProfile = options.Profile.DirectPlayProfiles
+ .FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectStreamSupported(x, item, audioStream));
- TranscodeReason transcodeReasons = 0;
+ if (directStreamProfile is not null)
+ {
+ directPlayProfile = directStreamProfile;
+ transcodeReasons |= TranscodeReason.ContainerNotSupported;
+ }
+ else
+ {
+ return (null, null, GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles));
+ }
+ }
// The profile describes what the device supports
// If device requirements are satisfied then allow both direct stream and direct play
- if (item.SupportsDirectPlay)
+ // Note: As of 10.10 codebase, SupportsDirectPlay is always true because the MediaSourceInfo initializes this key as true
+ // Need to check additionally for current transcode reasons
+ if (item.SupportsDirectPlay && transcodeReasons == 0)
{
if (!IsBitrateLimitExceeded(item, options.GetMaxBitrate(true) ?? 0))
{
@@ -414,7 +466,10 @@ namespace MediaBrowser.Model.Dlna
{
if (!IsBitrateLimitExceeded(item, options.GetMaxBitrate(true) ?? 0))
{
- if (options.EnableDirectStream)
+ // Note: as of 10.10 codebase, the options.EnableDirectStream is always false due to
+ // "direct-stream http streaming is currently broken"
+ // Don't check that option for audio as we always assume that is supported
+ if (transcodeReasons == TranscodeReason.ContainerNotSupported)
{
return (directPlayProfile, PlayMethod.DirectStream, transcodeReasons);
}
@@ -542,6 +597,7 @@ namespace MediaBrowser.Model.Dlna
playlistItem.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
playlistItem.BreakOnNonKeyFrames = transcodingProfile.BreakOnNonKeyFrames;
+ playlistItem.EnableAudioVbrEncoding = transcodingProfile.EnableAudioVbrEncoding;
if (transcodingProfile.MinSegments > 0)
{
@@ -852,7 +908,18 @@ namespace MediaBrowser.Model.Dlna
}
}
- var directAudioStream = candidateAudioStreams.FirstOrDefault(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec));
+ var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec)).FirstOrDefault();
+
+ var directAudioStream = audioStreamWithSupportedCodec?.Channels is not null && audioStreamWithSupportedCodec.Channels.Value <= (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue) ? audioStreamWithSupportedCodec : null;
+
+ var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && directAudioStream is null;
+
+ if (channelsExceedsLimit && playlistItem.TargetAudioStream is not null)
+ {
+ playlistItem.TranscodeReasons |= TranscodeReason.AudioChannelsNotSupported;
+ playlistItem.TargetAudioStream.Channels = playlistItem.TranscodingMaxAudioChannels;
+ }
+
playlistItem.AudioCodecs = audioCodecs;
if (directAudioStream is not null)
{
@@ -915,7 +982,7 @@ namespace MediaBrowser.Model.Dlna
}
// Honor requested max channels
- playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels;
+ playlistItem.GlobalMaxAudioChannels = channelsExceedsLimit ? playlistItem.TranscodingMaxAudioChannels : options.MaxAudioChannels;
int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(true) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem);
playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate);
@@ -2129,5 +2196,24 @@ namespace MediaBrowser.Model.Dlna
return true;
}
+
+ private static bool IsAudioDirectStreamSupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
+ {
+ // Check container type, this should NOT be supported
+ // If the container is supported, the file should be directly played
+ if (!profile.SupportsContainer(item.Container))
+ {
+ // Check audio codec, we cannot use the SupportsAudioCodec here
+ // Because that one assumes empty container supports all codec, which is just useless
+ string? audioCodec = audioStream?.Codec;
+ if (string.Equals(profile.AudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(profile.Container, audioCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
}
diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
index 75e5b6d18..c8a341d41 100644
--- a/MediaBrowser.Model/Dlna/StreamInfo.cs
+++ b/MediaBrowser.Model/Dlna/StreamInfo.cs
@@ -108,6 +108,8 @@ namespace MediaBrowser.Model.Dlna
public string? MediaSourceId => MediaSource?.Id;
+ public bool EnableAudioVbrEncoding { get; set; }
+
public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay)
&& PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay;
@@ -768,6 +770,8 @@ namespace MediaBrowser.Model.Dlna
}
list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+
+ list.Add(new NameValuePair("EnableAudioVbrEncoding", item.EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
}
list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty));
diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs
index 891448c66..a556799de 100644
--- a/MediaBrowser.Model/Dlna/TranscodingProfile.cs
+++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs
@@ -70,6 +70,10 @@ namespace MediaBrowser.Model.Dlna
public ProfileCondition[] Conditions { get; set; }
+ [DefaultValue(true)]
+ [XmlAttribute("enableAudioVbrEncoding")]
+ public bool EnableAudioVbrEncoding { get; set; } = true;
+
public string[] GetAudioCodecs()
{
return ContainerProfile.SplitValue(AudioCodec);
diff --git a/MediaBrowser.Model/Providers/ExternalIdInfo.cs b/MediaBrowser.Model/Providers/ExternalIdInfo.cs
index d026d574f..1f5163aa8 100644
--- a/MediaBrowser.Model/Providers/ExternalIdInfo.cs
+++ b/MediaBrowser.Model/Providers/ExternalIdInfo.cs
@@ -1,3 +1,5 @@
+using System;
+
namespace MediaBrowser.Model.Providers
{
/// <summary>
@@ -17,7 +19,9 @@ namespace MediaBrowser.Model.Providers
Name = name;
Key = key;
Type = type;
+#pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11
UrlFormatString = urlFormatString;
+#pragma warning restore CS0618 // Type or member is obsolete
}
/// <summary>
@@ -46,6 +50,7 @@ namespace MediaBrowser.Model.Providers
/// <summary>
/// Gets or sets the URL format string.
/// </summary>
+ [Obsolete("Obsolete in 10.10, to be removed in 10.11")]
public string? UrlFormatString { get; set; }
}
}
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index f2ca99da6..60d89a51b 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -69,11 +69,12 @@ namespace MediaBrowser.Providers.Manager
o.PoolInitialFill = 1;
});
- private IImageProvider[] _imageProviders = Array.Empty<IImageProvider>();
- private IMetadataService[] _metadataServices = Array.Empty<IMetadataService>();
- private IMetadataProvider[] _metadataProviders = Array.Empty<IMetadataProvider>();
- private IMetadataSaver[] _savers = Array.Empty<IMetadataSaver>();
- private IExternalId[] _externalIds = Array.Empty<IExternalId>();
+ private IImageProvider[] _imageProviders = [];
+ private IMetadataService[] _metadataServices = [];
+ private IMetadataProvider[] _metadataProviders = [];
+ private IMetadataSaver[] _savers = [];
+ private IExternalId[] _externalIds = [];
+ private IExternalUrlProvider[] _externalUrlProviders = [];
private bool _isProcessingRefreshQueue;
private bool _disposed;
@@ -132,12 +133,14 @@ namespace MediaBrowser.Providers.Manager
IEnumerable<IMetadataService> metadataServices,
IEnumerable<IMetadataProvider> metadataProviders,
IEnumerable<IMetadataSaver> metadataSavers,
- IEnumerable<IExternalId> externalIds)
+ IEnumerable<IExternalId> externalIds,
+ IEnumerable<IExternalUrlProvider> externalUrlProviders)
{
_imageProviders = imageProviders.ToArray();
_metadataServices = metadataServices.OrderBy(i => i.Order).ToArray();
_metadataProviders = metadataProviders.ToArray();
_externalIds = externalIds.OrderBy(i => i.ProviderName).ToArray();
+ _externalUrlProviders = externalUrlProviders.OrderBy(i => i.Name).ToArray();
_savers = metadataSavers.ToArray();
}
@@ -877,31 +880,35 @@ namespace MediaBrowser.Providers.Manager
/// <inheritdoc/>
public IEnumerable<ExternalUrl> GetExternalUrls(BaseItem item)
{
- return GetExternalIds(item)
+#pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11
+ var legacyExternalIdUrls = GetExternalIds(item)
.Select(i =>
- {
- if (string.IsNullOrEmpty(i.UrlFormatString))
{
- return null;
- }
+ var urlFormatString = i.UrlFormatString;
+ if (string.IsNullOrEmpty(urlFormatString)
+ || !item.TryGetProviderId(i.Key, out var providerId))
+ {
+ return null;
+ }
- var value = item.GetProviderId(i.Key);
+ return new ExternalUrl
+ {
+ Name = i.ProviderName,
+ Url = string.Format(
+ CultureInfo.InvariantCulture,
+ urlFormatString,
+ providerId)
+ };
+ })
+ .OfType<ExternalUrl>();
+#pragma warning restore CS0618 // Type or member is obsolete
- if (string.IsNullOrEmpty(value))
- {
- return null;
- }
+ var externalUrls = _externalUrlProviders
+ .SelectMany(p => p
+ .GetExternalUrls(item)
+ .Select(externalUrl => new ExternalUrl { Name = p.Name, Url = externalUrl }));
- return new ExternalUrl
- {
- Name = i.ProviderName,
- Url = string.Format(
- CultureInfo.InvariantCulture,
- i.UrlFormatString,
- value)
- };
- }).Where(i => i is not null)
- .Concat(item.GetRelatedUrls())!; // We just filtered out all the nulls
+ return legacyExternalIdUrls.Concat(externalUrls).OrderBy(u => u.Name);
}
/// <inheritdoc/>
@@ -912,7 +919,9 @@ namespace MediaBrowser.Providers.Manager
name: i.ProviderName,
key: i.Key,
type: i.Type,
+#pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11
urlFormatString: i.UrlFormatString));
+#pragma warning restore CS0618 // Type or member is obsolete
}
/// <inheritdoc/>
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
index 020e20fb8..612064190 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
@@ -18,7 +18,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
public class ProbeResultNormalizerTests
{
private readonly JsonSerializerOptions _jsonOptions;
- private readonly ProbeResultNormalizer _probeResultNormalizer = new ProbeResultNormalizer(new NullLogger<EncoderValidatorTests>(), null);
+ private readonly ProbeResultNormalizer _probeResultNormalizer = new ProbeResultNormalizer(new NullLogger<EncoderValidatorTests>(), new Mock<ILocalizationManager>().Object);
public ProbeResultNormalizerTests()
{
diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
index 6d88dbb8e..31ddd427c 100644
--- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
+++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
@@ -51,8 +51,8 @@ namespace Jellyfin.Model.Tests
[InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
- [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
- [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
+ [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450
// AndroidPixel
[InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("AndroidPixel", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
@@ -205,8 +205,8 @@ namespace Jellyfin.Model.Tests
[InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
- [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
- [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
+ [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450
// AndroidPixel
[InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("AndroidPixel", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450
@@ -432,7 +432,14 @@ namespace Jellyfin.Model.Tests
if (targetAudioStream?.IsExternal == false)
{
// Check expected audio codecs (1)
- Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs);
+ if ((why & TranscodeReason.AudioChannelsNotSupported) == 0)
+ {
+ Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs);
+ }
+ else
+ {
+ Assert.Equal(targetAudioStream.Channels, streamInfo.TranscodingMaxAudioChannels);
+ }
}
}
else if (transcodeMode.Equals("Remux", StringComparison.Ordinal))
diff --git a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs
index 6fccce049..cced2b1e2 100644
--- a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs
+++ b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs
@@ -585,15 +585,17 @@ namespace Jellyfin.Providers.Tests.Manager
IEnumerable<IMetadataService>? metadataServices = null,
IEnumerable<IMetadataProvider>? metadataProviders = null,
IEnumerable<IMetadataSaver>? metadataSavers = null,
- IEnumerable<IExternalId>? externalIds = null)
+ IEnumerable<IExternalId>? externalIds = null,
+ IEnumerable<IExternalUrlProvider>? externalUrlProviders = null)
{
imageProviders ??= Array.Empty<IImageProvider>();
metadataServices ??= Array.Empty<IMetadataService>();
metadataProviders ??= Array.Empty<IMetadataProvider>();
metadataSavers ??= Array.Empty<IMetadataSaver>();
externalIds ??= Array.Empty<IExternalId>();
+ externalUrlProviders ??= Array.Empty<IExternalUrlProvider>();
- providerManager.AddParts(imageProviders, metadataServices, metadataProviders, metadataSavers, externalIds);
+ providerManager.AddParts(imageProviders, metadataServices, metadataProviders, metadataSavers, externalIds, externalUrlProviders);
}
/// <summary>