Improve dynamic HDR metadata handling (#13277)

* 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
This commit is contained in:
gnattu 2025-04-03 08:06:02 +08:00 committed by GitHub
parent 9c7cf808aa
commit 49ac705867
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 2328 additions and 67 deletions

View file

@ -1675,7 +1675,7 @@ public class DynamicHlsController : BaseJellyfinApiController
} }
var audioCodec = _encodingHelper.GetAudioEncoder(state); var audioCodec = _encodingHelper.GetAudioEncoder(state);
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); var bitStreamArgs = _encodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
// opus, dts, truehd and flac (in FFmpeg 5 and older) are experimental in mp4 muxer // opus, dts, truehd and flac (in FFmpeg 5 and older) are experimental in mp4 muxer
var strictArgs = string.Empty; var strictArgs = string.Empty;
@ -1822,10 +1822,11 @@ public class DynamicHlsController : BaseJellyfinApiController
// Clients reporting Dolby Vision capabilities with fallbacks may only support the fallback layer. // Clients reporting Dolby Vision capabilities with fallbacks may only support the fallback layer.
// Only enable Dolby Vision remuxing if the client explicitly declares support for profiles without fallbacks. // Only enable Dolby Vision remuxing if the client explicitly declares support for profiles without fallbacks.
var clientSupportsDoVi = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase); var clientSupportsDoVi = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
var videoIsDoVi = state.VideoStream.VideoRangeType is VideoRangeType.DOVI or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG or VideoRangeType.DOVIWithSDR; var videoIsDoVi = EncodingHelper.IsDovi(state.VideoStream);
if (EncodingHelper.IsCopyCodec(codec) if (EncodingHelper.IsCopyCodec(codec)
&& (videoIsDoVi && clientSupportsDoVi)) && (videoIsDoVi && clientSupportsDoVi)
&& !_encodingHelper.IsDoviRemoved(state))
{ {
if (isActualOutputVideoCodecHevc) if (isActualOutputVideoCodecHevc)
{ {
@ -1855,7 +1856,7 @@ public class DynamicHlsController : BaseJellyfinApiController
// If h264_mp4toannexb is ever added, do not use it for live tv. // If h264_mp4toannexb is ever added, do not use it for live tv.
if (state.VideoStream is not null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) if (state.VideoStream is not null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
{ {
string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream); string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state, MediaStreamType.Video);
if (!string.IsNullOrEmpty(bitStreamArgs)) if (!string.IsNullOrEmpty(bitStreamArgs))
{ {
args += " " + bitStreamArgs; args += " " + bitStreamArgs;

View file

@ -345,13 +345,15 @@ public class DynamicHlsHelper
if (videoRange == VideoRange.HDR) if (videoRange == VideoRange.HDR)
{ {
if (videoRangeType == VideoRangeType.HLG) switch (videoRangeType)
{ {
builder.Append(",VIDEO-RANGE=HLG"); case VideoRangeType.HLG:
} case VideoRangeType.DOVIWithHLG:
else builder.Append(",VIDEO-RANGE=HLG");
{ break;
builder.Append(",VIDEO-RANGE=PQ"); default:
builder.Append(",VIDEO-RANGE=PQ");
break;
} }
} }
} }
@ -418,36 +420,67 @@ public class DynamicHlsHelper
/// <param name="state">StreamState of the current stream.</param> /// <param name="state">StreamState of the current stream.</param>
private void AppendPlaylistSupplementalCodecsField(StringBuilder builder, StreamState state) private void AppendPlaylistSupplementalCodecsField(StringBuilder builder, StreamState state)
{ {
// Dolby Vision currently cannot exist when transcoding // HDR dynamic metadata currently cannot exist when transcoding
if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
{ {
return; return;
} }
var dvProfile = state.VideoStream.DvProfile; if (EncodingHelper.IsDovi(state.VideoStream) && !_encodingHelper.IsDoviRemoved(state))
var dvLevel = state.VideoStream.DvLevel;
var dvRangeString = state.VideoStream.VideoRangeType switch
{ {
VideoRangeType.DOVIWithHDR10 => "db1p", AppendDvString();
VideoRangeType.DOVIWithHLG => "db4h", }
_ => string.Empty else if (EncodingHelper.IsHdr10Plus(state.VideoStream) && !_encodingHelper.IsHdr10PlusRemoved(state))
};
if (dvProfile is null || dvLevel is null || string.IsNullOrEmpty(dvRangeString))
{ {
return; AppendHdr10PlusString();
} }
var dvFourCc = string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase) ? "dav1" : "dvh1"; return;
builder.Append(",SUPPLEMENTAL-CODECS=\"")
.Append(dvFourCc) void AppendDvString()
.Append('.') {
.Append(dvProfile.Value.ToString("D2", CultureInfo.InvariantCulture)) var dvProfile = state.VideoStream.DvProfile;
.Append('.') var dvLevel = state.VideoStream.DvLevel;
.Append(dvLevel.Value.ToString("D2", CultureInfo.InvariantCulture)) var dvRangeString = state.VideoStream.VideoRangeType switch
.Append('/') {
.Append(dvRangeString) VideoRangeType.DOVIWithHDR10 => "db1p",
.Append('"'); VideoRangeType.DOVIWithHLG => "db4h",
VideoRangeType.DOVIWithHDR10Plus => "db1p", // The HDR10+ metadata would be removed if Dovi metadata is not removed
_ => string.Empty // Don't label Dovi with EL and SDR due to compatability issues, ignore invalid configurations
};
if (dvProfile is null || dvLevel is null || string.IsNullOrEmpty(dvRangeString))
{
return;
}
var dvFourCc = string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase) ? "dav1" : "dvh1";
builder.Append(",SUPPLEMENTAL-CODECS=\"")
.Append(dvFourCc)
.Append('.')
.Append(dvProfile.Value.ToString("D2", CultureInfo.InvariantCulture))
.Append('.')
.Append(dvLevel.Value.ToString("D2", CultureInfo.InvariantCulture))
.Append('/')
.Append(dvRangeString)
.Append('"');
}
void AppendHdr10PlusString()
{
var videoCodecLevel = GetOutputVideoCodecLevel(state);
if (string.IsNullOrEmpty(state.ActualOutputVideoCodec) || videoCodecLevel is null)
{
return;
}
var videoCodecString = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
builder.Append(",SUPPLEMENTAL-CODECS=\"")
.Append(videoCodecString)
.Append('/')
.Append("cdm4")
.Append('"');
}
} }
/// <summary> /// <summary>

View file

@ -45,6 +45,27 @@ public enum VideoRangeType
/// </summary> /// </summary>
DOVIWithSDR, DOVIWithSDR,
/// <summary>
/// Dolby Vision with Enhancment Layer (Profile 7).
/// </summary>
DOVIWithEL,
/// <summary>
/// Dolby Vision and HDR10+ Metadata coexists.
/// </summary>
DOVIWithHDR10Plus,
/// <summary>
/// Dolby Vision with Enhancment Layer (Profile 7) and HDR10+ Metadata coexists.
/// </summary>
DOVIWithELHDR10Plus,
/// <summary>
/// Dolby Vision with invalid configuration. e.g. Profile 8 compat id 6.
/// When using this range, the server would assume the video is still HDR10 after removing the Dolby Vision metadata.
/// </summary>
DOVIInvalid,
/// <summary> /// <summary>
/// HDR10+ video range type (10bit to 16bit). /// HDR10+ video range type (10bit to 16bit).
/// </summary> /// </summary>

View file

@ -140,6 +140,7 @@ public class MediaStreamRepository : IMediaStreamRepository
dto.DvBlSignalCompatibilityId = entity.DvBlSignalCompatibilityId; dto.DvBlSignalCompatibilityId = entity.DvBlSignalCompatibilityId;
dto.IsHearingImpaired = entity.IsHearingImpaired.GetValueOrDefault(); dto.IsHearingImpaired = entity.IsHearingImpaired.GetValueOrDefault();
dto.Rotation = entity.Rotation; dto.Rotation = entity.Rotation;
dto.Hdr10PlusPresentFlag = entity.Hdr10PlusPresentFlag;
if (dto.Type is MediaStreamType.Audio or MediaStreamType.Subtitle) if (dto.Type is MediaStreamType.Audio or MediaStreamType.Subtitle)
{ {
@ -207,7 +208,8 @@ public class MediaStreamRepository : IMediaStreamRepository
BlPresentFlag = dto.BlPresentFlag, BlPresentFlag = dto.BlPresentFlag,
DvBlSignalCompatibilityId = dto.DvBlSignalCompatibilityId, DvBlSignalCompatibilityId = dto.DvBlSignalCompatibilityId,
IsHearingImpaired = dto.IsHearingImpaired, IsHearingImpaired = dto.IsHearingImpaired,
Rotation = dto.Rotation Rotation = dto.Rotation,
Hdr10PlusPresentFlag = dto.Hdr10PlusPresentFlag,
}; };
return entity; return entity;
} }

View file

@ -0,0 +1,32 @@
namespace MediaBrowser.Controller.MediaEncoding;
/// <summary>
/// Enum BitStreamFilterOptionType.
/// </summary>
public enum BitStreamFilterOptionType
{
/// <summary>
/// hevc_metadata bsf with remove_dovi option.
/// </summary>
HevcMetadataRemoveDovi = 0,
/// <summary>
/// hevc_metadata bsf with remove_hdr10plus option.
/// </summary>
HevcMetadataRemoveHdr10Plus = 1,
/// <summary>
/// av1_metadata bsf with remove_dovi option.
/// </summary>
Av1MetadataRemoveDovi = 2,
/// <summary>
/// av1_metadata bsf with remove_hdr10plus option.
/// </summary>
Av1MetadataRemoveHdr10Plus = 3,
/// <summary>
/// dovi_rpu bsf with strip option.
/// </summary>
DoviRpuStrip = 4,
}

View file

@ -162,6 +162,13 @@ namespace MediaBrowser.Controller.MediaEncoding
_configurationManager = configurationManager; _configurationManager = configurationManager;
} }
private enum DynamicHdrMetadataRemovalPlan
{
None,
RemoveDovi,
RemoveHdr10Plus,
}
[GeneratedRegex(@"\s+")] [GeneratedRegex(@"\s+")]
private static partial Regex WhiteSpaceRegex(); private static partial Regex WhiteSpaceRegex();
@ -342,11 +349,8 @@ namespace MediaBrowser.Controller.MediaEncoding
return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder || isVideoToolBoxDecoder; return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder || isVideoToolBoxDecoder;
} }
return state.VideoStream.VideoRange == VideoRange.HDR // GPU tonemapping supports all HDR RangeTypes
&& (state.VideoStream.VideoRangeType == VideoRangeType.HDR10 return state.VideoStream.VideoRange == VideoRange.HDR;
|| state.VideoStream.VideoRangeType == VideoRangeType.HLG
|| state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10
|| state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG);
} }
private bool IsVulkanHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options) private bool IsVulkanHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
@ -381,8 +385,7 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
return state.VideoStream.VideoRange == VideoRange.HDR return state.VideoStream.VideoRange == VideoRange.HDR
&& (state.VideoStream.VideoRangeType == VideoRangeType.HDR10 && IsDoviWithHdr10Bl(state.VideoStream);
|| state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10);
} }
private bool IsVideoToolboxTonemapAvailable(EncodingJobInfo state, EncodingOptions options) private bool IsVideoToolboxTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
@ -397,7 +400,8 @@ namespace MediaBrowser.Controller.MediaEncoding
// Certain DV profile 5 video works in Safari with direct playing, but the VideoToolBox does not produce correct mapping results with transcoding. // Certain DV profile 5 video works in Safari with direct playing, but the VideoToolBox does not produce correct mapping results with transcoding.
// All other HDR formats working. // All other HDR formats working.
return state.VideoStream.VideoRange == VideoRange.HDR return state.VideoStream.VideoRange == VideoRange.HDR
&& state.VideoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG or VideoRangeType.HDR10Plus or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG; && (IsDoviWithHdr10Bl(state.VideoStream)
|| state.VideoStream.VideoRangeType is VideoRangeType.HLG);
} }
private bool IsVideoStreamHevcRext(EncodingJobInfo state) private bool IsVideoStreamHevcRext(EncodingJobInfo state)
@ -1301,6 +1305,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|| codec.Contains("hevc", StringComparison.OrdinalIgnoreCase); || codec.Contains("hevc", StringComparison.OrdinalIgnoreCase);
} }
public static bool IsAv1(MediaStream stream)
{
var codec = stream.Codec ?? string.Empty;
return codec.Contains("av1", StringComparison.OrdinalIgnoreCase);
}
public static bool IsAAC(MediaStream stream) public static bool IsAAC(MediaStream stream)
{ {
var codec = stream.Codec ?? string.Empty; var codec = stream.Codec ?? string.Empty;
@ -1308,8 +1319,125 @@ namespace MediaBrowser.Controller.MediaEncoding
return codec.Contains("aac", StringComparison.OrdinalIgnoreCase); return codec.Contains("aac", StringComparison.OrdinalIgnoreCase);
} }
public static string GetBitStreamArgs(MediaStream stream) public static bool IsDoviWithHdr10Bl(MediaStream stream)
{ {
var rangeType = stream?.VideoRangeType;
return rangeType is VideoRangeType.DOVIWithHDR10
or VideoRangeType.DOVIWithEL
or VideoRangeType.DOVIWithHDR10Plus
or VideoRangeType.DOVIWithELHDR10Plus
or VideoRangeType.DOVIInvalid;
}
public static bool IsDovi(MediaStream stream)
{
var rangeType = stream?.VideoRangeType;
return IsDoviWithHdr10Bl(stream)
|| (rangeType is VideoRangeType.DOVI
or VideoRangeType.DOVIWithHLG
or VideoRangeType.DOVIWithSDR);
}
public static bool IsHdr10Plus(MediaStream stream)
{
var rangeType = stream?.VideoRangeType;
return rangeType is VideoRangeType.HDR10Plus
or VideoRangeType.DOVIWithHDR10Plus
or VideoRangeType.DOVIWithELHDR10Plus;
}
/// <summary>
/// Check if dynamic HDR metadata should be removed during stream copy.
/// Please note this check assumes the range check has already been done
/// and trivial fallbacks like HDR10+ to HDR10, DOVIWithHDR10 to HDR10 is already checked.
/// </summary>
private static DynamicHdrMetadataRemovalPlan ShouldRemoveDynamicHdrMetadata(EncodingJobInfo state)
{
var videoStream = state.VideoStream;
if (videoStream.VideoRange is not VideoRange.HDR)
{
return DynamicHdrMetadataRemovalPlan.None;
}
var requestedRangeTypes = state.GetRequestedRangeTypes(state.VideoStream.Codec);
if (requestedRangeTypes.Length == 0)
{
return DynamicHdrMetadataRemovalPlan.None;
}
var requestHasHDR10 = requestedRangeTypes.Contains(VideoRangeType.HDR10.ToString(), StringComparison.OrdinalIgnoreCase);
var requestHasDOVI = requestedRangeTypes.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
var requestHasDOVIwithEL = requestedRangeTypes.Contains(VideoRangeType.DOVIWithEL.ToString(), StringComparison.OrdinalIgnoreCase);
var requestHasDOVIwithELHDR10plus = requestedRangeTypes.Contains(VideoRangeType.DOVIWithELHDR10Plus.ToString(), StringComparison.OrdinalIgnoreCase);
var shouldRemoveHdr10Plus = false;
// Case 1: Client supports HDR10, does not support DOVI with EL but EL presets
var shouldRemoveDovi = (!requestHasDOVIwithEL && requestHasHDR10) && videoStream.VideoRangeType == VideoRangeType.DOVIWithEL;
// Case 2: Client supports DOVI, does not support broken DOVI config
// Client does not report DOVI support should be allowed to copy bad data for remuxing as HDR10 players would not crash
shouldRemoveDovi = shouldRemoveDovi || (requestHasDOVI && videoStream.VideoRangeType == VideoRangeType.DOVIInvalid);
// Special case: we have a video with both EL and HDR10+
// If the client supports EL but not in the case of coexistence with HDR10+, remove HDR10+ for compatibility reasons.
// Otherwise, remove DOVI if the client is not a DOVI player
if (videoStream.VideoRangeType == VideoRangeType.DOVIWithELHDR10Plus)
{
shouldRemoveHdr10Plus = requestHasDOVIwithEL && !requestHasDOVIwithELHDR10plus;
shouldRemoveDovi = shouldRemoveDovi || !shouldRemoveHdr10Plus;
}
if (shouldRemoveDovi)
{
return DynamicHdrMetadataRemovalPlan.RemoveDovi;
}
// If the client is a Dolby Vision Player, remove the HDR10+ metadata to avoid playback issues
shouldRemoveHdr10Plus = shouldRemoveHdr10Plus || (requestHasDOVI && videoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10Plus);
return shouldRemoveHdr10Plus ? DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus : DynamicHdrMetadataRemovalPlan.None;
}
private bool CanEncoderRemoveDynamicHdrMetadata(DynamicHdrMetadataRemovalPlan plan, MediaStream videoStream)
{
return plan switch
{
DynamicHdrMetadataRemovalPlan.RemoveDovi => _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.DoviRpuStrip)
|| (IsH265(videoStream) && _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.HevcMetadataRemoveDovi))
|| (IsAv1(videoStream) && _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.Av1MetadataRemoveDovi)),
DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus => (IsH265(videoStream) && _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.HevcMetadataRemoveHdr10Plus))
|| (IsAv1(videoStream) && _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.Av1MetadataRemoveHdr10Plus)),
_ => true,
};
}
public bool IsDoviRemoved(EncodingJobInfo state)
{
return state?.VideoStream is not null && ShouldRemoveDynamicHdrMetadata(state) == DynamicHdrMetadataRemovalPlan.RemoveDovi
&& CanEncoderRemoveDynamicHdrMetadata(DynamicHdrMetadataRemovalPlan.RemoveDovi, state.VideoStream);
}
public bool IsHdr10PlusRemoved(EncodingJobInfo state)
{
return state?.VideoStream is not null && ShouldRemoveDynamicHdrMetadata(state) == DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus
&& CanEncoderRemoveDynamicHdrMetadata(DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus, state.VideoStream);
}
public string GetBitStreamArgs(EncodingJobInfo state, MediaStreamType streamType)
{
if (state is null)
{
return null;
}
var stream = streamType switch
{
MediaStreamType.Audio => state.AudioStream,
MediaStreamType.Video => state.VideoStream,
_ => state.VideoStream
};
// TODO This is auto inserted into the mpegts mux so it might not be needed. // TODO This is auto inserted into the mpegts mux so it might not be needed.
// https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb // https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb
if (IsH264(stream)) if (IsH264(stream))
@ -1317,21 +1445,57 @@ namespace MediaBrowser.Controller.MediaEncoding
return "-bsf:v h264_mp4toannexb"; return "-bsf:v h264_mp4toannexb";
} }
if (IsH265(stream))
{
return "-bsf:v hevc_mp4toannexb";
}
if (IsAAC(stream)) if (IsAAC(stream))
{ {
// Convert adts header(mpegts) to asc header(mp4). // Convert adts header(mpegts) to asc header(mp4).
return "-bsf:a aac_adtstoasc"; return "-bsf:a aac_adtstoasc";
} }
if (IsH265(stream))
{
var filter = "-bsf:v hevc_mp4toannexb";
// The following checks are not complete because the copy would be rejected
// if the encoder cannot remove required metadata.
// And if bsf is used, we must already be using copy codec.
switch (ShouldRemoveDynamicHdrMetadata(state))
{
default:
case DynamicHdrMetadataRemovalPlan.None:
break;
case DynamicHdrMetadataRemovalPlan.RemoveDovi:
filter += _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.HevcMetadataRemoveDovi)
? ",hevc_metadata=remove_dovi=1"
: ",dovi_rpu=strip=1";
break;
case DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus:
filter += ",hevc_metadata=remove_hdr10plus=1";
break;
}
return filter;
}
if (IsAv1(stream))
{
switch (ShouldRemoveDynamicHdrMetadata(state))
{
default:
case DynamicHdrMetadataRemovalPlan.None:
return null;
case DynamicHdrMetadataRemovalPlan.RemoveDovi:
return _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.Av1MetadataRemoveDovi)
? "-bsf:v av1_metadata=remove_dovi=1"
: "-bsf:v dovi_rpu=strip=1";
case DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus:
return "-bsf:v av1_metadata=remove_hdr10plus=1";
}
}
return null; return null;
} }
public static string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer) public string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer)
{ {
var bitStreamArgs = string.Empty; var bitStreamArgs = string.Empty;
var segmentFormat = GetSegmentFileExtension(segmentContainer).TrimStart('.'); var segmentFormat = GetSegmentFileExtension(segmentContainer).TrimStart('.');
@ -1342,7 +1506,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals(mediaSourceContainer, "aac", StringComparison.OrdinalIgnoreCase) || string.Equals(mediaSourceContainer, "aac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase))) || string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase)))
{ {
bitStreamArgs = GetBitStreamArgs(state.AudioStream); bitStreamArgs = GetBitStreamArgs(state, MediaStreamType.Audio);
bitStreamArgs = string.IsNullOrEmpty(bitStreamArgs) ? string.Empty : " " + bitStreamArgs; bitStreamArgs = string.IsNullOrEmpty(bitStreamArgs) ? string.Empty : " " + bitStreamArgs;
} }
@ -2169,7 +2333,6 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
// DOVIWithHDR10 should be compatible with HDR10 supporting players. Same goes with HLG and of course SDR. So allow copy of those formats // DOVIWithHDR10 should be compatible with HDR10 supporting players. Same goes with HLG and of course SDR. So allow copy of those formats
var requestHasHDR10 = requestedRangeTypes.Contains(VideoRangeType.HDR10.ToString(), StringComparison.OrdinalIgnoreCase); var requestHasHDR10 = requestedRangeTypes.Contains(VideoRangeType.HDR10.ToString(), StringComparison.OrdinalIgnoreCase);
var requestHasHLG = requestedRangeTypes.Contains(VideoRangeType.HLG.ToString(), StringComparison.OrdinalIgnoreCase); var requestHasHLG = requestedRangeTypes.Contains(VideoRangeType.HLG.ToString(), StringComparison.OrdinalIgnoreCase);
var requestHasSDR = requestedRangeTypes.Contains(VideoRangeType.SDR.ToString(), StringComparison.OrdinalIgnoreCase); var requestHasSDR = requestedRangeTypes.Contains(VideoRangeType.SDR.ToString(), StringComparison.OrdinalIgnoreCase);
@ -2177,9 +2340,17 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!requestedRangeTypes.Contains(videoStream.VideoRangeType.ToString(), StringComparison.OrdinalIgnoreCase) if (!requestedRangeTypes.Contains(videoStream.VideoRangeType.ToString(), StringComparison.OrdinalIgnoreCase)
&& !((requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10) && !((requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10)
|| (requestHasHLG && videoStream.VideoRangeType == VideoRangeType.DOVIWithHLG) || (requestHasHLG && videoStream.VideoRangeType == VideoRangeType.DOVIWithHLG)
|| (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR))) || (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR)
|| (requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.HDR10Plus)))
{ {
return false; // Check complicated cases where we need to remove dynamic metadata
// Conservatively refuse to copy if the encoder can't remove dynamic metadata,
// but a removal is required for compatability reasons.
var dynamicHdrMetadataRemovalPlan = ShouldRemoveDynamicHdrMetadata(state);
if (!CanEncoderRemoveDynamicHdrMetadata(dynamicHdrMetadataRemovalPlan, videoStream))
{
return false;
}
} }
} }
@ -7244,7 +7415,7 @@ namespace MediaBrowser.Controller.MediaEncoding
&& string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
{ {
string bitStreamArgs = GetBitStreamArgs(state.VideoStream); string bitStreamArgs = GetBitStreamArgs(state, MediaStreamType.Video);
if (!string.IsNullOrEmpty(bitStreamArgs)) if (!string.IsNullOrEmpty(bitStreamArgs))
{ {
args += " " + bitStreamArgs; args += " " + bitStreamArgs;

View file

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;

View file

@ -116,6 +116,13 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <returns><c>true</c> if the filter is supported, <c>false</c> otherwise.</returns> /// <returns><c>true</c> if the filter is supported, <c>false</c> otherwise.</returns>
bool SupportsFilterWithOption(FilterOptionType option); bool SupportsFilterWithOption(FilterOptionType option);
/// <summary>
/// Whether the bitstream filter is supported with the given option.
/// </summary>
/// <param name="option">The option.</param>
/// <returns><c>true</c> if the bitstream filter is supported, <c>false</c> otherwise.</returns>
bool SupportsBitStreamFilterWithOption(BitStreamFilterOptionType option);
/// <summary> /// <summary>
/// Extracts the audio image. /// Extracts the audio image.
/// </summary> /// </summary>

View file

@ -7,6 +7,7 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using MediaBrowser.Controller.MediaEncoding;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace MediaBrowser.MediaEncoding.Encoder namespace MediaBrowser.MediaEncoding.Encoder
@ -160,6 +161,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
{ 6, new string[] { "transpose_opencl", "rotate by half-turn" } } { 6, new string[] { "transpose_opencl", "rotate by half-turn" } }
}; };
private static readonly Dictionary<BitStreamFilterOptionType, (string, string)> _bsfOptionsDict = new Dictionary<BitStreamFilterOptionType, (string, string)>
{
{ BitStreamFilterOptionType.HevcMetadataRemoveDovi, ("hevc_metadata", "remove_dovi") },
{ BitStreamFilterOptionType.HevcMetadataRemoveHdr10Plus, ("hevc_metadata", "remove_hdr10plus") },
{ BitStreamFilterOptionType.Av1MetadataRemoveDovi, ("av1_metadata", "remove_dovi") },
{ BitStreamFilterOptionType.Av1MetadataRemoveHdr10Plus, ("av1_metadata", "remove_hdr10plus") },
{ BitStreamFilterOptionType.DoviRpuStrip, ("dovi_rpu", "strip") }
};
// These are the library versions that corresponds to our minimum ffmpeg version 4.4 according to the version table below // These are the library versions that corresponds to our minimum ffmpeg version 4.4 according to the version table below
// Refers to the versions in https://ffmpeg.org/download.html // Refers to the versions in https://ffmpeg.org/download.html
private static readonly Dictionary<string, Version> _ffmpegMinimumLibraryVersions = new Dictionary<string, Version> private static readonly Dictionary<string, Version> _ffmpegMinimumLibraryVersions = new Dictionary<string, Version>
@ -286,6 +296,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
public IDictionary<int, bool> GetFiltersWithOption() => GetFFmpegFiltersWithOption(); public IDictionary<int, bool> GetFiltersWithOption() => GetFFmpegFiltersWithOption();
public IDictionary<BitStreamFilterOptionType, bool> GetBitStreamFiltersWithOption() => _bsfOptionsDict
.ToDictionary(item => item.Key, item => CheckBitStreamFilterWithOption(item.Value.Item1, item.Value.Item2));
public Version? GetFFmpegVersion() public Version? GetFFmpegVersion()
{ {
string output; string output;
@ -495,6 +508,34 @@ namespace MediaBrowser.MediaEncoding.Encoder
return false; return false;
} }
public bool CheckBitStreamFilterWithOption(string filter, string option)
{
if (string.IsNullOrEmpty(filter) || string.IsNullOrEmpty(option))
{
return false;
}
string output;
try
{
output = GetProcessOutput(_encoderPath, "-h bsf=" + filter, false, null);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error detecting the given bit stream filter");
return false;
}
if (output.Contains("Bit stream filter " + filter, StringComparison.Ordinal))
{
return output.Contains(option, StringComparison.Ordinal);
}
_logger.LogWarning("Bit stream filter: {Name} with option {Option} is not available", filter, option);
return false;
}
public bool CheckSupportedRuntimeKey(string keyDesc, Version? ffmpegVersion) public bool CheckSupportedRuntimeKey(string keyDesc, Version? ffmpegVersion)
{ {
if (string.IsNullOrEmpty(keyDesc)) if (string.IsNullOrEmpty(keyDesc))
@ -523,6 +564,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
return !string.IsNullOrEmpty(flag) && GetProcessExitCode(_encoderPath, $"-loglevel quiet -hwaccel_flags +{flag} -hide_banner -f lavfi -i nullsrc=s=1x1:d=100 -f null -"); return !string.IsNullOrEmpty(flag) && GetProcessExitCode(_encoderPath, $"-loglevel quiet -hwaccel_flags +{flag} -hide_banner -f lavfi -i nullsrc=s=1x1:d=100 -f null -");
} }
public bool CheckSupportedProberOption(string option, string proberPath)
{
return !string.IsNullOrEmpty(option) && GetProcessExitCode(proberPath, $"-loglevel quiet -f lavfi -i nullsrc=s=1x1:d=1 -{option}");
}
private IEnumerable<string> GetCodecs(Codec codec) private IEnumerable<string> GetCodecs(Codec codec)
{ {
string codecstr = codec == Codec.Encoder ? "encoders" : "decoders"; string codecstr = codec == Codec.Encoder ? "encoders" : "decoders";

View file

@ -73,9 +73,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
private List<string> _hwaccels = new List<string>(); private List<string> _hwaccels = new List<string>();
private List<string> _filters = new List<string>(); private List<string> _filters = new List<string>();
private IDictionary<int, bool> _filtersWithOption = new Dictionary<int, bool>(); private IDictionary<int, bool> _filtersWithOption = new Dictionary<int, bool>();
private IDictionary<BitStreamFilterOptionType, bool> _bitStreamFiltersWithOption = new Dictionary<BitStreamFilterOptionType, bool>();
private bool _isPkeyPauseSupported = false; private bool _isPkeyPauseSupported = false;
private bool _isLowPriorityHwDecodeSupported = false; private bool _isLowPriorityHwDecodeSupported = false;
private bool _proberSupportsFirstVideoFrame = false;
private bool _isVaapiDeviceAmd = false; private bool _isVaapiDeviceAmd = false;
private bool _isVaapiDeviceInteliHD = false; private bool _isVaapiDeviceInteliHD = false;
@ -222,6 +224,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
SetAvailableEncoders(validator.GetEncoders()); SetAvailableEncoders(validator.GetEncoders());
SetAvailableFilters(validator.GetFilters()); SetAvailableFilters(validator.GetFilters());
SetAvailableFiltersWithOption(validator.GetFiltersWithOption()); SetAvailableFiltersWithOption(validator.GetFiltersWithOption());
SetAvailableBitStreamFiltersWithOption(validator.GetBitStreamFiltersWithOption());
SetAvailableHwaccels(validator.GetHwaccels()); SetAvailableHwaccels(validator.GetHwaccels());
SetMediaEncoderVersion(validator); SetMediaEncoderVersion(validator);
@ -229,6 +232,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding", _ffmpegVersion); _isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding", _ffmpegVersion);
_isLowPriorityHwDecodeSupported = validator.CheckSupportedHwaccelFlag("low_priority"); _isLowPriorityHwDecodeSupported = validator.CheckSupportedHwaccelFlag("low_priority");
_proberSupportsFirstVideoFrame = validator.CheckSupportedProberOption("only_first_vframe", _ffprobePath);
// Check the Vaapi device vendor // Check the Vaapi device vendor
if (OperatingSystem.IsLinux() if (OperatingSystem.IsLinux()
@ -342,6 +346,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
_filtersWithOption = dict; _filtersWithOption = dict;
} }
public void SetAvailableBitStreamFiltersWithOption(IDictionary<BitStreamFilterOptionType, bool> dict)
{
_bitStreamFiltersWithOption = dict;
}
public void SetMediaEncoderVersion(EncoderValidator validator) public void SetMediaEncoderVersion(EncoderValidator validator)
{ {
_ffmpegVersion = validator.GetFFmpegVersion(); _ffmpegVersion = validator.GetFFmpegVersion();
@ -382,6 +391,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
return false; return false;
} }
public bool SupportsBitStreamFilterWithOption(BitStreamFilterOptionType option)
{
return _bitStreamFiltersWithOption.TryGetValue(option, out var val) && val;
}
public bool CanEncodeToAudioCodec(string codec) public bool CanEncodeToAudioCodec(string codec)
{ {
if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase)) if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase))
@ -501,6 +515,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
var args = extractChapters var args = extractChapters
? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format" ? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
: "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format"; : "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
if (_proberSupportsFirstVideoFrame)
{
args += " -show_frames -only_first_vframe";
}
args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, _threads).Trim(); args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, _threads).Trim();
var process = new Process var process = new Process

