|
@@ -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
|
|
|
|
|
- var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
|
|
|
|
|
|
|
+ // 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))
|
|
|
|
|
+ {
|
|
|
|
|
+ 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.
|
|
|
|
|
- if (encodingOptions.AllowHevcEncoding
|
|
|
|
|
- && !encodingOptions.AllowAv1Encoding
|
|
|
|
|
- && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
|
|
|
|
- && state.VideoStream.VideoRange == VideoRange.HDR
|
|
|
|
|
- && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
|
+ // Provide AV1 and HEVC SDR entrances for backward compatibility.
|
|
|
|
|
+ foreach (var sdrVideoCodec in new[] { "av1", "hevc" })
|
|
|
{
|
|
{
|
|
|
- var requestedVideoProfiles = state.GetRequestedProfiles("hevc");
|
|
|
|
|
- if (requestedVideoProfiles is not null && requestedVideoProfiles.Length > 0)
|
|
|
|
|
|
|
+ var isAv1EncodingAllowed = encodingOptions.AllowAv1Encoding
|
|
|
|
|
+ && 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.
|
|
|
|
|
- state.OutputVideoCodec = "hevc";
|
|
|
|
|
- var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main");
|
|
|
|
|
- sdrVideoUrl += "&AllowVideoStreamCopy=false";
|
|
|
|
|
|
|
+ // Force AV1 and HEVC Main Profile and disable video stream copy.
|
|
|
|
|
+ state.OutputVideoCodec = sdrVideoCodec;
|
|
|
|
|
+
|
|
|
|
|
+ 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
|
|
|
|
|
- && !encodingOptions.AllowAv1Encoding
|
|
|
|
|
- && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
|
|
|
|
|
|
+ if (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();
|