Merge pull request #15833 from nyanmisaka/fix-h264-av1-sdr-hls-fallback

Fix missing H.264 and AV1 SDR fallbacks in HLS playlist
This commit is contained in:
Bond-009
2025-12-24 10:28:33 +01:00
committed by GitHub

View File

@@ -154,7 +154,7 @@ public class DynamicHlsHelper
// from universal audio service, need to override the AudioCodec when the actual request differs from original query // from universal audio service, need to override the AudioCodec when the actual request differs from original query
if (!string.Equals(state.OutputAudioCodec, _httpContextAccessor.HttpContext.Request.Query["AudioCodec"].ToString(), StringComparison.OrdinalIgnoreCase)) if (!string.Equals(state.OutputAudioCodec, _httpContextAccessor.HttpContext.Request.Query["AudioCodec"].ToString(), StringComparison.OrdinalIgnoreCase))
{ {
var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(_httpContextAccessor.HttpContext.Request.QueryString.ToString()); var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString);
newQuery["AudioCodec"] = state.OutputAudioCodec; newQuery["AudioCodec"] = state.OutputAudioCodec;
queryString = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(string.Empty, newQuery); queryString = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(string.Empty, newQuery);
} }
@@ -173,10 +173,21 @@ public class DynamicHlsHelper
queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons; queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
} }
// Main stream // Video rotation metadata is only supported in fMP4 remuxing
var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8"; if (state.VideoStream is not null
&& state.VideoRequest is not null
&& (state.VideoStream?.Rotation ?? 0) != 0
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
&& !string.Equals(state.Request.SegmentContainer, "mp4", StringComparison.OrdinalIgnoreCase))
{
queryString += "&AllowVideoStreamCopy=false";
}
playlistUrl += queryString; // Main stream
var baseUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
var playlistUrl = baseUrl + queryString;
var playlistQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString);
var subtitleStreams = state.MediaSource var subtitleStreams = state.MediaSource
.MediaStreams .MediaStreams
@@ -198,37 +209,36 @@ public class DynamicHlsHelper
AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User); AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
} }
// Video rotation metadata is only supported in fMP4 remuxing
if (state.VideoStream is not null
&& state.VideoRequest is not null
&& (state.VideoStream?.Rotation ?? 0) != 0
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
&& !string.Equals(state.Request.SegmentContainer, "mp4", StringComparison.OrdinalIgnoreCase))
{
playlistUrl += "&AllowVideoStreamCopy=false";
}
var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
if (state.VideoStream is not null && state.VideoRequest is not null) if (state.VideoStream is not null && state.VideoRequest is not null)
{ {
var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
// Provide SDR HEVC entrance for backward compatibility. // Provide AV1 and HEVC SDR entrances for backward compatibility.
if (encodingOptions.AllowHevcEncoding foreach (var sdrVideoCodec in new[] { "av1", "hevc" })
&& !encodingOptions.AllowAv1Encoding
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& state.VideoStream.VideoRange == VideoRange.HDR
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{ {
var requestedVideoProfiles = state.GetRequestedProfiles("hevc"); var isAv1EncodingAllowed = encodingOptions.AllowAv1Encoding
if (requestedVideoProfiles is not null && requestedVideoProfiles.Length > 0) && string.Equals(sdrVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)
&& string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase);
var isHevcEncodingAllowed = encodingOptions.AllowHevcEncoding
&& string.Equals(sdrVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase);
var isEncodingAllowed = isAv1EncodingAllowed || isHevcEncodingAllowed;
if (isEncodingAllowed
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& state.VideoStream.VideoRange == VideoRange.HDR)
{ {
// Force HEVC Main Profile and disable video stream copy. // Force AV1 and HEVC Main Profile and disable video stream copy.
state.OutputVideoCodec = "hevc"; state.OutputVideoCodec = sdrVideoCodec;
var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main");
sdrVideoUrl += "&AllowVideoStreamCopy=false"; var sdrPlaylistQuery = playlistQuery;
sdrPlaylistQuery["VideoCodec"] = sdrVideoCodec;
sdrPlaylistQuery[sdrVideoCodec + "-profile"] = "main";
sdrPlaylistQuery["AllowVideoStreamCopy"] = "false";
var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylistQuery);
// HACK: Use the same bitrate so that the client can choose by other attributes, such as color range. // HACK: Use the same bitrate so that the client can choose by other attributes, such as color range.
AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup); AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
@@ -238,12 +248,30 @@ public class DynamicHlsHelper
} }
} }
// Provide H.264 SDR entrance for backward compatibility.
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& state.VideoStream.VideoRange == VideoRange.HDR)
{
// Force H.264 and disable video stream copy.
state.OutputVideoCodec = "h264";
var sdrPlaylistQuery = playlistQuery;
sdrPlaylistQuery["VideoCodec"] = "h264";
sdrPlaylistQuery["AllowVideoStreamCopy"] = "false";
var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylistQuery);
// HACK: Use the same bitrate so that the client can choose by other attributes, such as color range.
AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
// Restore the video codec
state.OutputVideoCodec = "copy";
}
// Provide Level 5.0 entrance for backward compatibility. // Provide Level 5.0 entrance for backward compatibility.
// e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video, // e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
// but in fact it is capable of playing videos up to Level 6.1. // but in fact it is capable of playing videos up to Level 6.1.
if (encodingOptions.AllowHevcEncoding if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& !encodingOptions.AllowAv1Encoding
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& state.VideoStream.Level.HasValue && state.VideoStream.Level.HasValue
&& state.VideoStream.Level > 150 && state.VideoStream.Level > 150
&& state.VideoStream.VideoRange == VideoRange.SDR && state.VideoStream.VideoRange == VideoRange.SDR
@@ -273,12 +301,15 @@ public class DynamicHlsHelper
var variation = GetBitrateVariation(totalBitrate); var variation = GetBitrateVariation(totalBitrate);
var newBitrate = totalBitrate - variation; var newBitrate = totalBitrate - variation;
var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); var variantQuery = playlistQuery;
variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture);
var variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery);
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
variation *= 2; variation *= 2;
newBitrate = totalBitrate - variation; newBitrate = totalBitrate - variation;
variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture);
variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery);
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
} }
@@ -863,23 +894,6 @@ public class DynamicHlsHelper
return variation; return variation;
} }
private string ReplaceVideoBitrate(string url, int oldValue, int newValue)
{
return url.Replace(
"videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
"videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
StringComparison.OrdinalIgnoreCase);
}
private string ReplaceProfile(string url, string codec, string oldValue, string newValue)
{
string profileStr = codec + "-profile=";
return url.Replace(
profileStr + oldValue,
profileStr + newValue,
StringComparison.OrdinalIgnoreCase);
}
private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue) private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
{ {
var oldPlaylist = playlist.ToString(); var oldPlaylist = playlist.ToString();