mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-04-18 19:25:00 -04:00
* Add support for bitstream filter to remove dynamic hdr metadata * Add support for ffprobe's only_first_vframe for HDR10+ detection * Add BitStreamFilterOptionType for metadata removal check * Map HDR10+ metadata to VideoRangeType.cs Current implementation uses a hack that abuses the EL flag to avoid database schema changes. Should add proper field once EFCore migration is merged. * Add more Dolby Vision Range types Out of spec ones are problematic and should be marked as a dedicated invalid type and handled by the server to not crash the player. Profile 7 videos should not be treated as normal HDR10 videos at all and should remove the metadata before serving. * Remove dynamic hdr metadata when necessary * Allow direct playback of HDR10+ videos on HDR10 clients * Only use dovi codec tag when dovi metadata is not removed * Handle DV Profile 7 Videos better * Fix HDR10+ with new bitmask * Indicate the presence of HDR10+ in HLS SUPPLEMENTAL-CODECS * Fix Dovi 8.4 not labeled as HLG in HLS * Fallback to dovi_rpu bsf for av1 when possible * Fix dovi_rpu cli for av1 * Use correct EFCore db column for HDR10+ * Undo outdated migration * Add proper hdr10+ migration * Remove outdated migration * Rebase to new db code * Add migrations for Hdr10PlusPresentFlag * Directly use bsf enum * Add xmldocs for SupportsBitStreamFilterWithOption * Make `VideoRangeType.Unknown` explicitly default on api models. * Unset default for non-api model class * Use tuples for bsf dictionary for now
385 lines
17 KiB
C#
385 lines
17 KiB
C#
using System;
|
|
using System.Globalization;
|
|
using Jellyfin.Data.Enums;
|
|
using Jellyfin.Extensions;
|
|
using MediaBrowser.Model.MediaInfo;
|
|
|
|
namespace MediaBrowser.Model.Dlna
|
|
{
|
|
/// <summary>
|
|
/// The condition processor.
|
|
/// </summary>
|
|
public static class ConditionProcessor
|
|
{
|
|
/// <summary>
|
|
/// Checks if a video condition is satisfied.
|
|
/// </summary>
|
|
/// <param name="condition">The <see cref="ProfileCondition"/>.</param>
|
|
/// <param name="width">The width.</param>
|
|
/// <param name="height">The height.</param>
|
|
/// <param name="videoBitDepth">The bit depth.</param>
|
|
/// <param name="videoBitrate">The bitrate.</param>
|
|
/// <param name="videoProfile">The video profile.</param>
|
|
/// <param name="videoRangeType">The <see cref="VideoRangeType"/>.</param>
|
|
/// <param name="videoLevel">The video level.</param>
|
|
/// <param name="videoFramerate">The framerate.</param>
|
|
/// <param name="packetLength">The packet length.</param>
|
|
/// <param name="timestamp">The <see cref="TransportStreamTimestamp"/>.</param>
|
|
/// <param name="isAnamorphic">A value indicating whether the video is anamorphic.</param>
|
|
/// <param name="isInterlaced">A value indicating whether the video is interlaced.</param>
|
|
/// <param name="refFrames">The reference frames.</param>
|
|
/// <param name="numStreams">The number of streams.</param>
|
|
/// <param name="numVideoStreams">The number of video streams.</param>
|
|
/// <param name="numAudioStreams">The number of audio streams.</param>
|
|
/// <param name="videoCodecTag">The video codec tag.</param>
|
|
/// <param name="isAvc">A value indicating whether the video is AVC.</param>
|
|
/// <returns><b>True</b> if the condition is satisfied.</returns>
|
|
public static bool IsVideoConditionSatisfied(
|
|
ProfileCondition condition,
|
|
int? width,
|
|
int? height,
|
|
int? videoBitDepth,
|
|
int? videoBitrate,
|
|
string? videoProfile,
|
|
VideoRangeType? videoRangeType,
|
|
double? videoLevel,
|
|
float? videoFramerate,
|
|
int? packetLength,
|
|
TransportStreamTimestamp? timestamp,
|
|
bool? isAnamorphic,
|
|
bool? isInterlaced,
|
|
int? refFrames,
|
|
int numStreams,
|
|
int? numVideoStreams,
|
|
int? numAudioStreams,
|
|
string? videoCodecTag,
|
|
bool? isAvc)
|
|
{
|
|
switch (condition.Property)
|
|
{
|
|
case ProfileConditionValue.IsInterlaced:
|
|
return IsConditionSatisfied(condition, isInterlaced);
|
|
case ProfileConditionValue.IsAnamorphic:
|
|
return IsConditionSatisfied(condition, isAnamorphic);
|
|
case ProfileConditionValue.IsAvc:
|
|
return IsConditionSatisfied(condition, isAvc);
|
|
case ProfileConditionValue.VideoFramerate:
|
|
return IsConditionSatisfied(condition, videoFramerate);
|
|
case ProfileConditionValue.VideoLevel:
|
|
return IsConditionSatisfied(condition, videoLevel);
|
|
case ProfileConditionValue.VideoProfile:
|
|
return IsConditionSatisfied(condition, videoProfile);
|
|
case ProfileConditionValue.VideoRangeType:
|
|
return IsConditionSatisfied(condition, videoRangeType);
|
|
case ProfileConditionValue.VideoCodecTag:
|
|
return IsConditionSatisfied(condition, videoCodecTag);
|
|
case ProfileConditionValue.PacketLength:
|
|
return IsConditionSatisfied(condition, packetLength);
|
|
case ProfileConditionValue.VideoBitDepth:
|
|
return IsConditionSatisfied(condition, videoBitDepth);
|
|
case ProfileConditionValue.VideoBitrate:
|
|
return IsConditionSatisfied(condition, videoBitrate);
|
|
case ProfileConditionValue.Height:
|
|
return IsConditionSatisfied(condition, height);
|
|
case ProfileConditionValue.Width:
|
|
return IsConditionSatisfied(condition, width);
|
|
case ProfileConditionValue.RefFrames:
|
|
return IsConditionSatisfied(condition, refFrames);
|
|
case ProfileConditionValue.NumStreams:
|
|
return IsConditionSatisfied(condition, numStreams);
|
|
case ProfileConditionValue.NumAudioStreams:
|
|
return IsConditionSatisfied(condition, numAudioStreams);
|
|
case ProfileConditionValue.NumVideoStreams:
|
|
return IsConditionSatisfied(condition, numVideoStreams);
|
|
case ProfileConditionValue.VideoTimestamp:
|
|
return IsConditionSatisfied(condition, timestamp);
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a image condition is satisfied.
|
|
/// </summary>
|
|
/// <param name="condition">The <see cref="ProfileCondition"/>.</param>
|
|
/// <param name="width">The width.</param>
|
|
/// <param name="height">The height.</param>
|
|
/// <returns><b>True</b> if the condition is satisfied.</returns>
|
|
public static bool IsImageConditionSatisfied(ProfileCondition condition, int? width, int? height)
|
|
{
|
|
switch (condition.Property)
|
|
{
|
|
case ProfileConditionValue.Height:
|
|
return IsConditionSatisfied(condition, height);
|
|
case ProfileConditionValue.Width:
|
|
return IsConditionSatisfied(condition, width);
|
|
default:
|
|
throw new ArgumentException("Unexpected condition on image file: " + condition.Property);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if an audio condition is satisfied.
|
|
/// </summary>
|
|
/// <param name="condition">The <see cref="ProfileCondition"/>.</param>
|
|
/// <param name="audioChannels">The channel count.</param>
|
|
/// <param name="audioBitrate">The bitrate.</param>
|
|
/// <param name="audioSampleRate">The sample rate.</param>
|
|
/// <param name="audioBitDepth">The bit depth.</param>
|
|
/// <returns><b>True</b> if the condition is satisfied.</returns>
|
|
public static bool IsAudioConditionSatisfied(ProfileCondition condition, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth)
|
|
{
|
|
switch (condition.Property)
|
|
{
|
|
case ProfileConditionValue.AudioBitrate:
|
|
return IsConditionSatisfied(condition, audioBitrate);
|
|
case ProfileConditionValue.AudioChannels:
|
|
return IsConditionSatisfied(condition, audioChannels);
|
|
case ProfileConditionValue.AudioSampleRate:
|
|
return IsConditionSatisfied(condition, audioSampleRate);
|
|
case ProfileConditionValue.AudioBitDepth:
|
|
return IsConditionSatisfied(condition, audioBitDepth);
|
|
default:
|
|
throw new ArgumentException("Unexpected condition on audio file: " + condition.Property);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if an audio condition is satisfied for a video.
|
|
/// </summary>
|
|
/// <param name="condition">The <see cref="ProfileCondition"/>.</param>
|
|
/// <param name="audioChannels">The channel count.</param>
|
|
/// <param name="audioBitrate">The bitrate.</param>
|
|
/// <param name="audioSampleRate">The sample rate.</param>
|
|
/// <param name="audioBitDepth">The bit depth.</param>
|
|
/// <param name="audioProfile">The profile.</param>
|
|
/// <param name="isSecondaryTrack">A value indicating whether the audio is a secondary track.</param>
|
|
/// <returns><b>True</b> if the condition is satisfied.</returns>
|
|
public static bool IsVideoAudioConditionSatisfied(
|
|
ProfileCondition condition,
|
|
int? audioChannels,
|
|
int? audioBitrate,
|
|
int? audioSampleRate,
|
|
int? audioBitDepth,
|
|
string? audioProfile,
|
|
bool? isSecondaryTrack)
|
|
{
|
|
switch (condition.Property)
|
|
{
|
|
case ProfileConditionValue.AudioProfile:
|
|
return IsConditionSatisfied(condition, audioProfile);
|
|
case ProfileConditionValue.AudioBitrate:
|
|
return IsConditionSatisfied(condition, audioBitrate);
|
|
case ProfileConditionValue.AudioChannels:
|
|
return IsConditionSatisfied(condition, audioChannels);
|
|
case ProfileConditionValue.IsSecondaryAudio:
|
|
return IsConditionSatisfied(condition, isSecondaryTrack);
|
|
case ProfileConditionValue.AudioSampleRate:
|
|
return IsConditionSatisfied(condition, audioSampleRate);
|
|
case ProfileConditionValue.AudioBitDepth:
|
|
return IsConditionSatisfied(condition, audioBitDepth);
|
|
default:
|
|
throw new ArgumentException("Unexpected condition on audio file: " + condition.Property);
|
|
}
|
|
}
|
|
|
|
private static bool IsConditionSatisfied(ProfileCondition condition, int? currentValue)
|
|
{
|
|
if (!currentValue.HasValue)
|
|
{
|
|
// If the value is unknown, it satisfies if not marked as required
|
|
return !condition.IsRequired;
|
|
}
|
|
|
|
var conditionType = condition.Condition;
|
|
if (condition.Condition == ProfileConditionType.EqualsAny)
|
|
{
|
|
foreach (var singleConditionString in condition.Value.AsSpan().Split('|'))
|
|
{
|
|
if (int.TryParse(singleConditionString, NumberStyles.Integer, CultureInfo.InvariantCulture, out int conditionValue)
|
|
&& conditionValue.Equals(currentValue))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (int.TryParse(condition.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var expected))
|
|
{
|
|
switch (conditionType)
|
|
{
|
|
case ProfileConditionType.Equals:
|
|
return currentValue.Value.Equals(expected);
|
|
case ProfileConditionType.GreaterThanEqual:
|
|
return currentValue.Value >= expected;
|
|
case ProfileConditionType.LessThanEqual:
|
|
return currentValue.Value <= expected;
|
|
case ProfileConditionType.NotEquals:
|
|
return !currentValue.Value.Equals(expected);
|
|
default:
|
|
throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool IsConditionSatisfied(ProfileCondition condition, string? currentValue)
|
|
{
|
|
if (string.IsNullOrEmpty(currentValue))
|
|
{
|
|
// If the value is unknown, it satisfies if not marked as required
|
|
return !condition.IsRequired;
|
|
}
|
|
|
|
string expected = condition.Value;
|
|
|
|
switch (condition.Condition)
|
|
{
|
|
case ProfileConditionType.EqualsAny:
|
|
return expected.Split('|').Contains(currentValue, StringComparison.OrdinalIgnoreCase);
|
|
case ProfileConditionType.Equals:
|
|
return string.Equals(currentValue, expected, StringComparison.OrdinalIgnoreCase);
|
|
case ProfileConditionType.NotEquals:
|
|
return !string.Equals(currentValue, expected, StringComparison.OrdinalIgnoreCase);
|
|
default:
|
|
throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition);
|
|
}
|
|
}
|
|
|
|
private static bool IsConditionSatisfied(ProfileCondition condition, bool? currentValue)
|
|
{
|
|
if (!currentValue.HasValue)
|
|
{
|
|
// If the value is unknown, it satisfies if not marked as required
|
|
return !condition.IsRequired;
|
|
}
|
|
|
|
if (bool.TryParse(condition.Value, out var expected))
|
|
{
|
|
switch (condition.Condition)
|
|
{
|
|
case ProfileConditionType.Equals:
|
|
return currentValue.Value == expected;
|
|
case ProfileConditionType.NotEquals:
|
|
return currentValue.Value != expected;
|
|
default:
|
|
throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool IsConditionSatisfied(ProfileCondition condition, double? currentValue)
|
|
{
|
|
if (!currentValue.HasValue)
|
|
{
|
|
// If the value is unknown, it satisfies if not marked as required
|
|
return !condition.IsRequired;
|
|
}
|
|
|
|
var conditionType = condition.Condition;
|
|
if (condition.Condition == ProfileConditionType.EqualsAny)
|
|
{
|
|
foreach (var singleConditionString in condition.Value.AsSpan().Split('|'))
|
|
{
|
|
if (double.TryParse(singleConditionString, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out double conditionValue)
|
|
&& conditionValue.Equals(currentValue))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (double.TryParse(condition.Value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var expected))
|
|
{
|
|
switch (conditionType)
|
|
{
|
|
case ProfileConditionType.Equals:
|
|
return currentValue.Value.Equals(expected);
|
|
case ProfileConditionType.GreaterThanEqual:
|
|
return currentValue.Value >= expected;
|
|
case ProfileConditionType.LessThanEqual:
|
|
return currentValue.Value <= expected;
|
|
case ProfileConditionType.NotEquals:
|
|
return !currentValue.Value.Equals(expected);
|
|
default:
|
|
throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool IsConditionSatisfied(ProfileCondition condition, TransportStreamTimestamp? timestamp)
|
|
{
|
|
if (!timestamp.HasValue)
|
|
{
|
|
// If the value is unknown, it satisfies if not marked as required
|
|
return !condition.IsRequired;
|
|
}
|
|
|
|
var expected = (TransportStreamTimestamp)Enum.Parse(typeof(TransportStreamTimestamp), condition.Value, true);
|
|
|
|
switch (condition.Condition)
|
|
{
|
|
case ProfileConditionType.Equals:
|
|
return timestamp == expected;
|
|
case ProfileConditionType.NotEquals:
|
|
return timestamp != expected;
|
|
default:
|
|
throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition);
|
|
}
|
|
}
|
|
|
|
private static bool IsConditionSatisfied(ProfileCondition condition, VideoRangeType? currentValue)
|
|
{
|
|
if (!currentValue.HasValue || currentValue.Equals(VideoRangeType.Unknown))
|
|
{
|
|
// If the value is unknown, it satisfies if not marked as required
|
|
return !condition.IsRequired;
|
|
}
|
|
|
|
// Special case: HDR10 also satisfies if the video is HDR10Plus
|
|
if (currentValue.Value == VideoRangeType.HDR10Plus)
|
|
{
|
|
if (IsConditionSatisfied(condition, VideoRangeType.HDR10))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
var conditionType = condition.Condition;
|
|
if (conditionType == ProfileConditionType.EqualsAny)
|
|
{
|
|
foreach (var singleConditionString in condition.Value.AsSpan().Split('|'))
|
|
{
|
|
if (Enum.TryParse(singleConditionString, true, out VideoRangeType conditionValue)
|
|
&& conditionValue.Equals(currentValue))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (Enum.TryParse(condition.Value, true, out VideoRangeType expected))
|
|
{
|
|
return conditionType switch
|
|
{
|
|
ProfileConditionType.Equals => currentValue.Value == expected,
|
|
ProfileConditionType.NotEquals => currentValue.Value != expected,
|
|
_ => throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition)
|
|
};
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|