Merge branch 'release-10.8.z' of github.com:jellyfin/jellyfin into fix-websockets-close-gracefully-on-shutdown

This commit is contained in:
luke brown 2022-07-22 18:16:55 -05:00
commit 70f37f0527
132 changed files with 1735 additions and 721 deletions

View file

@ -13,7 +13,7 @@ namespace Emby.Dlna.Configuration
public DlnaOptions() public DlnaOptions()
{ {
EnablePlayTo = true; EnablePlayTo = true;
EnableServer = true; EnableServer = false;
BlastAliveMessages = true; BlastAliveMessages = true;
SendOnlyMatchedHost = true; SendOnlyMatchedHost = true;
ClientDiscoveryIntervalSeconds = 60; ClientDiscoveryIntervalSeconds = 60;

View file

@ -221,6 +221,7 @@ namespace Emby.Dlna.Didl
streamInfo.IsDirectStream, streamInfo.IsDirectStream,
streamInfo.RunTimeTicks ?? 0, streamInfo.RunTimeTicks ?? 0,
streamInfo.TargetVideoProfile, streamInfo.TargetVideoProfile,
streamInfo.TargetVideoRangeType,
streamInfo.TargetVideoLevel, streamInfo.TargetVideoLevel,
streamInfo.TargetFramerate ?? 0, streamInfo.TargetFramerate ?? 0,
streamInfo.TargetPacketLength, streamInfo.TargetPacketLength,
@ -376,6 +377,7 @@ namespace Emby.Dlna.Didl
targetHeight, targetHeight,
streamInfo.TargetVideoBitDepth, streamInfo.TargetVideoBitDepth,
streamInfo.TargetVideoProfile, streamInfo.TargetVideoProfile,
streamInfo.TargetVideoRangeType,
streamInfo.TargetVideoLevel, streamInfo.TargetVideoLevel,
streamInfo.TargetFramerate ?? 0, streamInfo.TargetFramerate ?? 0,
streamInfo.TargetPacketLength, streamInfo.TargetPacketLength,

View file

@ -313,7 +313,7 @@ namespace Emby.Dlna.Main
_logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address); _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address);
var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(false) + descriptorUri); var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(address, false) + descriptorUri);
var device = new SsdpRootDevice var device = new SsdpRootDevice
{ {

View file

@ -561,6 +561,7 @@ namespace Emby.Dlna.PlayTo
streamInfo.IsDirectStream, streamInfo.IsDirectStream,
streamInfo.RunTimeTicks ?? 0, streamInfo.RunTimeTicks ?? 0,
streamInfo.TargetVideoProfile, streamInfo.TargetVideoProfile,
streamInfo.TargetVideoRangeType,
streamInfo.TargetVideoLevel, streamInfo.TargetVideoLevel,
streamInfo.TargetFramerate ?? 0, streamInfo.TargetFramerate ?? 0,
streamInfo.TargetPacketLength, streamInfo.TargetPacketLength,

View file

@ -395,7 +395,13 @@ namespace Emby.Drawing
public string GetImageBlurHash(string path) public string GetImageBlurHash(string path)
{ {
var size = GetImageDimensions(path); var size = GetImageDimensions(path);
if (size.Width <= 0 || size.Height <= 0) return GetImageBlurHash(path, size);
}
/// <inheritdoc />
public string GetImageBlurHash(string path, ImageDimensions imageDimensions)
{
if (imageDimensions.Width <= 0 || imageDimensions.Height <= 0)
{ {
return string.Empty; return string.Empty;
} }
@ -403,8 +409,8 @@ namespace Emby.Drawing
// We want tiles to be as close to square as possible, and to *mostly* keep under 16 tiles for performance. // We want tiles to be as close to square as possible, and to *mostly* keep under 16 tiles for performance.
// One tile is (width / xComp) x (height / yComp) pixels, which means that ideally yComp = xComp * height / width. // One tile is (width / xComp) x (height / yComp) pixels, which means that ideally yComp = xComp * height / width.
// See more at https://github.com/woltapp/blurhash/#how-do-i-pick-the-number-of-x-and-y-components // See more at https://github.com/woltapp/blurhash/#how-do-i-pick-the-number-of-x-and-y-components
float xCompF = MathF.Sqrt(16.0f * size.Width / size.Height); float xCompF = MathF.Sqrt(16.0f * imageDimensions.Width / imageDimensions.Height);
float yCompF = xCompF * size.Height / size.Width; float yCompF = xCompF * imageDimensions.Height / imageDimensions.Width;
int xComp = Math.Min((int)xCompF + 1, 9); int xComp = Math.Min((int)xCompF + 1, 9);
int yComp = Math.Min((int)yCompF + 1, 9); int yComp = Math.Min((int)yCompF + 1, 9);
@ -439,47 +445,46 @@ namespace Emby.Drawing
.ToString("N", CultureInfo.InvariantCulture); .ToString("N", CultureInfo.InvariantCulture);
} }
private async Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified) private Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
{ {
var inputFormat = Path.GetExtension(originalImagePath) var inputFormat = Path.GetExtension(originalImagePath.AsSpan()).TrimStart('.').ToString();
.TrimStart('.')
.Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);
// These are just jpg files renamed as tbn // These are just jpg files renamed as tbn
if (string.Equals(inputFormat, "tbn", StringComparison.OrdinalIgnoreCase)) if (string.Equals(inputFormat, "tbn", StringComparison.OrdinalIgnoreCase))
{ {
return (originalImagePath, dateModified); return Task.FromResult((originalImagePath, dateModified));
} }
if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat)) // TODO _mediaEncoder.ConvertImage is not implemented
{ // if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat))
try // {
{ // try
string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture); // {
// string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
//
// string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png";
// var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
//
// var file = _fileSystem.GetFileInfo(outputPath);
// if (!file.Exists)
// {
// await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
// dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
// }
// else
// {
// dateModified = file.LastWriteTimeUtc;
// }
//
// originalImagePath = outputPath;
// }
// catch (Exception ex)
// {
// _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath);
// }
// }
string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png"; return Task.FromResult((originalImagePath, dateModified));
var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
var file = _fileSystem.GetFileInfo(outputPath);
if (!file.Exists)
{
await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
}
else
{
dateModified = file.LastWriteTimeUtc;
}
originalImagePath = outputPath;
}
catch (Exception ex)
{
_logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath);
}
}
return (originalImagePath, dateModified);
} }
/// <summary> /// <summary>

View file

@ -314,7 +314,7 @@ namespace Emby.Naming.Common
// This isn't a Kodi naming rule, but the expression below causes false positives, // This isn't a Kodi naming rule, but the expression below causes false positives,
// so we make sure this one gets tested first. // so we make sure this one gets tested first.
// "Foo Bar 889" // "Foo Bar 889"
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/x]*$") new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[0-9]{2,4}))*[^\\\/x]*$")
{ {
IsNamed = true IsNamed = true
}, },

View file

@ -36,7 +36,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId> <PackageId>Jellyfin.Naming</PackageId>
<VersionPrefix>10.8.0</VersionPrefix> <VersionPrefix>10.8.1</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>

View file

@ -1114,13 +1114,13 @@ namespace Emby.Server.Implementations
} }
/// <inheritdoc/> /// <inheritdoc/>
public string GetApiUrlForLocalAccess(bool allowHttps = true) public string GetApiUrlForLocalAccess(IPObject hostname = null, bool allowHttps = true)
{ {
// With an empty source, the port will be null // With an empty source, the port will be null
string smart = NetManager.GetBindInterface(string.Empty, out _); var smart = NetManager.GetBindInterface(hostname ?? IPHost.None, out _);
var scheme = !allowHttps ? Uri.UriSchemeHttp : null; var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
int? port = !allowHttps ? HttpPort : null; int? port = !allowHttps ? HttpPort : null;
return GetLocalApiUrl(smart.Trim('/'), scheme, port); return GetLocalApiUrl(smart, scheme, port);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -1134,11 +1134,13 @@ namespace Emby.Server.Implementations
// NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does // NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
// not. For consistency, always trim the trailing slash. // not. For consistency, always trim the trailing slash.
scheme ??= ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
var isHttps = string.Equals(scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase);
return new UriBuilder return new UriBuilder
{ {
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp), Scheme = scheme,
Host = hostname, Host = hostname,
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort), Port = port ?? (isHttps ? HttpsPort : HttpPort),
Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
}.ToString().TrimEnd('/'); }.ToString().TrimEnd('/');
} }

View file

