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) displayName: Set release version (stable)
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') 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' 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)' - 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="libse" Version="3.6.13" />
<PackageVersion Include="LrcParser" Version="2023.524.0" /> <PackageVersion Include="LrcParser" Version="2023.524.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.7" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.8" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.7" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.8" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.7" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.7" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.7" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.7" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.8" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" 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.Configuration.Json" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" 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.DependencyInjection" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.7" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.8" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.7" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.8" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" 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.Abstractions" Version="7.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" /> <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="Microsoft.SourceLink.GitHub" Version="1.1.1" />
<PackageVersion Include="MimeTypes" Version="2.4.0" /> <PackageVersion Include="MimeTypes" Version="2.4.0" />
<PackageVersion Include="Mono.Nat" Version="3.0.4" /> <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.File" Version="5.0.0" />
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.1" /> <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.1" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <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.NativeAssets.Linux" Version="2.88.3" />
<PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" /> <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="SkiaSharp" Version="2.88.3" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> <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"); return Path.Combine(_appPaths.DataPath, "collections");
} }
private Task<Folder?> GetCollectionsFolder(bool createIfNeeded) /// <inheritdoc />
public Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
{ {
return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded); return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
} }

View File

@ -30,12 +30,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
{ {
private static readonly string[] _disallowedSharedStreamExtensions = private static readonly string[] _disallowedMimeTypes =
{ {
".mkv", "video/x-matroska",
".mp4", "video/mp4",
".m3u8", "application/vnd.apple.mpegurl",
".mpd" "application/mpegurl",
"application/x-mpegurl",
"video/vnd.mpeg.dash.mpd"
}; };
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
@ -118,9 +120,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping) 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); 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; _httpClientFactory = httpClientFactory;
_appHost = appHost; _appHost = appHost;
OriginalStreamId = originalStreamId; OriginalStreamId = originalStreamId;
EnableStreamSharing = true;
} }
public override async Task Open(CancellationToken openCancellationToken) public override async Task Open(CancellationToken openCancellationToken)
@ -59,39 +58,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) .GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
.ConfigureAwait(false); .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); var taskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
_ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token); _ = 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.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
MediaSource.Protocol = MediaProtocol.Http; 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); var res = await taskCompletionSource.Task.ConfigureAwait(false);
if (!res) if (!res)
{ {
@ -108,15 +81,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
try try
{ {
Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath); 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 using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await StreamHelper.CopyToAsync( await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
stream, await StreamHelper.CopyToAsync(
fileStream, stream,
IODefaults.CopyToBufferSize, fileStream,
() => Resolve(openTaskCompletionSource), IODefaults.CopyToBufferSize,
cancellationToken).ConfigureAwait(false); () => Resolve(openTaskCompletionSource),
cancellationToken).ConfigureAwait(false);
}
} }
catch (OperationCanceledException ex) catch (OperationCanceledException ex)
{ {

View File

@ -109,5 +109,19 @@
"Sync": "समकालीन", "Sync": "समकालीन",
"SubtitleDownloadFailureFromForItem": "उपशीर्षकहरू {0} बाट {1} को लागि डाउनलोड गर्न असफल", "SubtitleDownloadFailureFromForItem": "उपशीर्षकहरू {0} बाट {1} को लागि डाउनलोड गर्न असफल",
"PluginUpdatedWithName": "{0} अद्यावधिक गरिएको थियो", "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.Helpers;
using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using Jellyfin.MediaEncoding.Hls.Playlist; using Jellyfin.MediaEncoding.Hls.Playlist;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
@ -1841,7 +1842,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
{ {
if (EncodingHelper.IsCopyCodec(codec) 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, "dovi", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase) || string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.VideoStream.CodecTag, "dvhe", 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); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
if (includeItemTypes.Length == 1 if (includeItemTypes.Length == 1
&& (includeItemTypes[0] == BaseItemKind.Playlist && includeItemTypes[0] == BaseItemKind.BoxSet)
|| includeItemTypes[0] == BaseItemKind.BoxSet))
{ {
parentId = null; parentId = null;
} }

View File

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
@ -216,9 +217,9 @@ public class DynamicHlsHelper
// Provide SDR HEVC entrance for backward compatibility. // Provide SDR HEVC entrance for backward compatibility.
if (encodingOptions.AllowHevcEncoding if (encodingOptions.AllowHevcEncoding
&& !encodingOptions.AllowAv1Encoding
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& !string.IsNullOrEmpty(state.VideoStream.VideoRange) && state.VideoStream.VideoRange == VideoRange.HDR
&& string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{ {
var requestedVideoProfiles = state.GetRequestedProfiles("hevc"); var requestedVideoProfiles = state.GetRequestedProfiles("hevc");
@ -258,11 +259,12 @@ public class DynamicHlsHelper
// Provide Level 5.0 entrance for backward compatibility. // 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, // 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. // 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.HasValue
&& state.VideoStream.Level > 150 && state.VideoStream.Level > 150
&& !string.IsNullOrEmpty(state.VideoStream.VideoRange) && state.VideoStream.VideoRange == VideoRange.SDR
&& string.Equals(state.VideoStream.VideoRange, "SDR", StringComparison.OrdinalIgnoreCase)
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{ {
var playlistCodecsField = new StringBuilder(); var playlistCodecsField = new StringBuilder();
@ -353,17 +355,17 @@ public class DynamicHlsHelper
/// <param name="state">StreamState of the current stream.</param> /// <param name="state">StreamState of the current stream.</param>
private void AppendPlaylistVideoRangeField(StringBuilder builder, StreamState state) 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; var videoRange = state.VideoStream.VideoRange;
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
{ {
if (string.Equals(videoRange, "SDR", StringComparison.OrdinalIgnoreCase)) if (videoRange == VideoRange.SDR)
{ {
builder.Append(",VIDEO-RANGE=SDR"); builder.Append(",VIDEO-RANGE=SDR");
} }
if (string.Equals(videoRange, "HDR", StringComparison.OrdinalIgnoreCase)) if (videoRange == VideoRange.HDR)
{ {
builder.Append(",VIDEO-RANGE=PQ"); builder.Append(",VIDEO-RANGE=PQ");
} }
@ -603,6 +605,12 @@ public class DynamicHlsHelper
levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120"; levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120";
levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString); 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)) if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
@ -614,11 +622,11 @@ public class DynamicHlsHelper
} }
/// <summary> /// <summary>
/// Get the H.26X profile of the output video stream. /// Get the profile of the output video stream.
/// </summary> /// </summary>
/// <param name="state">StreamState of the current stream.</param> /// <param name="state">StreamState of the current stream.</param>
/// <param name="codec">Video codec.</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) private string GetOutputVideoCodecProfile(StreamState state, string codec)
{ {
string profileString = string.Empty; string profileString = string.Empty;
@ -636,7 +644,8 @@ public class DynamicHlsHelper
} }
if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase) 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"; profileString ??= "main";
} }
@ -706,9 +715,9 @@ public class DynamicHlsHelper
{ {
if (level == 0) if (level == 0)
{ {
// This is 0 when there's no requested H.26X level in the device profile // This is 0 when there's no requested level in the device profile
// and the source is not encoded in H.26X // and the source is not encoded in H.26X or AV1
_logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist"); _logger.LogError("Got invalid level when building CODECS field for HLS master playlist");
return string.Empty; return string.Empty;
} }
@ -725,6 +734,22 @@ public class DynamicHlsHelper
return HlsCodecStringHelpers.GetH265String(profile, level); 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; return string.Empty;
} }

View File

@ -179,4 +179,62 @@ public static class HlsCodecStringHelpers
return result.ToString(); 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; var videoCodec = state.Request.VideoCodec;
if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) || if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{ {
return ".ts"; return ".ts";
} }
if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase))
{
return ".mp4";
}
if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase)) if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
{ {
return ".ogv"; 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> /// <param name="user">The user.</param>
/// <returns>IEnumerable{BaseItem}.</returns> /// <returns>IEnumerable{BaseItem}.</returns>
IEnumerable<BaseItem> CollapseItemsWithinBoxSets(IEnumerable<BaseItem> items, User user); 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 _minFFmpegImplictHwaccel = new Version(6, 0);
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0); private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3); private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3);
private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1);
private static readonly string[] _videoProfilesH264 = new[] private static readonly string[] _videoProfilesH264 = new[]
{ {
@ -65,6 +66,13 @@ namespace MediaBrowser.Controller.MediaEncoding
"Main10" "Main10"
}; };
private static readonly string[] _videoProfilesAv1 = new[]
{
"Main",
"High",
"Professional",
};
private static readonly HashSet<string> _mp4ContainerNames = new(StringComparer.OrdinalIgnoreCase) private static readonly HashSet<string> _mp4ContainerNames = new(StringComparer.OrdinalIgnoreCase)
{ {
"mp4", "mp4",
@ -120,12 +128,15 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions) public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
=> GetH264OrH265Encoder("libx264", "h264", state, encodingOptions); => GetH26xOrAv1Encoder("libx264", "h264", state, encodingOptions);
public string GetH265Encoder(EncodingJobInfo state, EncodingOptions 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. // 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 // 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) if (string.Equals(state.VideoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
&& string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) && state.VideoStream.VideoRange == VideoRange.HDR
&& string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase)) && state.VideoStream.VideoRangeType == VideoRangeType.DOVI)
{ {
// Only native SW decoder and HW accelerator can parse dovi rpu. // Only native SW decoder and HW accelerator can parse dovi rpu.
var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty; var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty;
@ -246,9 +257,9 @@ namespace MediaBrowser.Controller.MediaEncoding
return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder; return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder;
} }
return string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) return state.VideoStream.VideoRange == VideoRange.HDR
&& (string.Equals(state.VideoStream.VideoRangeType, "HDR10", StringComparison.OrdinalIgnoreCase) && (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
|| string.Equals(state.VideoStream.VideoRangeType, "HLG", StringComparison.OrdinalIgnoreCase)); || state.VideoStream.VideoRangeType == VideoRangeType.HLG);
} }
private bool IsVulkanHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options) private bool IsVulkanHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
@ -260,7 +271,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// libplacebo has partial Dolby Vision to SDR tonemapping support. // libplacebo has partial Dolby Vision to SDR tonemapping support.
return options.EnableTonemapping return options.EnableTonemapping
&& string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) && state.VideoStream.VideoRange == VideoRange.HDR
&& GetVideoColorBitDepth(state) == 10; && GetVideoColorBitDepth(state) == 10;
} }
@ -275,8 +286,8 @@ namespace MediaBrowser.Controller.MediaEncoding
// Native VPP tonemapping may come to QSV in the future. // Native VPP tonemapping may come to QSV in the future.
return string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) return state.VideoStream.VideoRange == VideoRange.HDR
&& string.Equals(state.VideoStream.VideoRangeType, "HDR10", StringComparison.OrdinalIgnoreCase); && state.VideoStream.VideoRangeType == VideoRangeType.HDR10;
} }
/// <summary> /// <summary>
@ -291,6 +302,11 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(codec)) if (!string.IsNullOrEmpty(codec))
{ {
if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
{
return GetAv1Encoder(state, encodingOptions);
}
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc", 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)); 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; return -1;
} }
@ -1234,6 +1255,11 @@ namespace MediaBrowser.Controller.MediaEncoding
return FormattableString.Invariant($" -b:v {bitrate}"); 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) if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoCodec, "libx265", StringComparison.OrdinalIgnoreCase)) || string.Equals(videoCodec, "libx265", StringComparison.OrdinalIgnoreCase))
{ {
@ -1241,14 +1267,16 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase) 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 // 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}"); return FormattableString.Invariant($" -rc cbr -qmin 0 -qmax 32 -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
} }
if (string.Equals(videoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) 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. // VBR in i965 driver may result in pixelated output.
if (_mediaEncoder.IsVaapiDeviceInteli965) if (_mediaEncoder.IsVaapiDeviceInteli965)
@ -1266,14 +1294,23 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel)) 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))
|| string.Equals(state.ActualOutputVideoCodec, "h265", 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. // 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. // 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 // https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
// MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880. // MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880.
if (requestLevel >= 150) if (requestLevel < 0 || requestLevel >= 150)
{ {
return "150"; return "150";
} }
@ -1283,7 +1320,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// Transcode to level 5.1 and lower for maximum compatibility. // 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. // 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 // https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
if (requestLevel >= 51) if (requestLevel < 0 || requestLevel >= 51)
{ {
return "51"; return "51";
} }
@ -1421,14 +1458,18 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc_qsv", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc_nvenc", 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; args += gopArg;
} }
else if (string.Equals(codec, "libx264", StringComparison.OrdinalIgnoreCase) else if (string.Equals(codec, "libx264", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "h264_vaapi", 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; args += keyFrameArg;
@ -1564,18 +1605,60 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -crf " + defaultCrf; param += " -crf " + defaultCrf;
} }
} }
else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv) else if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase))
|| string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_qsv)
{ {
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; param += " -preset " + encodingOptions.EncoderPreset;
} }
else else
{ {
param += " -preset 7"; param += " -preset veryfast";
} }
// Only h264_qsv has look_ahead option // 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) 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) switch (encodingOptions.EncoderPreset)
{ {
@ -1625,7 +1709,8 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
} }
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf) 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) switch (encodingOptions.EncoderPreset)
{ {
@ -1652,9 +1737,15 @@ namespace MediaBrowser.Controller.MediaEncoding
break; 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)) 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 else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // vp8
@ -1785,6 +1876,14 @@ namespace MediaBrowser.Controller.MediaEncoding
profile = "high"; 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, // h264_vaapi does not support Baseline profile, force Constrained Baseline in this case,
// which is compatible (and ugly). // which is compatible (and ugly).
if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
@ -1852,19 +1951,41 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -level " + (hevcLevel / 3); 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) 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; param += " -level " + level;
} }
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) || string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase))
|| string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase))
{ {
// level option may cause NVENC to fail. // level option may cause NVENC to fail.
// NVENC cannot adjust the given level, just throw an error. // 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. // 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)) else if (!string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
{ {
@ -1886,6 +2007,12 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -x265-params:0 no-info=1"; 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; return param;
} }
@ -1964,12 +2091,12 @@ namespace MediaBrowser.Controller.MediaEncoding
var requestedRangeTypes = state.GetRequestedRangeTypes(videoStream.Codec); var requestedRangeTypes = state.GetRequestedRangeTypes(videoStream.Codec);
if (requestedRangeTypes.Length > 0) if (requestedRangeTypes.Length > 0)
{ {
if (string.IsNullOrEmpty(videoStream.VideoRangeType)) if (videoStream.VideoRangeType == VideoRangeType.Unknown)
{ {
return false; return false;
} }
if (!requestedRangeTypes.Contains(videoStream.VideoRangeType, StringComparison.OrdinalIgnoreCase)) if (!requestedRangeTypes.Contains(videoStream.VideoRangeType.ToString(), StringComparison.OrdinalIgnoreCase))
{ {
return false; return false;
} }
@ -3675,7 +3802,7 @@ namespace MediaBrowser.Controller.MediaEncoding
mainFilters.Add(swDeintFilter); 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); var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
// sw scale // sw scale
mainFilters.Add(swScaleFilter); mainFilters.Add(swScaleFilter);
@ -3876,7 +4003,7 @@ namespace MediaBrowser.Controller.MediaEncoding
mainFilters.Add(swDeintFilter); 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); var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
// sw scale // sw scale
mainFilters.Add(swScaleFilter); mainFilters.Add(swScaleFilter);
@ -5849,19 +5976,25 @@ namespace MediaBrowser.Controller.MediaEncoding
private void ShiftVideoCodecsIfNeeded(List<string> videoCodecs, EncodingOptions encodingOptions) 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. // No need to shift if there is only one supported video codec.
if (videoCodecs.Count < 2) if (videoCodecs.Count < 2)
{ {
return; 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))) if (videoCodecs.All(i => shiftVideoCodecs.Contains(i, StringComparison.OrdinalIgnoreCase)))
{ {
return; return;

View File

@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
@ -367,22 +368,21 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary> /// <summary>
/// Gets the target video range type. /// Gets the target video range type.
/// </summary> /// </summary>
public string TargetVideoRangeType public VideoRangeType TargetVideoRangeType
{ {
get get
{ {
if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec)) if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec))
{ {
return VideoStream?.VideoRangeType; return VideoStream?.VideoRangeType ?? VideoRangeType.Unknown;
} }
var requestedRangeType = GetRequestedRangeTypes(ActualOutputVideoCodec).FirstOrDefault(); if (Enum.TryParse(GetRequestedRangeTypes(ActualOutputVideoCodec).FirstOrDefault() ?? "Unknown", true, out VideoRangeType requestedRangeType))
if (!string.IsNullOrEmpty(requestedRangeType))
{ {
return requestedRangeType; return requestedRangeType;
} }
return null; return VideoRangeType.Unknown;
} }
} }

