From e5bbb1ea0c0aa74ddb6f6d33c94583dfc1accf31 Mon Sep 17 00:00:00 2001 From: NoFear0411 <9083405+NoFear0411@users.noreply.github.com> Date: Sun, 29 Mar 2026 01:12:06 +0400 Subject: Add spec-compliant dvh1 HLS variant for Dolby Vision Profile 5 (#16362) * Add spec-compliant dvh1 HLS variant for Dolby Vision Profile 5 DV Profile 5 has no backward-compatible base layer, so SUPPLEMENTAL-CODECS cannot be used. The master playlist currently labels P5 streams as hvc1 in the CODECS field, even though DynamicHlsController already passes -tag:v:0 dvh1 -strict -2 to FFmpeg for P5 copy-codec streams, writing a dvh1 FourCC and dvvC configuration box into the fMP4 init segment. This mismatch between the manifest (hvc1) and the bitstream (dvh1) causes spec-compliant clients like Apple TV and webOS 24+ to set up an HDR10 pipeline instead of a Dolby Vision one. Add a dvh1 variant before the existing hvc1 variant for P5 copy-codec streams. Both variants point to the same stream URL. Spec-compliant clients select dvh1 and activate the DV decoder path. Legacy clients that reject dvh1 in CODECS fall through to the hvc1 variant and detect DV from the init segment, preserving existing behavior. Fixes #16179 * Address review: support AV1 DoVi P10, add client capability check - GetDoviString: add isAv1 parameter, return dav1 FourCC for AV1 DoVi (P10 bl_compat_id=0) and dvh1 for HEVC DoVi (P5) - Remove redundant IsDovi() check; VideoRangeType.DOVI is sufficient and correctly limits to profiles without a compatible base layer - Replace IsDoviRemoved() with client capability check using GetRequestedRangeTypes(state.VideoStream.Codec) to only emit the dvh1/dav1 variant for clients that declared DOVI support - Update comments and doc summary to reflect P5 + P10/bl0 scope * Use codec string instead of boolean for DoVi FourCC mapping Replace bool isAv1 with string codec in GetDoviString for future-proofing when DoVi extends to H.266/VVC or AV2. * Move AppendDoviPlaylist next to AppendPlaylist * Fix SA1508: remove blank line before closing brace * Use AppendLine() instead of Append(Environment.NewLine) --- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 78 +++++++++++++++++++++++++++ Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs | 21 ++++++++ 2 files changed, 99 insertions(+) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 44e1c6d5a..b09b27969 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -209,6 +209,25 @@ public class DynamicHlsHelper AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User); } + // For DoVi profiles without a compatible base layer (P5 HEVC, P10/bl0 AV1), + // add a spec-compliant dvh1/dav1 variant before the hvc1 hack variant. + // SUPPLEMENTAL-CODECS cannot be used for these profiles (no compatible BL to supplement). + // The DoVi variant is listed first so spec-compliant clients (Apple TV, webOS 24+) + // select it over the fallback when both have identical BANDWIDTH. + // Only emit for clients that explicitly declared DOVI support to avoid breaking + // non-compliant players that don't recognize dvh1/dav1 CODECS strings. + if (state.VideoStream is not null + && state.VideoRequest is not null + && EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + && state.VideoStream.VideoRangeType == VideoRangeType.DOVI + && state.VideoStream.DvProfile.HasValue + && state.VideoStream.DvLevel.HasValue + && state.GetRequestedRangeTypes(state.VideoStream.Codec) + .Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase)) + { + AppendDoviPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); + } + var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); if (state.VideoStream is not null && state.VideoRequest is not null) @@ -355,6 +374,65 @@ public class DynamicHlsHelper return playlistBuilder; } + /// + /// Appends a Dolby Vision variant with dvh1/dav1 CODECS for profiles without a compatible + /// base layer (P5 HEVC, P10/bl0 AV1). This enables spec-compliant HLS clients to detect + /// DoVi from the manifest rather than relying on init segment inspection. + /// + /// StringBuilder for the master playlist. + /// StreamState of the current stream. + /// Playlist URL for this variant. + /// Bitrate for the BANDWIDTH field. + /// Subtitle group identifier, or null. + private void AppendDoviPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup) + { + var dvProfile = state.VideoStream.DvProfile; + var dvLevel = state.VideoStream.DvLevel; + if (dvProfile is null || dvLevel is null) + { + return; + } + + var playlistBuilder = new StringBuilder(); + playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=") + .Append(bitrate.ToString(CultureInfo.InvariantCulture)) + .Append(",AVERAGE-BANDWIDTH=") + .Append(bitrate.ToString(CultureInfo.InvariantCulture)); + + playlistBuilder.Append(",VIDEO-RANGE=PQ"); + + var dvCodec = HlsCodecStringHelpers.GetDoviString(dvProfile.Value, dvLevel.Value, state.ActualOutputVideoCodec); + + string audioCodecs = string.Empty; + if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec)) + { + audioCodecs = GetPlaylistAudioCodecs(state); + } + + playlistBuilder.Append(",CODECS=\"") + .Append(dvCodec); + if (!string.IsNullOrEmpty(audioCodecs)) + { + playlistBuilder.Append(',').Append(audioCodecs); + } + + playlistBuilder.Append('"'); + + AppendPlaylistResolutionField(playlistBuilder, state); + AppendPlaylistFramerateField(playlistBuilder, state); + + if (!string.IsNullOrWhiteSpace(subtitleGroup)) + { + playlistBuilder.Append(",SUBTITLES=\"") + .Append(subtitleGroup) + .Append('"'); + } + + playlistBuilder.AppendLine(); + playlistBuilder.AppendLine(url); + builder.Append(playlistBuilder); + } + /// /// Appends a VIDEO-RANGE field containing the range of the output video stream. /// diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs index cf42d5f10..1ac2abcfb 100644 --- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs @@ -346,4 +346,25 @@ public static class HlsCodecStringHelpers return result.ToString(); } + + /// + /// Gets a Dolby Vision codec string for profiles without a compatible base layer. + /// + /// Dolby Vision profile number. + /// Dolby Vision level number. + /// Video codec name (e.g. "hevc", "av1") to determine the DoVi FourCC. + /// Dolby Vision codec string. + public static string GetDoviString(int dvProfile, int dvLevel, string codec) + { + // HEVC DoVi uses dvh1, AV1 DoVi uses dav1 (out-of-band parameter sets, recommended by Apple HLS spec Rule 1.10) + var fourCc = string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase) ? "dav1" : "dvh1"; + StringBuilder result = new StringBuilder(fourCc, 12); + + result.Append('.') + .AppendFormat(CultureInfo.InvariantCulture, "{0:D2}", dvProfile) + .Append('.') + .AppendFormat(CultureInfo.InvariantCulture, "{0:D2}", dvLevel); + + return result.ToString(); + } } -- cgit v1.2.3