@ -170,7 +170,15 @@ namespace Emby.Server.Implementations.Data
"CodecTimeBase", "CodecTimeBase",
"ColorPrimaries", "ColorPrimaries",
"ColorSpace", "ColorSpace",
"ColorTransfer" "ColorTransfer",
"DvVersionMajor",
"DvVersionMinor",
"DvProfile",
"DvLevel",
"RpuPresentFlag",
"ElPresentFlag",
"BlPresentFlag",
"DvBlSignalCompatibilityId"
}; };
private static readonly string _mediaStreamSaveColumnsInsertQuery = private static readonly string _mediaStreamSaveColumnsInsertQuery =
@ -341,7 +349,7 @@ namespace Emby.Server.Implementations.Data
public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager) public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager)
{ {
const string CreateMediaStreamsTableCommand const string CreateMediaStreamsTableCommand
= "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, PRIMARY KEY (ItemId, StreamIndex))"; = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, PRIMARY KEY (ItemId, StreamIndex))";
const string CreateMediaAttachmentsTableCommand const string CreateMediaAttachmentsTableCommand
= "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))"; = "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))";
@ -555,6 +563,15 @@ namespace Emby.Server.Implementations.Data
AddColumn(db, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames); AddColumn(db, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames);
AddColumn(db, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames); AddColumn(db, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames);
AddColumn(db, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames); AddColumn(db, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames);
AddColumn(db, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames);
AddColumn(db, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames);
AddColumn(db, "MediaStreams", "DvProfile", "INT", existingColumnNames);
AddColumn(db, "MediaStreams", "DvLevel", "INT", existingColumnNames);
AddColumn(db, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames);
AddColumn(db, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames);
AddColumn(db, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames);
AddColumn(db, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames);
}, },
TransactionMode); TransactionMode);
@ -2403,7 +2420,7 @@ namespace Emby.Server.Implementations.Data
} }
// genres, tags, studios, person, year? // genres, tags, studios, person, year?
builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId))"); builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from ItemValues where ItemId=@SimilarItemId))");
if (item is MusicArtist) if (item is MusicArtist)
{ {
@ -3058,12 +3075,12 @@ namespace Emby.Server.Implementations.Data
if (string.Equals(name, ItemSortBy.Artist, StringComparison.OrdinalIgnoreCase)) if (string.Equals(name, ItemSortBy.Artist, StringComparison.OrdinalIgnoreCase))
{ {
return "(select CleanValue from itemvalues where ItemId=Guid and Type=0 LIMIT 1)"; return "(select CleanValue from ItemValues where ItemId=Guid and Type=0 LIMIT 1)";
} }
if (string.Equals(name, ItemSortBy.AlbumArtist, StringComparison.OrdinalIgnoreCase)) if (string.Equals(name, ItemSortBy.AlbumArtist, StringComparison.OrdinalIgnoreCase))
{ {
return "(select CleanValue from itemvalues where ItemId=Guid and Type=1 LIMIT 1)"; return "(select CleanValue from ItemValues where ItemId=Guid and Type=1 LIMIT 1)";
} }
if (string.Equals(name, ItemSortBy.OfficialRating, StringComparison.OrdinalIgnoreCase)) if (string.Equals(name, ItemSortBy.OfficialRating, StringComparison.OrdinalIgnoreCase))
@ -3073,7 +3090,7 @@ namespace Emby.Server.Implementations.Data
if (string.Equals(name, ItemSortBy.Studio, StringComparison.OrdinalIgnoreCase)) if (string.Equals(name, ItemSortBy.Studio, StringComparison.OrdinalIgnoreCase))
{ {
return "(select CleanValue from itemvalues where ItemId=Guid and Type=3 LIMIT 1)"; return "(select CleanValue from ItemValues where ItemId=Guid and Type=3 LIMIT 1)";
} }
if (string.Equals(name, ItemSortBy.SeriesDatePlayed, StringComparison.OrdinalIgnoreCase)) if (string.Equals(name, ItemSortBy.SeriesDatePlayed, StringComparison.OrdinalIgnoreCase))
@ -3146,6 +3163,11 @@ namespace Emby.Server.Implementations.Data
return ItemSortBy.IndexNumber; return ItemSortBy.IndexNumber;
} }
if (string.Equals(name, ItemSortBy.SimilarityScore, StringComparison.OrdinalIgnoreCase))
{
return ItemSortBy.SimilarityScore;
}
// Unknown SortBy, just sort by the SortName. // Unknown SortBy, just sort by the SortName.
return ItemSortBy.SortName; return ItemSortBy.SortName;
} }
@ -3846,7 +3868,7 @@ namespace Emby.Server.Implementations.Data
{ {
var paramName = "@ArtistIds" + index; var paramName = "@ArtistIds" + index;
clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))"); clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
if (statement != null) if (statement != null)
{ {
statement.TryBind(paramName, artistId); statement.TryBind(paramName, artistId);
@ -3867,7 +3889,7 @@ namespace Emby.Server.Implementations.Data
{ {
var paramName = "@ArtistIds" + index; var paramName = "@ArtistIds" + index;
clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))"); clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))");
if (statement != null) if (statement != null)
{ {
statement.TryBind(paramName, artistId); statement.TryBind(paramName, artistId);
@ -3888,7 +3910,7 @@ namespace Emby.Server.Implementations.Data
{ {
var paramName = "@ArtistIds" + index; var paramName = "@ArtistIds" + index;
clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from itemvalues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from itemvalues where ItemId=Guid and Type=1))"); clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1))");
if (statement != null) if (statement != null)
{ {
statement.TryBind(paramName, artistId); statement.TryBind(paramName, artistId);
@ -3930,7 +3952,7 @@ namespace Emby.Server.Implementations.Data
{ {
var paramName = "@ExcludeArtistId" + index; var paramName = "@ExcludeArtistId" + index;
clauses.Add("(guid not in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))"); clauses.Add("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
if (statement != null) if (statement != null)
{ {
statement.TryBind(paramName, artistId); statement.TryBind(paramName, artistId);
@ -3951,7 +3973,7 @@ namespace Emby.Server.Implementations.Data
{ {
var paramName = "@GenreId" + index; var paramName = "@GenreId" + index;
clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))"); clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))");
if (statement != null) if (statement != null)
{ {
statement.TryBind(paramName, genreId); statement.TryBind(paramName, genreId);
@ -3970,7 +3992,7 @@ namespace Emby.Server.Implementations.Data
var index = 0; var index = 0;
foreach (var item in query.Genres) foreach (var item in query.Genres)
{ {
clauses.Add("@Genre" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=2)"); clauses.Add("@Genre" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=2)");
if (statement != null) if (statement != null)
{ {
statement.TryBind("@Genre" + index, GetCleanValue(item)); statement.TryBind("@Genre" + index, GetCleanValue(item));
@ -3989,7 +4011,7 @@ namespace Emby.Server.Implementations.Data
var index = 0; var index = 0;
foreach (var item in tags) foreach (var item in tags)
{ {
clauses.Add("@Tag" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=4)"); clauses.Add("@Tag" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=4)");
if (statement != null) if (statement != null)
{ {
statement.TryBind("@Tag" + index, GetCleanValue(item)); statement.TryBind("@Tag" + index, GetCleanValue(item));
@ -4008,7 +4030,7 @@ namespace Emby.Server.Implementations.Data
var index = 0; var index = 0;
foreach (var item in excludeTags) foreach (var item in excludeTags)
{ {
clauses.Add("@ExcludeTag" + index + " not in (select CleanValue from itemvalues where ItemId=Guid and Type=4)"); clauses.Add("@ExcludeTag" + index + " not in (select CleanValue from ItemValues where ItemId=Guid and Type=4)");
if (statement != null) if (statement != null)
{ {
statement.TryBind("@ExcludeTag" + index, GetCleanValue(item)); statement.TryBind("@ExcludeTag" + index, GetCleanValue(item));
@ -4029,7 +4051,7 @@ namespace Emby.Server.Implementations.Data
{ {
var paramName = "@StudioId" + index; var paramName = "@StudioId" + index;
clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=3))"); clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=3))");
if (statement != null) if (statement != null)
{ {
@ -4508,7 +4530,7 @@ namespace Emby.Server.Implementations.Data
{ {
int index = 0; int index = 0;
string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(_ => paramName + index++)); string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(_ => paramName + index++));
whereClauses.Add("((select CleanValue from itemvalues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)"); whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
} }
else else
{ {
@ -4743,11 +4765,11 @@ namespace Emby.Server.Implementations.Data
';', ';',
new string[] new string[]
{ {
"delete from itemvalues where type = 6", "delete from ItemValues where type = 6",
"insert into itemvalues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4", "insert into ItemValues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4",
@"insert into itemvalues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue @"insert into ItemValues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue
FROM AncestorIds FROM AncestorIds
LEFT JOIN ItemValues ON (AncestorIds.AncestorId = ItemValues.ItemId) LEFT JOIN ItemValues ON (AncestorIds.AncestorId = ItemValues.ItemId)
where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4 " where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4 "
@ -5854,6 +5876,15 @@ AND Type = @InternalPersonType)");
statement.TryBind("@ColorPrimaries" + index, stream.ColorPrimaries); statement.TryBind("@ColorPrimaries" + index, stream.ColorPrimaries);
statement.TryBind("@ColorSpace" + index, stream.ColorSpace); statement.TryBind("@ColorSpace" + index, stream.ColorSpace);
statement.TryBind("@ColorTransfer" + index, stream.ColorTransfer); statement.TryBind("@ColorTransfer" + index, stream.ColorTransfer);
statement.TryBind("@DvVersionMajor" + index, stream.DvVersionMajor);
statement.TryBind("@DvVersionMinor" + index, stream.DvVersionMinor);
statement.TryBind("@DvProfile" + index, stream.DvProfile);
statement.TryBind("@DvLevel" + index, stream.DvLevel);
statement.TryBind("@RpuPresentFlag" + index, stream.RpuPresentFlag);
statement.TryBind("@ElPresentFlag" + index, stream.ElPresentFlag);
statement.TryBind("@BlPresentFlag" + index, stream.BlPresentFlag);
statement.TryBind("@DvBlSignalCompatibilityId" + index, stream.DvBlSignalCompatibilityId);
} }
statement.Reset(); statement.Reset();
@ -6025,6 +6056,46 @@ AND Type = @InternalPersonType)");
item.ColorTransfer = colorTransfer; item.ColorTransfer = colorTransfer;
} }
if (reader.TryGetInt32(35, out var dvVersionMajor))
{
item.DvVersionMajor = dvVersionMajor;
}
if (reader.TryGetInt32(36, out var dvVersionMinor))
{
item.DvVersionMinor = dvVersionMinor;
}
if (reader.TryGetInt32(37, out var dvProfile))
{
item.DvProfile = dvProfile;
}
if (reader.TryGetInt32(38, out var dvLevel))
{
item.DvLevel = dvLevel;
}
if (reader.TryGetInt32(39, out var rpuPresentFlag))
{
item.RpuPresentFlag = rpuPresentFlag;
}
if (reader.TryGetInt32(40, out var elPresentFlag))
{
item.ElPresentFlag = elPresentFlag;
}
if (reader.TryGetInt32(41, out var blPresentFlag))
{
item.BlPresentFlag = blPresentFlag;
}
if (reader.TryGetInt32(42, out var dvBlSignalCompatibilityId))
{
item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId;
}
if (item.Type == MediaStreamType.Subtitle) if (item.Type == MediaStreamType.Subtitle)
{ {
item.LocalizedUndefined = _localization.GetLocalizedString("Undefined"); item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");

View file

@ -29,10 +29,10 @@
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.7" />
<PackageReference Include="Mono.Nat" Version="3.0.2" /> <PackageReference Include="Mono.Nat" Version="3.0.3" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.3" /> <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.4" />
<PackageReference Include="sharpcompress" Version="0.30.1" /> <PackageReference Include="sharpcompress" Version="0.32.1" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.3" /> <PackageReference Include="DotNet.Glob" Version="3.1.3" />
</ItemGroup> </ItemGroup>

View file

@ -262,6 +262,10 @@ namespace Emby.Server.Implementations.IO
_logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName); _logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
result.Exists = false; result.Exists = false;
} }
catch (UnauthorizedAccessException ex)
{
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
}
} }
} }

View file

@ -1860,7 +1860,9 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(item)); throw new ArgumentNullException(nameof(item));
} }
var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray(); var outdated = forceUpdate
? item.ImageInfos.Where(i => i.Path != null).ToArray()
: item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
// Skip image processing if current or live tv source // Skip image processing if current or live tv source
if (outdated.Length == 0 || item.SourceType != SourceType.Library) if (outdated.Length == 0 || item.SourceType != SourceType.Library)
{ {
@ -1883,7 +1885,7 @@ namespace Emby.Server.Implementations.Library
_logger.LogWarning("Cannot get image index for {ImagePath}", img.Path); _logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
continue; continue;
} }
catch (Exception ex) when (ex is InvalidOperationException || ex is IOException) catch (Exception ex) when (ex is InvalidOperationException or IOException)
{ {
_logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path); _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
continue; continue;
@ -1895,23 +1897,24 @@ namespace Emby.Server.Implementations.Library
} }
} }
ImageDimensions size;
try try
{ {
ImageDimensions size = _imageProcessor.GetImageDimensions(item, image); size = _imageProcessor.GetImageDimensions(item, image);
image.Width = size.Width; image.Width = size.Width;
image.Height = size.Height; image.Height = size.Height;
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path); _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
size = new ImageDimensions(0, 0);
image.Width = 0; image.Width = 0;
image.Height = 0; image.Height = 0;
continue;
} }
try try
{ {
image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path); image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path, size);
} }
catch (Exception ex) catch (Exception ex)
{ {

View file

@ -13,11 +13,11 @@ namespace Emby.Server.Implementations.Library
{ {
public static int? GetDefaultAudioStreamIndex(IReadOnlyList<MediaStream> streams, IReadOnlyList<string> preferredLanguages, bool preferDefaultTrack) public static int? GetDefaultAudioStreamIndex(IReadOnlyList<MediaStream> streams, IReadOnlyList<string> preferredLanguages, bool preferDefaultTrack)
{ {
var sortedStreams = GetSortedStreams(streams, MediaStreamType.Audio, preferredLanguages); var sortedStreams = GetSortedStreams(streams, MediaStreamType.Audio, preferredLanguages).ToList();
if (preferDefaultTrack) if (preferDefaultTrack)
{ {
var defaultStream = streams.FirstOrDefault(i => i.IsDefault); var defaultStream = sortedStreams.FirstOrDefault(i => i.IsDefault);
if (defaultStream != null) if (defaultStream != null)
{ {

View file

@ -225,7 +225,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
{ {
return ResolveVideos<Episode>(parent, files, true, collectionType, true); return ResolveVideos<Episode>(parent, files, false, collectionType, true);
} }
return null; return null;

View file

@ -329,13 +329,17 @@ namespace Emby.Server.Implementations.Session
} }
/// <inheritdoc /> /// <inheritdoc />
public void CloseIfNeeded(SessionInfo session) public async Task CloseIfNeededAsync(SessionInfo session)
{ {
if (!session.SessionControllers.Any(i => i.IsSessionActive)) if (!session.SessionControllers.Any(i => i.IsSessionActive))
{ {
var key = GetSessionKey(session.Client, session.DeviceId); var key = GetSessionKey(session.Client, session.DeviceId);
_activeConnections.TryRemove(key, out _); _activeConnections.TryRemove(key, out _);
if (!string.IsNullOrEmpty(session.PlayState?.LiveStreamId))
{
await _mediaSourceManager.CloseLiveStream(session.PlayState.LiveStreamId).ConfigureAwait(false);
}
OnSessionEnded(session); OnSessionEnded(session);
} }
@ -413,6 +417,7 @@ namespace Emby.Server.Implementations.Session
session.PlayState.IsPaused = info.IsPaused; session.PlayState.IsPaused = info.IsPaused;
session.PlayState.PositionTicks = info.PositionTicks; session.PlayState.PositionTicks = info.PositionTicks;
session.PlayState.MediaSourceId = info.MediaSourceId; session.PlayState.MediaSourceId = info.MediaSourceId;
session.PlayState.LiveStreamId = info.LiveStreamId;
session.PlayState.CanSeek = info.CanSeek; session.PlayState.CanSeek = info.CanSeek;
session.PlayState.IsMuted = info.IsMuted; session.PlayState.IsMuted = info.IsMuted;
session.PlayState.VolumeLevel = info.VolumeLevel; session.PlayState.VolumeLevel = info.VolumeLevel;
@ -770,6 +775,11 @@ namespace Emby.Server.Implementations.Session
await UpdateNowPlayingItem(session, info, libraryItem, !isAutomated).ConfigureAwait(false); await UpdateNowPlayingItem(session, info, libraryItem, !isAutomated).ConfigureAwait(false);
if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
{
ClearTranscodingInfo(session.DeviceId);
}
var users = GetUsers(session); var users = GetUsers(session);
// only update saved user data on actual check-ins, not automated ones // only update saved user data on actual check-ins, not automated ones

View file

@ -53,13 +53,13 @@ namespace Emby.Server.Implementations.Session
connection.Closed += OnConnectionClosed; connection.Closed += OnConnectionClosed;
} }
private void OnConnectionClosed(object? sender, EventArgs e) private async void OnConnectionClosed(object? sender, EventArgs e)
{ {
var connection = sender as IWebSocketConnection ?? throw new ArgumentException($"{nameof(sender)} is not of type {nameof(IWebSocketConnection)}", nameof(sender)); var connection = sender as IWebSocketConnection ?? throw new ArgumentException($"{nameof(sender)} is not of type {nameof(IWebSocketConnection)}", nameof(sender));
_logger.LogDebug("Removing websocket from session {Session}", _session.Id); _logger.LogDebug("Removing websocket from session {Session}", _session.Id);
_sockets.Remove(connection); _sockets.Remove(connection);
connection.Closed -= OnConnectionClosed; connection.Closed -= OnConnectionClosed;
_sessionManager.CloseIfNeeded(_session); await _sessionManager.CloseIfNeededAsync(_session).ConfigureAwait(false);
} }
/// <inheritdoc /> /// <inheritdoc />

View file

@ -0,0 +1,25 @@
using Emby.Dlna;
using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
namespace Jellyfin.Api.Attributes;
/// <inheritdoc />
public sealed class DlnaEnabledAttribute : ActionFilterAttribute
{
/// <inheritdoc />
public override void OnActionExecuting(ActionExecutingContext context)
{
var serverConfigurationManager = context.HttpContext.RequestServices.GetRequiredService<IServerConfigurationManager>();
var enabled = serverConfigurationManager.GetDlnaConfiguration().EnableServer;
if (!enabled)
{
context.Result = new StatusCodeResult(StatusCodes.Status503ServiceUnavailable);
}
}
}

View file

@ -20,6 +20,7 @@ namespace Jellyfin.Api.Controllers
/// Dlna Server Controller. /// Dlna Server Controller.
/// </summary> /// </summary>
[Route("Dlna")] [Route("Dlna")]
[DlnaEnabled]
[Authorize(Policy = Policies.AnonymousLanAccessPolicy)] [Authorize(Policy = Policies.AnonymousLanAccessPolicy)]
public class DlnaServerController : BaseJellyfinApiController public class DlnaServerController : BaseJellyfinApiController
{ {
@ -55,15 +56,10 @@ namespace Jellyfin.Api.Controllers
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult GetDescriptionXml([FromRoute, Required] string serverId) public ActionResult GetDescriptionXml([FromRoute, Required] string serverId)
{ {
if (DlnaEntryPoint.Enabled) var url = GetAbsoluteUri();
{ var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
var url = GetAbsoluteUri(); var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); return Ok(xml);
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
return Ok(xml);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@ -83,12 +79,7 @@ namespace Jellyfin.Api.Controllers
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetContentDirectory([FromRoute, Required] string serverId) public ActionResult GetContentDirectory([FromRoute, Required] string serverId)
{ {
if (DlnaEntryPoint.Enabled) return Ok(_contentDirectory.GetServiceXml());
{
return Ok(_contentDirectory.GetServiceXml());
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@ -108,12 +99,7 @@ namespace Jellyfin.Api.Controllers
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId) public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
{ {
if (DlnaEntryPoint.Enabled) return Ok(_mediaReceiverRegistrar.GetServiceXml());
{
return Ok(_mediaReceiverRegistrar.GetServiceXml());
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@ -133,12 +119,7 @@ namespace Jellyfin.Api.Controllers
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetConnectionManager([FromRoute, Required] string serverId) public ActionResult GetConnectionManager([FromRoute, Required] string serverId)
{ {
if (DlnaEntryPoint.Enabled) return Ok(_connectionManager.GetServiceXml());
{
return Ok(_connectionManager.GetServiceXml());
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@ -155,12 +136,7 @@ namespace Jellyfin.Api.Controllers
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId) public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId)
{ {
if (DlnaEntryPoint.Enabled) return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
{
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@ -177,12 +153,7 @@ namespace Jellyfin.Api.Controllers
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId) public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId)
{ {
if (DlnaEntryPoint.Enabled) return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
{
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@ -199,12 +170,7 @@ namespace Jellyfin.Api.Controllers
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId) public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId)
{ {
if (DlnaEntryPoint.Enabled) return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
{
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@ -224,12 +190,7 @@ namespace Jellyfin.Api.Controllers
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId) public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
{ {
if (DlnaEntryPoint.Enabled) return ProcessEventRequest(_mediaReceiverRegistrar);
{
return ProcessEventRequest(_mediaReceiverRegistrar);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@ -249,12 +210,7 @@ namespace Jellyfin.Api.Controllers
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId) public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
{ {
if (DlnaEntryPoint.Enabled) return ProcessEventRequest(_contentDirectory);
{
return ProcessEventRequest(_contentDirectory);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@ -274,12 +230,7 @@ namespace Jellyfin.Api.Controllers
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId) public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
{ {
if (DlnaEntryPoint.Enabled) return ProcessEventRequest(_connectionManager);
{
return ProcessEventRequest(_connectionManager);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@ -299,12 +250,7 @@ namespace Jellyfin.Api.Controllers
[ProducesImageFile] [ProducesImageFile]
public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName) public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName)
{ {
if (DlnaEntryPoint.Enabled) return GetIconInternal(fileName);
{
return GetIconInternal(fileName);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
/// <summary> /// <summary>
@ -322,12 +268,7 @@ namespace Jellyfin.Api.Controllers
[ProducesImageFile] [ProducesImageFile]
public ActionResult GetIcon([FromRoute, Required] string fileName) public ActionResult GetIcon([FromRoute, Required] string fileName)
{ {
if (DlnaEntryPoint.Enabled) return GetIconInternal(fileName);
{
return GetIconInternal(fileName);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
} }
private ActionResult GetIconInternal(string fileName) private ActionResult GetIconInternal(string fileName)

View file

@ -285,7 +285,7 @@ namespace Jellyfin.Api.Controllers
// Due to CTS.Token calling ThrowIfDisposed (https://github.com/dotnet/runtime/issues/29970) we have to "cache" the token // Due to CTS.Token calling ThrowIfDisposed (https://github.com/dotnet/runtime/issues/29970) we have to "cache" the token
// since it gets disposed when ffmpeg exits // since it gets disposed when ffmpeg exits
var cancellationToken = cancellationTokenSource.Token; var cancellationToken = cancellationTokenSource.Token;
using var state = await StreamingHelpers.GetStreamingState( var state = await StreamingHelpers.GetStreamingState(
streamingRequest, streamingRequest,
Request, Request,
_authContext, _authContext,
@ -1414,7 +1414,8 @@ namespace Jellyfin.Api.Controllers
state.RunTimeTicks ?? 0, state.RunTimeTicks ?? 0,
state.Request.SegmentContainer ?? string.Empty, state.Request.SegmentContainer ?? string.Empty,
"hls1/main/", "hls1/main/",
Request.QueryString.ToString()); Request.QueryString.ToString(),
EncodingHelper.IsCopyCodec(state.OutputVideoCodec));
var playlist = _dynamicHlsPlaylistGenerator.CreateMainPlaylist(request); var playlist = _dynamicHlsPlaylistGenerator.CreateMainPlaylist(request);
return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8")); return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8"));
@ -1431,7 +1432,7 @@ namespace Jellyfin.Api.Controllers
var cancellationTokenSource = new CancellationTokenSource(); var cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token; var cancellationToken = cancellationTokenSource.Token;
using var state = await StreamingHelpers.GetStreamingState( var state = await StreamingHelpers.GetStreamingState(
streamingRequest, streamingRequest,
Request, Request,
_authContext, _authContext,
@ -1711,20 +1712,30 @@ namespace Jellyfin.Api.Controllers
return audioTranscodeParams; return audioTranscodeParams;
} }
// flac and opus are experimental in mp4 muxer
var strictArgs = string.Empty;
if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
{
strictArgs = " -strict -2";
}
if (EncodingHelper.IsCopyCodec(audioCodec)) if (EncodingHelper.IsCopyCodec(audioCodec))
{ {
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs;
if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec)) if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
{ {
return "-codec:a:0 copy -strict -2 -copypriorss:a:0 0" + bitStreamArgs; return copyArgs + " -copypriorss:a:0 0";
} }
return "-codec:a:0 copy -strict -2" + bitStreamArgs; return copyArgs;
} }
var args = "-codec:a:0 " + audioCodec; var args = "-codec:a:0 " + audioCodec + strictArgs;
var channels = state.OutputAudioChannels; var channels = state.OutputAudioChannels;
@ -1779,11 +1790,12 @@ namespace Jellyfin.Api.Controllers
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
{ {
if (EncodingHelper.IsCopyCodec(codec) if (EncodingHelper.IsCopyCodec(codec)
&& (string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase) && (string.Equals(state.VideoStream.CodecTag, "dovi", 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)))
{ {
// Prefer dvh1 to dvhe // Prefer dvh1 to dvhe
args += " -tag:v:0 dvh1"; args += " -tag:v:0 dvh1 -strict -2";
} }
else else
{ {

View file

@ -1724,6 +1724,11 @@ namespace Jellyfin.Api.Controllers
[FromQuery, Range(0, 100)] int quality = 90) [FromQuery, Range(0, 100)] int quality = 90)
{ {
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
if (!brandingOptions.SplashscreenEnabled)
{
return NotFound();
}
string splashscreenPath; string splashscreenPath;
if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation) if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation)
@ -1776,6 +1781,7 @@ namespace Jellyfin.Api.Controllers
/// <summary> /// <summary>
/// Uploads a custom splashscreen. /// Uploads a custom splashscreen.
/// The body is expected to the image contents base64 encoded.
/// </summary> /// </summary>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
/// <response code="204">Successfully uploaded new splashscreen.</response> /// <response code="204">Successfully uploaded new splashscreen.</response>
@ -1799,7 +1805,13 @@ namespace Jellyfin.Api.Controllers
return BadRequest("Error reading mimetype from uploaded image"); return BadRequest("Error reading mimetype from uploaded image");
} }
var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + MimeTypes.ToExtension(mimeType.Value)); var extension = MimeTypes.ToExtension(mimeType.Value);
if (string.IsNullOrEmpty(extension))
{
return BadRequest("Error converting mimetype to an image extension");
}
var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
brandingOptions.SplashscreenLocation = filePath; brandingOptions.SplashscreenLocation = filePath;
_serverConfigurationManager.SaveConfiguration("branding", brandingOptions); _serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
@ -1812,6 +1824,29 @@ namespace Jellyfin.Api.Controllers
return NoContent(); return NoContent();
} }
/// <summary>
/// Delete a custom splashscreen.
/// </summary>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
/// <response code="204">Successfully deleted the custom splashscreen.</response>
/// <response code="403">User does not have permission to delete splashscreen..</response>
[HttpDelete("Branding/Splashscreen")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult DeleteCustomSplashscreen()
{
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
if (!string.IsNullOrEmpty(brandingOptions.SplashscreenLocation)
&& System.IO.File.Exists(brandingOptions.SplashscreenLocation))
{
System.IO.File.Delete(brandingOptions.SplashscreenLocation);
brandingOptions.SplashscreenLocation = null;
_serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
}
return NoContent();
}
private static async Task<MemoryStream> GetMemoryStream(Stream inputStream) private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
{ {
using var reader = new StreamReader(inputStream); using var reader = new StreamReader(inputStream);

View file

@ -89,6 +89,11 @@ namespace Jellyfin.Api.Controllers
/// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param> /// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
/// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param> /// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
/// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param> /// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
/// <param name="isMovie">Optional filter for live tv movies.</param>
/// <param name="isSeries">Optional filter for live tv series.</param>
/// <param name="isNews">Optional filter for live tv news.</param>
/// <param name="isKids">Optional filter for live tv kids.</param>
/// <param name="isSports">Optional filter for live tv sports.</param>
/// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param> /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="limit">Optional. The maximum number of records to return.</param>
@ -173,6 +178,11 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasImdbId, [FromQuery] bool? hasImdbId,
[FromQuery] bool? hasTmdbId, [FromQuery] bool? hasTmdbId,
[FromQuery] bool? hasTvdbId, [FromQuery] bool? hasTvdbId,
[FromQuery] bool? isMovie,
[FromQuery] bool? isSeries,
[FromQuery] bool? isNews,
[FromQuery] bool? isKids,
[FromQuery] bool? isSports,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
@ -316,6 +326,11 @@ namespace Jellyfin.Api.Controllers
Is3D = is3D, Is3D = is3D,
HasTvdbId = hasTvdbId, HasTvdbId = hasTvdbId,
HasTmdbId = hasTmdbId, HasTmdbId = hasTmdbId,
IsMovie = isMovie,
IsSeries = isSeries,
IsNews = isNews,
IsKids = isKids,
IsSports = isSports,
HasOverview = hasOverview, HasOverview = hasOverview,
HasOfficialRating = hasOfficialRating, HasOfficialRating = hasOfficialRating,
HasParentalRating = hasParentalRating, HasParentalRating = hasParentalRating,
@ -515,8 +530,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param> /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
/// <param name="isHd">Optional filter by items that are HD or not.</param> /// <param name="isHd">Optional filter by items that are HD or not.</param>
/// <param name="is4K">Optional filter by items that are 4K or not.</param> /// <param name="is4K">Optional filter by items that are 4K or not.</param>
/// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.</param> /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
/// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted.</param> /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
/// <param name="isMissing">Optional filter by items that are missing episodes or not.</param> /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
/// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param> /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param> /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
@ -529,42 +544,47 @@ namespace Jellyfin.Api.Controllers
/// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param> /// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
/// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param> /// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
/// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param> /// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
/// <param name="excludeItemIds">Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.</param> /// <param name="isMovie">Optional filter for live tv movies.</param>
/// <param name="isSeries">Optional filter for live tv series.</param>
/// <param name="isNews">Optional filter for live tv news.</param>
/// <param name="isKids">Optional filter for live tv kids.</param>
/// <param name="isSports">Optional filter for live tv sports.</param>
/// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param> /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
/// <param name="searchTerm">Optional. Filter based on a search term.</param> /// <param name="searchTerm">Optional. Filter based on a search term.</param>
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param> /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param> /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted.</param> /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param>
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param> /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param> /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param> /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param> /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param> /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
/// <param name="isPlayed">Optional filter by items that are played, or not.</param> /// <param name="isPlayed">Optional filter by items that are played, or not.</param>
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.</param> /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.</param> /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.</param> /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.</param> /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
/// <param name="enableUserData">Optional, include user data.</param> /// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param> /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param> /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param> /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param> /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param> /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.</param> /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
/// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted.</param> /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param>
/// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted.</param> /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
/// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param> /// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
/// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param> /// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
/// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param> /// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
/// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.</param> /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
/// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted.</param> /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
/// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param> /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
/// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.</param> /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
/// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param> /// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
/// <param name="isLocked">Optional filter by items that are locked.</param> /// <param name="isLocked">Optional filter by items that are locked.</param>
/// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param> /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
@ -575,12 +595,12 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param> /// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
/// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param> /// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
/// <param name="is3D">Optional filter by items that are 3D, or not.</param> /// <param name="is3D">Optional filter by items that are 3D, or not.</param>
/// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimeted.</param> /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimeted.</param> /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted.</param> /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param> /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
/// <param name="enableImages">Optional, include image information in output.</param> /// <param name="enableImages">Optional, include image information in output.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
@ -613,6 +633,11 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasImdbId, [FromQuery] bool? hasImdbId,
[FromQuery] bool? hasTmdbId, [FromQuery] bool? hasTmdbId,
[FromQuery] bool? hasTvdbId, [FromQuery] bool? hasTvdbId,
[FromQuery] bool? isMovie,
[FromQuery] bool? isSeries,
[FromQuery] bool? isNews,
[FromQuery] bool? isKids,
[FromQuery] bool? isSports,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
@ -695,6 +720,11 @@ namespace Jellyfin.Api.Controllers
hasImdbId, hasImdbId,
hasTmdbId, hasTmdbId,
hasTvdbId, hasTvdbId,
isMovie,
isSeries,
isNews,
isKids,
isSports,
excludeItemIds, excludeItemIds,
startIndex, startIndex,
limit, limit,

View file

@ -6,6 +6,7 @@ using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -187,7 +188,7 @@ namespace Jellyfin.Api.Controllers
result.AlbumArtist = album.AlbumArtist; result.AlbumArtist = album.AlbumArtist;
break; break;
case Audio song: case Audio song:
result.AlbumArtist = song.AlbumArtists?[0]; result.AlbumArtist = song.AlbumArtists?.FirstOrDefault();
result.Artists = song.Artists; result.Artists = song.Artists;
MusicAlbum musicAlbum = song.AlbumEntity; MusicAlbum musicAlbum = song.AlbumEntity;

View file

@ -57,6 +57,11 @@ namespace Jellyfin.Api.Controllers
/// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param> /// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
/// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param> /// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
/// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param> /// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
/// <param name="isMovie">Optional filter for live tv movies.</param>
/// <param name="isSeries">Optional filter for live tv series.</param>
/// <param name="isNews">Optional filter for live tv news.</param>
/// <param name="isKids">Optional filter for live tv kids.</param>
/// <param name="isSports">Optional filter for live tv sports.</param>
/// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param> /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="limit">Optional. The maximum number of records to return.</param>
@ -140,6 +145,11 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasImdbId, [FromQuery] bool? hasImdbId,
[FromQuery] bool? hasTmdbId, [FromQuery] bool? hasTmdbId,
[FromQuery] bool? hasTvdbId, [FromQuery] bool? hasTvdbId,
[FromQuery] bool? isMovie,
[FromQuery] bool? isSeries,
[FromQuery] bool? isNews,
[FromQuery] bool? isKids,
[FromQuery] bool? isSports,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
@ -224,6 +234,11 @@ namespace Jellyfin.Api.Controllers
hasImdbId, hasImdbId,
hasTmdbId, hasTmdbId,
hasTvdbId, hasTvdbId,
isMovie,
isSeries,
isNews,
isKids,
isSports,
excludeItemIds, excludeItemIds,
startIndex, startIndex,
limit, limit,

View file

@ -282,16 +282,19 @@ namespace Jellyfin.Api.Controllers
} }
else else
{ {
var success = await _userManager.AuthenticateUser( if (!HttpContext.User.IsInRole(UserRoles.Administrator))
user.Username,
request.CurrentPw,
request.CurrentPw,
HttpContext.GetNormalizedRemoteIp().ToString(),
false).ConfigureAwait(false);
if (success == null)
{ {
return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered."); var success = await _userManager.AuthenticateUser(
user.Username,
request.CurrentPw,
request.CurrentPw,
HttpContext.GetNormalizedRemoteIp().ToString(),
false).ConfigureAwait(false);
if (success == null)
{
return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered.");
}
} }
await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false); await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);

View file

@ -427,7 +427,7 @@ namespace Jellyfin.Api.Controllers
StreamOptions = streamOptions StreamOptions = streamOptions
}; };
using var state = await StreamingHelpers.GetStreamingState( var state = await StreamingHelpers.GetStreamingState(
streamingRequest, streamingRequest,
Request, Request,
_authContext, _authContext,

View file

@ -216,7 +216,7 @@ namespace Jellyfin.Api.Helpers
var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main"); var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main");
sdrVideoUrl += "&AllowVideoStreamCopy=false"; sdrVideoUrl += "&AllowVideoStreamCopy=false";
var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec) ?? 0; var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0; var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0;
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate; var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;

View file

@ -256,9 +256,17 @@ namespace Jellyfin.Api.Helpers
streamInfo.StartPositionTicks = startTimeTicks; streamInfo.StartPositionTicks = startTimeTicks;
mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay; mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay;
// Players do not handle this being set according to PlayMethod // Players do not handle this being set according to PlayMethod
mediaSource.SupportsDirectStream = options.EnableDirectStream ? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream : streamInfo.PlayMethod == PlayMethod.DirectPlay; mediaSource.SupportsDirectStream =
mediaSource.SupportsTranscoding = streamInfo.PlayMethod == PlayMethod.DirectStream || mediaSource.TranscodingContainer != null; options.EnableDirectStream
? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream
: streamInfo.PlayMethod == PlayMethod.DirectPlay;
mediaSource.SupportsTranscoding =
streamInfo.PlayMethod == PlayMethod.DirectStream
|| mediaSource.TranscodingContainer != null
|| profile.TranscodingProfiles.Any(i => i.Type == streamInfo.MediaType && i.Context == options.Context);
if (item is Audio) if (item is Audio)
{ {
@ -290,7 +298,7 @@ namespace Jellyfin.Api.Helpers
} }
else else
{ {
if (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream) if (!mediaSource.SupportsDirectPlay && (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream))
{ {
streamInfo.PlayMethod = PlayMethod.Transcode; streamInfo.PlayMethod = PlayMethod.Transcode;
mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');

View file

@ -179,7 +179,7 @@ namespace Jellyfin.Api.Helpers
{ {
containerInternal = streamingRequest.Static ? containerInternal = streamingRequest.Static ?
StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, null, DlnaProfileType.Audio) StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, null, DlnaProfileType.Audio)
: GetOutputFileExtension(state); : GetOutputFileExtension(state, mediaSource);
} }
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.'); state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
@ -235,7 +235,7 @@ namespace Jellyfin.Api.Helpers
ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, httpRequest, streamingRequest.DeviceProfileId, streamingRequest.Static); ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, httpRequest, streamingRequest.DeviceProfileId, streamingRequest.Static);
var ext = string.IsNullOrWhiteSpace(state.OutputContainer) var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
? GetOutputFileExtension(state) ? GetOutputFileExtension(state, mediaSource)
: ("." + state.OutputContainer); : ("." + state.OutputContainer);
state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId); state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
@ -312,7 +312,7 @@ namespace Jellyfin.Api.Helpers
responseHeaders.Add( responseHeaders.Add(
"contentFeatures.dlna.org", "contentFeatures.dlna.org",
ContentFeatureBuilder.BuildVideoHeader(profile, state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty); ContentFeatureBuilder.BuildVideoHeader(profile, state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoRangeType, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty);
} }
} }
@ -409,8 +409,9 @@ namespace Jellyfin.Api.Helpers
/// Gets the output file extension. /// Gets the output file extension.
/// </summary> /// </summary>
/// <param name="state">The state.</param> /// <param name="state">The state.</param>
/// <param name="mediaSource">The mediaSource.</param>
/// <returns>System.String.</returns> /// <returns>System.String.</returns>
private static string? GetOutputFileExtension(StreamState state) private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
{ {
var ext = Path.GetExtension(state.RequestedUrl); var ext = Path.GetExtension(state.RequestedUrl);
@ -425,7 +426,7 @@ namespace Jellyfin.Api.Helpers
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, "h265", StringComparison.OrdinalIgnoreCase)) string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{ {
return ".ts"; return ".ts";
} }
@ -474,6 +475,13 @@ namespace Jellyfin.Api.Helpers
} }
} }
// Fallback to the container of mediaSource
if (!string.IsNullOrEmpty(mediaSource?.Container))
{
var idx = mediaSource.Container.IndexOf(',', StringComparison.OrdinalIgnoreCase);
return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim();
}
return null; return null;
} }
@ -533,6 +541,7 @@ namespace Jellyfin.Api.Helpers
state.TargetVideoBitDepth, state.TargetVideoBitDepth,
state.OutputVideoBitrate, state.OutputVideoBitrate,
state.TargetVideoProfile, state.TargetVideoProfile,
state.TargetVideoRangeType,
state.TargetVideoLevel, state.TargetVideoLevel,
state.TargetFramerate, state.TargetFramerate,
state.TargetPacketLength, state.TargetPacketLength,

View file

@ -654,8 +654,8 @@ namespace Jellyfin.Api.Helpers
{ {
if (EnableThrottling(state)) if (EnableThrottling(state))
{ {
transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem); transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem);
state.TranscodingThrottler.Start(); transcodingJob.TranscodingThrottler.Start();
} }
} }
@ -663,18 +663,11 @@ namespace Jellyfin.Api.Helpers
{ {
var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
// enable throttling when NOT using hardware acceleration return state.InputProtocol == MediaProtocol.File &&
if (string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType)) state.RunTimeTicks.HasValue &&
{ state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
return state.InputProtocol == MediaProtocol.File && state.IsInputVideo &&
state.RunTimeTicks.HasValue && state.VideoType == VideoType.VideoFile;
state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
state.IsInputVideo &&
state.VideoType == VideoType.VideoFile &&
!EncodingHelper.IsCopyCodec(state.OutputVideoCodec);
}
return false;
} }
/// <summary> /// <summary>

View file

@ -17,10 +17,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.4" /> <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.7" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.3.0" /> <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.3.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -47,11 +47,6 @@ namespace Jellyfin.Api.Models.StreamingDtos
} }
} }
/// <summary>
/// Gets or sets the transcoding throttler.
/// </summary>
public TranscodingThrottler? TranscodingThrottler { get; set; }
/// <summary> /// <summary>
/// Gets the video request. /// Gets the video request.
/// </summary> /// </summary>
@ -191,11 +186,8 @@ namespace Jellyfin.Api.Models.StreamingDtos
{ {
_mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult(); _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult();
} }
TranscodingThrottler?.Dispose();
} }
TranscodingThrottler = null;
TranscodingJob = null; TranscodingJob = null;
_disposed = true; _disposed = true;

View file

@ -18,7 +18,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Data</PackageId> <PackageId>Jellyfin.Data</PackageId>
<VersionPrefix>10.8.0</VersionPrefix> <VersionPrefix>10.8.1</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>

View file

@ -18,8 +18,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="BlurHashSharp" Version="1.2.0" /> <PackageReference Include="BlurHashSharp" Version="1.2.0" />
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.2.0" /> <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
<PackageReference Include="SkiaSharp" Version="2.80.3" /> <PackageReference Include="SkiaSharp" Version="2.88.1-preview.79" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.3" /> <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.1-preview.79" />
<PackageReference Include="SkiaSharp.Svg" Version="1.60.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -10,7 +10,7 @@ using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Drawing;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SkiaSharp; using SkiaSharp;
using static Jellyfin.Drawing.Skia.SkiaHelper; using SKSvg = SkiaSharp.Extended.Svg.SKSvg;
namespace Jellyfin.Drawing.Skia namespace Jellyfin.Drawing.Skia
{ {
@ -19,8 +19,7 @@ namespace Jellyfin.Drawing.Skia
/// </summary> /// </summary>
public class SkiaEncoder : IImageEncoder public class SkiaEncoder : IImageEncoder
{ {
private static readonly HashSet<string> _transparentImageTypes private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
private readonly ILogger<SkiaEncoder> _logger; private readonly ILogger<SkiaEncoder> _logger;
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
@ -71,7 +70,7 @@ namespace Jellyfin.Drawing.Skia
/// <inheritdoc/> /// <inheritdoc/>
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
=> new HashSet<ImageFormat>() { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png }; => new HashSet<ImageFormat> { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
/// <summary> /// <summary>
/// Check if the native lib is available. /// Check if the native lib is available.
@ -109,9 +108,7 @@ namespace Jellyfin.Drawing.Skia
} }
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentNullException">The path is null.</exception>
/// <exception cref="FileNotFoundException">The path is not valid.</exception> /// <exception cref="FileNotFoundException">The path is not valid.</exception>
/// <exception cref="SkiaCodecException">The file at the specified path could not be used to generate a codec.</exception>
public ImageDimensions GetImageSize(string path) public ImageDimensions GetImageSize(string path)
{ {
if (!File.Exists(path)) if (!File.Exists(path))
@ -119,12 +116,27 @@ namespace Jellyfin.Drawing.Skia
throw new FileNotFoundException("File not found", path); throw new FileNotFoundException("File not found", path);
} }
var extension = Path.GetExtension(path.AsSpan());
if (extension.Equals(".svg", StringComparison.OrdinalIgnoreCase))
{
var svg = new SKSvg();
svg.Load(path);
return new ImageDimensions(Convert.ToInt32(svg.Picture.CullRect.Width), Convert.ToInt32(svg.Picture.CullRect.Height));
}
using var codec = SKCodec.Create(path, out SKCodecResult result); using var codec = SKCodec.Create(path, out SKCodecResult result);
EnsureSuccess(result); switch (result)
{
var info = codec.Info; case SKCodecResult.Success:
var info = codec.Info;
return new ImageDimensions(info.Width, info.Height); return new ImageDimensions(info.Width, info.Height);
case SKCodecResult.Unimplemented:
_logger.LogDebug("Image format not supported: {FilePath}", path);
return new ImageDimensions(0, 0);
default:
_logger.LogError("Unable to determine image dimensions for {FilePath}: {SkCodecResult}", path, result);
return new ImageDimensions(0, 0);
}
} }
/// <inheritdoc /> /// <inheritdoc />
@ -138,6 +150,13 @@ namespace Jellyfin.Drawing.Skia
throw new ArgumentNullException(nameof(path)); throw new ArgumentNullException(nameof(path));
} }
var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
if (!SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
_logger.LogDebug("Unable to compute blur hash due to unsupported format: {ImagePath}", path);
return string.Empty;
}
// Any larger than 128x128 is too slow and there's no visually discernible difference // Any larger than 128x128 is too slow and there's no visually discernible difference
return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128); return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
} }
@ -378,6 +397,13 @@ namespace Jellyfin.Drawing.Skia
throw new ArgumentException("String can't be empty.", nameof(outputPath)); throw new ArgumentException("String can't be empty.", nameof(outputPath));
} }
var inputFormat = Path.GetExtension(inputPath.AsSpan()).TrimStart('.');
if (!SupportedInputFormats.Contains(inputFormat, StringComparison.OrdinalIgnoreCase))
{
_logger.LogDebug("Unable to encode image due to unsupported format: {ImagePath}", inputPath);
return inputPath;
}
var skiaOutputFormat = GetImageFormat(outputFormat); var skiaOutputFormat = GetImageFormat(outputFormat);
var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor); var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);

