Merge branch 'master' into trickplay

pull/9554/head
Nick 11 months ago
commit 6d9e43cfe0

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -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);
public string GetAv1Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
=> GetH26xOrAv1Encoder("libsvtav1", "av1", state, encodingOptions);
private string GetH264OrH265Encoder(string defaultEncoder, string hwEncoder, EncodingJobInfo state, EncodingOptions encodingOptions) private string GetH26xOrAv1Encoder(string defaultEncoder, string hwEncoder, EncodingJobInfo state, EncodingOptions encodingOptions)
{ {
// Only use alternative encoders for video files. // 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, "libsvtav1", StringComparison.OrdinalIgnoreCase))
{
// Default to use the recommended preset 10.
// Omit presets < 5, which are too slow for on the fly encoding.
// https://gitlab.com/AOMediaCodec/SVT-AV1/-/blob/master/Docs/Ffmpeg.md
param += encodingOptions.EncoderPreset switch
{
"veryslow" => " -preset 5",
"slower" => " -preset 6",
"slow" => " -preset 7",
"medium" => " -preset 8",
"fast" => " -preset 9",
"faster" => " -preset 10",
"veryfast" => " -preset 11",
"superfast" => " -preset 12",
"ultrafast" => " -preset 13",
_ => " -preset 10"
};
}
else if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "av1_vaapi", StringComparison.OrdinalIgnoreCase))
{
// -compression_level is not reliable on AMD.
if (_mediaEncoder.IsVaapiDeviceInteliHD)
{
param += encodingOptions.EncoderPreset switch
{
"veryslow" => " -compression_level 1",
"slower" => " -compression_level 2",
"slow" => " -compression_level 3",
"medium" => " -compression_level 4",
"fast" => " -compression_level 5",
"faster" => " -compression_level 6",
"veryfast" => " -compression_level 7",
"superfast" => " -compression_level 7",
"ultrafast" => " -compression_level 7",
_ => string.Empty
};
}
}
else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv) else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv)
|| string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_qsv) || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase) // hevc (hevc_qsv)
|| string.Equals(videoEncoder, "av1_qsv", StringComparison.OrdinalIgnoreCase)) // av1 (av1_qsv)
{ {
string[] valid_h264_qsv = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" }; string[] valid_presets = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" };
if (valid_h264_qsv.Contains(encodingOptions.EncoderPreset, StringComparison.OrdinalIgnoreCase)) if (valid_presets.Contains(encodingOptions.EncoderPreset, StringComparison.OrdinalIgnoreCase))
{ {
param += " -preset " + encodingOptions.EncoderPreset; 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. // No need to shift if there is only one supported video codec.
if (encodingOptions.AllowHevcEncoding) if (videoCodecs.Count < 2)
{ {
return; return;
} }
// No need to shift if there is only one supported video codec. // Shift codecs to the end of list if it's not allowed.
if (videoCodecs.Count < 2) var shiftVideoCodecs = new List<string>();
if (!encodingOptions.AllowHevcEncoding)
{ {
return; shiftVideoCodecs.Add("hevc");
shiftVideoCodecs.Add("h265");
}
if (!encodingOptions.AllowAv1Encoding)
{
shiftVideoCodecs.Add("av1");
} }
var shiftVideoCodecs = new[] { "hevc", "h265" };
if (videoCodecs.All(i => shiftVideoCodecs.Contains(i, StringComparison.OrdinalIgnoreCase))) if (videoCodecs.All(i => shiftVideoCodecs.Contains(i, StringComparison.OrdinalIgnoreCase)))
{ {
return; return;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save