Merge branch 'master' into trickplay
This commit is contained in:
commit
6d9e43cfe0
|
@ -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)'
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,7 +81,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
try
|
||||
{
|
||||
Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath);
|
||||
using var message = response;
|
||||
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(
|
||||
|
@ -118,6 +92,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
() => Resolve(openTaskCompletionSource),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
Logger.LogInformation("Copying of {StreamType} to {FilePath} was canceled", GetType().Name, TempFilePath);
|
||||
|
|
|
@ -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": "कीफ्रेम एक्स्ट्रक्टर"
|
||||
}
|
||||
|
|
|
@ -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 } };
|
||||
}
|
||||
}
|
|
@ -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)))
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
Jellyfin.Data/Enums/VideoRange.cs
Normal file
22
Jellyfin.Data/Enums/VideoRange.cs
Normal 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
|
||||
}
|
37
Jellyfin.Data/Enums/VideoRangeType.cs
Normal file
37
Jellyfin.Data/Enums/VideoRangeType.cs
Normal 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
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user