View file

@ -8,19 +8,6 @@ namespace Jellyfin.Drawing.Skia
/// </summary> /// </summary>
public static class SkiaHelper public static class SkiaHelper
{ {
/// <summary>
/// Ensures the result is a success
/// by throwing an exception when that's not the case.
/// </summary>
/// <param name="result">The result returned by Skia.</param>
public static void EnsureSuccess(SKCodecResult result)
{
if (result != SKCodecResult.Success)
{
throw new SkiaCodecException(result);
}
}
/// <summary> /// <summary>
/// Gets the next valid image as a bitmap. /// Gets the next valid image as a bitmap.
/// </summary> /// </summary>

View file

@ -27,13 +27,13 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Linq.Async" Version="6.0.1" /> <PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.4"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.4"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.7">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View file

@ -4,6 +4,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -16,11 +17,16 @@ namespace Jellyfin.Server.Implementations.Security
{ {
private readonly JellyfinDbProvider _jellyfinDbProvider; private readonly JellyfinDbProvider _jellyfinDbProvider;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IServerApplicationHost _serverApplicationHost;
public AuthorizationContext(JellyfinDbProvider jellyfinDb, IUserManager userManager) public AuthorizationContext(
JellyfinDbProvider jellyfinDb,
IUserManager userManager,
IServerApplicationHost serverApplicationHost)
{ {
_jellyfinDbProvider = jellyfinDb; _jellyfinDbProvider = jellyfinDb;
_userManager = userManager; _userManager = userManager;
_serverApplicationHost = serverApplicationHost;
} }
public Task<AuthorizationInfo> GetAuthorizationInfo(HttpContext requestContext) public Task<AuthorizationInfo> GetAuthorizationInfo(HttpContext requestContext)
@ -187,17 +193,17 @@ namespace Jellyfin.Server.Implementations.Security
authInfo.Token = key.AccessToken; authInfo.Token = key.AccessToken;
if (string.IsNullOrWhiteSpace(authInfo.DeviceId)) if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
{ {
authInfo.DeviceId = string.Empty; authInfo.DeviceId = _serverApplicationHost.SystemId;
} }
if (string.IsNullOrWhiteSpace(authInfo.Device)) if (string.IsNullOrWhiteSpace(authInfo.Device))
{ {
authInfo.Device = string.Empty; authInfo.Device = _serverApplicationHost.Name;
} }
if (string.IsNullOrWhiteSpace(authInfo.Version)) if (string.IsNullOrWhiteSpace(authInfo.Version))
{ {
authInfo.Version = string.Empty; authInfo.Version = _serverApplicationHost.ApplicationVersionString;
} }
authInfo.IsApiKey = true; authInfo.IsApiKey = true;

View file

@ -440,6 +440,12 @@ namespace Jellyfin.Server.Extensions
.Cast<IOpenApiAny>() .Cast<IOpenApiAny>()
.ToArray() .ToArray()
}); });
// Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it.
options.MapType<Version>(() => new OpenApiSchema
{
Type = "string"
});
} }
} }
} }

View file

@ -1,3 +1,6 @@
using System;
using Jellyfin.Extensions;
using Jellyfin.Server.Migrations;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
@ -15,6 +18,8 @@ namespace Jellyfin.Server.Filters
/// </summary> /// </summary>
public class AdditionalModelFilter : IDocumentFilter public class AdditionalModelFilter : IDocumentFilter
{ {
// Array of options that should not be visible in the api spec.
private static readonly Type[] _ignoredConfigurations = { typeof(MigrationOptions) };
private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IServerConfigurationManager _serverConfigurationManager;
/// <summary> /// <summary>
@ -44,6 +49,11 @@ namespace Jellyfin.Server.Filters
foreach (var configuration in _serverConfigurationManager.GetConfigurationStores()) foreach (var configuration in _serverConfigurationManager.GetConfigurationStores())
{ {
if (_ignoredConfigurations.IndexOf(configuration.ConfigurationType) != -1)
{
continue;
}
context.SchemaGenerator.GenerateSchema(configuration.ConfigurationType, context.SchemaRepository); context.SchemaGenerator.GenerateSchema(configuration.ConfigurationType, context.SchemaRepository);
} }
} }

View file

@ -34,11 +34,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" /> <PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.4" /> <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.7" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.4" /> <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.7" />
<PackageReference Include="prometheus-net" Version="6.0.0" /> <PackageReference Include="prometheus-net" Version="6.0.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" /> <PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" /> <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
@ -48,7 +48,7 @@
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" /> <PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.Graylog" Version="2.3.0" /> <PackageReference Include="Serilog.Sinks.Graylog" Version="2.3.0" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.7" /> <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -19,41 +19,44 @@ namespace Jellyfin.Server.Middleware
private readonly RequestDelegate _next; private readonly RequestDelegate _next;
private readonly ILogger<ResponseTimeMiddleware> _logger; private readonly ILogger<ResponseTimeMiddleware> _logger;
private readonly bool _enableWarning;
private readonly long _warningThreshold;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ResponseTimeMiddleware"/> class. /// Initializes a new instance of the <see cref="ResponseTimeMiddleware"/> class.
/// </summary> /// </summary>
/// <param name="next">Next request delegate.</param> /// <param name="next">Next request delegate.</param>
/// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
public ResponseTimeMiddleware( public ResponseTimeMiddleware(
RequestDelegate next, RequestDelegate next,
ILogger<ResponseTimeMiddleware> logger, ILogger<ResponseTimeMiddleware> logger)
IServerConfigurationManager serverConfigurationManager)
{ {
_next = next; _next = next;
_logger = logger; _logger = logger;
_enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning;
_warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs;
} }
/// <summary> /// <summary>
/// Invoke request. /// Invoke request.
/// </summary> /// </summary>
/// <param name="context">Request context.</param> /// <param name="context">Request context.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <returns>Task.</returns> /// <returns>Task.</returns>
public async Task Invoke(HttpContext context) public async Task Invoke(HttpContext context, IServerConfigurationManager serverConfigurationManager)
{ {
var watch = new Stopwatch(); var watch = new Stopwatch();
watch.Start(); watch.Start();
var enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning;
var warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs;
context.Response.OnStarting(() => context.Response.OnStarting(() =>
{ {
watch.Stop(); watch.Stop();
LogWarning(context, watch); if (enableWarning && watch.ElapsedMilliseconds > warningThreshold)
{
_logger.LogWarning(
"Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}",
context.Request.GetDisplayUrl(),
context.GetNormalizedRemoteIp(),
watch.Elapsed,
context.Response.StatusCode);
}
var responseTimeForCompleteRequest = watch.ElapsedMilliseconds; var responseTimeForCompleteRequest = watch.ElapsedMilliseconds;
context.Response.Headers[ResponseHeaderResponseTime] = responseTimeForCompleteRequest.ToString(CultureInfo.InvariantCulture); context.Response.Headers[ResponseHeaderResponseTime] = responseTimeForCompleteRequest.ToString(CultureInfo.InvariantCulture);
return Task.CompletedTask; return Task.CompletedTask;
@ -62,18 +65,5 @@ namespace Jellyfin.Server.Middleware
// Call the next delegate/middleware in the pipeline // Call the next delegate/middleware in the pipeline
await this._next(context).ConfigureAwait(false); await this._next(context).ConfigureAwait(false);
} }
private void LogWarning(HttpContext context, Stopwatch watch)
{
if (_enableWarning && watch.ElapsedMilliseconds > _warningThreshold)
{
_logger.LogWarning(
"Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}",
context.Request.GetDisplayUrl(),
context.GetNormalizedRemoteIp(),
watch.Elapsed,
context.Response.StatusCode);
}
}
} }
} }

View file

@ -545,12 +545,14 @@ namespace Jellyfin.Server
const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json"; const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath) Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)
?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'"); ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await using (resource.ConfigureAwait(false)) await using (resource.ConfigureAwait(false))
await using (dst.ConfigureAwait(false))
{ {
// Copy the resource contents to the expected file path for the config file Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await resource.CopyToAsync(dst).ConfigureAwait(false); await using (dst.ConfigureAwait(false))
{
// Copy the resource contents to the expected file path for the config file
await resource.CopyToAsync(dst).ConfigureAwait(false);
}
} }
} }

View file

@ -8,7 +8,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Common</PackageId> <PackageId>Jellyfin.Common</PackageId>
<VersionPrefix>10.8.0</VersionPrefix> <VersionPrefix>10.8.1</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>

View file

@ -50,6 +50,14 @@ namespace MediaBrowser.Controller.Drawing
/// <returns>BlurHash.</returns> /// <returns>BlurHash.</returns>
string GetImageBlurHash(string path); string GetImageBlurHash(string path);
/// <summary>
/// Gets the blurhash of the image.
/// </summary>
/// <param name="path">Path to the image file.</param>
/// <param name="imageDimensions">The image dimensions.</param>
/// <returns>BlurHash.</returns>
string GetImageBlurHash(string path, ImageDimensions imageDimensions);
/// <summary> /// <summary>
/// Gets the image cache tag. /// Gets the image cache tag.
/// </summary> /// </summary>

View file