View file

@ -30,5 +30,12 @@ namespace MediaBrowser.MediaEncoding.Probing
/// <value>The chapters.</value> /// <value>The chapters.</value>
[JsonPropertyName("chapters")] [JsonPropertyName("chapters")]
public IReadOnlyList<MediaChapter> Chapters { get; set; } public IReadOnlyList<MediaChapter> Chapters { get; set; }
/// <summary>
/// Gets or sets the frames.
/// </summary>
/// <value>The streams.</value>
[JsonPropertyName("frames")]
public IReadOnlyList<MediaFrameInfo> Frames { get; set; }
} }
} }

View file

@ -0,0 +1,184 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace MediaBrowser.MediaEncoding.Probing;
/// <summary>
/// Class MediaFrameInfo.
/// </summary>
public class MediaFrameInfo
{
/// <summary>
/// Gets or sets the media type.
/// </summary>
[JsonPropertyName("media_type")]
public string? MediaType { get; set; }
/// <summary>
/// Gets or sets the StreamIndex.
/// </summary>
[JsonPropertyName("stream_index")]
public int? StreamIndex { get; set; }
/// <summary>
/// Gets or sets the KeyFrame.
/// </summary>
[JsonPropertyName("key_frame")]
public int? KeyFrame { get; set; }
/// <summary>
/// Gets or sets the Pts.
/// </summary>
[JsonPropertyName("pts")]
public long? Pts { get; set; }
/// <summary>
/// Gets or sets the PtsTime.
/// </summary>
[JsonPropertyName("pts_time")]
public string? PtsTime { get; set; }
/// <summary>
/// Gets or sets the BestEffortTimestamp.
/// </summary>
[JsonPropertyName("best_effort_timestamp")]
public long BestEffortTimestamp { get; set; }
/// <summary>
/// Gets or sets the BestEffortTimestampTime.
/// </summary>
[JsonPropertyName("best_effort_timestamp_time")]
public string? BestEffortTimestampTime { get; set; }
/// <summary>
/// Gets or sets the Duration.
/// </summary>
[JsonPropertyName("duration")]
public int Duration { get; set; }
/// <summary>
/// Gets or sets the DurationTime.
/// </summary>
[JsonPropertyName("duration_time")]
public string? DurationTime { get; set; }
/// <summary>
/// Gets or sets the PktPos.
/// </summary>
[JsonPropertyName("pkt_pos")]
public string? PktPos { get; set; }
/// <summary>
/// Gets or sets the PktSize.
/// </summary>
[JsonPropertyName("pkt_size")]
public string? PktSize { get; set; }
/// <summary>
/// Gets or sets the Width.
/// </summary>
[JsonPropertyName("width")]
public int? Width { get; set; }
/// <summary>
/// Gets or sets the Height.
/// </summary>
[JsonPropertyName("height")]
public int? Height { get; set; }
/// <summary>
/// Gets or sets the CropTop.
/// </summary>
[JsonPropertyName("crop_top")]
public int? CropTop { get; set; }
/// <summary>
/// Gets or sets the CropBottom.
/// </summary>
[JsonPropertyName("crop_bottom")]
public int? CropBottom { get; set; }
/// <summary>
/// Gets or sets the CropLeft.
/// </summary>
[JsonPropertyName("crop_left")]
public int? CropLeft { get; set; }
/// <summary>
/// Gets or sets the CropRight.
/// </summary>
[JsonPropertyName("crop_right")]
public int? CropRight { get; set; }
/// <summary>
/// Gets or sets the PixFmt.
/// </summary>
[JsonPropertyName("pix_fmt")]
public string? PixFmt { get; set; }
/// <summary>
/// Gets or sets the SampleAspectRatio.
/// </summary>
[JsonPropertyName("sample_aspect_ratio")]
public string? SampleAspectRatio { get; set; }
/// <summary>
/// Gets or sets the PictType.
/// </summary>
[JsonPropertyName("pict_type")]
public string? PictType { get; set; }
/// <summary>
/// Gets or sets the InterlacedFrame.
/// </summary>
[JsonPropertyName("interlaced_frame")]
public int? InterlacedFrame { get; set; }
/// <summary>
/// Gets or sets the TopFieldFirst.
/// </summary>
[JsonPropertyName("top_field_first")]
public int? TopFieldFirst { get; set; }
/// <summary>
/// Gets or sets the RepeatPict.
/// </summary>
[JsonPropertyName("repeat_pict")]
public int? RepeatPict { get; set; }
/// <summary>
/// Gets or sets the ColorRange.
/// </summary>
[JsonPropertyName("color_range")]
public string? ColorRange { get; set; }
/// <summary>
/// Gets or sets the ColorSpace.
/// </summary>
[JsonPropertyName("color_space")]
public string? ColorSpace { get; set; }
/// <summary>
/// Gets or sets the ColorPrimaries.
/// </summary>
[JsonPropertyName("color_primaries")]
public string? ColorPrimaries { get; set; }
/// <summary>
/// Gets or sets the ColorTransfer.
/// </summary>
[JsonPropertyName("color_transfer")]
public string? ColorTransfer { get; set; }
/// <summary>
/// Gets or sets the ChromaLocation.
/// </summary>
[JsonPropertyName("chroma_location")]
public string? ChromaLocation { get; set; }
/// <summary>
/// Gets or sets the SideDataList.
/// </summary>
[JsonPropertyName("side_data_list")]
public IReadOnlyList<MediaFrameSideDataInfo>? SideDataList { get; set; }
}

