2
0
Эх сурвалжийг харах

Merge branch 'master' into trickplay

Nick 1 жил өмнө
parent
commit
6d9e43cfe0
34 өөрчлөгдсөн 682 нэмэгдсэн , 171 устгасан
  1. 1 1
      .ci/azure-pipelines-package.yml
  2. 12 10
      Directory.Packages.props
  3. 2 1
      Emby.Server.Implementations/Collections/CollectionManager.cs
  4. 14 7
      Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
  5. 11 36
      Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
  6. 15 1
      Emby.Server.Implementations/Localization/Core/ne.json
  7. 119 0
      Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs
  8. 2 1
      Jellyfin.Api/Controllers/DynamicHlsController.cs
  9. 1 2
      Jellyfin.Api/Controllers/ItemsController.cs
  10. 39 14
      Jellyfin.Api/Helpers/DynamicHlsHelper.cs
  11. 58 0
      Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
  12. 7 2
      Jellyfin.Api/Helpers/StreamingHelpers.cs
  13. 22 0
      Jellyfin.Data/Enums/VideoRange.cs
  14. 37 0
      Jellyfin.Data/Enums/VideoRangeType.cs
  15. 7 0
      MediaBrowser.Controller/Collections/ICollectionManager.cs
  16. 172 39
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  17. 5 5
      MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
  18. 5 0
      MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
  19. 6 0
      MediaBrowser.Model/Configuration/EncodingOptions.cs
  20. 90 3
      MediaBrowser.Model/Dlna/ConditionProcessor.cs
  21. 2 1
      MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
  22. 2 1
      MediaBrowser.Model/Dlna/DeviceProfile.cs
  23. 0 22
      MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
  24. 8 3
      MediaBrowser.Model/Dlna/StreamBuilder.cs
  25. 7 5
      MediaBrowser.Model/Dlna/StreamInfo.cs
  26. 11 10
      MediaBrowser.Model/Entities/MediaStream.cs
  27. 2 0
      MediaBrowser.Model/Users/UserPolicy.cs
  28. 1 1
      deployment/Dockerfile.centos.amd64
  29. 1 1
      deployment/Dockerfile.fedora.amd64
  30. 1 1
      deployment/Dockerfile.ubuntu.amd64
  31. 1 1
      deployment/Dockerfile.ubuntu.arm64
  32. 1 1
      deployment/Dockerfile.ubuntu.armhf
  33. 2 0
      src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
  34. 18 2
      src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs

+ 1 - 1
.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)'

+ 12 - 10
Directory.Packages.props

@@ -23,13 +23,13 @@
     <PackageVersion Include="libse" Version="3.6.13" />
     <PackageVersion Include="LrcParser" Version="2023.524.0" />
     <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
-    <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.7" />
-    <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.7" />
+    <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.8" />
+    <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.8" />
     <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
-    <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.7" />
-    <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.7" />
-    <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.7" />
-    <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.7" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.8" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.8" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.8" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.8" />
     <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
@@ -38,14 +38,14 @@
     <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
-    <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.7" />
-    <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.7" />
+    <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.8" />
+    <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.8" />
     <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
     <PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" />
-    <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
+    <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
     <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
     <PackageVersion Include="MimeTypes" Version="2.4.0" />
     <PackageVersion Include="Mono.Nat" Version="3.0.4" />
@@ -64,9 +64,11 @@
     <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
     <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.1" />
     <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
-    <PackageVersion Include="SharpFuzz" Version="2.0.2" />
+    <PackageVersion Include="SharpFuzz" Version="2.1.0" />
     <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" />
     <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" />
+    <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.3" />
+    <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.3" />
     <PackageVersion Include="SkiaSharp" Version="2.88.3" />
     <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
     <PackageVersion Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />

+ 2 - 1
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -112,7 +112,8 @@ namespace Emby.Server.Implementations.Collections
             return Path.Combine(_appPaths.DataPath, "collections");
         }
 