@ -4,6 +4,7 @@
using System.Net; using System.Net;
using MediaBrowser.Common; using MediaBrowser.Common;
using MediaBrowser.Common.Net;
using MediaBrowser.Model.System; using MediaBrowser.Model.System;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -74,9 +75,10 @@ namespace MediaBrowser.Controller
/// <summary> /// <summary>
/// Gets an URL that can be used to access the API over LAN. /// Gets an URL that can be used to access the API over LAN.
/// </summary> /// </summary>
/// <param name="hostname">An optional hostname to use.</param>
/// <param name="allowHttps">A value indicating whether to allow HTTPS.</param> /// <param name="allowHttps">A value indicating whether to allow HTTPS.</param>
/// <returns>The API URL.</returns> /// <returns>The API URL.</returns>
string GetApiUrlForLocalAccess(bool allowHttps = true); string GetApiUrlForLocalAccess(IPObject hostname = null, bool allowHttps = true);
/// <summary> /// <summary>
/// Gets a local (LAN) URL that can be used to access the API. /// Gets a local (LAN) URL that can be used to access the API.

View file

@ -8,7 +8,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Controller</PackageId> <PackageId>Jellyfin.Controller</PackageId>
<VersionPrefix>10.8.0</VersionPrefix> <VersionPrefix>10.8.1</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>

View file

@ -75,6 +75,12 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <value>The profile.</value> /// <value>The profile.</value>
public string Profile { get; set; } public string Profile { get; set; }
/// <summary>
/// Gets or sets the video range type.
/// </summary>
/// <value>The video range type.</value>
public string VideoRangeType { get; set; }
/// <summary> /// <summary>
/// Gets or sets the level. /// Gets or sets the level.
/// </summary> /// </summary>

View file

@ -13,11 +13,13 @@ using System.Threading;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Configuration;
namespace MediaBrowser.Controller.MediaEncoding namespace MediaBrowser.Controller.MediaEncoding
{ {
@ -32,6 +34,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly ISubtitleEncoder _subtitleEncoder; private readonly ISubtitleEncoder _subtitleEncoder;
private readonly IConfiguration _config;
private static readonly string[] _videoProfilesH264 = new[] private static readonly string[] _videoProfilesH264 = new[]
{ {
@ -54,11 +57,13 @@ namespace MediaBrowser.Controller.MediaEncoding
public EncodingHelper( public EncodingHelper(
IApplicationPaths appPaths, IApplicationPaths appPaths,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
ISubtitleEncoder subtitleEncoder) ISubtitleEncoder subtitleEncoder,
IConfiguration config)
{ {
_appPaths = appPaths; _appPaths = appPaths;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_subtitleEncoder = subtitleEncoder; _subtitleEncoder = subtitleEncoder;
_config = config;
} }
public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions) public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
@ -120,6 +125,7 @@ namespace MediaBrowser.Controller.MediaEncoding
&& _mediaEncoder.SupportsFilter("scale_vaapi") && _mediaEncoder.SupportsFilter("scale_vaapi")
&& _mediaEncoder.SupportsFilter("deinterlace_vaapi") && _mediaEncoder.SupportsFilter("deinterlace_vaapi")
&& _mediaEncoder.SupportsFilter("tonemap_vaapi") && _mediaEncoder.SupportsFilter("tonemap_vaapi")
&& _mediaEncoder.SupportsFilter("procamp_vaapi")
&& _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayVaapiFrameSync) && _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayVaapiFrameSync)
&& _mediaEncoder.SupportsFilter("hwupload_vaapi"); && _mediaEncoder.SupportsFilter("hwupload_vaapi");
} }
@ -144,29 +150,44 @@ namespace MediaBrowser.Controller.MediaEncoding
private bool IsHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options) private bool IsHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
{ {
if (state.VideoStream == null) if (state.VideoStream == null
|| !options.EnableTonemapping
|| GetVideoColorBitDepth(state) != 10)
{ {
return false; return false;
} }
return options.EnableTonemapping if (string.Equals(state.VideoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
&& (string.Equals(state.VideoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase) && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.VideoStream.ColorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase)) && string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase))
&& GetVideoColorBitDepth(state) == 10; {
// Only native SW decoder and HW accelerator can parse dovi rpu.
var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty;
var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
var isNvdecDecoder = vidDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase);
var isVaapiDecoder = vidDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
var isD3d11vaDecoder = vidDecoder.Contains("d3d11va", StringComparison.OrdinalIgnoreCase);
return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder;
}
return string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
&& (string.Equals(state.VideoStream.VideoRangeType, "HDR10", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.VideoStream.VideoRangeType, "HLG", StringComparison.OrdinalIgnoreCase));
} }
private bool IsVaapiVppTonemapAvailable(EncodingJobInfo state, EncodingOptions options) private bool IsVaapiVppTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
{ {
if (state.VideoStream == null) if (state.VideoStream == null
|| !options.EnableVppTonemapping
|| GetVideoColorBitDepth(state) != 10)
{ {
return false; return false;
} }
// Native VPP tonemapping may come to QSV in the future. // Native VPP tonemapping may come to QSV in the future.
return options.EnableVppTonemapping return string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
&& string.Equals(state.VideoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase) && string.Equals(state.VideoStream.VideoRangeType, "HDR10", StringComparison.OrdinalIgnoreCase);
&& GetVideoColorBitDepth(state) == 10;
} }
/// <summary> /// <summary>
@ -516,8 +537,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(codec, "flac", StringComparison.OrdinalIgnoreCase)) if (string.Equals(codec, "flac", StringComparison.OrdinalIgnoreCase))
{ {
// flac is experimental in mp4 muxer return "flac";
return "flac -strict -2";
} }
return codec.ToLowerInvariant(); return codec.ToLowerInvariant();
@ -696,6 +716,9 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
else if (_mediaEncoder.IsVaapiDeviceInteli965) else if (_mediaEncoder.IsVaapiDeviceInteli965)
{ {
// Only override i965 since it has lower priority than iHD in libva lookup.
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME", "i965");
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME_JELLYFIN", "i965");
args.Append(GetVaapiDeviceArgs(null, "i965", null, VaapiAlias)); args.Append(GetVaapiDeviceArgs(null, "i965", null, VaapiAlias));
} }
else else
@ -1024,7 +1047,8 @@ 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))
{ {
return FormattableString.Invariant($" -qmin 18 -qmax 32 -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}"); // Override the too high default qmin 18 in transcoding preset
return FormattableString.Invariant($" -rc cbr -qmin 0 -qmax 32 -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
} }
if (string.Equals(videoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) if (string.Equals(videoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
@ -1222,10 +1246,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// Example: we encoded half of desired length, then codec detected // Example: we encoded half of desired length, then codec detected
// scene cut and inserted a keyframe; next forced keyframe would // scene cut and inserted a keyframe; next forced keyframe would
// be created outside of segment, which breaks seeking. // be created outside of segment, which breaks seeking.
// -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe.
gopArg = string.Format( gopArg = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
" -g:v:0 {0} -keyint_min:v:0 {0} -sc_threshold:v:0 0", " -g:v:0 {0} -keyint_min:v:0 {0}",
Math.Ceiling(segmentLength * framerate.Value)); Math.Ceiling(segmentLength * framerate.Value));
} }
@ -1245,6 +1268,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) || string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase))
{ {
args += keyFrameArg; args += keyFrameArg;
// prevent the libx264 from post processing to break the set keyframe.
if (string.Equals(codec, "libx264", StringComparison.OrdinalIgnoreCase))
{
args += " -sc_threshold:v:0 0";
}
} }
else else
{ {
@ -1684,6 +1713,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// Can't stream copy if we're burning in subtitles // Can't stream copy if we're burning in subtitles
if (request.SubtitleStreamIndex.HasValue if (request.SubtitleStreamIndex.HasValue
&& request.SubtitleStreamIndex.Value >= 0
&& state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
{ {
return false; return false;
@ -1730,6 +1760,20 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
} }
var requestedRangeTypes = state.GetRequestedRangeTypes(videoStream.Codec);
if (requestedRangeTypes.Length > 0)
{
if (string.IsNullOrEmpty(videoStream.VideoRangeType))
{
return false;
}
if (!requestedRangeTypes.Contains(videoStream.VideoRangeType, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
// Video width must fall within requested value // Video width must fall within requested value
if (request.MaxWidth.HasValue if (request.MaxWidth.HasValue
&& (!videoStream.Width.HasValue || videoStream.Width.Value > request.MaxWidth.Value)) && (!videoStream.Width.HasValue || videoStream.Width.Value > request.MaxWidth.Value))
@ -1870,7 +1914,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return request.EnableAutoStreamCopy; return request.EnableAutoStreamCopy;
} }
public int? GetVideoBitrateParamValue(BaseEncodingJobOptions request, MediaStream videoStream, string outputVideoCodec) public int GetVideoBitrateParamValue(BaseEncodingJobOptions request, MediaStream videoStream, string outputVideoCodec)
{ {
var bitrate = request.VideoBitRate; var bitrate = request.VideoBitRate;
@ -1902,7 +1946,8 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
} }
return bitrate; // Cap the max target bitrate to intMax/2 to satisify the bufsize=bitrate*2.
return Math.Min(bitrate ?? 0, int.MaxValue / 2);
} }
private int GetMinBitrate(int sourceBitrate, int requestedBitrate) private int GetMinBitrate(int sourceBitrate, int requestedBitrate)
@ -1982,6 +2027,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase) if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase) || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "vorbis", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase) || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase)) || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
{ {
@ -2249,7 +2296,10 @@ namespace MediaBrowser.Controller.MediaEncoding
int audioStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.AudioStream); int audioStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.AudioStream);
if (state.AudioStream.IsExternal) if (state.AudioStream.IsExternal)
{ {
bool hasExternalGraphicsSubs = state.SubtitleStream != null && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream; bool hasExternalGraphicsSubs = state.SubtitleStream != null
&& state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode
&& state.SubtitleStream.IsExternal
&& !state.SubtitleStream.IsTextSubtitleStream;
int externalAudioMapIndex = hasExternalGraphicsSubs ? 2 : 1; int externalAudioMapIndex = hasExternalGraphicsSubs ? 2 : 1;
args += string.Format( args += string.Format(
@ -2521,7 +2571,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format( return string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/{2})*{2}:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2", "scale=trunc(min(max(iw\\,ih*a)\\,min({0}\\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\\,ih)\\,min({0}/a\\,{1}))/2)*2",
maxWidthParam, maxWidthParam,
maxHeightParam, maxHeightParam,
scaleVal); scaleVal);
@ -2565,7 +2615,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format( return string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"scale=trunc(min(max(iw\\,ih*dar)\\,{0})/{1})*{1}:trunc(ow/dar/2)*2", "scale=trunc(min(max(iw\\,ih*a)\\,{0})/{1})*{1}:trunc(ow/a/2)*2",
maxWidthParam, maxWidthParam,
scaleVal); scaleVal);
} }
@ -2577,7 +2627,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format( return string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"scale=trunc(oh*a/{1})*{1}:min(max(iw/dar\\,ih)\\,{0})", "scale=trunc(oh*a/{1})*{1}:min(max(iw/a\\,ih)\\,{0})",
maxHeightParam, maxHeightParam,
scaleVal); scaleVal);
} }
@ -2626,7 +2676,7 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
else else
{ {
filter = "scale={0}:trunc({0}/dar/2)*2"; filter = "scale={0}:trunc({0}/a/2)*2";
} }
} }
@ -2677,7 +2727,18 @@ namespace MediaBrowser.Controller.MediaEncoding
var args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709"; var args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709";
if (!hwTonemapSuffix.Contains("vaapi", StringComparison.OrdinalIgnoreCase)) if (hwTonemapSuffix.Contains("vaapi", StringComparison.OrdinalIgnoreCase))
{
args += ",procamp_vaapi=b={2}:c={3}:extra_hw_frames=16";
return string.Format(
CultureInfo.InvariantCulture,
args,
hwTonemapSuffix,
videoFormat ?? "nv12",
options.VppTonemappingBrightness,
options.VppTonemappingContrast);
}
else
{ {
args += ":tonemap={2}:peak={3}:desat={4}"; args += ":tonemap={2}:peak={3}:desat={4}";
@ -2780,8 +2841,8 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
else if (hasGraphicalSubs) else if (hasGraphicalSubs)
{ {
// [0:s]scale=s=1280x720 // [0:s]scale=expr
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); var subSwScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter); subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0"); overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
} }
@ -2879,7 +2940,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// sw => hw // sw => hw
if (doCuTonemap) if (doCuTonemap)
{ {
mainFilters.Add("hwupload"); mainFilters.Add("hwupload=derive_device=cuda");
} }
} }
@ -2959,7 +3020,7 @@ namespace MediaBrowser.Controller.MediaEncoding
subFilters.Add(subTextSubtitlesFilter); subFilters.Add(subTextSubtitlesFilter);
} }
subFilters.Add("hwupload"); subFilters.Add("hwupload=derive_device=cuda");
overlayFilters.Add("overlay_cuda=eof_action=endall:shortest=1:repeatlast=0"); overlayFilters.Add("overlay_cuda=eof_action=endall:shortest=1:repeatlast=0");
} }
} }
@ -2967,7 +3028,9 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
if (hasGraphicalSubs) if (hasGraphicalSubs)
{ {
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); var subSwScaleFilter = isSwDecoder
? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
: GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter); subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0"); overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
} }
@ -3069,7 +3132,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// sw => hw // sw => hw
if (doOclTonemap) if (doOclTonemap)
{ {
mainFilters.Add("hwupload"); mainFilters.Add("hwupload=derive_device=d3d11va:extra_hw_frames=16");
mainFilters.Add("format=d3d11");
mainFilters.Add("hwmap=derive_device=opencl");
} }
} }
@ -3096,7 +3161,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var memoryOutput = false; var memoryOutput = false;
var isUploadForOclTonemap = isSwDecoder && doOclTonemap; var isUploadForOclTonemap = isSwDecoder && doOclTonemap;
if ((isD3d11vaDecoder && isSwEncoder) || isUploadForOclTonemap) if (isD3d11vaDecoder && isSwEncoder)
{ {
memoryOutput = true; memoryOutput = true;
@ -3108,7 +3173,7 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
// OUTPUT yuv420p surface // OUTPUT yuv420p surface
if (isSwDecoder && isAmfEncoder) if (isSwDecoder && isAmfEncoder && !isUploadForOclTonemap)
{ {
memoryOutput = true; memoryOutput = true;
} }
@ -3123,7 +3188,7 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
} }
if (isDxInDxOut && !hasSubs) if ((isDxInDxOut || isUploadForOclTonemap) && !hasSubs)
{ {
// OUTPUT d3d11(nv12) surface(vram) // OUTPUT d3d11(nv12) surface(vram)
// reverse-mapping via d3d11-opencl interop. // reverse-mapping via d3d11-opencl interop.
@ -3134,7 +3199,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/* Make sub and overlay filters for subtitle stream */ /* Make sub and overlay filters for subtitle stream */
var subFilters = new List<string>(); var subFilters = new List<string>();
var overlayFilters = new List<string>(); var overlayFilters = new List<string>();
if (isDxInDxOut) if (isDxInDxOut || isUploadForOclTonemap)
{ {
if (hasSubs) if (hasSubs)
{ {
@ -3155,7 +3220,7 @@ namespace MediaBrowser.Controller.MediaEncoding
subFilters.Add(subTextSubtitlesFilter); subFilters.Add(subTextSubtitlesFilter);
} }
subFilters.Add("hwupload"); subFilters.Add("hwupload=derive_device=opencl");
overlayFilters.Add("overlay_opencl=eof_action=endall:shortest=1:repeatlast=0"); overlayFilters.Add("overlay_opencl=eof_action=endall:shortest=1:repeatlast=0");
overlayFilters.Add("hwmap=derive_device=d3d11va:reverse=1"); overlayFilters.Add("hwmap=derive_device=d3d11va:reverse=1");
overlayFilters.Add("format=d3d11"); overlayFilters.Add("format=d3d11");
@ -3165,7 +3230,9 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
if (hasGraphicalSubs) if (hasGraphicalSubs)
{ {
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); var subSwScaleFilter = isSwDecoder
? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
: GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter); subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0"); overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
} }
@ -3287,7 +3354,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// sw => hw // sw => hw
if (doOclTonemap) if (doOclTonemap)
{ {
mainFilters.Add("hwupload"); mainFilters.Add("hwupload=derive_device=opencl");
} }
} }
else if (isD3d11vaDecoder || isQsvDecoder) else if (isD3d11vaDecoder || isQsvDecoder)
@ -3394,7 +3461,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// qsv requires a fixed pool size. // qsv requires a fixed pool size.
// default to 64 otherwise it will fail on certain iGPU. // default to 64 otherwise it will fail on certain iGPU.
subFilters.Add("hwupload=extra_hw_frames=64"); subFilters.Add("hwupload=derive_device=qsv:extra_hw_frames=64");
var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, reqMaxH); var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
var overlaySize = (overlayW.HasValue && overlayH.HasValue) var overlaySize = (overlayW.HasValue && overlayH.HasValue)
@ -3411,7 +3478,9 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
if (hasGraphicalSubs) if (hasGraphicalSubs)
{ {
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); var subSwScaleFilter = isSwDecoder
? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
: GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter); subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0"); overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
} }
@ -3482,7 +3551,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// sw => hw // sw => hw
if (doOclTonemap) if (doOclTonemap)
{ {
mainFilters.Add("hwupload"); mainFilters.Add("hwupload=derive_device=opencl");
} }
} }
else if (isVaapiDecoder || isQsvDecoder) else if (isVaapiDecoder || isQsvDecoder)
@ -3603,7 +3672,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// qsv requires a fixed pool size. // qsv requires a fixed pool size.
// default to 64 otherwise it will fail on certain iGPU. // default to 64 otherwise it will fail on certain iGPU.
subFilters.Add("hwupload=extra_hw_frames=64"); subFilters.Add("hwupload=derive_device=qsv:extra_hw_frames=64");
var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, reqMaxH); var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
var overlaySize = (overlayW.HasValue && overlayH.HasValue) var overlaySize = (overlayW.HasValue && overlayH.HasValue)
@ -3620,7 +3689,9 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
if (hasGraphicalSubs) if (hasGraphicalSubs)
{ {
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); var subSwScaleFilter = isSwDecoder
? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
: GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter); subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0"); overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
} }
@ -3664,7 +3735,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var newfilters = new List<string>(); var newfilters = new List<string>();
var noOverlay = swFilterChain.OverlayFilters.Count == 0; var noOverlay = swFilterChain.OverlayFilters.Count == 0;
newfilters.AddRange(noOverlay ? swFilterChain.MainFilters : swFilterChain.OverlayFilters); newfilters.AddRange(noOverlay ? swFilterChain.MainFilters : swFilterChain.OverlayFilters);
newfilters.Add("hwupload"); newfilters.Add("hwupload=derive_device=vaapi");
var mainFilters = noOverlay ? newfilters : swFilterChain.MainFilters; var mainFilters = noOverlay ? newfilters : swFilterChain.MainFilters;
var overlayFilters = noOverlay ? swFilterChain.OverlayFilters : newfilters; var overlayFilters = noOverlay ? swFilterChain.OverlayFilters : newfilters;
@ -3745,7 +3816,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// sw => hw // sw => hw
if (doOclTonemap) if (doOclTonemap)
{ {
mainFilters.Add("hwupload"); mainFilters.Add("hwupload=derive_device=opencl");
} }
} }
else if (isVaapiDecoder) else if (isVaapiDecoder)
@ -3850,7 +3921,7 @@ namespace MediaBrowser.Controller.MediaEncoding
subFilters.Add(subTextSubtitlesFilter); subFilters.Add(subTextSubtitlesFilter);
} }
subFilters.Add("hwupload"); subFilters.Add("hwupload=derive_device=vaapi");
var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, reqMaxH); var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
var overlaySize = (overlayW.HasValue && overlayH.HasValue) var overlaySize = (overlayW.HasValue && overlayH.HasValue)
@ -3867,7 +3938,9 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
if (hasGraphicalSubs) if (hasGraphicalSubs)
{ {
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); var subSwScaleFilter = isSwDecoder
? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
: GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter); subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0"); overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
@ -3939,7 +4012,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// sw => hw // sw => hw
if (doOclTonemap) if (doOclTonemap)
{ {
mainFilters.Add("hwupload"); mainFilters.Add("hwupload=derive_device=opencl");
} }
} }
else if (isVaapiDecoder) else if (isVaapiDecoder)
@ -3969,7 +4042,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
mainFilters.Add("hwdownload"); mainFilters.Add("hwdownload");
mainFilters.Add("format=p010le"); mainFilters.Add("format=p010le");
mainFilters.Add("hwupload"); mainFilters.Add("hwupload=derive_device=opencl");
} }
} }
@ -4042,7 +4115,9 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
if (hasGraphicalSubs) if (hasGraphicalSubs)
{ {
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); var subSwScaleFilter = isSwDecoder
? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
: GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter); subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0"); overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
@ -4218,6 +4293,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return videoStream.BitDepth.Value; return videoStream.BitDepth.Value;
} }
else if (string.Equals(videoStream.PixelFormat, "yuv420p", StringComparison.OrdinalIgnoreCase) else if (string.Equals(videoStream.PixelFormat, "yuv420p", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.PixelFormat, "yuvj420p", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.PixelFormat, "yuv444p", StringComparison.OrdinalIgnoreCase)) || string.Equals(videoStream.PixelFormat, "yuv444p", StringComparison.OrdinalIgnoreCase))
{ {
return 8; return 8;
@ -4250,14 +4326,18 @@ namespace MediaBrowser.Controller.MediaEncoding
protected string GetHardwareVideoDecoder(EncodingJobInfo state, EncodingOptions options) protected string GetHardwareVideoDecoder(EncodingJobInfo state, EncodingOptions options)
{ {
var videoStream = state.VideoStream; var videoStream = state.VideoStream;
if (videoStream == null) var mediaSource = state.MediaSource;
if (videoStream == null || mediaSource == null)
{ {
return null; return null;
} }
// Only use alternative encoders for video files. // HWA decoders can handle both video files and video folders.
var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile; var videoType = mediaSource.VideoType;
if (videoType != VideoType.VideoFile) if (videoType != VideoType.VideoFile
&& videoType != VideoType.Iso
&& videoType != VideoType.Dvd
&& videoType != VideoType.BluRay)
{ {
return null; return null;
} }
@ -4504,7 +4584,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var hwSurface = (isIntelDx11OclSupported || isIntelVaapiOclSupported) var hwSurface = (isIntelDx11OclSupported || isIntelVaapiOclSupported)
&& _mediaEncoder.SupportsFilter("alphasrc"); && _mediaEncoder.SupportsFilter("alphasrc");
var is8bitSwFormatsQsv = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8bitSwFormatsQsv = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
var is8_10bitSwFormatsQsv = is8bitSwFormatsQsv || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsQsv = is8bitSwFormatsQsv || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
// TODO: add more 8/10bit and 4:4:4 formats for Qsv after finishing the ffcheck tool // TODO: add more 8/10bit and 4:4:4 formats for Qsv after finishing the ffcheck tool
@ -4563,7 +4644,8 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
var hwSurface = IsCudaFullSupported() && _mediaEncoder.SupportsFilter("alphasrc"); var hwSurface = IsCudaFullSupported() && _mediaEncoder.SupportsFilter("alphasrc");
var is8bitSwFormatsNvdec = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8bitSwFormatsNvdec = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
var is8_10bitSwFormatsNvdec = is8bitSwFormatsNvdec || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsNvdec = is8bitSwFormatsNvdec || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
// TODO: add more 8/10/12bit and 4:4:4 formats for Nvdec after finishing the ffcheck tool // TODO: add more 8/10/12bit and 4:4:4 formats for Nvdec after finishing the ffcheck tool
@ -4629,7 +4711,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var hwSurface = _mediaEncoder.SupportsHwaccel("d3d11va") var hwSurface = _mediaEncoder.SupportsHwaccel("d3d11va")
&& IsOpenclFullSupported() && IsOpenclFullSupported()
&& _mediaEncoder.SupportsFilter("alphasrc"); && _mediaEncoder.SupportsFilter("alphasrc");
var is8bitSwFormatsAmf = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8bitSwFormatsAmf = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
var is8_10bitSwFormatsAmf = is8bitSwFormatsAmf || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsAmf = is8bitSwFormatsAmf || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
if (is8bitSwFormatsAmf) if (is8bitSwFormatsAmf)
@ -4649,11 +4732,6 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
return GetHwaccelType(state, options, "vc1", bitDepth, hwSurface); return GetHwaccelType(state, options, "vc1", bitDepth, hwSurface);
} }
if (string.Equals("mpeg4", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
return GetHwaccelType(state, options, "mpeg4", bitDepth, hwSurface);
}
} }
if (is8_10bitSwFormatsAmf) if (is8_10bitSwFormatsAmf)
@ -4690,7 +4768,8 @@ namespace MediaBrowser.Controller.MediaEncoding
&& IsVaapiFullSupported() && IsVaapiFullSupported()
&& IsOpenclFullSupported() && IsOpenclFullSupported()
&& _mediaEncoder.SupportsFilter("alphasrc"); && _mediaEncoder.SupportsFilter("alphasrc");
var is8bitSwFormatsVaapi = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8bitSwFormatsVaapi = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
var is8_10bitSwFormatsVaapi = is8bitSwFormatsVaapi || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsVaapi = is8bitSwFormatsVaapi || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
if (is8bitSwFormatsVaapi) if (is8bitSwFormatsVaapi)
@ -4747,7 +4826,8 @@ namespace MediaBrowser.Controller.MediaEncoding
return null; return null;
} }
var is8bitSwFormatsVt = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8bitSwFormatsVt = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
var is8_10bitSwFormatsVt = is8bitSwFormatsVt || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsVt = is8bitSwFormatsVt || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
if (is8bitSwFormatsVt) if (is8bitSwFormatsVt)
@ -4853,22 +4933,21 @@ namespace MediaBrowser.Controller.MediaEncoding
public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions, string segmentContainer) public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions, string segmentContainer)
{ {
var inputModifier = string.Empty; var inputModifier = string.Empty;
var probeSizeArgument = string.Empty; var analyzeDurationArgument = string.Empty;
string analyzeDurationArgument; // Apply -analyzeduration as per the environment variable,
if (state.MediaSource.AnalyzeDurationMs.HasValue) // otherwise ffmpeg will break on certain files due to default value is 0.
// The default value of -probesize is more than enough, so leave it as is.
var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
{
analyzeDurationArgument = "-analyzeduration " + ffmpegAnalyzeDuration;
}
else if (state.MediaSource.AnalyzeDurationMs.HasValue)
{ {
analyzeDurationArgument = "-analyzeduration " + (state.MediaSource.AnalyzeDurationMs.Value * 1000).ToString(CultureInfo.InvariantCulture); analyzeDurationArgument = "-analyzeduration " + (state.MediaSource.AnalyzeDurationMs.Value * 1000).ToString(CultureInfo.InvariantCulture);
} }
else
{
analyzeDurationArgument = string.Empty;
}
if (!string.IsNullOrEmpty(probeSizeArgument))
{
inputModifier += " " + probeSizeArgument;
}
if (!string.IsNullOrEmpty(analyzeDurationArgument)) if (!string.IsNullOrEmpty(analyzeDurationArgument))
{ {

View file

@ -366,6 +366,28 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
} }
/// <summary>
/// Gets the target video range type.
/// </summary>
public string TargetVideoRangeType
{
get
{
if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec))
{
return VideoStream?.VideoRangeType;
}
var requestedRangeType = GetRequestedRangeTypes(ActualOutputVideoCodec).FirstOrDefault();
if (!string.IsNullOrEmpty(requestedRangeType))
{
return requestedRangeType;
}
return null;
}
}
public string TargetVideoCodecTag public string TargetVideoCodecTag
{ {
get get
@ -579,6 +601,26 @@ namespace MediaBrowser.Controller.MediaEncoding
return Array.Empty<string>(); return Array.Empty<string>();
} }
public string[] GetRequestedRangeTypes(string codec)
{
if (!string.IsNullOrEmpty(BaseRequest.VideoRangeType))
{
return BaseRequest.VideoRangeType.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
}
if (!string.IsNullOrEmpty(codec))
{
var rangetype = BaseRequest.GetOption(codec, "rangetype");
if (!string.IsNullOrEmpty(rangetype))
{
return rangetype.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
}
}
return Array.Empty<string>();
}
public string GetRequestedLevel(string codec) public string GetRequestedLevel(string codec)
{ {
if (!string.IsNullOrEmpty(BaseRequest.Level)) if (!string.IsNullOrEmpty(BaseRequest.Level))

View file

@ -111,7 +111,7 @@ namespace MediaBrowser.Controller.MediaEncoding
percent = 100.0 * currentMs / totalMs; percent = 100.0 * currentMs / totalMs;
transcodingPosition = val; transcodingPosition = TimeSpan.FromMilliseconds(currentMs);
} }
} }
else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase)) else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase))