View file

@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
namespace MediaBrowser.MediaEncoding.Probing;
/// <summary>
/// Class MediaFrameSideDataInfo.
/// Currently only records the SideDataType for HDR10+ detection.
/// </summary>
public class MediaFrameSideDataInfo
{
/// <summary>
/// Gets or sets the SideDataType.
/// </summary>
[JsonPropertyName("side_data_type")]
public string? SideDataType { get; set; }
}

View file

@ -105,8 +105,9 @@ namespace MediaBrowser.MediaEncoding.Probing
SetSize(data, info); SetSize(data, info);
var internalStreams = data.Streams ?? Array.Empty<MediaStreamInfo>(); var internalStreams = data.Streams ?? Array.Empty<MediaStreamInfo>();
var internalFrames = data.Frames ?? Array.Empty<MediaFrameInfo>();
info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, s, data.Format)) info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, s, data.Format, internalFrames))
.Where(i => i is not null) .Where(i => i is not null)
// Drop subtitle streams if we don't know the codec because it will just cause failures if we don't know how to handle them // Drop subtitle streams if we don't know the codec because it will just cause failures if we don't know how to handle them
.Where(i => i.Type != MediaStreamType.Subtitle || !string.IsNullOrWhiteSpace(i.Codec)) .Where(i => i.Type != MediaStreamType.Subtitle || !string.IsNullOrWhiteSpace(i.Codec))
@ -685,8 +686,9 @@ namespace MediaBrowser.MediaEncoding.Probing
/// <param name="isAudio">if set to <c>true</c> [is info].</param> /// <param name="isAudio">if set to <c>true</c> [is info].</param>
/// <param name="streamInfo">The stream info.</param> /// <param name="streamInfo">The stream info.</param>
/// <param name="formatInfo">The format info.</param> /// <param name="formatInfo">The format info.</param>
/// <param name="frameInfoList">The frame info.</param>
/// <returns>MediaStream.</returns> /// <returns>MediaStream.</returns>
private MediaStream GetMediaStream(bool isAudio, MediaStreamInfo streamInfo, MediaFormatInfo formatInfo) private MediaStream GetMediaStream(bool isAudio, MediaStreamInfo streamInfo, MediaFormatInfo formatInfo, IReadOnlyList<MediaFrameInfo> frameInfoList)
{ {
// These are mp4 chapters // These are mp4 chapters
if (string.Equals(streamInfo.CodecName, "mov_text", StringComparison.OrdinalIgnoreCase)) if (string.Equals(streamInfo.CodecName, "mov_text", StringComparison.OrdinalIgnoreCase))
@ -904,6 +906,15 @@ namespace MediaBrowser.MediaEncoding.Probing
} }
} }
} }
var frameInfo = frameInfoList?.FirstOrDefault(i => i.StreamIndex == stream.Index);
if (frameInfo?.SideDataList != null)
{
if (frameInfo.SideDataList.Any(data => string.Equals(data.SideDataType, "HDR Dynamic Metadata SMPTE2094-40 (HDR10+)", StringComparison.OrdinalIgnoreCase)))
{
stream.Hdr10PlusPresentFlag = true;
}
}
} }
else if (streamInfo.CodecType == CodecType.Data) else if (streamInfo.CodecType == CodecType.Data)
{ {

View file

@ -345,6 +345,15 @@ namespace MediaBrowser.Model.Dlna
return !condition.IsRequired; 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; var conditionType = condition.Condition;
if (conditionType == ProfileConditionType.EqualsAny) if (conditionType == ProfileConditionType.EqualsAny)
{ {

View file

@ -2,6 +2,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Text; using System.Text;

View file

@ -153,6 +153,8 @@ namespace MediaBrowser.Model.Entities
/// <value>The title.</value> /// <value>The title.</value>
public string Title { get; set; } public string Title { get; set; }
public bool? Hdr10PlusPresentFlag { get; set; }
/// <summary> /// <summary>
/// Gets the video range. /// Gets the video range.
/// </summary> /// </summary>
@ -172,6 +174,7 @@ namespace MediaBrowser.Model.Entities
/// Gets the video range type. /// Gets the video range type.
/// </summary> /// </summary>
/// <value>The video range type.</value> /// <value>The video range type.</value>
[DefaultValue(VideoRangeType.Unknown)]
public VideoRangeType VideoRangeType public VideoRangeType VideoRangeType
{ {
get get
@ -779,8 +782,8 @@ namespace MediaBrowser.Model.Entities
var blPresentFlag = BlPresentFlag == 1; var blPresentFlag = BlPresentFlag == 1;
var dvBlCompatId = DvBlSignalCompatibilityId; var dvBlCompatId = DvBlSignalCompatibilityId;
var isDoViProfile = dvProfile == 5 || dvProfile == 7 || dvProfile == 8 || dvProfile == 10; var isDoViProfile = dvProfile is 5 or 7 or 8 or 10;
var isDoViFlag = rpuPresentFlag && blPresentFlag && (dvBlCompatId == 0 || dvBlCompatId == 1 || dvBlCompatId == 4 || dvBlCompatId == 2 || dvBlCompatId == 6); var isDoViFlag = rpuPresentFlag && blPresentFlag && dvBlCompatId is 0 or 1 or 4 or 2 or 6;
if ((isDoViProfile && isDoViFlag) if ((isDoViProfile && isDoViFlag)
|| string.Equals(codecTag, "dovi", StringComparison.OrdinalIgnoreCase) || string.Equals(codecTag, "dovi", StringComparison.OrdinalIgnoreCase)
@ -788,7 +791,7 @@ namespace MediaBrowser.Model.Entities
|| string.Equals(codecTag, "dvhe", StringComparison.OrdinalIgnoreCase) || string.Equals(codecTag, "dvhe", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codecTag, "dav1", StringComparison.OrdinalIgnoreCase)) || string.Equals(codecTag, "dav1", StringComparison.OrdinalIgnoreCase))
{ {
return dvProfile switch var dvRangeSet = dvProfile switch
{ {
5 => (VideoRange.HDR, VideoRangeType.DOVI), 5 => (VideoRange.HDR, VideoRangeType.DOVI),
8 => dvBlCompatId switch 8 => dvBlCompatId switch
@ -796,32 +799,40 @@ namespace MediaBrowser.Model.Entities
1 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10), 1 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10),
4 => (VideoRange.HDR, VideoRangeType.DOVIWithHLG), 4 => (VideoRange.HDR, VideoRangeType.DOVIWithHLG),
2 => (VideoRange.SDR, VideoRangeType.DOVIWithSDR), 2 => (VideoRange.SDR, VideoRangeType.DOVIWithSDR),
// While not in Dolby Spec, Profile 8 CCid 6 media are possible to create, and since CCid 6 stems from Bluray (Profile 7 originally) an HDR10 base layer is guaranteed to exist. // Out of Dolby Spec files should be marked as invalid
6 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10), _ => (VideoRange.HDR, VideoRangeType.DOVIInvalid)
// There is no other case to handle here as per Dolby Spec. Default case included for completeness and linting purposes
_ => (VideoRange.SDR, VideoRangeType.SDR)
}, },
7 => (VideoRange.HDR, VideoRangeType.HDR10), 7 => (VideoRange.HDR, VideoRangeType.DOVIWithEL),
10 => dvBlCompatId switch 10 => dvBlCompatId switch
{ {
0 => (VideoRange.HDR, VideoRangeType.DOVI), 0 => (VideoRange.HDR, VideoRangeType.DOVI),
1 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10), 1 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10),
2 => (VideoRange.SDR, VideoRangeType.DOVIWithSDR), 2 => (VideoRange.SDR, VideoRangeType.DOVIWithSDR),
4 => (VideoRange.HDR, VideoRangeType.DOVIWithHLG), 4 => (VideoRange.HDR, VideoRangeType.DOVIWithHLG),
// While not in Dolby Spec, Profile 8 CCid 6 media are possible to create, and since CCid 6 stems from Bluray (Profile 7 originally) an HDR10 base layer is guaranteed to exist. // Out of Dolby Spec files should be marked as invalid
6 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10), _ => (VideoRange.HDR, VideoRangeType.DOVIInvalid)
// There is no other case to handle here as per Dolby Spec. Default case included for completeness and linting purposes
_ => (VideoRange.SDR, VideoRangeType.SDR)
}, },
_ => (VideoRange.SDR, VideoRangeType.SDR) _ => (VideoRange.SDR, VideoRangeType.SDR)
}; };
if (Hdr10PlusPresentFlag == true)
{
return dvRangeSet.Item2 switch
{
VideoRangeType.DOVIWithHDR10 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10Plus),
VideoRangeType.DOVIWithEL => (VideoRange.HDR, VideoRangeType.DOVIWithELHDR10Plus),
_ => dvRangeSet
};
}
return dvRangeSet;
} }
var colorTransfer = ColorTransfer; var colorTransfer = ColorTransfer;
if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)) if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase))
{ {
return (VideoRange.HDR, VideoRangeType.HDR10); return Hdr10PlusPresentFlag == true ? (VideoRange.HDR, VideoRangeType.HDR10Plus) : (VideoRange.HDR, VideoRangeType.HDR10);
} }
else if (string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
{ {

View file

@ -99,4 +99,6 @@ public class MediaStreamInfo
public int? Rotation { get; set; } public int? Rotation { get; set; }
public string? KeyFrames { get; set; } public string? KeyFrames { get; set; }
public bool? Hdr10PlusPresentFlag { get; set; }
} }

View file

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
/// <inheritdoc />
public partial class AddHdr10PlusFlag : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "Hdr10PlusPresentFlag",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Hdr10PlusPresentFlag",
table: "MediaStreamInfos");
}
}
}

View file

@ -845,6 +845,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<int?>("ElPresentFlag") b.Property<int?>("ElPresentFlag")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<bool?>("Hdr10PlusPresentFlag")
.HasColumnType("INTEGER");
b.Property<int?>("Height") b.Property<int?>("Height")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");