-        private Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
+        /// <inheritdoc />
+        public Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
         {
             return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
         }

+ 14 - 7
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);
                 }

+ 11 - 36
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<bool>(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)
                     {

+ 15 - 1
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": "कीफ्रेम एक्स्ट्रक्टर"
 }

+ 119 - 0
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;
+
+/// <summary>
+/// Deletes Path references from collections that no longer exists.
+/// </summary>
+public class CleanupCollectionPathsTask : IScheduledTask
+{
+    private readonly ILocalizationManager _localization;
+    private readonly ICollectionManager _collectionManager;
+    private readonly ILogger<CleanupCollectionPathsTask> _logger;
+    private readonly IProviderManager _providerManager;
+    private readonly IFileSystem _fileSystem;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="CleanupCollectionPathsTask"/> class.
+    /// </summary>
+    /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+    /// <param name="collectionManager">Instance of the <see cref="ICollectionManager"/> interface.</param>
+    /// <param name="logger">The logger.</param>
+    /// <param name="providerManager">The provider manager.</param>
+    /// <param name="fileSystem">The filesystem.</param>
+    public CleanupCollectionPathsTask(
+        ILocalizationManager localization,
+        ICollectionManager collectionManager,
+        ILogger<CleanupCollectionPathsTask> logger,
+        IProviderManager providerManager,
+        IFileSystem fileSystem)
+    {
+        _localization = localization;
+        _collectionManager = collectionManager;
+        _logger = logger;
+        _providerManager = providerManager;
+        _fileSystem = fileSystem;
+    }
+
+    /// <inheritdoc />
+    public string Name => _localization.GetLocalizedString("TaskCleanCollections");
+
+    /// <inheritdoc />
+    public string Key => "CleanCollections";
+
+    /// <inheritdoc />
+    public string Description => _localization.GetLocalizedString("TaskCleanCollectionsDescription");
+
+    /// <inheritdoc />
+    public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+
+    /// <inheritdoc />
+    public async Task ExecuteAsync(IProgress<double> 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<BoxSet>().ToArray();
+        _logger.LogDebug("Found {CollectionLength} Boxsets", collections.Length);
+
+        var itemsToRemove = new List<LinkedChild>();
+        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));
+        }
+    }
+
+    /// <inheritdoc />
+    public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+    {
+        return new[] { new TaskTriggerInfo() { Type = TaskTriggerInfo.TriggerStartup } };
+    }
+}

+ 2 - 1
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)))

+ 1 - 2
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;
         }

+ 39 - 14
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
     /// <param name="state">StreamState of the current stream.</param>
     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
     }
 
     /// <summary>
-    /// Get the H.26X profile of the output video stream.
+    /// Get the profile of the output video stream.
     /// </summary>
     /// <param name="state">StreamState of the current stream.</param>
     /// <param name="codec">Video codec.</param>
-    /// <returns>H.26X profile of the output video stream.</returns>
+    /// <returns>Profile of the output video stream.</returns>
     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;
     }
 

+ 58 - 0
Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs

@@ -179,4 +179,62 @@ public static class HlsCodecStringHelpers
 
         return result.ToString();
     }
+
+    /// <summary>
+    /// Gets an AV1 codec string.
+    /// </summary>
+    /// <param name="profile">AV1 profile.</param>
+    /// <param name="level">AV1 level.</param>
+    /// <param name="tierFlag">AV1 tier flag.</param>
+    /// <param name="bitDepth">AV1 bit depth.</param>
+    /// <returns>The AV1 codec string.</returns>
+    public static string GetAv1String(string? profile, int level, bool tierFlag, int bitDepth)
+    {
+        // https://aomedia.org/av1/specification/annex-a/
+        // 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();
+    }
 }

+ 7 - 2
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";

+ 22 - 0
Jellyfin.Data/Enums/VideoRange.cs