View file

@ -34,8 +34,8 @@ namespace MediaBrowser.Controller.Providers
public bool IsReplacingImage(ImageType type) public bool IsReplacingImage(ImageType type)
{ {
return ImageRefreshMode == MetadataRefreshMode.FullRefresh && return ImageRefreshMode == MetadataRefreshMode.FullRefresh
(ReplaceAllImages || ReplaceImages.Contains(type)); && (ReplaceAllImages || ReplaceImages.Contains(type));
} }
} }
} }

View file

@ -352,6 +352,6 @@ namespace MediaBrowser.Controller.Session
/// <returns>Task.</returns> /// <returns>Task.</returns>
Task RevokeUserTokens(Guid userId, string currentAccessToken); Task RevokeUserTokens(Guid userId, string currentAccessToken);
void CloseIfNeeded(SessionInfo session); Task CloseIfNeededAsync(SessionInfo session);
} }
} }

View file

@ -100,6 +100,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
"scale_vaapi", "scale_vaapi",
"deinterlace_vaapi", "deinterlace_vaapi",
"tonemap_vaapi", "tonemap_vaapi",
"procamp_vaapi",
"overlay_vaapi", "overlay_vaapi",
"hwupload_vaapi" "hwupload_vaapi"
}; };

View file

@ -16,6 +16,7 @@ using MediaBrowser.Common;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.MediaEncoding.Probing; using MediaBrowser.MediaEncoding.Probing;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
@ -49,6 +50,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private readonly IServerConfigurationManager _configurationManager; private readonly IServerConfigurationManager _configurationManager;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
private readonly IConfiguration _config;
private readonly string _startupOptionFFmpegPath; private readonly string _startupOptionFFmpegPath;
private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(2, 2); private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(2, 2);
@ -85,6 +87,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_configurationManager = configurationManager; _configurationManager = configurationManager;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_localization = localization; _localization = localization;
_config = config;
_startupOptionFFmpegPath = config.GetValue<string>(Controller.Extensions.ConfigurationExtensions.FfmpegPathKey) ?? string.Empty; _startupOptionFFmpegPath = config.GetValue<string>(Controller.Extensions.ConfigurationExtensions.FfmpegPathKey) ?? string.Empty;
_jsonSerializerOptions = JsonDefaults.Options; _jsonSerializerOptions = JsonDefaults.Options;
} }
@ -371,8 +374,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
var inputFile = request.MediaSource.Path; var inputFile = request.MediaSource.Path;
string analyzeDuration = string.Empty; string analyzeDuration = string.Empty;
string ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
if (request.MediaSource.AnalyzeDurationMs > 0) if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
{
analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration;
}
else if (request.MediaSource.AnalyzeDurationMs > 0)
{ {
analyzeDuration = "-analyzeduration " + analyzeDuration = "-analyzeduration " +
(request.MediaSource.AnalyzeDurationMs * 1000).ToString(); (request.MediaSource.AnalyzeDurationMs * 1000).ToString();
@ -629,10 +637,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
filters.Add("thumbnail=n=" + (useLargerBatchSize ? "50" : "24")); filters.Add("thumbnail=n=" + (useLargerBatchSize ? "50" : "24"));
} }
// Use SW tonemap on HDR video stream only when the zscale filter is available. // Use SW tonemap on HDR10/HLG video stream only when the zscale filter is available.
var enableHdrExtraction = string.Equals(videoStream?.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) && SupportsFilter("zscale"); var enableHdrExtraction = false;
if (enableHdrExtraction)
if ((string.Equals(videoStream?.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream?.ColorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
&& SupportsFilter("zscale"))
{ {
enableHdrExtraction = true;
filters.Add("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:peak=100,zscale=t=bt709:m=bt709,format=yuv420p"); filters.Add("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:peak=100,zscale=t=bt709:m=bt709,format=yuv420p");
} }

View file

@ -30,7 +30,7 @@
<PackageReference Include="libse" Version="3.6.5" /> <PackageReference Include="libse" Version="3.6.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
<PackageReference Include="UTF.Unknown" Version="2.5.0" /> <PackageReference Include="UTF.Unknown" Version="2.5.1" />
</ItemGroup> </ItemGroup>
<!-- Code Analyzers--> <!-- Code Analyzers-->

View file

@ -310,5 +310,12 @@ namespace MediaBrowser.MediaEncoding.Probing
/// <value>The color primaries.</value> /// <value>The color primaries.</value>
[JsonPropertyName("color_primaries")] [JsonPropertyName("color_primaries")]
public string ColorPrimaries { get; set; } public string ColorPrimaries { get; set; }
/// <summary>
/// Gets or sets the side_data_list.
/// </summary>
/// <value>The side_data_list.</value>
[JsonPropertyName("side_data_list")]
public IReadOnlyList<MediaStreamInfoSideData> SideDataList { get; set; }
} }
} }

View file

@ -0,0 +1,74 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace MediaBrowser.MediaEncoding.Probing
{
/// <summary>
/// Class MediaStreamInfoSideData.
/// </summary>
public class MediaStreamInfoSideData
{
/// <summary>
/// Gets or sets the SideDataType.
/// </summary>
/// <value>The SideDataType.</value>
[JsonPropertyName("side_data_type")]
public string? SideDataType { get; set; }
/// <summary>
/// Gets or sets the DvVersionMajor.
/// </summary>
/// <value>The DvVersionMajor.</value>
[JsonPropertyName("dv_version_major")]
public int? DvVersionMajor { get; set; }
/// <summary>
/// Gets or sets the DvVersionMinor.
/// </summary>
/// <value>The DvVersionMinor.</value>
[JsonPropertyName("dv_version_minor")]
public int? DvVersionMinor { get; set; }
/// <summary>
/// Gets or sets the DvProfile.
/// </summary>
/// <value>The DvProfile.</value>
[JsonPropertyName("dv_profile")]
public int? DvProfile { get; set; }
/// <summary>
/// Gets or sets the DvLevel.
/// </summary>
/// <value>The DvLevel.</value>
[JsonPropertyName("dv_level")]
public int? DvLevel { get; set; }
/// <summary>
/// Gets or sets the RpuPresentFlag.
/// </summary>
/// <value>The RpuPresentFlag.</value>
[JsonPropertyName("rpu_present_flag")]
public int? RpuPresentFlag { get; set; }
/// <summary>
/// Gets or sets the ElPresentFlag.
/// </summary>
/// <value>The ElPresentFlag.</value>
[JsonPropertyName("el_present_flag")]
public int? ElPresentFlag { get; set; }
/// <summary>
/// Gets or sets the BlPresentFlag.
/// </summary>
/// <value>The BlPresentFlag.</value>
[JsonPropertyName("bl_present_flag")]
public int? BlPresentFlag { get; set; }
/// <summary>
/// Gets or sets the DvBlSignalCompatibilityId.
/// </summary>
/// <value>The DvBlSignalCompatibilityId.</value>
[JsonPropertyName("dv_bl_signal_compatibility_id")]
public int? DvBlSignalCompatibilityId { get; set; }
}
}

View file

@ -841,6 +841,27 @@ namespace MediaBrowser.MediaEncoding.Probing
{ {
stream.ColorPrimaries = streamInfo.ColorPrimaries; stream.ColorPrimaries = streamInfo.ColorPrimaries;
} }
if (streamInfo.SideDataList != null)
{
foreach (var data in streamInfo.SideDataList)
{
// Parse Dolby Vision metadata from side_data
if (string.Equals(data.SideDataType, "DOVI configuration record", StringComparison.OrdinalIgnoreCase))
{
stream.DvVersionMajor = data.DvVersionMajor;
stream.DvVersionMinor = data.DvVersionMinor;
stream.DvProfile = data.DvProfile;
stream.DvLevel = data.DvLevel;
stream.RpuPresentFlag = data.RpuPresentFlag;
stream.ElPresentFlag = data.ElPresentFlag;
stream.BlPresentFlag = data.BlPresentFlag;
stream.DvBlSignalCompatibilityId = data.DvBlSignalCompatibilityId;
break;
}
}
}
} }
else else
{ {

View file

@ -0,0 +1,54 @@
using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Subtitles
{
/// <summary>
/// ASS subtitle writer.
/// </summary>
public class AssWriter : ISubtitleWriter
{
/// <inheritdoc />
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
{
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{
var trackEvents = info.TrackEvents;
var timeFormat = @"hh\:mm\:ss\.ff";
// Write ASS header
writer.WriteLine("[Script Info]");
writer.WriteLine("Title: Jellyfin transcoded ASS subtitle");
writer.WriteLine("ScriptType: v4.00+");
writer.WriteLine();
writer.WriteLine("[V4+ Styles]");
writer.WriteLine("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding");
writer.WriteLine("Style: Default,Arial,20,&H00FFFFFF,&H00FFFFFF,&H19333333,&H910E0807,0,0,0,0,100,100,0,0,0,1,0,2,10,10,10,1");
writer.WriteLine();
writer.WriteLine("[Events]");
writer.WriteLine("Format: Layer, Start, End, Style, Text");
for (int i = 0; i < trackEvents.Count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var trackEvent = trackEvents[i];
var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
var text = Regex.Replace(trackEvent.Text, @"\n", "\\n", RegexOptions.IgnoreCase);
writer.WriteLine(
"Dialogue: 0,{0},{1},Default,{2}",
startTime,
endTime,
text);
}
}
}
}
}

View file

@ -0,0 +1,54 @@
using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Subtitles
{
/// <summary>
/// SSA subtitle writer.
/// </summary>
public class SsaWriter : ISubtitleWriter
{
/// <inheritdoc />
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
{
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{
var trackEvents = info.TrackEvents;
var timeFormat = @"hh\:mm\:ss\.ff";
// Write SSA header
writer.WriteLine("[Script Info]");
writer.WriteLine("Title: Jellyfin transcoded SSA subtitle");
writer.WriteLine("ScriptType: v4.00");
writer.WriteLine();
writer.WriteLine("[V4 Styles]");
writer.WriteLine("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding");
writer.WriteLine("Style: Default,Arial,20,&H00FFFFFF,&H00FFFFFF,&H19333333,&H19333333,0,0,0,1,0,2,10,10,10,0,1");
writer.WriteLine();
writer.WriteLine("[Events]");
writer.WriteLine("Format: Layer, Start, End, Style, Text");
for (int i = 0; i < trackEvents.Count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var trackEvent = trackEvents[i];
var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
var text = Regex.Replace(trackEvent.Text, @"\n", "\\n", RegexOptions.IgnoreCase);
writer.WriteLine(
"Dialogue: 0,{0},{1},Default,{2}",
startTime,
endTime,
text);
}
}
}
}
}

View file

@ -283,6 +283,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
private bool TryGetWriter(string format, [NotNullWhen(true)] out ISubtitleWriter? value) private bool TryGetWriter(string format, [NotNullWhen(true)] out ISubtitleWriter? value)
{ {
if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
{
value = new AssWriter();
return true;
}
if (string.IsNullOrEmpty(format)) if (string.IsNullOrEmpty(format))
{ {
throw new ArgumentNullException(nameof(format)); throw new ArgumentNullException(nameof(format));
@ -294,12 +300,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles
return true; return true;
} }
if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase)) if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase) || string.Equals(format, SubtitleFormat.SUBRIP, StringComparison.OrdinalIgnoreCase))
{ {
value = new SrtWriter(); value = new SrtWriter();
return true; return true;
} }
if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase))
{
value = new SsaWriter();
return true;
}
if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase)) if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase))
{ {
value = new VttWriter(); value = new VttWriter();
@ -681,11 +693,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (!string.Equals(text, newText, StringComparison.Ordinal)) if (!string.Equals(text, newText, StringComparison.Ordinal))
{ {
var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
var writer = new StreamWriter(fileStream, encoding);
await using (fileStream.ConfigureAwait(false)) await using (fileStream.ConfigureAwait(false))
await using (writer.ConfigureAwait(false))
{ {
await writer.WriteAsync(newText.AsMemory(), cancellationToken).ConfigureAwait(false); var writer = new StreamWriter(fileStream, encoding);
await using (writer.ConfigureAwait(false))
{
await writer.WriteAsync(newText.AsMemory(), cancellationToken).ConfigureAwait(false);
}
} }
} }
} }

View file

@ -20,6 +20,11 @@ public class BrandingOptions
/// <value>The custom CSS.</value> /// <value>The custom CSS.</value>
public string? CustomCss { get; set; } public string? CustomCss { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to enable the splashscreen.
/// </summary>
public bool SplashscreenEnabled { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets the splashscreen location on disk. /// Gets or sets the splashscreen location on disk.
/// </summary> /// </summary>

View file

@ -26,6 +26,8 @@ namespace MediaBrowser.Model.Configuration
TonemappingThreshold = 0.8; TonemappingThreshold = 0.8;
TonemappingPeak = 100; TonemappingPeak = 100;
TonemappingParam = 0; TonemappingParam = 0;
VppTonemappingBrightness = 0;
VppTonemappingContrast = 1.2;
H264Crf = 23; H264Crf = 23;
H265Crf = 28; H265Crf = 28;
DeinterlaceDoubleRate = false; DeinterlaceDoubleRate = false;
@ -39,7 +41,7 @@ namespace MediaBrowser.Model.Configuration
EnableHardwareEncoding = true; EnableHardwareEncoding = true;
AllowHevcEncoding = false; AllowHevcEncoding = false;
EnableSubtitleExtraction = true; EnableSubtitleExtraction = true;
AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = Array.Empty<string>(); AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" };
HardwareDecodingCodecs = new string[] { "h264", "vc1" }; HardwareDecodingCodecs = new string[] { "h264", "vc1" };
} }
@ -89,6 +91,10 @@ namespace MediaBrowser.Model.Configuration
public double TonemappingParam { get; set; } public double TonemappingParam { get; set; }
public double VppTonemappingBrightness { get; set; }
public double VppTonemappingContrast { get; set; }
public int H264Crf { get; set; } public int H264Crf { get; set; }
public int H265Crf { get; set; } public int H265Crf { get; set; }

View file

@ -17,7 +17,7 @@ namespace MediaBrowser.Model.Configuration
RequirePerfectSubtitleMatch = true; RequirePerfectSubtitleMatch = true;
AllowEmbeddedSubtitles = EmbeddedSubtitleOptions.AllowAll; AllowEmbeddedSubtitles = EmbeddedSubtitleOptions.AllowAll;
AutomaticallyAddToCollection = true; AutomaticallyAddToCollection = false;
EnablePhotos = true; EnablePhotos = true;
SaveSubtitlesWithMedia = true; SaveSubtitlesWithMedia = true;
EnableRealtimeMonitor = true; EnableRealtimeMonitor = true;

