diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index 1618237f1a..c28b1bf7f0 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -47,7 +47,7 @@ jobs:
displayName: Set release version (stable)
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
+ - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) --label "org.opencontainers.image.url=$(Build.Repository.Uri)" --label "org.opencontainers.image.revision=$(Build.SourceVersion)" deployment'
displayName: 'Build Dockerfile'
- script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 28059fe845..c3532467af 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -23,13 +23,13 @@
-
-
+
+
-
-
-
-
+
+
+
+
@@ -38,14 +38,14 @@
-
-
+
+
-
+
@@ -64,9 +64,11 @@
-
+
+
+
diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs
index 1796830552..b34d0f21ef 100644
--- a/Emby.Server.Implementations/Collections/CollectionManager.cs
+++ b/Emby.Server.Implementations/Collections/CollectionManager.cs
@@ -112,7 +112,8 @@ namespace Emby.Server.Implementations.Collections
return Path.Combine(_appPaths.DataPath, "collections");
}
- private Task GetCollectionsFolder(bool createIfNeeded)
+ ///
+ public Task GetCollectionsFolder(bool createIfNeeded)
{
return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index bcb42e1626..acf3964c8c 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -30,12 +30,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
{
- private static readonly string[] _disallowedSharedStreamExtensions =
+ private static readonly string[] _disallowedMimeTypes =
{
- ".mkv",
- ".mp4",
- ".m3u8",
- ".mpd"
+ "video/x-matroska",
+ "video/mp4",
+ "application/vnd.apple.mpegurl",
+ "application/mpegurl",
+ "application/x-mpegurl",
+ "video/vnd.mpeg.dash.mpd"
};
private readonly IHttpClientFactory _httpClientFactory;
@@ -118,9 +120,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping)
{
- var extension = Path.GetExtension(mediaSource.Path) ?? string.Empty;
+ using var message = new HttpRequestMessage(HttpMethod.Head, mediaSource.Path);
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .SendAsync(message, cancellationToken)
+ .ConfigureAwait(false);
- if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
+ response.EnsureSuccessStatusCode();
+
+ if (!_disallowedMimeTypes.Contains(response.Content.Headers.ContentType?.ToString(), StringComparison.OrdinalIgnoreCase))
{
return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
index e84e1e074c..51f46f4dac 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -38,7 +38,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
_httpClientFactory = httpClientFactory;
_appHost = appHost;
OriginalStreamId = originalStreamId;
- EnableStreamSharing = true;
}
public override async Task Open(CancellationToken openCancellationToken)
@@ -59,39 +58,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
.ConfigureAwait(false);
- var contentType = response.Content.Headers.ContentType?.ToString() ?? string.Empty;
- if (contentType.Contains("matroska", StringComparison.OrdinalIgnoreCase)
- || contentType.Contains("mp4", StringComparison.OrdinalIgnoreCase)
- || contentType.Contains("dash", StringComparison.OrdinalIgnoreCase)
- || contentType.Contains("mpegURL", StringComparison.OrdinalIgnoreCase)
- || contentType.Contains("text/", StringComparison.OrdinalIgnoreCase))
- {
- // Close the stream without any sharing features
- response.Dispose();
- return;
- }
-
- SetTempFilePath("ts");
-
var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
_ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token);
- // OpenedMediaSource.Protocol = MediaProtocol.File;
- // OpenedMediaSource.Path = tempFile;
- // OpenedMediaSource.ReadAtNativeFramerate = true;
-
MediaSource.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
MediaSource.Protocol = MediaProtocol.Http;
- // OpenedMediaSource.Path = TempFilePath;
- // OpenedMediaSource.Protocol = MediaProtocol.File;
-
- // OpenedMediaSource.Path = _tempFilePath;
- // OpenedMediaSource.Protocol = MediaProtocol.File;
- // OpenedMediaSource.SupportsDirectPlay = false;
- // OpenedMediaSource.SupportsDirectStream = true;
- // OpenedMediaSource.SupportsTranscoding = true;
var res = await taskCompletionSource.Task.ConfigureAwait(false);
if (!res)
{
@@ -108,15 +81,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
try
{
Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath);
- using var message = response;
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
- await StreamHelper.CopyToAsync(
- stream,
- fileStream,
- IODefaults.CopyToBufferSize,
- () => Resolve(openTaskCompletionSource),
- cancellationToken).ConfigureAwait(false);
+ using (response)
+ {
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
+ await StreamHelper.CopyToAsync(
+ stream,
+ fileStream,
+ IODefaults.CopyToBufferSize,
+ () => Resolve(openTaskCompletionSource),
+ cancellationToken).ConfigureAwait(false);
+ }
}
catch (OperationCanceledException ex)
{
diff --git a/Emby.Server.Implementations/Localization/Core/ne.json b/Emby.Server.Implementations/Localization/Core/ne.json
index 4c8e820a5c..7c6b08fb36 100644
--- a/Emby.Server.Implementations/Localization/Core/ne.json
+++ b/Emby.Server.Implementations/Localization/Core/ne.json
@@ -109,5 +109,19 @@
"Sync": "समकालीन",
"SubtitleDownloadFailureFromForItem": "उपशीर्षकहरू {0} बाट {1} को लागि डाउनलोड गर्न असफल",
"PluginUpdatedWithName": "{0} अद्यावधिक गरिएको थियो",
- "PluginUninstalledWithName": "{0} को स्थापना रद्द गरिएको थियो"
+ "PluginUninstalledWithName": "{0} को स्थापना रद्द गरिएको थियो",
+ "HearingImpaired": "सुन्न नसक्ने",
+ "TaskUpdatePluginsDescription": "स्वचालित रूपमा अद्यावधिक गर्न कन्फिगर गरिएका प्लगइनहरूका लागि अद्यावधिकहरू डाउनलोड र स्थापना गर्दछ।",
+ "TaskCleanTranscode": "सफा ट्रान्सकोड निर्देशिका",
+ "TaskCleanTranscodeDescription": "एक दिन भन्दा पुराना ट्रान्सकोड फाइलहरू मेटाउँछ।",
+ "TaskRefreshChannels": "च्यानलहरू ताजा गर्नुहोस्",
+ "TaskDownloadMissingSubtitlesDescription": "मेटाडेटा कन्फिगरेसनमा आधारित हराइरहेको उपशीर्षकहरूको लागि इन्टरनेट खोज्छ।",
+ "TaskOptimizeDatabase": "डेटाबेस अप्टिमाइज गर्नुहोस्",
+ "TaskOptimizeDatabaseDescription": "डाटाबेस कम्प्याक्ट र खाली ठाउँ काट्छ। पुस्तकालय स्क्यान गरेपछि वा डाटाबेस परिमार्जनलाई संकेत गर्ने अन्य परिवर्तनहरू गरेपछि यो कार्य चलाउँदा कार्यसम्पादनमा सुधार हुन सक्छ।",
+ "TaskKeyframeExtractorDescription": "थप सटीक एचएलएस प्लेलिस्टहरू सिर्जना गर्न भिडियो फाइलहरूबाट कीफ्रेमहरू निकाल्छ। यो कार्य लामो समय सम्म चल्न सक्छ।",
+ "TaskUpdatePlugins": "प्लगइनहरू अपडेट गर्नुहोस्",
+ "TaskRefreshPeopleDescription": "तपाईंको मिडिया लाइब्रेरीमा अभिनेता र निर्देशकहरूको लागि मेटाडेटा अपडेट गर्दछ।",
+ "TaskRefreshChannelsDescription": "इन्टरनेट च्यानल जानकारी ताजा गर्दछ।",
+ "TaskDownloadMissingSubtitles": "छुटेका उपशीर्षकहरू डाउनलोड गर्नुहोस्",
+ "TaskKeyframeExtractor": "कीफ्रेम एक्स्ट्रक्टर"
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs
new file mode 100644
index 0000000000..f78fc6f970
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Collections;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
+
+///
+/// Deletes Path references from collections that no longer exists.
+///
+public class CleanupCollectionPathsTask : IScheduledTask
+{
+ private readonly ILocalizationManager _localization;
+ private readonly ICollectionManager _collectionManager;
+ private readonly ILogger _logger;
+ private readonly IProviderManager _providerManager;
+ private readonly IFileSystem _fileSystem;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// The logger.
+ /// The provider manager.
+ /// The filesystem.
+ public CleanupCollectionPathsTask(
+ ILocalizationManager localization,
+ ICollectionManager collectionManager,
+ ILogger logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem)
+ {
+ _localization = localization;
+ _collectionManager = collectionManager;
+ _logger = logger;
+ _providerManager = providerManager;
+ _fileSystem = fileSystem;
+ }
+
+ ///
+ public string Name => _localization.GetLocalizedString("TaskCleanCollections");
+
+ ///
+ public string Key => "CleanCollections";
+
+ ///
+ public string Description => _localization.GetLocalizedString("TaskCleanCollectionsDescription");
+
+ ///
+ public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+
+ ///
+ public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken)
+ {
+ var collectionsFolder = await _collectionManager.GetCollectionsFolder(false).ConfigureAwait(false);
+ if (collectionsFolder is null)
+ {
+ _logger.LogDebug("There is no collection folder to be found");
+ return;
+ }
+
+ var collections = collectionsFolder.Children.OfType().ToArray();
+ _logger.LogDebug("Found {CollectionLength} Boxsets", collections.Length);
+
+ var itemsToRemove = new List();
+ for (var index = 0; index < collections.Length; index++)
+ {
+ var collection = collections[index];
+ _logger.LogDebug("Check Boxset {CollectionName}", collection.Name);
+
+ foreach (var collectionLinkedChild in collection.LinkedChildren)
+ {
+ if (!File.Exists(collectionLinkedChild.Path))
+ {
+ _logger.LogInformation("Item in boxset {CollectionName} cannot be found at {ItemPath}", collection.Name, collectionLinkedChild.Path);
+ itemsToRemove.Add(collectionLinkedChild);
+ }
+ }
+
+ if (itemsToRemove.Count != 0)
+ {
+ _logger.LogDebug("Update Boxset {CollectionName}", collection.Name);
+ collection.LinkedChildren = collection.LinkedChildren.Except(itemsToRemove).ToArray();
+ await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken)
+ .ConfigureAwait(false);
+
+ _providerManager.QueueRefresh(
+ collection.Id,
+ new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+ {
+ ForceSave = true
+ },
+ RefreshPriority.High);
+
+ itemsToRemove.Clear();
+ }
+
+ progress.Report(100D / collections.Length * (index + 1));
+ }
+ }
+
+ ///
+ public IEnumerable GetDefaultTriggers()
+ {
+ return new[] { new TaskTriggerInfo() { Type = TaskTriggerInfo.TriggerStartup } };
+ }
+}
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index acd8111435..4b89738a1b 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -12,6 +12,7 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using Jellyfin.MediaEncoding.Hls.Playlist;
using MediaBrowser.Common.Configuration;
@@ -1841,7 +1842,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
{
if (EncodingHelper.IsCopyCodec(codec)
- && (string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase)
+ && (state.VideoStream.VideoRangeType == VideoRangeType.DOVI
|| string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase)))
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 7650b861f4..80128536da 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -256,8 +256,7 @@ public class ItemsController : BaseJellyfinApiController
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
if (includeItemTypes.Length == 1
- && (includeItemTypes[0] == BaseItemKind.Playlist
- || includeItemTypes[0] == BaseItemKind.BoxSet))
+ && includeItemTypes[0] == BaseItemKind.BoxSet)
{
parentId = null;
}
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index bd30910558..45e0d475d7 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
@@ -216,9 +217,9 @@ public class DynamicHlsHelper
// Provide SDR HEVC entrance for backward compatibility.
if (encodingOptions.AllowHevcEncoding
+ && !encodingOptions.AllowAv1Encoding
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
- && !string.IsNullOrEmpty(state.VideoStream.VideoRange)
- && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
+ && state.VideoStream.VideoRange == VideoRange.HDR
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{
var requestedVideoProfiles = state.GetRequestedProfiles("hevc");
@@ -258,11 +259,12 @@ public class DynamicHlsHelper
// Provide Level 5.0 entrance for backward compatibility.
// e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
// but in fact it is capable of playing videos up to Level 6.1.
- if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ if (encodingOptions.AllowHevcEncoding
+ && !encodingOptions.AllowAv1Encoding
+ && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& state.VideoStream.Level.HasValue
&& state.VideoStream.Level > 150
- && !string.IsNullOrEmpty(state.VideoStream.VideoRange)
- && string.Equals(state.VideoStream.VideoRange, "SDR", StringComparison.OrdinalIgnoreCase)
+ && state.VideoStream.VideoRange == VideoRange.SDR
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{
var playlistCodecsField = new StringBuilder();
@@ -353,17 +355,17 @@ public class DynamicHlsHelper
/// StreamState of the current stream.
private void AppendPlaylistVideoRangeField(StringBuilder builder, StreamState state)
{
- if (state.VideoStream is not null && !string.IsNullOrEmpty(state.VideoStream.VideoRange))
+ if (state.VideoStream is not null && state.VideoStream.VideoRange != VideoRange.Unknown)
{
var videoRange = state.VideoStream.VideoRange;
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
{
- if (string.Equals(videoRange, "SDR", StringComparison.OrdinalIgnoreCase))
+ if (videoRange == VideoRange.SDR)
{
builder.Append(",VIDEO-RANGE=SDR");
}
- if (string.Equals(videoRange, "HDR", StringComparison.OrdinalIgnoreCase))
+ if (videoRange == VideoRange.HDR)
{
builder.Append(",VIDEO-RANGE=PQ");
}
@@ -603,6 +605,12 @@ public class DynamicHlsHelper
levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120";
levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
}
+
+ if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
+ {
+ levelString = state.GetRequestedLevel("av1") ?? "19";
+ levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
+ }
}
if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
@@ -614,11 +622,11 @@ public class DynamicHlsHelper
}
///
- /// Get the H.26X profile of the output video stream.
+ /// Get the profile of the output video stream.
///
/// StreamState of the current stream.
/// Video codec.
- /// H.26X profile of the output video stream.
+ /// Profile of the output video stream.
private string GetOutputVideoCodecProfile(StreamState state, string codec)
{
string profileString = string.Empty;
@@ -636,7 +644,8 @@ public class DynamicHlsHelper
}
if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
{
profileString ??= "main";
}
@@ -706,9 +715,9 @@ public class DynamicHlsHelper
{
if (level == 0)
{
- // This is 0 when there's no requested H.26X level in the device profile
- // and the source is not encoded in H.26X
- _logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist");
+ // This is 0 when there's no requested level in the device profile
+ // and the source is not encoded in H.26X or AV1
+ _logger.LogError("Got invalid level when building CODECS field for HLS master playlist");
return string.Empty;
}
@@ -725,6 +734,22 @@ public class DynamicHlsHelper
return HlsCodecStringHelpers.GetH265String(profile, level);
}
+ if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
+ {
+ string profile = GetOutputVideoCodecProfile(state, "av1");
+
+ // Currently we only transcode to 8 bits AV1
+ int bitDepth = 8;
+ if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ && state.VideoStream != null
+ && state.VideoStream.BitDepth.HasValue)
+ {
+ bitDepth = state.VideoStream.BitDepth.Value;
+ }
+
+ return HlsCodecStringHelpers.GetAv1String(profile, level, false, bitDepth);
+ }
+
return string.Empty;
}
diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
index 995488397e..9a141a16d9 100644
--- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
@@ -179,4 +179,62 @@ public static class HlsCodecStringHelpers
return result.ToString();
}
+
+ ///
+ /// Gets an AV1 codec string.
+ ///
+ /// AV1 profile.
+ /// AV1 level.
+ /// AV1 tier flag.
+ /// AV1 bit depth.
+ /// The AV1 codec string.
+ public static string GetAv1String(string? profile, int level, bool tierFlag, int bitDepth)
+ {
+ // https://aomedia.org/av1/specification/annex-a/
+ // FORMAT: [codecTag].[profile].[level][tier].[bitDepth]
+ StringBuilder result = new StringBuilder("av01", 13);
+
+ if (string.Equals(profile, "Main", StringComparison.OrdinalIgnoreCase))
+ {
+ result.Append(".0");
+ }
+ else if (string.Equals(profile, "High", StringComparison.OrdinalIgnoreCase))
+ {
+ result.Append(".1");
+ }
+ else if (string.Equals(profile, "Professional", StringComparison.OrdinalIgnoreCase))
+ {
+ result.Append(".2");
+ }
+ else
+ {
+ // Default to Main
+ result.Append(".0");
+ }
+
+ if (level <= 0
+ || level > 31)
+ {
+ // Default to the maximum defined level 6.3
+ level = 19;
+ }
+
+ if (bitDepth != 8
+ && bitDepth != 10
+ && bitDepth != 12)
+ {
+ // Default to 8 bits
+ bitDepth = 8;
+ }
+
+ result.Append('.')
+ .Append(level)
+ .Append(tierFlag ? 'H' : 'M');
+
+ string bitDepthD2 = bitDepth.ToString("D2", CultureInfo.InvariantCulture);
+ result.Append('.')
+ .Append(bitDepthD2);
+
+ return result.ToString();
+ }
}
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 9c91dcc6fe..782cd65685 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -430,12 +430,17 @@ public static class StreamingHelpers
{
var videoCodec = state.Request.VideoCodec;
- if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
{
return ".ts";
}
+ if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase))
+ {
+ return ".mp4";
+ }
+
if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
{
return ".ogv";
diff --git a/Jellyfin.Data/Enums/VideoRange.cs b/Jellyfin.Data/Enums/VideoRange.cs
new file mode 100644
index 0000000000..5072e5ba3e
--- /dev/null
+++ b/Jellyfin.Data/Enums/VideoRange.cs
@@ -0,0 +1,22 @@
+namespace Jellyfin.Data.Enums;
+
+///
+/// An enum representing video ranges.
+///
+public enum VideoRange
+{
+ ///
+ /// Unknown video range.
+ ///
+ Unknown,
+
+ ///
+ /// SDR video range.
+ ///
+ SDR,
+
+ ///
+ /// HDR video range.
+ ///
+ HDR
+}
diff --git a/Jellyfin.Data/Enums/VideoRangeType.cs b/Jellyfin.Data/Enums/VideoRangeType.cs
new file mode 100644
index 0000000000..7ac7bc20a3
--- /dev/null
+++ b/Jellyfin.Data/Enums/VideoRangeType.cs
@@ -0,0 +1,37 @@
+namespace Jellyfin.Data.Enums;
+
+///
+/// An enum representing types of video ranges.
+///
+public enum VideoRangeType
+{
+ ///
+ /// Unknown video range type.
+ ///
+ Unknown,
+
+ ///
+ /// SDR video range type (8bit).
+ ///
+ SDR,
+
+ ///
+ /// HDR10 video range type (10bit).
+ ///
+ HDR10,
+
+ ///
+ /// HLG video range type (10bit).
+ ///
+ HLG,
+
+ ///
+ /// Dolby Vision video range type (12bit).
+ ///
+ DOVI,
+
+ ///
+ /// HDR10+ video range type (10bit to 16bit).
+ ///
+ HDR10Plus
+}
diff --git a/MediaBrowser.Controller/Collections/ICollectionManager.cs b/MediaBrowser.Controller/Collections/ICollectionManager.cs
index b8c33ee5a0..38a78a67b5 100644
--- a/MediaBrowser.Controller/Collections/ICollectionManager.cs
+++ b/MediaBrowser.Controller/Collections/ICollectionManager.cs
@@ -56,5 +56,12 @@ namespace MediaBrowser.Controller.Collections
/// The user.
/// IEnumerable{BaseItem}.
IEnumerable CollapseItemsWithinBoxSets(IEnumerable items, User user);
+
+ ///
+ /// Gets the folder where collections are stored.
+ ///
+ /// Will create the collection folder on the storage if set to true.
+ /// The folder instance referencing the collection storage.
+ Task GetCollectionsFolder(bool createIfNeeded);
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 01b6e31e9a..0c2a85a023 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -46,6 +46,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0);
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3);
+ private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1);
private static readonly string[] _videoProfilesH264 = new[]
{
@@ -65,6 +66,13 @@ namespace MediaBrowser.Controller.MediaEncoding
"Main10"
};
+ private static readonly string[] _videoProfilesAv1 = new[]
+ {
+ "Main",
+ "High",
+ "Professional",
+ };
+
private static readonly HashSet _mp4ContainerNames = new(StringComparer.OrdinalIgnoreCase)
{
"mp4",
@@ -120,12 +128,15 @@ namespace MediaBrowser.Controller.MediaEncoding
}
public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
- => GetH264OrH265Encoder("libx264", "h264", state, encodingOptions);
+ => GetH26xOrAv1Encoder("libx264", "h264", state, encodingOptions);
public string GetH265Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
- => GetH264OrH265Encoder("libx265", "hevc", state, encodingOptions);
+ => GetH26xOrAv1Encoder("libx265", "hevc", state, encodingOptions);
+
+ public string GetAv1Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
+ => GetH26xOrAv1Encoder("libsvtav1", "av1", state, encodingOptions);
- private string GetH264OrH265Encoder(string defaultEncoder, string hwEncoder, EncodingJobInfo state, EncodingOptions encodingOptions)
+ private string GetH26xOrAv1Encoder(string defaultEncoder, string hwEncoder, EncodingJobInfo state, EncodingOptions encodingOptions)
{
// Only use alternative encoders for video files.
// When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
@@ -234,8 +245,8 @@ namespace MediaBrowser.Controller.MediaEncoding
}
if (string.Equals(state.VideoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
- && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
- && string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase))
+ && state.VideoStream.VideoRange == VideoRange.HDR
+ && state.VideoStream.VideoRangeType == VideoRangeType.DOVI)
{
// Only native SW decoder and HW accelerator can parse dovi rpu.
var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty;
@@ -246,9 +257,9 @@ namespace MediaBrowser.Controller.MediaEncoding
return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder;
}
- return string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
- && (string.Equals(state.VideoStream.VideoRangeType, "HDR10", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.VideoStream.VideoRangeType, "HLG", StringComparison.OrdinalIgnoreCase));
+ return state.VideoStream.VideoRange == VideoRange.HDR
+ && (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
+ || state.VideoStream.VideoRangeType == VideoRangeType.HLG);
}
private bool IsVulkanHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
@@ -260,7 +271,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// libplacebo has partial Dolby Vision to SDR tonemapping support.
return options.EnableTonemapping
- && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
+ && state.VideoStream.VideoRange == VideoRange.HDR
&& GetVideoColorBitDepth(state) == 10;
}
@@ -275,8 +286,8 @@ namespace MediaBrowser.Controller.MediaEncoding
// Native VPP tonemapping may come to QSV in the future.
- return string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
- && string.Equals(state.VideoStream.VideoRangeType, "HDR10", StringComparison.OrdinalIgnoreCase);
+ return state.VideoStream.VideoRange == VideoRange.HDR
+ && state.VideoStream.VideoRangeType == VideoRangeType.HDR10;
}
///
@@ -291,6 +302,11 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(codec))
{
+ if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetAv1Encoder(state, encodingOptions);
+ }
+
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
{
@@ -595,6 +611,11 @@ namespace MediaBrowser.Controller.MediaEncoding
return Array.FindIndex(_videoProfilesH265, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase));
}
+ if (string.Equals("av1", videoCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ return Array.FindIndex(_videoProfilesAv1, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase));
+ }
+
return -1;
}
@@ -1234,6 +1255,11 @@ namespace MediaBrowser.Controller.MediaEncoding
return FormattableString.Invariant($" -b:v {bitrate}");
}
+ if (string.Equals(videoCodec, "libsvtav1", StringComparison.OrdinalIgnoreCase))
+ {
+ return FormattableString.Invariant($" -b:v {bitrate} -bufsize {bufsize}");
+ }
+
if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoCodec, "libx265", StringComparison.OrdinalIgnoreCase))
{
@@ -1241,14 +1267,16 @@ namespace MediaBrowser.Controller.MediaEncoding
}
if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "av1_amf", StringComparison.OrdinalIgnoreCase))
{
// Override the too high default qmin 18 in transcoding preset
return FormattableString.Invariant($" -rc cbr -qmin 0 -qmax 32 -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
}
if (string.Equals(videoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoCodec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(videoCodec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "av1_vaapi", StringComparison.OrdinalIgnoreCase))
{
// VBR in i965 driver may result in pixelated output.
if (_mediaEncoder.IsVaapiDeviceInteli965)
@@ -1266,14 +1294,23 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel))
{
- if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
+ {
+ // Transcode to level 5.3 (15) and lower for maximum compatibility.
+ // https://en.wikipedia.org/wiki/AV1#Levels
+ if (requestLevel < 0 || requestLevel >= 15)
+ {
+ return "15";
+ }
+ }
+ else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
{
// Transcode to level 5.0 and lower for maximum compatibility.
// Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it.
// https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
// MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880.
- if (requestLevel >= 150)
+ if (requestLevel < 0 || requestLevel >= 150)
{
return "150";
}
@@ -1283,7 +1320,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// Transcode to level 5.1 and lower for maximum compatibility.
// h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4.
// https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
- if (requestLevel >= 51)
+ if (requestLevel < 0 || requestLevel >= 51)
{
return "51";
}
@@ -1421,14 +1458,18 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(codec, "av1_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "av1_nvenc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "av1_amf", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "libsvtav1", StringComparison.OrdinalIgnoreCase))
{
args += gopArg;
}
else if (string.Equals(codec, "libx264", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "av1_vaapi", StringComparison.OrdinalIgnoreCase))
{
args += keyFrameArg;
@@ -1564,18 +1605,60 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -crf " + defaultCrf;
}
}
+ else if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase))
+ {
+ // Default to use the recommended preset 10.
+ // Omit presets < 5, which are too slow for on the fly encoding.
+ // https://gitlab.com/AOMediaCodec/SVT-AV1/-/blob/master/Docs/Ffmpeg.md
+ param += encodingOptions.EncoderPreset switch
+ {
+ "veryslow" => " -preset 5",
+ "slower" => " -preset 6",
+ "slow" => " -preset 7",
+ "medium" => " -preset 8",
+ "fast" => " -preset 9",
+ "faster" => " -preset 10",
+ "veryfast" => " -preset 11",
+ "superfast" => " -preset 12",
+ "ultrafast" => " -preset 13",
+ _ => " -preset 10"
+ };
+ }
+ else if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "av1_vaapi", StringComparison.OrdinalIgnoreCase))
+ {
+ // -compression_level is not reliable on AMD.
+ if (_mediaEncoder.IsVaapiDeviceInteliHD)
+ {
+ param += encodingOptions.EncoderPreset switch
+ {
+ "veryslow" => " -compression_level 1",
+ "slower" => " -compression_level 2",
+ "slow" => " -compression_level 3",
+ "medium" => " -compression_level 4",
+ "fast" => " -compression_level 5",
+ "faster" => " -compression_level 6",
+ "veryfast" => " -compression_level 7",
+ "superfast" => " -compression_level 7",
+ "ultrafast" => " -compression_level 7",
+ _ => string.Empty
+ };
+ }
+ }
else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv)
- || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_qsv)
+ || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase) // hevc (hevc_qsv)
+ || string.Equals(videoEncoder, "av1_qsv", StringComparison.OrdinalIgnoreCase)) // av1 (av1_qsv)
{
- string[] valid_h264_qsv = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" };
+ string[] valid_presets = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" };
- if (valid_h264_qsv.Contains(encodingOptions.EncoderPreset, StringComparison.OrdinalIgnoreCase))
+ if (valid_presets.Contains(encodingOptions.EncoderPreset, StringComparison.OrdinalIgnoreCase))
{
param += " -preset " + encodingOptions.EncoderPreset;
}
else
{
- param += " -preset 7";
+ param += " -preset veryfast";
}
// Only h264_qsv has look_ahead option
@@ -1585,7 +1668,8 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc)
- || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_nvenc)
+ || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) // hevc (hevc_nvenc)
+ || string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase)) // av1 (av1_nvenc)
{
switch (encodingOptions.EncoderPreset)
{
@@ -1625,7 +1709,8 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf)
- || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_amf)
+ || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) // hevc (hevc_amf)
+ || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase)) // av1 (av1_amf)
{
switch (encodingOptions.EncoderPreset)
{
@@ -1652,9 +1737,15 @@ namespace MediaBrowser.Controller.MediaEncoding
break;
}
+ if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase))
+ {
+ param += " -header_insertion_mode gop";
+ }
+
if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
{
- param += " -header_insertion_mode gop -gops_per_idr 1";
+ param += " -gops_per_idr 1";
}
}
else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // vp8
@@ -1785,6 +1876,14 @@ namespace MediaBrowser.Controller.MediaEncoding
profile = "high";
}
+ // We only need Main profile of AV1 encoders.
+ if (videoEncoder.Contains("av1", StringComparison.OrdinalIgnoreCase)
+ && (profile.Contains("high", StringComparison.OrdinalIgnoreCase)
+ || profile.Contains("professional", StringComparison.OrdinalIgnoreCase)))
+ {
+ profile = "main";
+ }
+
// h264_vaapi does not support Baseline profile, force Constrained Baseline in this case,
// which is compatible (and ugly).
if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
@@ -1852,19 +1951,41 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -level " + (hevcLevel / 3);
}
}
+ else if (string.Equals(videoEncoder, "av1_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase))
+ {
+ // libsvtav1 and av1_qsv use -level 60 instead of -level 16
+ // https://aomedia.org/av1/specification/annex-a/
+ if (int.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out int av1Level))
+ {
+ var x = 2 + (av1Level >> 2);
+ var y = av1Level & 3;
+ var res = (x * 10) + y;
+ param += " -level " + res;
+ }
+ }
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase))
{
param += " -level " + level;
}
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase))
{
// level option may cause NVENC to fail.
// NVENC cannot adjust the given level, just throw an error.
+ }
+ else if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "av1_vaapi", StringComparison.OrdinalIgnoreCase))
+ {
// level option may cause corrupted frames on AMD VAAPI.
+ if (_mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965)
+ {
+ param += " -level " + level;
+ }
}
else if (!string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
{
@@ -1886,6 +2007,12 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -x265-params:0 no-info=1";
}
+ if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)
+ && _mediaEncoder.EncoderVersion >= _minFFmpegSvtAv1Params)
+ {
+ param += " -svtav1-params:0 rc=1:tune=0:film-grain=0:enable-overlays=1:enable-tf=0";
+ }
+
return param;
}
@@ -1964,12 +2091,12 @@ namespace MediaBrowser.Controller.MediaEncoding
var requestedRangeTypes = state.GetRequestedRangeTypes(videoStream.Codec);
if (requestedRangeTypes.Length > 0)
{
- if (string.IsNullOrEmpty(videoStream.VideoRangeType))
+ if (videoStream.VideoRangeType == VideoRangeType.Unknown)
{
return false;
}
- if (!requestedRangeTypes.Contains(videoStream.VideoRangeType, StringComparison.OrdinalIgnoreCase))
+ if (!requestedRangeTypes.Contains(videoStream.VideoRangeType.ToString(), StringComparison.OrdinalIgnoreCase))
{
return false;
}
@@ -3675,7 +3802,7 @@ namespace MediaBrowser.Controller.MediaEncoding
mainFilters.Add(swDeintFilter);
}
- var outFormat = doOclTonemap ? "yuv420p10le" : "yuv420p";
+ var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12");
var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
// sw scale
mainFilters.Add(swScaleFilter);
@@ -3876,7 +4003,7 @@ namespace MediaBrowser.Controller.MediaEncoding
mainFilters.Add(swDeintFilter);
}
- var outFormat = doOclTonemap ? "yuv420p10le" : "yuv420p";
+ var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12");
var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
// sw scale
mainFilters.Add(swScaleFilter);
@@ -5849,19 +5976,25 @@ namespace MediaBrowser.Controller.MediaEncoding
private void ShiftVideoCodecsIfNeeded(List videoCodecs, EncodingOptions encodingOptions)
{
- // Shift hevc/h265 to the end of list if hevc encoding is not allowed.
- if (encodingOptions.AllowHevcEncoding)
+ // No need to shift if there is only one supported video codec.
+ if (videoCodecs.Count < 2)
{
return;
}
- // No need to shift if there is only one supported video codec.
- if (videoCodecs.Count < 2)
+ // Shift codecs to the end of list if it's not allowed.
+ var shiftVideoCodecs = new List();
+ if (!encodingOptions.AllowHevcEncoding)
{
- return;
+ shiftVideoCodecs.Add("hevc");
+ shiftVideoCodecs.Add("h265");
+ }
+
+ if (!encodingOptions.AllowAv1Encoding)
+ {
+ shiftVideoCodecs.Add("av1");
}
- var shiftVideoCodecs = new[] { "hevc", "h265" };
if (videoCodecs.All(i => shiftVideoCodecs.Contains(i, StringComparison.OrdinalIgnoreCase)))
{
return;
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index a6b5416601..17813559a8 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
@@ -367,22 +368,21 @@ namespace MediaBrowser.Controller.MediaEncoding
///
/// Gets the target video range type.
///
- public string TargetVideoRangeType
+ public VideoRangeType TargetVideoRangeType
{
get
{
if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec))
{
- return VideoStream?.VideoRangeType;
+ return VideoStream?.VideoRangeType ?? VideoRangeType.Unknown;
}
- var requestedRangeType = GetRequestedRangeTypes(ActualOutputVideoCodec).FirstOrDefault();
- if (!string.IsNullOrEmpty(requestedRangeType))
+ if (Enum.TryParse(GetRequestedRangeTypes(ActualOutputVideoCodec).FirstOrDefault() ?? "Unknown", true, out VideoRangeType requestedRangeType))
{
return requestedRangeType;
}
- return null;
+ return VideoRangeType.Unknown;
}
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index d3843796f7..e1a0e8d670 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -52,6 +52,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
"libx264",
"libx265",
+ "libsvtav1",
"mpeg4",
"msmpeg4",
"libvpx",
@@ -69,12 +70,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
"srt",
"h264_amf",
"hevc_amf",
+ "av1_amf",
"h264_qsv",
"hevc_qsv",
+ "av1_qsv",
"h264_nvenc",
"hevc_nvenc",
+ "av1_nvenc",
"h264_vaapi",
"hevc_vaapi",
+ "av1_vaapi",
"h264_v4l2m2m",
"h264_videotoolbox",
"hevc_videotoolbox"
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index 2990cedcd1..84c735f9ca 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -49,6 +49,7 @@ public class EncodingOptions
EnableIntelLowPowerHevcHwEncoder = false;
EnableHardwareEncoding = true;
AllowHevcEncoding = false;
+ AllowAv1Encoding = false;
AllowMjpegEncoding = false;
EnableSubtitleExtraction = true;
AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" };
@@ -250,6 +251,11 @@ public class EncodingOptions
///
public bool AllowHevcEncoding { get; set; }
+ ///
+ /// Gets or sets a value indicating whether AV1 encoding is enabled.
+ ///
+ public bool AllowAv1Encoding { get; set; }
+
///
/// Gets or sets a value indicating whether MJPEG encoding is enabled.
///
diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
index f5e1a3c496..af0787990d 100644
--- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs
+++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
@@ -1,14 +1,38 @@
-#pragma warning disable CS1591
-
using System;
using System.Globalization;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.Model.Dlna
{
+ ///
+ /// The condition processor.
+ ///
public static class ConditionProcessor
{
+ ///
+ /// Checks if a video condition is satisfied.
+ ///
+ /// The .
+ /// The width.
+ /// The height.
+ /// The bit depth.
+ /// The bitrate.
+ /// The video profile.
+ /// The .
+ /// The video level.
+ /// The framerate.
+ /// The packet length.
+ /// The .
+ /// A value indicating whether tthe video is anamorphic.
+ /// A value indicating whether tthe video is interlaced.
+ /// The reference frames.
+ /// The number of video streams.
+ /// The number of audio streams.
+ /// The video codec tag.
+ /// A value indicating whether the video is AVC.
+ /// True if the condition is satisfied.
public static bool IsVideoConditionSatisfied(
ProfileCondition condition,
int? width,
@@ -16,7 +40,7 @@ namespace MediaBrowser.Model.Dlna
int? videoBitDepth,
int? videoBitrate,
string? videoProfile,
- string? videoRangeType,
+ VideoRangeType? videoRangeType,
double? videoLevel,
float? videoFramerate,
int? packetLength,
@@ -70,6 +94,13 @@ namespace MediaBrowser.Model.Dlna
}
}
+ ///
+ /// Checks if a image condition is satisfied.
+ ///
+ /// The .
+ /// The width.
+ /// The height.
+ /// True if the condition is satisfied.
public static bool IsImageConditionSatisfied(ProfileCondition condition, int? width, int? height)
{
switch (condition.Property)
@@ -83,6 +114,15 @@ namespace MediaBrowser.Model.Dlna
}
}
+ ///
+ /// Checks if an audio condition is satisfied.
+ ///
+ /// The .
+ /// The channel count.
+ /// The bitrate.
+ /// The sample rate.
+ /// The bit depth.
+ /// True if the condition is satisfied.
public static bool IsAudioConditionSatisfied(ProfileCondition condition, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth)
{
switch (condition.Property)
@@ -100,6 +140,17 @@ namespace MediaBrowser.Model.Dlna
}
}
+ ///
+ /// Checks if an audio condition is satisfied for a video.
+ ///
+ /// The .
+ /// The channel count.
+ /// The bitrate.
+ /// The sample rate.
+ /// The bit depth.
+ /// The profile.
+ /// A value indicating whether the audio is a secondary track.
+ /// True if the condition is satisfied.
public static bool IsVideoAudioConditionSatisfied(
ProfileCondition condition,
int? audioChannels,
@@ -281,5 +332,41 @@ namespace MediaBrowser.Model.Dlna
throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition);
}
}
+
+ private static bool IsConditionSatisfied(ProfileCondition condition, VideoRangeType? currentValue)
+ {
+ if (!currentValue.HasValue || currentValue.Equals(VideoRangeType.Unknown))
+ {
+ // If the value is unknown, it satisfies if not marked as required
+ return !condition.IsRequired;
+ }
+
+ var conditionType = condition.Condition;
+ if (conditionType == ProfileConditionType.EqualsAny)
+ {
+ foreach (var singleConditionString in condition.Value.AsSpan().Split('|'))
+ {
+ if (Enum.TryParse(singleConditionString, true, out VideoRangeType conditionValue)
+ && conditionValue.Equals(currentValue))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ if (Enum.TryParse(condition.Value, true, out VideoRangeType expected))
+ {
+ return conditionType switch
+ {
+ ProfileConditionType.Equals => currentValue.Value == expected,
+ ProfileConditionType.NotEquals => currentValue.Value != expected,
+ _ => throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition)
+ };
+ }
+
+ return false;
+ }
}
}
diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
index 1d5d0b1de3..f29022b54e 100644
--- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
+++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.Model.Dlna
@@ -128,7 +129,7 @@ namespace MediaBrowser.Model.Dlna
bool isDirectStream,
long? runtimeTicks,
string videoProfile,
- string videoRangeType,
+ VideoRangeType videoRangeType,
double? videoLevel,
float? videoFramerate,
int? packetLength,
diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs
index 79ae951708..b7c23669df 100644
--- a/MediaBrowser.Model/Dlna/DeviceProfile.cs
+++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs
@@ -2,6 +2,7 @@
using System;
using System.ComponentModel;
using System.Xml.Serialization;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Model.MediaInfo;
@@ -445,7 +446,7 @@ namespace MediaBrowser.Model.Dlna
int? bitDepth,
int? videoBitrate,
string videoProfile,
- string videoRangeType,
+ VideoRangeType videoRangeType,
double? videoLevel,
float? videoFramerate,
int? packetLength,
diff --git a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
index ce422a2288..5d7daa81aa 100644
--- a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
+++ b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
@@ -73,27 +73,5 @@ namespace MediaBrowser.Model.Dlna
return null;
}
-
- private static double GetVideoBitrateScaleFactor(string codec)
- {
- if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
- {
- return .6;
- }
-
- return 1;
- }
-
- public static int ScaleBitrate(int bitrate, string inputVideoCodec, string outputVideoCodec)
- {
- var inputScaleFactor = GetVideoBitrateScaleFactor(inputVideoCodec);
- var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec);
- var scaleFactor = outputScaleFactor / inputScaleFactor;
- var newBitrate = scaleFactor * bitrate;
-
- return Convert.ToInt32(newBitrate);
- }
}
}
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index 0a955e917f..f6b882c3e6 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
@@ -23,7 +24,7 @@ namespace MediaBrowser.Model.Dlna
private readonly ILogger _logger;
private readonly ITranscoderSupport _transcoderSupport;
- private static readonly string[] _supportedHlsVideoCodecs = new string[] { "h264", "hevc" };
+ private static readonly string[] _supportedHlsVideoCodecs = new string[] { "h264", "hevc", "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" };
@@ -889,7 +890,7 @@ namespace MediaBrowser.Model.Dlna
int? videoBitrate = videoStream?.BitRate;
double? videoLevel = videoStream?.Level;
string? videoProfile = videoStream?.Profile;
- string? videoRangeType = videoStream?.VideoRangeType;
+ VideoRangeType? videoRangeType = videoStream?.VideoRangeType;
float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
bool? isAnamorphic = videoStream?.IsAnamorphic;
bool? isInterlaced = videoStream?.IsInterlaced;
@@ -1144,7 +1145,7 @@ namespace MediaBrowser.Model.Dlna
int? videoBitrate = videoStream?.BitRate;
double? videoLevel = videoStream?.Level;
string? videoProfile = videoStream?.Profile;
- string? videoRangeType = videoStream?.VideoRangeType;
+ VideoRangeType? videoRangeType = videoStream?.VideoRangeType;
float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
bool? isAnamorphic = videoStream?.IsAnamorphic;
bool? isInterlaced = videoStream?.IsInterlaced;
@@ -1932,6 +1933,10 @@ namespace MediaBrowser.Model.Dlna
{
item.SetOption(qualifier, "rangetype", string.Join(',', values));
}
+ else if (condition.Condition == ProfileConditionType.NotEquals)
+ {
+ item.SetOption(qualifier, "rangetype", string.Join(',', Enum.GetNames(typeof(VideoRangeType)).Except(values)));
+ }
else if (condition.Condition == ProfileConditionType.EqualsAny)
{
var currentValue = item.GetOption(qualifier, "rangetype");
diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
index a78a28e13a..00543616d1 100644
--- a/MediaBrowser.Model/Dlna/StreamInfo.cs
+++ b/MediaBrowser.Model/Dlna/StreamInfo.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -281,23 +282,24 @@ namespace MediaBrowser.Model.Dlna
///
/// Gets the target video range type that will be in the output stream.
///
- public string TargetVideoRangeType
+ public VideoRangeType TargetVideoRangeType
{
get
{
if (IsDirectStream)
{
- return TargetVideoStream?.VideoRangeType;
+ return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown;
}
var targetVideoCodecs = TargetVideoCodec;
var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
- if (!string.IsNullOrEmpty(videoCodec))
+ if (!string.IsNullOrEmpty(videoCodec)
+ && Enum.TryParse(GetOption(videoCodec, "rangetype"), true, out VideoRangeType videoRangeType))
{
- return GetOption(videoCodec, "rangetype");
+ return videoRangeType;
}
- return TargetVideoStream?.VideoRangeType;
+ return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown;
}
}
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index 47341f4e17..34642b83aa 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Extensions;
@@ -148,7 +149,7 @@ namespace MediaBrowser.Model.Entities
/// Gets the video range.
///
/// The video range.
- public string VideoRange
+ public VideoRange VideoRange
{
get
{
@@ -162,7 +163,7 @@ namespace MediaBrowser.Model.Entities
/// Gets the video range type.
///
/// The video range type.
- public string VideoRangeType
+ public VideoRangeType VideoRangeType
{
get
{
@@ -306,9 +307,9 @@ namespace MediaBrowser.Model.Entities
attributes.Add(Codec.ToUpperInvariant());
}
- if (!string.IsNullOrEmpty(VideoRange))
+ if (VideoRange != VideoRange.Unknown)
{
- attributes.Add(VideoRange.ToUpperInvariant());
+ attributes.Add(VideoRange.ToString());
}
if (!string.IsNullOrEmpty(Title))
@@ -677,23 +678,23 @@ namespace MediaBrowser.Model.Entities
return true;
}
- public (string VideoRange, string VideoRangeType) GetVideoColorRange()
+ public (VideoRange VideoRange, VideoRangeType VideoRangeType) GetVideoColorRange()
{
if (Type != MediaStreamType.Video)
{
- return (null, null);
+ return (VideoRange.Unknown, VideoRangeType.Unknown);
}
var colorTransfer = ColorTransfer;
if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase))
{
- return ("HDR", "HDR10");
+ return (VideoRange.HDR, VideoRangeType.HDR10);
}
if (string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
{
- return ("HDR", "HLG");
+ return (VideoRange.HDR, VideoRangeType.HLG);
}
var codecTag = CodecTag;
@@ -711,10 +712,10 @@ namespace MediaBrowser.Model.Entities
|| string.Equals(codecTag, "dvhe", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codecTag, "dav1", StringComparison.OrdinalIgnoreCase))
{
- return ("HDR", "DOVI");
+ return (VideoRange.HDR, VideoRangeType.DOVI);
}
- return ("SDR", "SDR");
+ return (VideoRange.SDR, VideoRangeType.SDR);
}
}
}
diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs
index 80f5e2c37e..8354c60efb 100644
--- a/MediaBrowser.Model/Users/UserPolicy.cs
+++ b/MediaBrowser.Model/Users/UserPolicy.cs
@@ -2,6 +2,7 @@
#pragma warning disable CS1591, CA1819
using System;
+using System.ComponentModel;
using System.Xml.Serialization;
using Jellyfin.Data.Enums;
using AccessSchedule = Jellyfin.Data.Entities.AccessSchedule;
@@ -79,6 +80,7 @@ namespace MediaBrowser.Model.Users
/// Gets or sets a value indicating whether this instance can manage collections.
///
/// true if this instance is hidden; otherwise, false.
+ [DefaultValue(false)]
public bool EnableCollectionManagement { get; set; }
///
diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64
index ee1bc911bb..771675519d 100644
--- a/deployment/Dockerfile.centos.amd64
+++ b/deployment/Dockerfile.centos.amd64
@@ -13,7 +13,7 @@ RUN yum update -yq \
&& yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/9c86d7b4-acb2-4be4-8a89-d13bc3c3f28f/1d044c7c29df018e8f2837bb343e8a84/dotnet-sdk-7.0.304-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/87a55ae3-917d-449e-a4e8-776f82976e91/03380e598c326c2f9465d262c6a88c45/dotnet-sdk-7.0.305-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64
index 0a2ee7b3f3..c552f06b0b 100644
--- a/deployment/Dockerfile.fedora.amd64
+++ b/deployment/Dockerfile.fedora.amd64
@@ -12,7 +12,7 @@ RUN dnf update -yq \
&& dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/9c86d7b4-acb2-4be4-8a89-d13bc3c3f28f/1d044c7c29df018e8f2837bb343e8a84/dotnet-sdk-7.0.304-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/87a55ae3-917d-449e-a4e8-776f82976e91/03380e598c326c2f9465d262c6a88c45/dotnet-sdk-7.0.305-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index 457f28f50c..30100d20d9 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -17,7 +17,7 @@ RUN apt-get update -yqq \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/9c86d7b4-acb2-4be4-8a89-d13bc3c3f28f/1d044c7c29df018e8f2837bb343e8a84/dotnet-sdk-7.0.304-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/87a55ae3-917d-449e-a4e8-776f82976e91/03380e598c326c2f9465d262c6a88c45/dotnet-sdk-7.0.305-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64
index e3f1032a82..bac2adfafb 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/9c86d7b4-acb2-4be4-8a89-d13bc3c3f28f/1d044c7c29df018e8f2837bb343e8a84/dotnet-sdk-7.0.304-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/87a55ae3-917d-449e-a4e8-776f82976e91/03380e598c326c2f9465d262c6a88c45/dotnet-sdk-7.0.305-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf
index aebcaeccff..37a1ed5ff3 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/9c86d7b4-acb2-4be4-8a89-d13bc3c3f28f/1d044c7c29df018e8f2837bb343e8a84/dotnet-sdk-7.0.304-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/87a55ae3-917d-449e-a4e8-776f82976e91/03380e598c326c2f9465d262c6a88c45/dotnet-sdk-7.0.305-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index 3b03332994..0346913226 100644
--- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -21,6 +21,8 @@
+
+
diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
index eee24c4236..a7a3338df4 100644
--- a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
+++ b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
@@ -3,13 +3,14 @@ using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using SkiaSharp;
+using SkiaSharp.HarfBuzz;
namespace Jellyfin.Drawing.Skia;
///
/// Used to build collages of multiple images arranged in vertical strips.
///
-public class StripCollageBuilder
+public partial class StripCollageBuilder
{
private readonly SkiaEncoder _skiaEncoder;
@@ -22,6 +23,9 @@ public class StripCollageBuilder
_skiaEncoder = skiaEncoder;
}
+ [GeneratedRegex(@"\p{IsArabic}|\p{IsArmenian}|\p{IsHebrew}|\p{IsSyriac}|\p{IsThaana}")]
+ private static partial Regex IsRtlTextRegex();
+
///
/// Check which format an image has been encoded with using its filename extension.
///
@@ -144,7 +148,19 @@ public class StripCollageBuilder
textPaint.TextSize = 0.9f * width * textPaint.TextSize / textWidth;
}
- canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint);
+ if (string.IsNullOrWhiteSpace(libraryName))
+ {
+ return bitmap;
+ }
+
+ if (IsRtlTextRegex().IsMatch(libraryName))
+ {
+ canvas.DrawShapedText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint);
+ }
+ else
+ {
+ canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint);
+ }
return bitmap;
}