@@ -0,0 +1,22 @@
+namespace Jellyfin.Data.Enums;
+
+/// <summary>
+/// An enum representing video ranges.
+/// </summary>
+public enum VideoRange
+{
+    /// <summary>
+    /// Unknown video range.
+    /// </summary>
+    Unknown,
+
+    /// <summary>
+    /// SDR video range.
+    /// </summary>
+    SDR,
+
+    /// <summary>
+    /// HDR video range.
+    /// </summary>
+    HDR
+}

+ 37 - 0
Jellyfin.Data/Enums/VideoRangeType.cs

@@ -0,0 +1,37 @@
+namespace Jellyfin.Data.Enums;
+
+/// <summary>
+/// An enum representing types of video ranges.
+/// </summary>
+public enum VideoRangeType
+{
+    /// <summary>
+    /// Unknown video range type.
+    /// </summary>
+    Unknown,
+
+    /// <summary>
+    /// SDR video range type (8bit).
+    /// </summary>
+    SDR,
+
+    /// <summary>
+    /// HDR10 video range type (10bit).
+    /// </summary>
+    HDR10,
+
+    /// <summary>
+    /// HLG video range type (10bit).
+    /// </summary>
+    HLG,
+
+    /// <summary>
+    /// Dolby Vision video range type (12bit).
+    /// </summary>
+    DOVI,
+
+    /// <summary>
+    /// HDR10+ video range type (10bit to 16bit).
+    /// </summary>
+    HDR10Plus
+}

+ 7 - 0
MediaBrowser.Controller/Collections/ICollectionManager.cs

@@ -56,5 +56,12 @@ namespace MediaBrowser.Controller.Collections
         /// <param name="user">The user.</param>
         /// <returns>IEnumerable{BaseItem}.</returns>
         IEnumerable<BaseItem> CollapseItemsWithinBoxSets(IEnumerable<BaseItem> items, User user);
+
+        /// <summary>
+        /// Gets the folder where collections are stored.
+        /// </summary>
+        /// <param name="createIfNeeded">Will create the collection folder on the storage if set to true.</param>
+        /// <returns>The folder instance referencing the collection storage.</returns>
+        Task<Folder?> GetCollectionsFolder(bool createIfNeeded);
     }
 }

+ 172 - 39
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<string> _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;
         }
 
         /// <summary>
@@ -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<string> 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<string>();
+            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;

+ 5 - 5
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
         /// <summary>
         /// Gets the target video range type.
         /// </summary>
-        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;
             }
         }
 

+ 5 - 0
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"

+ 6 - 0
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
     /// </summary>
     public bool AllowHevcEncoding { get; set; }
 
+    /// <summary>
+    /// Gets or sets a value indicating whether AV1 encoding is enabled.
+    /// </summary>
+    public bool AllowAv1Encoding { get; set; }
+
     /// <summary>
     /// Gets or sets a value indicating whether MJPEG encoding is enabled.
     /// </summary>

+ 90 - 3
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
 {
+    /// <summary>
+    /// The condition processor.
+    /// </summary>
     public static class ConditionProcessor
     {
+        /// <summary>
+        /// Checks if a video condition is satisfied.
+        /// </summary>
+        /// <param name="condition">The <see cref="ProfileCondition"/>.</param>
+        /// <param name="width">The width.</param>
+        /// <param name="height">The height.</param>
+        /// <param name="videoBitDepth">The bit depth.</param>
+        /// <param name="videoBitrate">The bitrate.</param>
+        /// <param name="videoProfile">The video profile.</param>
+        /// <param name="videoRangeType">The <see cref="VideoRangeType"/>.</param>
+        /// <param name="videoLevel">The video level.</param>
+        /// <param name="videoFramerate">The framerate.</param>
+        /// <param name="packetLength">The packet length.</param>
+        /// <param name="timestamp">The <see cref="TransportStreamTimestamp"/>.</param>
+        /// <param name="isAnamorphic">A value indicating whether tthe video is anamorphic.</param>
+        /// <param name="isInterlaced">A value indicating whether tthe video is interlaced.</param>
+        /// <param name="refFrames">The reference frames.</param>
+        /// <param name="numVideoStreams">The number of video streams.</param>
+        /// <param name="numAudioStreams">The number of audio streams.</param>
+        /// <param name="videoCodecTag">The video codec tag.</param>
+        /// <param name="isAvc">A value indicating whether the video is AVC.</param>
+        /// <returns><b>True</b> if the condition is satisfied.</returns>
         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
             }
         }
 