View file

@ -16,6 +16,7 @@ namespace MediaBrowser.Model.Dlna
int? videoBitDepth, int? videoBitDepth,
int? videoBitrate, int? videoBitrate,
string? videoProfile, string? videoProfile,
string? videoRangeType,
double? videoLevel, double? videoLevel,
float? videoFramerate, float? videoFramerate,
int? packetLength, int? packetLength,
@ -42,6 +43,8 @@ namespace MediaBrowser.Model.Dlna
return IsConditionSatisfied(condition, videoLevel); return IsConditionSatisfied(condition, videoLevel);
case ProfileConditionValue.VideoProfile: case ProfileConditionValue.VideoProfile:
return IsConditionSatisfied(condition, videoProfile); return IsConditionSatisfied(condition, videoProfile);
case ProfileConditionValue.VideoRangeType:
return IsConditionSatisfied(condition, videoRangeType);
case ProfileConditionValue.VideoCodecTag: case ProfileConditionValue.VideoCodecTag:
return IsConditionSatisfied(condition, videoCodecTag); return IsConditionSatisfied(condition, videoCodecTag);
case ProfileConditionValue.PacketLength: case ProfileConditionValue.PacketLength:

View file

@ -128,6 +128,7 @@ namespace MediaBrowser.Model.Dlna
bool isDirectStream, bool isDirectStream,
long? runtimeTicks, long? runtimeTicks,
string videoProfile, string videoProfile,
string videoRangeType,
double? videoLevel, double? videoLevel,
float? videoFramerate, float? videoFramerate,
int? packetLength, int? packetLength,
@ -176,6 +177,7 @@ namespace MediaBrowser.Model.Dlna
bitDepth, bitDepth,
videoBitrate, videoBitrate,
videoProfile, videoProfile,
videoRangeType,
videoLevel, videoLevel,
videoFramerate, videoFramerate,
packetLength, packetLength,

View file

@ -423,6 +423,7 @@ namespace MediaBrowser.Model.Dlna
/// <param name="bitDepth">The bit depth.</param> /// <param name="bitDepth">The bit depth.</param>
/// <param name="videoBitrate">The video bitrate.</param> /// <param name="videoBitrate">The video bitrate.</param>
/// <param name="videoProfile">The video profile.</param> /// <param name="videoProfile">The video profile.</param>
/// <param name="videoRangeType">The video range type.</param>
/// <param name="videoLevel">The video level.</param> /// <param name="videoLevel">The video level.</param>
/// <param name="videoFramerate">The video framerate.</param> /// <param name="videoFramerate">The video framerate.</param>
/// <param name="packetLength">The packet length.</param> /// <param name="packetLength">The packet length.</param>
@ -444,6 +445,7 @@ namespace MediaBrowser.Model.Dlna
int? bitDepth, int? bitDepth,
int? videoBitrate, int? videoBitrate,
string videoProfile, string videoProfile,
string videoRangeType,
double? videoLevel, double? videoLevel,
float? videoFramerate, float? videoFramerate,
int? packetLength, int? packetLength,
@ -483,7 +485,7 @@ namespace MediaBrowser.Model.Dlna
var anyOff = false; var anyOff = false;
foreach (ProfileCondition c in i.Conditions) foreach (ProfileCondition c in i.Conditions)
{ {
if (!ConditionProcessor.IsVideoConditionSatisfied(GetModelProfileCondition(c), width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)) if (!ConditionProcessor.IsVideoConditionSatisfied(GetModelProfileCondition(c), width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))
{ {
anyOff = true; anyOff = true;
break; break;

View file

@ -26,6 +26,7 @@ namespace MediaBrowser.Model.Dlna
IsAvc = 20, IsAvc = 20,
IsInterlaced = 21, IsInterlaced = 21,
AudioSampleRate = 22, AudioSampleRate = 22,
AudioBitDepth = 23 AudioBitDepth = 23,
VideoRangeType = 24
} }
} }

View file

@ -221,6 +221,9 @@ namespace MediaBrowser.Model.Dlna
case ProfileConditionValue.VideoProfile: case ProfileConditionValue.VideoProfile:
return TranscodeReason.VideoProfileNotSupported; return TranscodeReason.VideoProfileNotSupported;
case ProfileConditionValue.VideoRangeType:
return TranscodeReason.VideoRangeTypeNotSupported;
case ProfileConditionValue.VideoTimestamp: case ProfileConditionValue.VideoTimestamp:
// TODO // TODO
return 0; return 0;
@ -385,7 +388,7 @@ namespace MediaBrowser.Model.Dlna
// If device requirements are satisfied then allow both direct stream and direct play // If device requirements are satisfied then allow both direct stream and direct play
if (item.SupportsDirectPlay) if (item.SupportsDirectPlay)
{ {
if (IsItemBitrateEligibleForDirectPlay(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectPlay)) if (IsItemBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectPlay))
{ {
if (options.EnableDirectPlay) if (options.EnableDirectPlay)
{ {
@ -401,7 +404,7 @@ namespace MediaBrowser.Model.Dlna
// While options takes the network and other factors into account. Only applies to direct stream // While options takes the network and other factors into account. Only applies to direct stream
if (item.SupportsDirectStream) if (item.SupportsDirectStream)
{ {
if (IsItemBitrateEligibleForDirectPlay(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream)) if (IsItemBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream))
{ {
if (options.EnableDirectStream) if (options.EnableDirectStream)
{ {
@ -604,11 +607,11 @@ namespace MediaBrowser.Model.Dlna
var videoStream = item.VideoStream; var videoStream = item.VideoStream;
var directPlayEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectPlay); var directPlayBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectPlay);
var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectStream); var directStreamBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectStream);
bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult == 0); bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayBitrateEligibility == 0);
bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directPlayEligibilityResult == 0); bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamBitrateEligibility == 0);
var transcodeReasons = directPlayEligibilityResult | directStreamEligibilityResult; var transcodeReasons = directPlayBitrateEligibility | directStreamBitrateEligibility;
_logger.LogDebug( _logger.LogDebug(
"Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}", "Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
@ -625,7 +628,7 @@ namespace MediaBrowser.Model.Dlna
var directPlay = directPlayInfo.PlayMethod; var directPlay = directPlayInfo.PlayMethod;
transcodeReasons |= directPlayInfo.TranscodeReasons; transcodeReasons |= directPlayInfo.TranscodeReasons;
if (directPlay != null) if (directPlay.HasValue)
{ {
directPlayProfile = directPlayInfo.Profile; directPlayProfile = directPlayInfo.Profile;
playlistItem.PlayMethod = directPlay.Value; playlistItem.PlayMethod = directPlay.Value;
@ -676,7 +679,7 @@ namespace MediaBrowser.Model.Dlna
playlistItem.TranscodeReasons = transcodeReasons; playlistItem.TranscodeReasons = transcodeReasons;
if (playlistItem.PlayMethod != PlayMethod.DirectStream || !options.EnableDirectStream) if (playlistItem.PlayMethod != PlayMethod.DirectStream && playlistItem.PlayMethod != PlayMethod.DirectPlay)
{ {
// Can't direct play, find the transcoding profile // Can't direct play, find the transcoding profile
// If we do this for direct-stream we will overwrite the info // If we do this for direct-stream we will overwrite the info
@ -687,6 +690,8 @@ namespace MediaBrowser.Model.Dlna
BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, transcodingProfile.Container, transcodingProfile.VideoCodec, transcodingProfile.AudioCodec); BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, transcodingProfile.Container, transcodingProfile.VideoCodec, transcodingProfile.AudioCodec);
playlistItem.PlayMethod = PlayMethod.Transcode;
if (subtitleStream != null) if (subtitleStream != null)
{ {
var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode, _transcoderSupport, transcodingProfile.Container, transcodingProfile.Protocol); var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode, _transcoderSupport, transcodingProfile.Container, transcodingProfile.Protocol);
@ -696,14 +701,9 @@ namespace MediaBrowser.Model.Dlna
playlistItem.SubtitleCodecs = new[] { subtitleProfile.Format }; playlistItem.SubtitleCodecs = new[] { subtitleProfile.Format };
} }
if (playlistItem.PlayMethod != PlayMethod.DirectPlay) if ((playlistItem.TranscodeReasons & (VideoReasons | TranscodeReason.ContainerBitrateExceedsLimit)) != 0)
{ {
playlistItem.PlayMethod = PlayMethod.Transcode; ApplyTranscodingConditions(playlistItem, transcodingProfile.Conditions, null, true, true);
if ((playlistItem.TranscodeReasons & (VideoReasons | TranscodeReason.ContainerBitrateExceedsLimit)) != 0)
{
ApplyTranscodingConditions(playlistItem, transcodingProfile.Conditions, null, true, true);
}
} }
} }
} }
@ -751,9 +751,9 @@ namespace MediaBrowser.Model.Dlna
var appliedVideoConditions = options.Profile.CodecProfiles var appliedVideoConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.Video && .Where(i => i.Type == CodecType.Video &&
i.ContainsAnyCodec(videoCodec, container) && i.ContainsAnyCodec(videoCodec, container) &&
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC))) i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC)))
.Select(i => .Select(i =>
i.Conditions.All(condition => ConditionProcessor.IsVideoConditionSatisfied(condition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC))); i.Conditions.All(condition => ConditionProcessor.IsVideoConditionSatisfied(condition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC)));
// An empty appliedVideoConditions means that the codec has no conditions for the current video stream // An empty appliedVideoConditions means that the codec has no conditions for the current video stream
var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied); var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied);
@ -771,12 +771,19 @@ namespace MediaBrowser.Model.Dlna
private void BuildStreamVideoItem(StreamInfo playlistItem, VideoOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, string container, string videoCodec, string audioCodec) private void BuildStreamVideoItem(StreamInfo playlistItem, VideoOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, string container, string videoCodec, string audioCodec)
{ {
// prefer matching video codecs // Prefer matching video codecs
var videoCodecs = ContainerProfile.SplitValue(videoCodec); var videoCodecs = ContainerProfile.SplitValue(videoCodec);
var directVideoCodec = ContainerProfile.ContainsContainer(videoCodecs, videoStream?.Codec) ? videoStream?.Codec : null; var directVideoCodec = ContainerProfile.ContainsContainer(videoCodecs, videoStream?.Codec) ? videoStream?.Codec : null;
playlistItem.VideoCodecs = directVideoCodec != null ? new[] { directVideoCodec } : videoCodecs; if (directVideoCodec != null)
{
// merge directVideoCodec to videoCodecs
Array.Resize(ref videoCodecs, videoCodecs.Length + 1);
videoCodecs[^1] = directVideoCodec;
}
// copy video codec options as a starting point, this applies to transcode and direct-stream playlistItem.VideoCodecs = videoCodecs;
// Copy video codec options as a starting point, this applies to transcode and direct-stream
playlistItem.MaxFramerate = videoStream?.AverageFrameRate; playlistItem.MaxFramerate = videoStream?.AverageFrameRate;
var qualifier = videoStream?.Codec; var qualifier = videoStream?.Codec;
if (videoStream?.Level != null) if (videoStream?.Level != null)
@ -799,7 +806,7 @@ namespace MediaBrowser.Model.Dlna
playlistItem.SetOption(qualifier, "level", videoStream.Level.ToString()); playlistItem.SetOption(qualifier, "level", videoStream.Level.ToString());
} }
// prefer matching audio codecs, could do better here // Prefer matching audio codecs, could do better here
var audioCodecs = ContainerProfile.SplitValue(audioCodec); var audioCodecs = ContainerProfile.SplitValue(audioCodec);
var directAudioStream = candidateAudioStreams.FirstOrDefault(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec)); var directAudioStream = candidateAudioStreams.FirstOrDefault(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec));
playlistItem.AudioCodecs = audioCodecs; playlistItem.AudioCodecs = audioCodecs;
@ -809,7 +816,7 @@ namespace MediaBrowser.Model.Dlna
playlistItem.AudioStreamIndex = audioStream.Index; playlistItem.AudioStreamIndex = audioStream.Index;
playlistItem.AudioCodecs = new[] { audioStream.Codec }; playlistItem.AudioCodecs = new[] { audioStream.Codec };
// copy matching audio codec options // Copy matching audio codec options
playlistItem.AudioSampleRate = audioStream.SampleRate; playlistItem.AudioSampleRate = audioStream.SampleRate;
playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels.ToString()); playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels.ToString());
@ -830,6 +837,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;
float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
bool? isAnamorphic = videoStream?.IsAnamorphic; bool? isAnamorphic = videoStream?.IsAnamorphic;
bool? isInterlaced = videoStream?.IsInterlaced; bool? isInterlaced = videoStream?.IsInterlaced;
@ -846,7 +854,7 @@ namespace MediaBrowser.Model.Dlna
var appliedVideoConditions = options.Profile.CodecProfiles var appliedVideoConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.Video && .Where(i => i.Type == CodecType.Video &&
i.ContainsAnyCodec(videoCodec, container) && i.ContainsAnyCodec(videoCodec, container) &&
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))); i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)));
var isFirstAppliedCodecProfile = true; var isFirstAppliedCodecProfile = true;
foreach (var i in appliedVideoConditions) foreach (var i in appliedVideoConditions)
{ {
@ -1070,19 +1078,20 @@ namespace MediaBrowser.Model.Dlna
DeviceProfile profile = options.Profile; DeviceProfile profile = options.Profile;
string container = mediaSource.Container; string container = mediaSource.Container;
// video // Video
int? width = videoStream?.Width; int? width = videoStream?.Width;
int? height = videoStream?.Height; int? height = videoStream?.Height;
int? bitDepth = videoStream?.BitDepth; int? bitDepth = videoStream?.BitDepth;
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;
float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
bool? isAnamorphic = videoStream?.IsAnamorphic; bool? isAnamorphic = videoStream?.IsAnamorphic;
bool? isInterlaced = videoStream?.IsInterlaced; bool? isInterlaced = videoStream?.IsInterlaced;
string videoCodecTag = videoStream?.CodecTag; string videoCodecTag = videoStream?.CodecTag;
bool? isAvc = videoStream?.IsAVC; bool? isAvc = videoStream?.IsAVC;
// audio // Audio
var defaultLanguage = audioStream?.Language ?? string.Empty; var defaultLanguage = audioStream?.Language ?? string.Empty;
var defaultMarked = audioStream?.IsDefault ?? false; var defaultMarked = audioStream?.IsDefault ?? false;
@ -1094,7 +1103,7 @@ namespace MediaBrowser.Model.Dlna
int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video); int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video);
var checkVideoConditions = (ProfileCondition[] conditions) => var checkVideoConditions = (ProfileCondition[] conditions) =>
conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)); conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc));
// Check container conditions // Check container conditions
var containerProfileReasons = AggregateFailureConditions( var containerProfileReasons = AggregateFailureConditions(
@ -1211,6 +1220,7 @@ namespace MediaBrowser.Model.Dlna
return (Result: (Profile: directPlayProfile, PlayMethod: playMethod, AudioStreamIndex: selectedAudioStream?.Index, TranscodeReason: failureReasons), Order: order, Rank: ranked); return (Result: (Profile: directPlayProfile, PlayMethod: playMethod, AudioStreamIndex: selectedAudioStream?.Index, TranscodeReason: failureReasons), Order: order, Rank: ranked);
}) })
.OrderByDescending(analysis => analysis.Result.PlayMethod) .OrderByDescending(analysis => analysis.Result.PlayMethod)
.ThenByDescending(analysis => analysis.Rank)
.ThenBy(analysis => analysis.Order) .ThenBy(analysis => analysis.Order)
.ToArray() .ToArray()
.ToLookup(analysis => analysis.Result.PlayMethod != null); .ToLookup(analysis => analysis.Result.PlayMethod != null);
@ -1223,7 +1233,7 @@ namespace MediaBrowser.Model.Dlna
return profileMatch; return profileMatch;
} }
var failureReasons = analyzedProfiles[false].OrderBy(a => a.Result.TranscodeReason).ThenBy(analysis => analysis.Order).FirstOrDefault().Result.TranscodeReason; var failureReasons = analyzedProfiles[false].Select(analysis => analysis.Result).FirstOrDefault().TranscodeReason;
if (failureReasons == 0) if (failureReasons == 0)
{ {
failureReasons = TranscodeReason.DirectPlayError; failureReasons = TranscodeReason.DirectPlayError;
@ -1269,13 +1279,13 @@ namespace MediaBrowser.Model.Dlna
mediaSource.Path ?? "Unknown path"); mediaSource.Path ?? "Unknown path");
} }
private TranscodeReason IsEligibleForDirectPlay( private TranscodeReason IsBitrateEligibleForDirectPlayback(
MediaSourceInfo item, MediaSourceInfo item,
long maxBitrate, long maxBitrate,
VideoOptions options, VideoOptions options,
PlayMethod playMethod) PlayMethod playMethod)
{ {
bool result = IsItemBitrateEligibleForDirectPlay(item, maxBitrate, playMethod); bool result = IsItemBitrateEligibleForDirectPlayback(item, maxBitrate, playMethod);
if (!result) if (!result)
{ {
return TranscodeReason.ContainerBitrateExceedsLimit; return TranscodeReason.ContainerBitrateExceedsLimit;
@ -1443,7 +1453,7 @@ namespace MediaBrowser.Model.Dlna
return null; return null;
} }
private bool IsItemBitrateEligibleForDirectPlay(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod) private bool IsItemBitrateEligibleForDirectPlayback(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod)
{ {
// Don't restrict by bitrate if coming from an external domain // Don't restrict by bitrate if coming from an external domain
if (item.IsRemote) if (item.IsRemote)
@ -1847,6 +1857,38 @@ namespace MediaBrowser.Model.Dlna
break; break;
} }
case ProfileConditionValue.VideoRangeType:
{
if (string.IsNullOrEmpty(qualifier))
{
continue;
}
// change from split by | to comma
// strip spaces to avoid having to encode
var values = value
.Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (condition.Condition == ProfileConditionType.Equals)
{
item.SetOption(qualifier, "rangetype", string.Join(',', values));
}
else if (condition.Condition == ProfileConditionType.EqualsAny)
{
var currentValue = item.GetOption(qualifier, "rangetype");
if (!string.IsNullOrEmpty(currentValue) && values.Any(v => string.Equals(v, currentValue, StringComparison.OrdinalIgnoreCase)))
{
item.SetOption(qualifier, "rangetype", currentValue);
}
else
{
item.SetOption(qualifier, "rangetype", string.Join(',', values));
}
}
break;
}
case ProfileConditionValue.Height: case ProfileConditionValue.Height:
{ {
if (!enableNonQualifiedConditions) if (!enableNonQualifiedConditions)

View file

@ -280,6 +280,29 @@ namespace MediaBrowser.Model.Dlna
} }
} }
/// <summary>
/// Gets the target video range type that will be in the output stream.
/// </summary>
public string TargetVideoRangeType
{
get
{
if (IsDirectStream)
{
return TargetVideoStream?.VideoRangeType;
}
var targetVideoCodecs = TargetVideoCodec;
var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
if (!string.IsNullOrEmpty(videoCodec))
{
return GetOption(videoCodec, "rangetype");
}
return TargetVideoStream?.VideoRangeType;
}
}
/// <summary> /// <summary>
/// Gets the target video codec tag. /// Gets the target video codec tag.
/// </summary> /// </summary>

View file

