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
21 changed files with 2328 additions and 67 deletions

View File

@@ -162,6 +162,13 @@ namespace MediaBrowser.Controller.MediaEncoding
_configurationManager = configurationManager;
}
private enum DynamicHdrMetadataRemovalPlan
{
None,
RemoveDovi,
RemoveHdr10Plus,
}
[GeneratedRegex(@"\s+")]
private static partial Regex WhiteSpaceRegex();
@@ -342,11 +349,8 @@ namespace MediaBrowser.Controller.MediaEncoding
return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder || isVideoToolBoxDecoder;
}
return state.VideoStream.VideoRange == VideoRange.HDR
&& (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
|| state.VideoStream.VideoRangeType == VideoRangeType.HLG
|| state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10
|| state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG);
// GPU tonemapping supports all HDR RangeTypes
return state.VideoStream.VideoRange == VideoRange.HDR;
}
private bool IsVulkanHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
@@ -381,8 +385,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
return state.VideoStream.VideoRange == VideoRange.HDR
&& (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
|| state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10);
&& IsDoviWithHdr10Bl(state.VideoStream);
}
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.
// All other HDR formats working.
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)
@@ -1301,6 +1305,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|| 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)
{
var codec = stream.Codec ?? string.Empty;
@@ -1308,8 +1319,125 @@ namespace MediaBrowser.Controller.MediaEncoding
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.
// https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb
if (IsH264(stream))
@@ -1317,21 +1445,57 @@ namespace MediaBrowser.Controller.MediaEncoding
return "-bsf:v h264_mp4toannexb";
}
if (IsH265(stream))
{
return "-bsf:v hevc_mp4toannexb";
}
if (IsAAC(stream))
{
// Convert adts header(mpegts) to asc header(mp4).
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;
}
public static string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer)
public string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer)
{
var bitStreamArgs = string.Empty;
var segmentFormat = GetSegmentFileExtension(segmentContainer).TrimStart('.');
@@ -1342,7 +1506,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals(mediaSourceContainer, "aac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase)))
{
bitStreamArgs = GetBitStreamArgs(state.AudioStream);
bitStreamArgs = GetBitStreamArgs(state, MediaStreamType.Audio);
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
var requestHasHDR10 = requestedRangeTypes.Contains(VideoRangeType.HDR10.ToString(), StringComparison.OrdinalIgnoreCase);
var requestHasHLG = requestedRangeTypes.Contains(VideoRangeType.HLG.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)
&& !((requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10)
|| (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.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
{
string bitStreamArgs = GetBitStreamArgs(state.VideoStream);
string bitStreamArgs = GetBitStreamArgs(state, MediaStreamType.Video);
if (!string.IsNullOrEmpty(bitStreamArgs))
{
args += " " + bitStreamArgs;