Merge branch 'master' into trickplay

This commit is contained in:
Nick 2023-06-28 21:43:23 -07:00
commit 6d9e43cfe0
34 changed files with 684 additions and 173 deletions

View File

@ -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)'

View File

@ -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" />

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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)
{

View File

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

View File

@ -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 } };
}
}

View File

@ -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)))

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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";

View File

@ -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
}

View File

@ -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
}

View File

@ -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);
}
}

View File

@ -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);
private string GetH264OrH265Encoder(string defaultEncoder, string hwEncoder, EncodingJobInfo state, EncodingOptions encodingOptions)
public string GetAv1Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
=> GetH26xOrAv1Encoder("libsvtav1", "av1", state, 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, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv)
|| string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_qsv)
else if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase))
{
string[] valid_h264_qsv = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" };
// 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, "av1_qsv", StringComparison.OrdinalIgnoreCase)) // av1 (av1_qsv)
{
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)
{
return;
}
// No need to shift if there is only one supported video codec.
if (videoCodecs.Count < 2)
{
return;
}
var shiftVideoCodecs = new[] { "hevc", "h265" };
// Shift codecs to the end of list if it's not allowed.
var shiftVideoCodecs = new List<string>();
if (!encodingOptions.AllowHevcEncoding)
{
shiftVideoCodecs.Add("hevc");
shiftVideoCodecs.Add("h265");
}
if (!encodingOptions.AllowAv1Encoding)
{
shiftVideoCodecs.Add("av1");
}
if (videoCodecs.All(i => shiftVideoCodecs.Contains(i, StringComparison.OrdinalIgnoreCase)))
{
return;

View File

@ -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;
}
}

View File

@ -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"

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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);
}
}
}

View File

@ -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");

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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;
}