aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authornyanmisaka <nst799610810@gmail.com>2025-12-28 07:22:34 -0500
committerBond_009 <bond.009@outlook.com>2025-12-28 07:22:34 -0500
commit7f0e71578dc278da36c5efeae2fe2674e4677230 (patch)
treecd27b5fcb28d4c411ad44157918afad7249e748b
parent45e881c93e694a174409fcec6743feef9ccf0b70 (diff)
Backport pull request #15833 from jellyfin/release-10.11.z
Fix missing H.264 and AV1 SDR fallbacks in HLS playlist Original-merge: 01b20d3b75e00909e7a531a625eda5ecb1a76f42 Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com>
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs112
1 files changed, 63 insertions, 49 deletions
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index a38ad379c..16e51151d 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -154,7 +154,7 @@ public class DynamicHlsHelper
// 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))
{
- var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(_httpContextAccessor.HttpContext.Request.QueryString.ToString());
+ var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString);
newQuery["AudioCodec"] = state.OutputAudioCodec;
queryString = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(string.Empty, newQuery);
}
@@ -173,10 +173,21 @@ public class DynamicHlsHelper
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
.MediaStreams
@@ -198,37 +209,36 @@ public class DynamicHlsHelper
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);
if (state.VideoStream is not null && state.VideoRequest is not null)
{
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.
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.
// 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.
- if (encodingOptions.AllowHevcEncoding
- && !encodingOptions.AllowAv1Encoding
- && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& state.VideoStream.Level.HasValue
&& state.VideoStream.Level > 150
&& state.VideoStream.VideoRange == VideoRange.SDR
@@ -273,12 +301,15 @@ public class DynamicHlsHelper
var variation = GetBitrateVariation(totalBitrate);
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);
variation *= 2;
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);
}
@@ -863,23 +894,6 @@ public class DynamicHlsHelper
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)
{
var oldPlaylist = playlist.ToString();