+        /// <summary>
+        /// Checks if a image condition is satisfied.
+        /// </summary>
+        /// <param name="condition">The <see cref="ProfileCondition"/>.</param>
+        /// <param name="width">The width.</param>
+        /// <param name="height">The height.</param>
+        /// <returns><b>True</b> if the condition is satisfied.</returns>
         public static bool IsImageConditionSatisfied(ProfileCondition condition, int? width, int? height)
         {
             switch (condition.Property)
@@ -83,6 +114,15 @@ namespace MediaBrowser.Model.Dlna
             }
         }
 
+        /// <summary>
+        /// Checks if an audio condition is satisfied.
+        /// </summary>
+        /// <param name="condition">The <see cref="ProfileCondition"/>.</param>
+        /// <param name="audioChannels">The channel count.</param>
+        /// <param name="audioBitrate">The bitrate.</param>
+        /// <param name="audioSampleRate">The sample rate.</param>
+        /// <param name="audioBitDepth">The bit depth.</param>
+        /// <returns><b>True</b> if the condition is satisfied.</returns>
         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
             }
         }
 
+        /// <summary>
+        /// Checks if an audio condition is satisfied for a video.
+        /// </summary>
+        /// <param name="condition">The <see cref="ProfileCondition"/>.</param>
+        /// <param name="audioChannels">The channel count.</param>
+        /// <param name="audioBitrate">The bitrate.</param>
+        /// <param name="audioSampleRate">The sample rate.</param>
+        /// <param name="audioBitDepth">The bit depth.</param>
+        /// <param name="audioProfile">The profile.</param>
+        /// <param name="isSecondaryTrack">A value indicating whether the audio is a secondary track.</param>
+        /// <returns><b>True</b> if the condition is satisfied.</returns>
         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;
+        }
     }
 }

+ 2 - 1
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,

+ 2 - 1
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,

+ 0 - 22
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);
-        }
     }
 }

+ 8 - 3
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");

+ 7 - 5
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
         /// <summary>
         /// Gets the target video range type that will be in the output stream.
         /// </summary>
-        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;
             }
         }
 

+ 11 - 10
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.
         /// </summary>
         /// <value>The video range.</value>
-        public string VideoRange
+        public VideoRange VideoRange
         {
             get
             {
@@ -162,7 +163,7 @@ namespace MediaBrowser.Model.Entities
         /// Gets the video range type.
         /// </summary>
         /// <value>The video range type.</value>
-        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);
         }
     }
 }

+ 2 - 0
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.
         /// </summary>
         /// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value>
+        [DefaultValue(false)]
         public bool EnableCollectionManagement { get; set; }
 
         /// <summary>

+ 1 - 1
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

+ 1 - 1
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

+ 1 - 1
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

+ 1 - 1
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

+ 1 - 1
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

+ 2 - 0
src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj

@@ -21,6 +21,8 @@
     <PackageReference Include="SkiaSharp" />
     <PackageReference Include="SkiaSharp.NativeAssets.Linux" />
     <PackageReference Include="SkiaSharp.Svg" />
+    <PackageReference Include="SkiaSharp.HarfBuzz" />
+    <PackageReference Include="HarfBuzzSharp.NativeAssets.Linux" />
   </ItemGroup>
 
   <ItemGroup>

+ 18 - 2
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;
 
 /// <summary>
 /// Used to build collages of multiple images arranged in vertical strips.
 /// </summary>
-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();
+
     /// <summary>
     /// Check which format an image has been encoded with using its filename extension.
     /// </summary>
@@ -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;
     }