View File

@ -52,6 +52,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
{ {
"libx264", "libx264",
"libx265", "libx265",
"libsvtav1",
"mpeg4", "mpeg4",
"msmpeg4", "msmpeg4",
"libvpx", "libvpx",
@ -69,12 +70,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
"srt", "srt",
"h264_amf", "h264_amf",
"hevc_amf", "hevc_amf",
"av1_amf",
"h264_qsv", "h264_qsv",
"hevc_qsv", "hevc_qsv",
"av1_qsv",
"h264_nvenc", "h264_nvenc",
"hevc_nvenc", "hevc_nvenc",
"av1_nvenc",
"h264_vaapi", "h264_vaapi",
"hevc_vaapi", "hevc_vaapi",
"av1_vaapi",
"h264_v4l2m2m", "h264_v4l2m2m",
"h264_videotoolbox", "h264_videotoolbox",
"hevc_videotoolbox" "hevc_videotoolbox"

View File

@ -49,6 +49,7 @@ public class EncodingOptions
EnableIntelLowPowerHevcHwEncoder = false; EnableIntelLowPowerHevcHwEncoder = false;
EnableHardwareEncoding = true; EnableHardwareEncoding = true;
AllowHevcEncoding = false; AllowHevcEncoding = false;
AllowAv1Encoding = false;
AllowMjpegEncoding = false; AllowMjpegEncoding = false;
EnableSubtitleExtraction = true; EnableSubtitleExtraction = true;
AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" }; AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" };
@ -250,6 +251,11 @@ public class EncodingOptions
/// </summary> /// </summary>
public bool AllowHevcEncoding { get; set; } public bool AllowHevcEncoding { get; set; }
/// <summary>
/// Gets or sets a value indicating whether AV1 encoding is enabled.
/// </summary>
public bool AllowAv1Encoding { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether MJPEG encoding is enabled. /// Gets or sets a value indicating whether MJPEG encoding is enabled.
/// </summary> /// </summary>

View File

@ -1,14 +1,38 @@
#pragma warning disable CS1591
using System; using System;
using System.Globalization; using System.Globalization;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.Model.Dlna namespace MediaBrowser.Model.Dlna
{ {
/// <summary>
/// The condition processor.
/// </summary>
public static class ConditionProcessor 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( public static bool IsVideoConditionSatisfied(
ProfileCondition condition, ProfileCondition condition,
int? width, int? width,
@ -16,7 +40,7 @@ namespace MediaBrowser.Model.Dlna
int? videoBitDepth, int? videoBitDepth,
int? videoBitrate, int? videoBitrate,
string? videoProfile, string? videoProfile,
string? videoRangeType, VideoRangeType? videoRangeType,
double? videoLevel, double? videoLevel,
float? videoFramerate, float? videoFramerate,
int? packetLength, 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) public static bool IsImageConditionSatisfied(ProfileCondition condition, int? width, int? height)
{ {
switch (condition.Property) 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) public static bool IsAudioConditionSatisfied(ProfileCondition condition, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth)
{ {
switch (condition.Property) 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( public static bool IsVideoAudioConditionSatisfied(
ProfileCondition condition, ProfileCondition condition,
int? audioChannels, int? audioChannels,
@ -281,5 +332,41 @@ namespace MediaBrowser.Model.Dlna
throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition); 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.Model.Dlna namespace MediaBrowser.Model.Dlna
@ -128,7 +129,7 @@ namespace MediaBrowser.Model.Dlna
bool isDirectStream, bool isDirectStream,
long? runtimeTicks, long? runtimeTicks,
string videoProfile, string videoProfile,
string videoRangeType, VideoRangeType videoRangeType,
double? videoLevel, double? videoLevel,
float? videoFramerate, float? videoFramerate,
int? packetLength, int? packetLength,

View File

@ -2,6 +2,7 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Xml.Serialization; using System.Xml.Serialization;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
@ -445,7 +446,7 @@ namespace MediaBrowser.Model.Dlna
int? bitDepth, int? bitDepth,
int? videoBitrate, int? videoBitrate,
string videoProfile, string videoProfile,
string videoRangeType, VideoRangeType videoRangeType,
double? videoLevel, double? videoLevel,
float? videoFramerate, float? videoFramerate,
int? packetLength, int? packetLength,

View File

@ -73,27 +73,5 @@ namespace MediaBrowser.Model.Dlna
return null; 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.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
@ -23,7 +24,7 @@ namespace MediaBrowser.Model.Dlna
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ITranscoderSupport _transcoderSupport; 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[] _supportedHlsAudioCodecsTs = new string[] { "aac", "ac3", "eac3", "mp3" };
private static readonly string[] _supportedHlsAudioCodecsMp4 = new string[] { "aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd" }; 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; int? videoBitrate = videoStream?.BitRate;
double? videoLevel = videoStream?.Level; double? videoLevel = videoStream?.Level;
string? videoProfile = videoStream?.Profile; string? videoProfile = videoStream?.Profile;
string? videoRangeType = videoStream?.VideoRangeType; VideoRangeType? videoRangeType = videoStream?.VideoRangeType;
float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
bool? isAnamorphic = videoStream?.IsAnamorphic; bool? isAnamorphic = videoStream?.IsAnamorphic;
bool? isInterlaced = videoStream?.IsInterlaced; bool? isInterlaced = videoStream?.IsInterlaced;
@ -1144,7 +1145,7 @@ namespace MediaBrowser.Model.Dlna
int? videoBitrate = videoStream?.BitRate; int? videoBitrate = videoStream?.BitRate;
double? videoLevel = videoStream?.Level; double? videoLevel = videoStream?.Level;
string? videoProfile = videoStream?.Profile; string? videoProfile = videoStream?.Profile;
string? videoRangeType = videoStream?.VideoRangeType; VideoRangeType? videoRangeType = videoStream?.VideoRangeType;
float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
bool? isAnamorphic = videoStream?.IsAnamorphic; bool? isAnamorphic = videoStream?.IsAnamorphic;
bool? isInterlaced = videoStream?.IsInterlaced; bool? isInterlaced = videoStream?.IsInterlaced;
@ -1932,6 +1933,10 @@ namespace MediaBrowser.Model.Dlna
{ {
item.SetOption(qualifier, "rangetype", string.Join(',', values)); 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) else if (condition.Condition == ProfileConditionType.EqualsAny)
{ {
var currentValue = item.GetOption(qualifier, "rangetype"); var currentValue = item.GetOption(qualifier, "rangetype");

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -281,23 +282,24 @@ namespace MediaBrowser.Model.Dlna
/// <summary> /// <summary>
/// Gets the target video range type that will be in the output stream. /// Gets the target video range type that will be in the output stream.
/// </summary> /// </summary>
public string TargetVideoRangeType public VideoRangeType TargetVideoRangeType
{ {
get get
{ {
if (IsDirectStream) if (IsDirectStream)
{ {
return TargetVideoStream?.VideoRangeType; return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown;
} }
var targetVideoCodecs = TargetVideoCodec; var targetVideoCodecs = TargetVideoCodec;
var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; 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.Globalization;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Extensions;
@ -148,7 +149,7 @@ namespace MediaBrowser.Model.Entities
/// Gets the video range. /// Gets the video range.
/// </summary> /// </summary>
/// <value>The video range.</value> /// <value>The video range.</value>
public string VideoRange public VideoRange VideoRange
{ {
get get
{ {
@ -162,7 +163,7 @@ namespace MediaBrowser.Model.Entities
/// Gets the video range type. /// Gets the video range type.
/// </summary> /// </summary>
/// <value>The video range type.</value> /// <value>The video range type.</value>
public string VideoRangeType public VideoRangeType VideoRangeType
{ {
get get
{ {
@ -306,9 +307,9 @@ namespace MediaBrowser.Model.Entities
attributes.Add(Codec.ToUpperInvariant()); attributes.Add(Codec.ToUpperInvariant());
} }
if (!string.IsNullOrEmpty(VideoRange)) if (VideoRange != VideoRange.Unknown)
{ {
attributes.Add(VideoRange.ToUpperInvariant()); attributes.Add(VideoRange.ToString());
} }
if (!string.IsNullOrEmpty(Title)) if (!string.IsNullOrEmpty(Title))
@ -677,23 +678,23 @@ namespace MediaBrowser.Model.Entities
return true; return true;
} }
public (string VideoRange, string VideoRangeType) GetVideoColorRange() public (VideoRange VideoRange, VideoRangeType VideoRangeType) GetVideoColorRange()
{ {
if (Type != MediaStreamType.Video) if (Type != MediaStreamType.Video)
{ {
return (null, null); return (VideoRange.Unknown, VideoRangeType.Unknown);
} }
var colorTransfer = ColorTransfer; var colorTransfer = ColorTransfer;
if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)) if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase))
{ {
return ("HDR", "HDR10"); return (VideoRange.HDR, VideoRangeType.HDR10);
} }
if (string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase)) if (string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
{ {
return ("HDR", "HLG"); return (VideoRange.HDR, VideoRangeType.HLG);
} }
var codecTag = CodecTag; var codecTag = CodecTag;
@ -711,10 +712,10 @@ namespace MediaBrowser.Model.Entities
|| string.Equals(codecTag, "dvhe", StringComparison.OrdinalIgnoreCase) || string.Equals(codecTag, "dvhe", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codecTag, "dav1", 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 #pragma warning disable CS1591, CA1819
using System; using System;
using System.ComponentModel;
using System.Xml.Serialization; using System.Xml.Serialization;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using AccessSchedule = Jellyfin.Data.Entities.AccessSchedule; 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. /// Gets or sets a value indicating whether this instance can manage collections.
/// </summary> /// </summary>
/// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value> /// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value>
[DefaultValue(false)]
public bool EnableCollectionManagement { get; set; } public bool EnableCollectionManagement { get; set; }
/// <summary> /// <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 && 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 # 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 \ && mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet && 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 && 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 # 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 \ && mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet && 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 libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# Install dotnet repository # 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 \ && mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet && 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 mmv build-essential lsb-release
# Install dotnet repository # 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 \ && mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet && 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 mmv build-essential lsb-release
# Install dotnet repository # 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 \ && mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

View File

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

View File

@ -3,13 +3,14 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using SkiaSharp; using SkiaSharp;
using SkiaSharp.HarfBuzz;
namespace Jellyfin.Drawing.Skia; namespace Jellyfin.Drawing.Skia;
/// <summary> /// <summary>
/// Used to build collages of multiple images arranged in vertical strips. /// Used to build collages of multiple images arranged in vertical strips.
/// </summary> /// </summary>
public class StripCollageBuilder public partial class StripCollageBuilder
{ {
private readonly SkiaEncoder _skiaEncoder; private readonly SkiaEncoder _skiaEncoder;
@ -22,6 +23,9 @@ public class StripCollageBuilder
_skiaEncoder = skiaEncoder; _skiaEncoder = skiaEncoder;
} }
[GeneratedRegex(@"\p{IsArabic}|\p{IsArmenian}|\p{IsHebrew}|\p{IsSyriac}|\p{IsThaana}")]
private static partial Regex IsRtlTextRegex();
/// <summary> /// <summary>
/// Check which format an image has been encoded with using its filename extension. /// Check which format an image has been encoded with using its filename extension.
/// </summary> /// </summary>
@ -144,7 +148,19 @@ public class StripCollageBuilder
textPaint.TextSize = 0.9f * width * textPaint.TextSize / textWidth; 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; return bitmap;
} }