@ -72,6 +72,54 @@ namespace MediaBrowser.Model.Entities
/// <value>The color primaries.</value> /// <value>The color primaries.</value>
public string ColorPrimaries { get; set; } public string ColorPrimaries { get; set; }
/// <summary>
/// Gets or sets the Dolby Vision version major.
/// </summary>
/// <value>The Dolby Vision version major.</value>
public int? DvVersionMajor { get; set; }
/// <summary>
/// Gets or sets the Dolby Vision version minor.
/// </summary>
/// <value>The Dolby Vision version minor.</value>
public int? DvVersionMinor { get; set; }
/// <summary>
/// Gets or sets the Dolby Vision profile.
/// </summary>
/// <value>The Dolby Vision profile.</value>
public int? DvProfile { get; set; }
/// <summary>
/// Gets or sets the Dolby Vision level.
/// </summary>
/// <value>The Dolby Vision level.</value>
public int? DvLevel { get; set; }
/// <summary>
/// Gets or sets the Dolby Vision rpu present flag.
/// </summary>
/// <value>The Dolby Vision rpu present flag.</value>
public int? RpuPresentFlag { get; set; }
/// <summary>
/// Gets or sets the Dolby Vision el present flag.
/// </summary>
/// <value>The Dolby Vision el present flag.</value>
public int? ElPresentFlag { get; set; }
/// <summary>
/// Gets or sets the Dolby Vision bl present flag.
/// </summary>
/// <value>The Dolby Vision bl present flag.</value>
public int? BlPresentFlag { get; set; }
/// <summary>
/// Gets or sets the Dolby Vision bl signal compatibility id.
/// </summary>
/// <value>The Dolby Vision bl signal compatibility id.</value>
public int? DvBlSignalCompatibilityId { get; set; }
/// <summary> /// <summary>
/// Gets or sets the comment. /// Gets or sets the comment.
/// </summary> /// </summary>
@ -104,33 +152,64 @@ namespace MediaBrowser.Model.Entities
{ {
get get
{ {
if (Type != MediaStreamType.Video) var (videoRange, _) = GetVideoColorRange();
return videoRange;
}
}
/// <summary>
/// Gets the video range type.
/// </summary>
/// <value>The video range type.</value>
public string VideoRangeType
{
get
{
var (_, videoRangeType) = GetVideoColorRange();
return videoRangeType;
}
}
/// <summary>
/// Gets the video dovi title.
/// </summary>
/// <value>The video dovi title.</value>
public string VideoDoViTitle
{
get
{
var dvProfile = DvProfile;
var rpuPresentFlag = RpuPresentFlag == 1;
var blPresentFlag = BlPresentFlag == 1;
var dvBlCompatId = DvBlSignalCompatibilityId;
if (rpuPresentFlag
&& blPresentFlag
&& (dvProfile == 4
|| dvProfile == 5
|| dvProfile == 7
|| dvProfile == 8
|| dvProfile == 9))
{ {
return null; var title = "DV Profile " + dvProfile;
if (dvBlCompatId > 0)
{
title += "." + dvBlCompatId;
}
return dvBlCompatId switch
{
1 => title + " (HDR10)",
2 => title + " (SDR)",
4 => title + " (HLG)",
_ => title
};
} }
var colorTransfer = ColorTransfer; return null;
if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)
|| string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
{
return "HDR";
}
// For some Dolby Vision files, no color transfer is provided, so check the codec
var codecTag = CodecTag;
if (string.Equals(codecTag, "dva1", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codecTag, "dvav", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codecTag, "dvhe", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codecTag, "dav1", StringComparison.OrdinalIgnoreCase))
{
return "HDR";
}
return "SDR";
} }
} }
@ -509,15 +588,22 @@ namespace MediaBrowser.Model.Entities
return Width switch return Width switch
{ {
<= 720 when Height <= 480 => IsInterlaced ? "480i" : "480p", // 256x144 (16:9 square pixel format)
// 720x576 (PAL) (768 when rescaled for square pixels) <= 256 when Height <= 144 => IsInterlaced ? "144i" : "144p",
<= 768 when Height <= 576 => IsInterlaced ? "576i" : "576p", // 426x240 (16:9 square pixel format)
// 960x540 (sometimes 544 which is multiple of 16) <= 426 when Height <= 240 => IsInterlaced ? "240i" : "240p",
// 640x360 (16:9 square pixel format)
<= 640 when Height <= 360 => IsInterlaced ? "360i" : "360p",
// 854x480 (16:9 square pixel format)
<= 854 when Height <= 480 => IsInterlaced ? "480i" : "480p",
// 960x544 (16:9 square pixel format)
<= 960 when Height <= 544 => IsInterlaced ? "540i" : "540p", <= 960 when Height <= 544 => IsInterlaced ? "540i" : "540p",
// 1024x576 (16:9 square pixel format)
<= 1024 when Height <= 576 => IsInterlaced ? "576i" : "576p",
// 1280x720 // 1280x720
<= 1280 when Height <= 962 => IsInterlaced ? "720i" : "720p", <= 1280 when Height <= 962 => IsInterlaced ? "720i" : "720p",
// 1920x1080 // 2560x1080 (FHD ultra wide 21:9) using 1440px width to accomodate WQHD
<= 1920 when Height <= 1440 => IsInterlaced ? "1080i" : "1080p", <= 2560 when Height <= 1440 => IsInterlaced ? "1080i" : "1080p",
// 4K // 4K
<= 4096 when Height <= 3072 => "4K", <= 4096 when Height <= 3072 => "4K",
// 8K // 8K
@ -572,5 +658,45 @@ namespace MediaBrowser.Model.Entities
return true; return true;
} }
public (string VideoRange, string VideoRangeType) GetVideoColorRange()
{
if (Type != MediaStreamType.Video)
{
return (null, null);
}
var colorTransfer = ColorTransfer;
if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase))
{
return ("HDR", "HDR10");
}
if (string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
{
return ("HDR", "HLG");
}
var codecTag = CodecTag;
var dvProfile = DvProfile;
var rpuPresentFlag = RpuPresentFlag == 1;
var blPresentFlag = BlPresentFlag == 1;
var dvBlCompatId = DvBlSignalCompatibilityId;
var isDoViHDRProfile = dvProfile == 5 || dvProfile == 7 || dvProfile == 8;
var isDoViHDRFlag = rpuPresentFlag && blPresentFlag && (dvBlCompatId == 0 || dvBlCompatId == 1 || dvBlCompatId == 4);
if ((isDoViHDRProfile && isDoViHDRFlag)
|| string.Equals(codecTag, "dovi", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codecTag, "dvhe", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codecTag, "dav1", StringComparison.OrdinalIgnoreCase))
{
return ("HDR", "DOVI");
}
return ("SDR", "SDR");
}
} }
} }

View file

@ -8,7 +8,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Model</PackageId> <PackageId>Jellyfin.Model</PackageId>
<VersionPrefix>10.8.0</VersionPrefix> <VersionPrefix>10.8.1</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
@ -35,12 +35,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
<PackageReference Include="MimeTypes" Version="2.3.0"> <PackageReference Include="MimeTypes" Version="2.4.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="System.Globalization" Version="4.3.0" /> <PackageReference Include="System.Globalization" Version="4.3.0" />
<PackageReference Include="System.Text.Json" Version="6.0.3" /> <PackageReference Include="System.Text.Json" Version="6.0.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -5,6 +5,7 @@ namespace MediaBrowser.Model.MediaInfo
public static class SubtitleFormat public static class SubtitleFormat
{ {
public const string SRT = "srt"; public const string SRT = "srt";
public const string SUBRIP = "subrip";
public const string SSA = "ssa"; public const string SSA = "ssa";
public const string ASS = "ass"; public const string ASS = "ass";
public const string VTT = "vtt"; public const string VTT = "vtt";

View file

@ -1,5 +1,3 @@
#pragma warning disable CS1591
namespace MediaBrowser.Model.Querying namespace MediaBrowser.Model.Querying
{ {
/// <summary> /// <summary>
@ -7,6 +5,9 @@ namespace MediaBrowser.Model.Querying
/// </summary> /// </summary>
public static class ItemSortBy public static class ItemSortBy
{ {
/// <summary>
/// The aired episode order.
/// </summary>
public const string AiredEpisodeOrder = "AiredEpisodeOrder"; public const string AiredEpisodeOrder = "AiredEpisodeOrder";
/// <summary> /// <summary>
@ -44,6 +45,9 @@ namespace MediaBrowser.Model.Querying
/// </summary> /// </summary>
public const string PremiereDate = "PremiereDate"; public const string PremiereDate = "PremiereDate";
/// <summary>
/// The start date.
/// </summary>
public const string StartDate = "StartDate"; public const string StartDate = "StartDate";
/// <summary> /// <summary>
@ -51,6 +55,9 @@ namespace MediaBrowser.Model.Querying
/// </summary> /// </summary>
public const string SortName = "SortName"; public const string SortName = "SortName";
/// <summary>
/// The name.
/// </summary>
public const string Name = "Name"; public const string Name = "Name";
/// <summary> /// <summary>
@ -83,28 +90,69 @@ namespace MediaBrowser.Model.Querying
/// </summary> /// </summary>
public const string CriticRating = "CriticRating"; public const string CriticRating = "CriticRating";
/// <summary>
/// The IsFolder boolean.
/// </summary>
public const string IsFolder = "IsFolder"; public const string IsFolder = "IsFolder";
/// <summary>
/// The IsUnplayed boolean.
/// </summary>
public const string IsUnplayed = "IsUnplayed"; public const string IsUnplayed = "IsUnplayed";
/// <summary>
/// The IsPlayed boolean.
/// </summary>
public const string IsPlayed = "IsPlayed"; public const string IsPlayed = "IsPlayed";
/// <summary>
/// The series sort.
/// </summary>
public const string SeriesSortName = "SeriesSortName"; public const string SeriesSortName = "SeriesSortName";
/// <summary>
/// The video bitrate.
/// </summary>
public const string VideoBitRate = "VideoBitRate"; public const string VideoBitRate = "VideoBitRate";
/// <summary>
/// The air time.
/// </summary>
public const string AirTime = "AirTime"; public const string AirTime = "AirTime";
/// <summary>
/// The studio.
/// </summary>
public const string Studio = "Studio"; public const string Studio = "Studio";
/// <summary>
/// The IsFavouriteOrLiked boolean.
/// </summary>
public const string IsFavoriteOrLiked = "IsFavoriteOrLiked"; public const string IsFavoriteOrLiked = "IsFavoriteOrLiked";
/// <summary>
/// The last content added date.
/// </summary>
public const string DateLastContentAdded = "DateLastContentAdded"; public const string DateLastContentAdded = "DateLastContentAdded";
/// <summary>
/// The series last played date.
/// </summary>
public const string SeriesDatePlayed = "SeriesDatePlayed"; public const string SeriesDatePlayed = "SeriesDatePlayed";
/// <summary>
/// The parent index number.
/// </summary>
public const string ParentIndexNumber = "ParentIndexNumber"; public const string ParentIndexNumber = "ParentIndexNumber";
/// <summary>
/// The index number.
/// </summary>
public const string IndexNumber = "IndexNumber"; public const string IndexNumber = "IndexNumber";
/// <summary>
/// The similarity score.
/// </summary>
public const string SimilarityScore = "SimilarityScore";
} }
} }

View file

@ -14,9 +14,9 @@ public class GeneralCommand
} }
[JsonConstructor] [JsonConstructor]
public GeneralCommand(Dictionary<string, string> arguments) public GeneralCommand(Dictionary<string, string>? arguments)
{ {
Arguments = arguments; Arguments = arguments ?? new Dictionary<string, string>();
} }
public GeneralCommandType Name { get; set; } public GeneralCommandType Name { get; set; }

View file

@ -64,5 +64,11 @@ namespace MediaBrowser.Model.Session
/// </summary> /// </summary>
/// <value>The repeat mode.</value> /// <value>The repeat mode.</value>
public RepeatMode RepeatMode { get; set; } public RepeatMode RepeatMode { get; set; }
/// <summary>
/// Gets or sets the now playing live stream identifier.
/// </summary>
/// <value>The live stream identifier.</value>
public string LiveStreamId { get; set; }
} }
} }

View file

@ -17,6 +17,7 @@ namespace MediaBrowser.Model.Session
// Video Constraints // Video Constraints
VideoProfileNotSupported = 1 << 6, VideoProfileNotSupported = 1 << 6,
VideoRangeTypeNotSupported = 1 << 24,
VideoLevelNotSupported = 1 << 7, VideoLevelNotSupported = 1 << 7,
VideoResolutionNotSupported = 1 << 8, VideoResolutionNotSupported = 1 << 8,
VideoBitDepthNotSupported = 1 << 9, VideoBitDepthNotSupported = 1 << 9,

View file

@ -655,8 +655,6 @@ namespace MediaBrowser.Providers.Manager
}; };
temp.Item.Path = item.Path; temp.Item.Path = item.Path;
var userDataList = new List<UserItemData>();
// If replacing all metadata, run internet providers first // If replacing all metadata, run internet providers first
if (options.ReplaceAllMetadata) if (options.ReplaceAllMetadata)
{ {
@ -670,7 +668,7 @@ namespace MediaBrowser.Providers.Manager
var hasLocalMetadata = false; var hasLocalMetadata = false;
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>().ToList()) foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
{ {
var providerName = provider.GetType().Name; var providerName = provider.GetType().Name;
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName); Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
@ -687,6 +685,11 @@ namespace MediaBrowser.Providers.Manager
{ {
try try
{ {
if (!options.IsReplacingImage(remoteImage.Type))
{
continue;
}
await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false); await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false);
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
} }
@ -701,11 +704,6 @@ namespace MediaBrowser.Providers.Manager
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
} }
if (localItem.UserDataList != null)
{
userDataList.AddRange(localItem.UserDataList);
}
MergeData(localItem, temp, Array.Empty<MetadataField>(), !options.ReplaceAllMetadata, true); MergeData(localItem, temp, Array.Empty<MetadataField>(), !options.ReplaceAllMetadata, true);
refreshResult.UpdateType |= ItemUpdateType.MetadataImport; refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
@ -764,15 +762,11 @@ namespace MediaBrowser.Providers.Manager
} }
} }
// var isUnidentified = failedProviderCount > 0 && successfulProviderCount == 0;
foreach (var provider in customProviders.Where(i => i is not IPreRefreshProvider)) foreach (var provider in customProviders.Where(i => i is not IPreRefreshProvider))
{ {
await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwait(false); await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwait(false);
} }
// ImportUserData(item, userDataList, cancellationToken);
return refreshResult; return refreshResult;
} }

View file

@ -20,8 +20,8 @@
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="OptimizedPriorityQueue" Version="5.0.0" /> <PackageReference Include="OptimizedPriorityQueue" Version="5.1.0" />
<PackageReference Include="PlaylistsNET" Version="1.1.3" /> <PackageReference Include="PlaylistsNET" Version="1.2.1" />
<PackageReference Include="TMDbLib" Version="1.9.2" /> <PackageReference Include="TMDbLib" Version="1.9.2" />
</ItemGroup> </ItemGroup>

View file

@ -4,6 +4,7 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.MediaInfo namespace MediaBrowser.Providers.MediaInfo
{ {
@ -15,16 +16,19 @@ namespace MediaBrowser.Providers.MediaInfo
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AudioResolver"/> class for external audio file processing. /// Initializes a new instance of the <see cref="AudioResolver"/> class for external audio file processing.
/// </summary> /// </summary>
/// <param name="logger">The logger.</param>
/// <param name="localizationManager">The localization manager.</param> /// <param name="localizationManager">The localization manager.</param>
/// <param name="mediaEncoder">The media encoder.</param> /// <param name="mediaEncoder">The media encoder.</param>
/// <param name="fileSystem">The file system.</param> /// <param name="fileSystem">The file system.</param>
/// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param> /// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
public AudioResolver( public AudioResolver(
ILogger<AudioResolver> logger,
ILocalizationManager localizationManager, ILocalizationManager localizationManager,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
IFileSystem fileSystem, IFileSystem fileSystem,
NamingOptions namingOptions) NamingOptions namingOptions)
: base( : base(
logger,
localizationManager, localizationManager,
mediaEncoder, mediaEncoder,
fileSystem, fileSystem,

View file

@ -47,7 +47,6 @@ namespace MediaBrowser.Providers.MediaInfo
private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None); private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
public FFProbeProvider( public FFProbeProvider(
ILogger<FFProbeProvider> logger,
IMediaSourceManager mediaSourceManager, IMediaSourceManager mediaSourceManager,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
IItemRepository itemRepo, IItemRepository itemRepo,
@ -59,11 +58,12 @@ namespace MediaBrowser.Providers.MediaInfo
IChapterManager chapterManager, IChapterManager chapterManager,
ILibraryManager libraryManager, ILibraryManager libraryManager,
IFileSystem fileSystem, IFileSystem fileSystem,
ILoggerFactory loggerFactory,
NamingOptions namingOptions) NamingOptions namingOptions)
{ {
_logger = logger; _logger = loggerFactory.CreateLogger<FFProbeProvider>();
_audioResolver = new AudioResolver(localization, mediaEncoder, fileSystem, namingOptions); _audioResolver = new AudioResolver(loggerFactory.CreateLogger<AudioResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
_subtitleResolver = new SubtitleResolver(localization, mediaEncoder, fileSystem, namingOptions); _subtitleResolver = new SubtitleResolver(loggerFactory.CreateLogger<SubtitleResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
_videoProber = new FFProbeVideoInfo( _videoProber = new FFProbeVideoInfo(
_logger, _logger,
mediaSourceManager, mediaSourceManager,

View file

@ -15,6 +15,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.MediaInfo namespace MediaBrowser.Providers.MediaInfo
{ {
@ -33,6 +34,7 @@ namespace MediaBrowser.Providers.MediaInfo
/// </summary> /// </summary>
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly ILogger _logger;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
/// <summary> /// <summary>
@ -48,18 +50,21 @@ namespace MediaBrowser.Providers.MediaInfo
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="MediaInfoResolver"/> class. /// Initializes a new instance of the <see cref="MediaInfoResolver"/> class.
/// </summary> /// </summary>
/// <param name="logger">The logger.</param>
/// <param name="localizationManager">The localization manager.</param> /// <param name="localizationManager">The localization manager.</param>
/// <param name="mediaEncoder">The media encoder.</param> /// <param name="mediaEncoder">The media encoder.</param>
/// <param name="fileSystem">The file system.</param> /// <param name="fileSystem">The file system.</param>
/// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param> /// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
/// <param name="type">The <see cref="DlnaProfileType"/> of the parsed file.</param> /// <param name="type">The <see cref="DlnaProfileType"/> of the parsed file.</param>
protected MediaInfoResolver( protected MediaInfoResolver(
ILogger logger,
ILocalizationManager localizationManager, ILocalizationManager localizationManager,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
IFileSystem fileSystem, IFileSystem fileSystem,
NamingOptions namingOptions, NamingOptions namingOptions,
DlnaProfileType type) DlnaProfileType type)
{ {
_logger = logger;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_namingOptions = namingOptions; _namingOptions = namingOptions;
@ -101,25 +106,43 @@ namespace MediaBrowser.Providers.MediaInfo
{ {
if (!pathInfo.Path.AsSpan().EndsWith(".strm", StringComparison.OrdinalIgnoreCase)) if (!pathInfo.Path.AsSpan().EndsWith(".strm", StringComparison.OrdinalIgnoreCase))
{ {
var mediaInfo = await GetMediaInfo(pathInfo.Path, _type, cancellationToken).ConfigureAwait(false); try
if (mediaInfo.MediaStreams.Count == 1)
{ {
MediaStream mediaStream = mediaInfo.MediaStreams[0]; var mediaInfo = await GetMediaInfo(pathInfo.Path, _type, cancellationToken).ConfigureAwait(false);
mediaStream.Index = startIndex++;
mediaStream.IsDefault = pathInfo.IsDefault || mediaStream.IsDefault;
mediaStream.IsForced = pathInfo.IsForced || mediaStream.IsForced;
mediaStreams.Add(MergeMetadata(mediaStream, pathInfo)); if (mediaInfo.MediaStreams.Count == 1)
}
else
{
foreach (MediaStream mediaStream in mediaInfo.MediaStreams)
{ {
mediaStream.Index = startIndex++; MediaStream mediaStream = mediaInfo.MediaStreams[0];
mediaStreams.Add(MergeMetadata(mediaStream, pathInfo)); if ((mediaStream.Type == MediaStreamType.Audio && _type == DlnaProfileType.Audio)
|| (mediaStream.Type == MediaStreamType.Subtitle && _type == DlnaProfileType.Subtitle))
{
mediaStream.Index = startIndex++;
mediaStream.IsDefault = pathInfo.IsDefault || mediaStream.IsDefault;
mediaStream.IsForced = pathInfo.IsForced || mediaStream.IsForced;
mediaStreams.Add(MergeMetadata(mediaStream, pathInfo));
}
} }
else
{
foreach (MediaStream mediaStream in mediaInfo.MediaStreams)
{
if ((mediaStream.Type == MediaStreamType.Audio && _type == DlnaProfileType.Audio)
|| (mediaStream.Type == MediaStreamType.Subtitle && _type == DlnaProfileType.Subtitle))
{
mediaStream.Index = startIndex++;
mediaStreams.Add(MergeMetadata(mediaStream, pathInfo));
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting external streams from {Path}", pathInfo.Path);
continue;
} }
} }
} }
@ -222,13 +245,6 @@ namespace MediaBrowser.Providers.MediaInfo
mediaStream.Title = string.IsNullOrEmpty(mediaStream.Title) ? (string.IsNullOrEmpty(pathInfo.Title) ? null : pathInfo.Title) : mediaStream.Title; mediaStream.Title = string.IsNullOrEmpty(mediaStream.Title) ? (string.IsNullOrEmpty(pathInfo.Title) ? null : pathInfo.Title) : mediaStream.Title;
mediaStream.Language = string.IsNullOrEmpty(mediaStream.Language) ? (string.IsNullOrEmpty(pathInfo.Language) ? null : pathInfo.Language) : mediaStream.Language; mediaStream.Language = string.IsNullOrEmpty(mediaStream.Language) ? (string.IsNullOrEmpty(pathInfo.Language) ? null : pathInfo.Language) : mediaStream.Language;
mediaStream.Type = _type switch
{
DlnaProfileType.Audio => MediaStreamType.Audio,
DlnaProfileType.Subtitle => MediaStreamType.Subtitle,
_ => mediaStream.Type
};
return mediaStream; return mediaStream;
} }
} }

View file

@ -4,6 +4,7 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.MediaInfo namespace MediaBrowser.Providers.MediaInfo
{ {
@ -15,16 +16,19 @@ namespace MediaBrowser.Providers.MediaInfo
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SubtitleResolver"/> class for external subtitle file processing. /// Initializes a new instance of the <see cref="SubtitleResolver"/> class for external subtitle file processing.
/// </summary> /// </summary>
/// <param name="logger">The logger.</param>
/// <param name="localizationManager">The localization manager.</param> /// <param name="localizationManager">The localization manager.</param>
/// <param name="mediaEncoder">The media encoder.</param> /// <param name="mediaEncoder">The media encoder.</param>
/// <param name="fileSystem">The file system.</param> /// <param name="fileSystem">The file system.</param>
/// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param> /// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
public SubtitleResolver( public SubtitleResolver(
ILogger<SubtitleResolver> logger,
ILocalizationManager localizationManager, ILocalizationManager localizationManager,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
IFileSystem fileSystem, IFileSystem fileSystem,
NamingOptions namingOptions) NamingOptions namingOptions)
: base( : base(
logger,
localizationManager, localizationManager,
mediaEncoder, mediaEncoder,
fileSystem, fileSystem,

View file

@ -465,7 +465,8 @@ namespace MediaBrowser.Providers.Music
ValidationType = ValidationType.None, ValidationType = ValidationType.None,
CheckCharacters = false, CheckCharacters = false,
IgnoreProcessingInstructions = true, IgnoreProcessingInstructions = true,
IgnoreComments = true IgnoreComments = true,
Async = true
}; };
using var reader = XmlReader.Create(oReader, settings); using var reader = XmlReader.Create(oReader, settings);

View file

@ -1,4 +1,4 @@
using System.Reflection; using System.Reflection;
[assembly: AssemblyVersion("10.8.0")] [assembly: AssemblyVersion("10.8.1")]
[assembly: AssemblyFileVersion("10.8.0")] [assembly: AssemblyFileVersion("10.8.1")]

View file

@ -1,7 +1,7 @@
--- ---
# We just wrap `build` so this is really it # We just wrap `build` so this is really it
name: "jellyfin" name: "jellyfin"
version: "10.8.0" version: "10.8.1"
packages: packages:
- debian.amd64 - debian.amd64
- debian.arm64 - debian.arm64

12
debian/changelog vendored
View file

@ -1,14 +1,14 @@
jellyfin-server (10.8.0~beta2) unstable; urgency=medium jellyfin-server (10.8.1-1) unstable; urgency=medium
* New upstream version 10.8.0-beta2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.0-beta2 * New upstream version 10.8.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.1
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 17 Apr 2022 15:51:43 -0400 -- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 26 Jun 2022 20:59:36 -0400
jellyfin-server (10.8.0~beta1) unstable; urgency=medium jellyfin-server (10.8.0-1) unstable; urgency=medium
* New upstream version 10.8.0-beta1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.0-beta1 * New upstream version 10.8.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.0
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 25 Mar 2022 22:22:51 -0400 -- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 10 Jun 2022 22:15:12 -0400
jellyfin-server (10.7.0-1) unstable; urgency=medium jellyfin-server (10.7.0-1) unstable; urgency=medium

View file

@ -30,8 +30,4 @@ Defaults!RESTARTSERVER_INITD !requiretty
Defaults!STARTSERVER_INITD !requiretty Defaults!STARTSERVER_INITD !requiretty
Defaults!STOPSERVER_INITD !requiretty Defaults!STOPSERVER_INITD !requiretty
#Allow the server to mount iso images
jellyfin ALL=(ALL) NOPASSWD: /bin/mount
jellyfin ALL=(ALL) NOPASSWD: /bin/umount
Defaults:jellyfin !requiretty Defaults:jellyfin !requiretty

View file

@ -3,5 +3,53 @@
# Use this file to override the user or environment file location. # Use this file to override the user or environment file location.
[Service] [Service]
# Alter the user that Jellyfin runs as
#User = jellyfin #User = jellyfin
# Alter where environment variables are sourced from
#EnvironmentFile = /etc/default/jellyfin #EnvironmentFile = /etc/default/jellyfin
# Service hardening options
# These were added in PR #6953 to solve issue #6952, but some combination of
# them causes "restart.sh" functionality to break with the following error:
# sudo: effective uid is not 0, is /usr/bin/sudo on a file system with the
# 'nosuid' option set or an NFS file system without root privileges?
# See issue #7503 for details on the troubleshooting that went into this.
# Since these were added for NixOS specifically and are above and beyond
# what 99% of systemd units do, they have been moved here as optional
# additional flags to set for maximum system security and can be enabled at
# the administrator's or package maintainer's discretion.
# Uncomment these only if you know what you're doing, and doing so may cause
# bugs with in-server Restart and potentially other functionality as well.
#NoNewPrivileges=true
#SystemCallArchitectures=native
#RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
#RestrictNamespaces=false
#RestrictRealtime=true
#RestrictSUIDSGID=true
#ProtectControlGroups=false
#ProtectHostname=true
#ProtectKernelLogs=false
#ProtectKernelModules=false
#ProtectKernelTunables=false
#LockPersonality=true
#PrivateTmp=false
#PrivateDevices=false
#PrivateUsers=true
#RemoveIPC=true
#SystemCallFilter=~@clock
#SystemCallFilter=~@aio
#SystemCallFilter=~@chown
#SystemCallFilter=~@cpu-emulation
#SystemCallFilter=~@debug
#SystemCallFilter=~@keyring
#SystemCallFilter=~@memlock
#SystemCallFilter=~@module
#SystemCallFilter=~@mount
#SystemCallFilter=~@obsolete
#SystemCallFilter=~@privileged
#SystemCallFilter=~@raw-io
#SystemCallFilter=~@reboot
#SystemCallFilter=~@setuid
#SystemCallFilter=~@swap
#SystemCallErrorNumber=EPERM

View file

@ -13,38 +13,5 @@ Restart = on-failure
TimeoutSec = 15 TimeoutSec = 15
SuccessExitStatus=0 143 SuccessExitStatus=0 143
NoNewPrivileges=true
SystemCallArchitectures=native
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
RestrictNamespaces=false
RestrictRealtime=true
RestrictSUIDSGID=true
ProtectControlGroups=false
ProtectHostname=true
ProtectKernelLogs=false
ProtectKernelModules=false
ProtectKernelTunables=false
LockPersonality=true
PrivateTmp=false
PrivateDevices=false
PrivateUsers=true
RemoveIPC=true
SystemCallFilter=~@clock
SystemCallFilter=~@aio
SystemCallFilter=~@chown
SystemCallFilter=~@cpu-emulation
SystemCallFilter=~@debug
SystemCallFilter=~@keyring
SystemCallFilter=~@memlock
SystemCallFilter=~@module
SystemCallFilter=~@mount
SystemCallFilter=~@obsolete
SystemCallFilter=~@privileged
SystemCallFilter=~@raw-io
SystemCallFilter=~@reboot
SystemCallFilter=~@setuid
SystemCallFilter=~@swap
SystemCallErrorNumber=EPERM
[Install] [Install]
WantedBy = multi-user.target WantedBy = multi-user.target

View file

@ -5,7 +5,7 @@ Homepage: https://jellyfin.org
Standards-Version: 3.9.2 Standards-Version: 3.9.2
Package: jellyfin Package: jellyfin
Version: 10.8.0~beta2 Version: 10.8.1
Maintainer: Jellyfin Packaging Team <packaging@jellyfin.org> Maintainer: Jellyfin Packaging Team <packaging@jellyfin.org>
Depends: jellyfin-server, jellyfin-web Depends: jellyfin-server, jellyfin-web
Description: Provides the Jellyfin Free Software Media System Description: Provides the Jellyfin Free Software Media System

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/9d8c7137-2091-4fc6-a419-60ba59c8b9de/db0c5cda94f31d2260d369123de32d59/dotnet-sdk-6.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0e83f50a-0619-45e6-8f16-dc4f41d1bb16/e0de908b2f070ef9e7e3b6ddea9d268c/dotnet-sdk-6.0.302-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

@ -1,4 +1,4 @@
FROM fedora:33 FROM fedora:36
# Docker build arguments # Docker build arguments
ARG SOURCE_DIR=/jellyfin ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist ARG ARTIFACT_DIR=/dist
@ -9,10 +9,10 @@ ENV IS_DOCKER=YES
# Prepare Fedora environment # Prepare Fedora environment
RUN dnf update -yq \ 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 && 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/9d8c7137-2091-4fc6-a419-60ba59c8b9de/db0c5cda94f31d2260d369123de32d59/dotnet-sdk-6.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0e83f50a-0619-45e6-8f16-dc4f41d1bb16/e0de908b2f070ef9e7e3b6ddea9d268c/dotnet-sdk-6.0.302-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/9d8c7137-2091-4fc6-a419-60ba59c8b9de/db0c5cda94f31d2260d369123de32d59/dotnet-sdk-6.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0e83f50a-0619-45e6-8f16-dc4f41d1bb16/e0de908b2f070ef9e7e3b6ddea9d268c/dotnet-sdk-6.0.302-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/9d8c7137-2091-4fc6-a419-60ba59c8b9de/db0c5cda94f31d2260d369123de32d59/dotnet-sdk-6.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0e83f50a-0619-45e6-8f16-dc4f41d1bb16/e0de908b2f070ef9e7e3b6ddea9d268c/dotnet-sdk-6.0.302-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/9d8c7137-2091-4fc6-a419-60ba59c8b9de/db0c5cda94f31d2260d369123de32d59/dotnet-sdk-6.0.202-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0e83f50a-0619-45e6-8f16-dc4f41d1bb16/e0de908b2f070ef9e7e3b6ddea9d268c/dotnet-sdk-6.0.302-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

@ -7,14 +7,13 @@ SRPM := jellyfin-$(subst -,~,$(VERSION))-$(RELEASE)$(shell rpm --eval %dist).
TARBALL :=$(NAME)-$(subst -,~,$(VERSION)).tar.gz TARBALL :=$(NAME)-$(subst -,~,$(VERSION)).tar.gz
epel-7-x86_64_repos := https://packages.microsoft.com/rhel/7/prod/ epel-7-x86_64_repos := https://packages.microsoft.com/rhel/7/prod/
epel-8-x86_64_repos := https://download.copr.fedorainfracloud.org/results/@dotnet-sig/dotnet-preview/$(TARGET)/
fedora_repos := https://download.copr.fedorainfracloud.org/results/@dotnet-sig/dotnet-preview/$(TARGET)/ fed_ver := $(shell rpm -E %fedora)
fedora-34-x86_64_repos := $(fedora_repos) # fallback when not running on Fedora
fedora-35-x86_64_repos := $(fedora_repos) fed_ver ?= 36
fedora-34-x86_64_repos := $(fedora_repos) TARGET ?= fedora-$(fed_ver)-x86_64
outdir ?= $(PWD)/$(DIR)/ outdir ?= $(PWD)/$(DIR)/
TARGET ?= fedora-35-x86_64
srpm: $(DIR)/$(SRPM) srpm: $(DIR)/$(SRPM)
tarball: $(DIR)/$(TARBALL) tarball: $(DIR)/$(TARBALL)

View file

@ -18,14 +18,6 @@ $ sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-re
$ sudo yum localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm $ sudo yum localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm
``` ```
## ISO mounting
To allow Jellyfin to mount/umount ISO files uncomment these two lines in `/etc/sudoers.d/jellyfin-sudoers`
```
# %jellyfin ALL=(ALL) NOPASSWD: /bin/mount
# %jellyfin ALL=(ALL) NOPASSWD: /bin/umount
```
## Building with dotnet ## Building with dotnet
Jellyfin is build with `--self-contained` so no dotnet required for runtime. Jellyfin is build with `--self-contained` so no dotnet required for runtime.
@ -40,4 +32,4 @@ $ sudo rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-
## TODO ## TODO
- [ ] OpenSUSE - [ ] OpenSUSE

View file

@ -21,7 +21,7 @@ JELLYFIN_LOG_DIR="/var/log/jellyfin"
JELLYFIN_CACHE_DIR="/var/cache/jellyfin" JELLYFIN_CACHE_DIR="/var/cache/jellyfin"
# web client path, installed by the jellyfin-web package # web client path, installed by the jellyfin-web package
JELLYFIN_WEB_OPT="--webdir=/usr/share/jellyfin-web" # JELLYFIN_WEB_OPT="--webdir=/usr/share/jellyfin-web"
# In-App service control # In-App service control
JELLYFIN_RESTART_OPT="--restartpath=/usr/libexec/jellyfin/restart.sh" JELLYFIN_RESTART_OPT="--restartpath=/usr/libexec/jellyfin/restart.sh"

View file

@ -1,16 +1,16 @@
%global debug_package %{nil} %global debug_package %{nil}
# Set the dotnet runtime # Set the dotnet runtime
%if 0%{?fedora} %if 0%{?fedora}
%global dotnet_runtime fedora-x64 %global dotnet_runtime fedora.%{fedora}-x64
%else %else
%global dotnet_runtime centos-x64 %global dotnet_runtime centos-x64
%endif %endif
Name: jellyfin Name: jellyfin
Version: 10.8.0~beta2 Version: 10.8.1
Release: 1%{?dist} Release: 1%{?dist}
Summary: The Free Software Media System Summary: The Free Software Media System
License: GPLv3 License: GPLv2
URL: https://jellyfin.org URL: https://jellyfin.org
# Jellyfin Server tarball created by `make -f .copr/Makefile srpm`, real URL ends with `v%%{version}.tar.gz` # Jellyfin Server tarball created by `make -f .copr/Makefile srpm`, real URL ends with `v%%{version}.tar.gz`
Source0: jellyfin-server-%{version}.tar.gz Source0: jellyfin-server-%{version}.tar.gz
@ -25,13 +25,16 @@ Source17: jellyfin-server-lowports.conf
%{?systemd_requires} %{?systemd_requires}
BuildRequires: systemd BuildRequires: systemd
BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel, glibc-devel, libicu-devel BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel, glibc-devel, libicu-devel
# Requirements not packaged in main repos # Requirements not packaged in RHEL 7 main repos, added via Makefile
# COPR @dotnet-sig/dotnet or
# https://packages.microsoft.com/rhel/7/prod/ # https://packages.microsoft.com/rhel/7/prod/
BuildRequires: dotnet-runtime-6.0, dotnet-sdk-6.0 BuildRequires: dotnet-runtime-6.0, dotnet-sdk-6.0
Requires: %{name}-server = %{version}-%{release}, %{name}-web = %{version}-%{release} Requires: %{name}-server = %{version}-%{release}, %{name}-web = %{version}-%{release}
# Disable Automatic Dependency Processing
AutoReqProv: no # Temporary (hopefully?) fix for https://github.com/jellyfin/jellyfin/issues/7471
%if 0%{?fedora} >= 36
%global __requires_exclude ^liblttng-ust\\.so\\.0.*$
%endif
%description %description
Jellyfin is a free software media system that puts you in control of managing and streaming your media. Jellyfin is a free software media system that puts you in control of managing and streaming your media.
@ -59,54 +62,74 @@ the Jellyfin server to bind to ports 80 and/or 443 for example.
%prep %prep
%autosetup -n jellyfin-server-%{version} -b 0 %autosetup -n jellyfin-server-%{version} -b 0
%build %build
export DOTNET_CLI_TELEMETRY_OPTOUT=1
export PATH=$PATH:/usr/local/bin
# cannot use --output due to https://github.com/dotnet/sdk/issues/22220
dotnet publish --configuration Release --self-contained --runtime %{dotnet_runtime} \
"-p:DebugSymbols=false;DebugType=none" Jellyfin.Server
%install %install
export DOTNET_CLI_TELEMETRY_OPTOUT=1 # Jellyfin files
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 %{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir}
export PATH=$PATH:/usr/local/bin %{__cp} -r Jellyfin.Server/bin/Release/net6.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin
dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} \ ln -srf %{_libdir}/jellyfin/jellyfin %{buildroot}%{_bindir}/jellyfin
"-p:DebugSymbols=false;DebugType=none" Jellyfin.Server %{__install} -D %{SOURCE14} %{buildroot}%{_libexecdir}/jellyfin/restart.sh
%{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/jellyfin/LICENSE
%{__install} -D -m 0644 %{SOURCE15} %{buildroot}%{_sysconfdir}/systemd/system/jellyfin.service.d/override.conf # Jellyfin config
%{__install} -D -m 0644 %{SOURCE17} %{buildroot}%{_unitdir}/jellyfin.service.d/jellyfin-server-lowports.conf %{__install} -D Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json
%{__install} -D -m 0644 Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json %{__install} -D %{SOURCE12} %{buildroot}%{_sysconfdir}/sysconfig/jellyfin
%{__mkdir} -p %{buildroot}%{_bindir}
tee %{buildroot}%{_bindir}/jellyfin << EOF # system config
#!/bin/sh %{__install} -D %{SOURCE16} %{buildroot}%{_prefix}/lib/firewalld/services/jellyfin.xml
exec %{_libdir}/jellyfin/jellyfin \${@} %{__install} -D %{SOURCE13} %{buildroot}%{_sysconfdir}/sudoers.d/jellyfin-sudoers
EOF %{__install} -D %{SOURCE15} %{buildroot}%{_sysconfdir}/systemd/system/jellyfin.service.d/override.conf
%{__install} -D %{SOURCE11} %{buildroot}%{_unitdir}/jellyfin.service
# empty directories
%{__mkdir} -p %{buildroot}%{_sharedstatedir}/jellyfin %{__mkdir} -p %{buildroot}%{_sharedstatedir}/jellyfin
%{__mkdir} -p %{buildroot}%{_sysconfdir}/jellyfin %{__mkdir} -p %{buildroot}%{_sysconfdir}/jellyfin
%{__mkdir} -p %{buildroot}%{_var}/log/jellyfin
%{__mkdir} -p %{buildroot}%{_var}/cache/jellyfin %{__mkdir} -p %{buildroot}%{_var}/cache/jellyfin
%{__mkdir} -p %{buildroot}%{_var}/log/jellyfin
# jellyfin-server-lowports subpackage
%{__install} -D -m 0644 %{SOURCE17} %{buildroot}%{_unitdir}/jellyfin.service.d/jellyfin-server-lowports.conf
%{__install} -D -m 0644 %{SOURCE11} %{buildroot}%{_unitdir}/jellyfin.service
%{__install} -D -m 0644 %{SOURCE12} %{buildroot}%{_sysconfdir}/sysconfig/jellyfin
%{__install} -D -m 0600 %{SOURCE13} %{buildroot}%{_sysconfdir}/sudoers.d/jellyfin-sudoers
%{__install} -D -m 0755 %{SOURCE14} %{buildroot}%{_libexecdir}/jellyfin/restart.sh
%{__install} -D -m 0644 %{SOURCE16} %{buildroot}%{_prefix}/lib/firewalld/services/jellyfin.xml
%files %files
# empty as this is just a meta-package # empty as this is just a meta-package
%files server %files server
%attr(755,root,root) %{_bindir}/jellyfin %defattr(644,root,root,755)
%{_libdir}/jellyfin/*
# Jellyfin files
%{_bindir}/jellyfin
# Needs 755 else only root can run it since binary build by dotnet is 722 # Needs 755 else only root can run it since binary build by dotnet is 722
%attr(755,root,root) %{_libdir}/jellyfin/createdump
%attr(755,root,root) %{_libdir}/jellyfin/jellyfin %attr(755,root,root) %{_libdir}/jellyfin/jellyfin
%{_unitdir}/jellyfin.service %{_libdir}/jellyfin/*
%{_libexecdir}/jellyfin/restart.sh %attr(755,root,root) %{_libexecdir}/jellyfin/restart.sh
%{_prefix}/lib/firewalld/services/jellyfin.xml
%attr(755,jellyfin,jellyfin) %dir %{_sysconfdir}/jellyfin # Jellyfin config
%config(noreplace) %attr(644,jellyfin,jellyfin) %{_sysconfdir}/jellyfin/logging.json
%config %{_sysconfdir}/sysconfig/jellyfin %config %{_sysconfdir}/sysconfig/jellyfin
# system config
%{_prefix}/lib/firewalld/services/jellyfin.xml
%{_unitdir}/jellyfin.service
%config(noreplace) %attr(600,root,root) %{_sysconfdir}/sudoers.d/jellyfin-sudoers %config(noreplace) %attr(600,root,root) %{_sysconfdir}/sudoers.d/jellyfin-sudoers
%config(noreplace) %{_sysconfdir}/systemd/system/jellyfin.service.d/override.conf %config(noreplace) %{_sysconfdir}/systemd/system/jellyfin.service.d/override.conf
%config(noreplace) %attr(644,jellyfin,jellyfin) %{_sysconfdir}/jellyfin/logging.json
# empty directories
%attr(750,jellyfin,jellyfin) %dir %{_sharedstatedir}/jellyfin %attr(750,jellyfin,jellyfin) %dir %{_sharedstatedir}/jellyfin
%attr(-,jellyfin,jellyfin) %dir %{_var}/log/jellyfin %attr(755,jellyfin,jellyfin) %dir %{_sysconfdir}/jellyfin
%attr(750,jellyfin,jellyfin) %dir %{_var}/cache/jellyfin %attr(750,jellyfin,jellyfin) %dir %{_var}/cache/jellyfin
%{_datadir}/licenses/jellyfin/LICENSE %attr(-, jellyfin,jellyfin) %dir %{_var}/log/jellyfin
%license LICENSE
%files server-lowports %files server-lowports
%{_unitdir}/jellyfin.service.d/jellyfin-server-lowports.conf %{_unitdir}/jellyfin.service.d/jellyfin-server-lowports.conf
@ -153,10 +176,10 @@ fi
%systemd_postun_with_restart jellyfin.service %systemd_postun_with_restart jellyfin.service
%changelog %changelog
* Sun Apr 17 2022 Jellyfin Packaging Team <packaging@jellyfin.org> * Sun Jun 26 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.8.0-beta2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.0-beta2 - New upstream version 10.8.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.1
* Fri Mar 25 2022 Jellyfin Packaging Team <packaging@jellyfin.org> * Fri Jun 10 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.8.0-beta1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.0-beta1 - New upstream version 10.8.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.0
* Mon Nov 29 2021 Brian J. Murrell <brian@interlinx.bc.ca> * Mon Nov 29 2021 Brian J. Murrell <brian@interlinx.bc.ca>
- Add jellyfin-server-lowports.service drop-in in a server-lowports - Add jellyfin-server-lowports.service drop-in in a server-lowports
subpackage to allow binding to low ports subpackage to allow binding to low ports

Some files were not shown because too many files have changed in this diff Show more