diff options
| author | Cody Robibero <cody@robibe.ro> | 2023-02-04 10:21:49 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-02-04 10:21:49 -0700 |
| commit | d1af317d98a6190711af406af17b569844aebbd7 (patch) | |
| tree | 3422bb577d2821a9798465439e983932690aa2e3 /Jellyfin.Api/Helpers/DynamicHlsHelper.cs | |
| parent | e0519189b25bc4605889e46d9583fea9aef41732 (diff) | |
| parent | dfea1229e12764a77f5d392194b1848f80b87042 (diff) | |
Merge pull request #9215 from Shadowghost/api-scoped-namespace
Diffstat (limited to 'Jellyfin.Api/Helpers/DynamicHlsHelper.cs')
| -rw-r--r-- | Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 1163 |
1 files changed, 581 insertions, 582 deletions
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 4a338efff..245239233 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -25,725 +25,724 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; -namespace Jellyfin.Api.Helpers +namespace Jellyfin.Api.Helpers; + +/// <summary> +/// Dynamic hls helper. +/// </summary> +public class DynamicHlsHelper { + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + private readonly IDlnaManager _dlnaManager; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly IDeviceManager _deviceManager; + private readonly TranscodingJobHelper _transcodingJobHelper; + private readonly INetworkManager _networkManager; + private readonly ILogger<DynamicHlsHelper> _logger; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly EncodingHelper _encodingHelper; + /// <summary> - /// Dynamic hls helper. + /// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class. /// </summary> - public class DynamicHlsHelper + /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> + /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> + /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> + /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> + /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> + /// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param> + /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> + /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param> + /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> + /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> + public DynamicHlsHelper( + ILibraryManager libraryManager, + IUserManager userManager, + IDlnaManager dlnaManager, + IMediaSourceManager mediaSourceManager, + IServerConfigurationManager serverConfigurationManager, + IMediaEncoder mediaEncoder, + IDeviceManager deviceManager, + TranscodingJobHelper transcodingJobHelper, + INetworkManager networkManager, + ILogger<DynamicHlsHelper> logger, + IHttpContextAccessor httpContextAccessor, + EncodingHelper encodingHelper) + { + _libraryManager = libraryManager; + _userManager = userManager; + _dlnaManager = dlnaManager; + _mediaSourceManager = mediaSourceManager; + _serverConfigurationManager = serverConfigurationManager; + _mediaEncoder = mediaEncoder; + _deviceManager = deviceManager; + _transcodingJobHelper = transcodingJobHelper; + _networkManager = networkManager; + _logger = logger; + _httpContextAccessor = httpContextAccessor; + _encodingHelper = encodingHelper; + } + + /// <summary> + /// Get master hls playlist. + /// </summary> + /// <param name="transcodingJobType">Transcoding job type.</param> + /// <param name="streamingRequest">Streaming request dto.</param> + /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param> + /// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns> + public async Task<ActionResult> GetMasterHlsPlaylist( + TranscodingJobType transcodingJobType, + StreamingRequestDto streamingRequest, + bool enableAdaptiveBitrateStreaming) + { + var isHeadRequest = _httpContextAccessor.HttpContext?.Request.Method == WebRequestMethods.Http.Head; + // CTS lifecycle is managed internally. + var cancellationTokenSource = new CancellationTokenSource(); + return await GetMasterPlaylistInternal( + streamingRequest, + isHeadRequest, + enableAdaptiveBitrateStreaming, + transcodingJobType, + cancellationTokenSource).ConfigureAwait(false); + } + + private async Task<ActionResult> GetMasterPlaylistInternal( + StreamingRequestDto streamingRequest, + bool isHeadRequest, + bool enableAdaptiveBitrateStreaming, + TranscodingJobType transcodingJobType, + CancellationTokenSource cancellationTokenSource) { - private readonly ILibraryManager _libraryManager; - private readonly IUserManager _userManager; - private readonly IDlnaManager _dlnaManager; - private readonly IMediaSourceManager _mediaSourceManager; - private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly IMediaEncoder _mediaEncoder; - private readonly IDeviceManager _deviceManager; - private readonly TranscodingJobHelper _transcodingJobHelper; - private readonly INetworkManager _networkManager; - private readonly ILogger<DynamicHlsHelper> _logger; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly EncodingHelper _encodingHelper; - - /// <summary> - /// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class. - /// </summary> - /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> - /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> - /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> - /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> - /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> - /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> - /// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param> - /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> - /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param> - /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> - /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> - public DynamicHlsHelper( - ILibraryManager libraryManager, - IUserManager userManager, - IDlnaManager dlnaManager, - IMediaSourceManager mediaSourceManager, - IServerConfigurationManager serverConfigurationManager, - IMediaEncoder mediaEncoder, - IDeviceManager deviceManager, - TranscodingJobHelper transcodingJobHelper, - INetworkManager networkManager, - ILogger<DynamicHlsHelper> logger, - IHttpContextAccessor httpContextAccessor, - EncodingHelper encodingHelper) - { - _libraryManager = libraryManager; - _userManager = userManager; - _dlnaManager = dlnaManager; - _mediaSourceManager = mediaSourceManager; - _serverConfigurationManager = serverConfigurationManager; - _mediaEncoder = mediaEncoder; - _deviceManager = deviceManager; - _transcodingJobHelper = transcodingJobHelper; - _networkManager = networkManager; - _logger = logger; - _httpContextAccessor = httpContextAccessor; - _encodingHelper = encodingHelper; - } - - /// <summary> - /// Get master hls playlist. - /// </summary> - /// <param name="transcodingJobType">Transcoding job type.</param> - /// <param name="streamingRequest">Streaming request dto.</param> - /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param> - /// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns> - public async Task<ActionResult> GetMasterHlsPlaylist( - TranscodingJobType transcodingJobType, - StreamingRequestDto streamingRequest, - bool enableAdaptiveBitrateStreaming) - { - var isHeadRequest = _httpContextAccessor.HttpContext?.Request.Method == WebRequestMethods.Http.Head; - // CTS lifecycle is managed internally. - var cancellationTokenSource = new CancellationTokenSource(); - return await GetMasterPlaylistInternal( + if (_httpContextAccessor.HttpContext is null) + { + throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext)); + } + + using var state = await StreamingHelpers.GetStreamingState( streamingRequest, - isHeadRequest, - enableAdaptiveBitrateStreaming, + _httpContextAccessor.HttpContext, + _mediaSourceManager, + _userManager, + _libraryManager, + _serverConfigurationManager, + _mediaEncoder, + _encodingHelper, + _dlnaManager, + _deviceManager, + _transcodingJobHelper, transcodingJobType, - cancellationTokenSource).ConfigureAwait(false); - } + cancellationTokenSource.Token) + .ConfigureAwait(false); - private async Task<ActionResult> GetMasterPlaylistInternal( - StreamingRequestDto streamingRequest, - bool isHeadRequest, - bool enableAdaptiveBitrateStreaming, - TranscodingJobType transcodingJobType, - CancellationTokenSource cancellationTokenSource) + _httpContextAccessor.HttpContext.Response.Headers.Add(HeaderNames.Expires, "0"); + if (isHeadRequest) { - if (_httpContextAccessor.HttpContext is null) - { - throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext)); - } + return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8")); + } - using var state = await StreamingHelpers.GetStreamingState( - streamingRequest, - _httpContextAccessor.HttpContext, - _mediaSourceManager, - _userManager, - _libraryManager, - _serverConfigurationManager, - _mediaEncoder, - _encodingHelper, - _dlnaManager, - _deviceManager, - _transcodingJobHelper, - transcodingJobType, - cancellationTokenSource.Token) - .ConfigureAwait(false); - - _httpContextAccessor.HttpContext.Response.Headers.Add(HeaderNames.Expires, "0"); - if (isHeadRequest) - { - return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8")); - } + var totalBitrate = (state.OutputAudioBitrate ?? 0) + (state.OutputVideoBitrate ?? 0); - var totalBitrate = (state.OutputAudioBitrate ?? 0) + (state.OutputVideoBitrate ?? 0); + var builder = new StringBuilder(); - var builder = new StringBuilder(); + builder.AppendLine("#EXTM3U"); - builder.AppendLine("#EXTM3U"); + var isLiveStream = state.IsSegmentedLiveStream; - var isLiveStream = state.IsSegmentedLiveStream; + var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString(); - var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString(); + // from universal audio service + if (!string.IsNullOrWhiteSpace(state.Request.SegmentContainer) + && !queryString.Contains("SegmentContainer", StringComparison.OrdinalIgnoreCase)) + { + queryString += "&SegmentContainer=" + state.Request.SegmentContainer; + } - // from universal audio service - if (!string.IsNullOrWhiteSpace(state.Request.SegmentContainer) - && !queryString.Contains("SegmentContainer", StringComparison.OrdinalIgnoreCase)) - { - queryString += "&SegmentContainer=" + state.Request.SegmentContainer; - } + // from universal audio service + if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) + && !queryString.Contains("TranscodeReasons=", StringComparison.OrdinalIgnoreCase)) + { + queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons; + } - // from universal audio service - if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) - && !queryString.Contains("TranscodeReasons=", StringComparison.OrdinalIgnoreCase)) - { - queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons; - } + // Main stream + var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8"; - // Main stream - var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8"; + playlistUrl += queryString; - playlistUrl += queryString; + var subtitleStreams = state.MediaSource + .MediaStreams + .Where(i => i.IsTextSubtitleStream) + .ToList(); - var subtitleStreams = state.MediaSource - .MediaStreams - .Where(i => i.IsTextSubtitleStream) - .ToList(); + var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest!.EnableSubtitlesInManifest) + ? "subs" + : null; - var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest!.EnableSubtitlesInManifest) - ? "subs" - : null; + // If we're burning in subtitles then don't add additional subs to the manifest + if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) + { + subtitleGroup = null; + } - // If we're burning in subtitles then don't add additional subs to the manifest - if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) - { - subtitleGroup = null; - } + if (!string.IsNullOrWhiteSpace(subtitleGroup)) + { + AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User); + } + + var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); - if (!string.IsNullOrWhiteSpace(subtitleGroup)) + if (state.VideoStream is not null && state.VideoRequest is not null) + { + // Provide a workaround for the case issue between flac and fLaC. + var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString()); + if (!string.IsNullOrEmpty(flacWaPlaylist)) { - AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User); + builder.Append(flacWaPlaylist); } - var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); + var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); - if (state.VideoStream is not null && state.VideoRequest is not null) + // Provide SDR HEVC entrance for backward compatibility. + if (encodingOptions.AllowHevcEncoding + && EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + && !string.IsNullOrEmpty(state.VideoStream.VideoRange) + && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) + && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) { - // Provide a workaround for the case issue between flac and fLaC. - var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString()); - if (!string.IsNullOrEmpty(flacWaPlaylist)) - { - builder.Append(flacWaPlaylist); - } - - var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); - - // Provide SDR HEVC entrance for backward compatibility. - if (encodingOptions.AllowHevcEncoding - && EncodingHelper.IsCopyCodec(state.OutputVideoCodec) - && !string.IsNullOrEmpty(state.VideoStream.VideoRange) - && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) - && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) - { - var requestedVideoProfiles = state.GetRequestedProfiles("hevc"); - if (requestedVideoProfiles is not null && requestedVideoProfiles.Length > 0) - { - // Force HEVC Main Profile and disable video stream copy. - state.OutputVideoCodec = "hevc"; - var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main"); - sdrVideoUrl += "&AllowVideoStreamCopy=false"; - - var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec); - var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0; - var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate; - - var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup); - - // Provide a workaround for the case issue between flac and fLaC. - flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString()); - if (!string.IsNullOrEmpty(flacWaPlaylist)) - { - builder.Append(flacWaPlaylist); - } - - // 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 (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) - && state.VideoStream.Level.HasValue - && state.VideoStream.Level > 150 - && !string.IsNullOrEmpty(state.VideoStream.VideoRange) - && string.Equals(state.VideoStream.VideoRange, "SDR", StringComparison.OrdinalIgnoreCase) - && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) + var requestedVideoProfiles = state.GetRequestedProfiles("hevc"); + if (requestedVideoProfiles is not null && requestedVideoProfiles.Length > 0) { - var playlistCodecsField = new StringBuilder(); - AppendPlaylistCodecsField(playlistCodecsField, state); + // 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 the video level to 5.0. - var originalLevel = state.VideoStream.Level; - state.VideoStream.Level = 150; - var newPlaylistCodecsField = new StringBuilder(); - AppendPlaylistCodecsField(newPlaylistCodecsField, state); + var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec); + var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0; + var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate; - // Restore the video level. - state.VideoStream.Level = originalLevel; - var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField); - builder.Append(newPlaylist); + var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup); // Provide a workaround for the case issue between flac and fLaC. - flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist); + flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString()); if (!string.IsNullOrEmpty(flacWaPlaylist)) { builder.Append(flacWaPlaylist); } + + // Restore the video codec + state.OutputVideoCodec = "copy"; } } - if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIp())) - { - var requestedVideoBitrate = state.VideoRequest is null ? 0 : state.VideoRequest.VideoBitRate ?? 0; - - // By default, vary by just 200k - var variation = GetBitrateVariation(totalBitrate); - - var newBitrate = totalBitrate - variation; - var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); - AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); + // 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 (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + && state.VideoStream.Level.HasValue + && state.VideoStream.Level > 150 + && !string.IsNullOrEmpty(state.VideoStream.VideoRange) + && string.Equals(state.VideoStream.VideoRange, "SDR", StringComparison.OrdinalIgnoreCase) + && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) + { + var playlistCodecsField = new StringBuilder(); + AppendPlaylistCodecsField(playlistCodecsField, state); + + // Force the video level to 5.0. + var originalLevel = state.VideoStream.Level; + state.VideoStream.Level = 150; + var newPlaylistCodecsField = new StringBuilder(); + AppendPlaylistCodecsField(newPlaylistCodecsField, state); + + // Restore the video level. + state.VideoStream.Level = originalLevel; + var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField); + builder.Append(newPlaylist); - variation *= 2; - newBitrate = totalBitrate - variation; - variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); - AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); + // Provide a workaround for the case issue between flac and fLaC. + flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist); + if (!string.IsNullOrEmpty(flacWaPlaylist)) + { + builder.Append(flacWaPlaylist); + } } - - return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); } - private StringBuilder AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup) + if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIp())) { - var playlistBuilder = new StringBuilder(); - playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=") - .Append(bitrate.ToString(CultureInfo.InvariantCulture)) - .Append(",AVERAGE-BANDWIDTH=") - .Append(bitrate.ToString(CultureInfo.InvariantCulture)); + var requestedVideoBitrate = state.VideoRequest is null ? 0 : state.VideoRequest.VideoBitRate ?? 0; - AppendPlaylistVideoRangeField(playlistBuilder, state); + // By default, vary by just 200k + var variation = GetBitrateVariation(totalBitrate); - AppendPlaylistCodecsField(playlistBuilder, state); + var newBitrate = totalBitrate - variation; + var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); + AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); - AppendPlaylistResolutionField(playlistBuilder, state); + variation *= 2; + newBitrate = totalBitrate - variation; + variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); + AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); + } - AppendPlaylistFramerateField(playlistBuilder, state); + return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); + } - if (!string.IsNullOrWhiteSpace(subtitleGroup)) - { - playlistBuilder.Append(",SUBTITLES=\"") - .Append(subtitleGroup) - .Append('"'); - } + private StringBuilder AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup) + { + 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(Environment.NewLine); - playlistBuilder.AppendLine(url); - builder.Append(playlistBuilder); + AppendPlaylistVideoRangeField(playlistBuilder, state); - return playlistBuilder; + AppendPlaylistCodecsField(playlistBuilder, state); + + AppendPlaylistResolutionField(playlistBuilder, state); + + AppendPlaylistFramerateField(playlistBuilder, state); + + if (!string.IsNullOrWhiteSpace(subtitleGroup)) + { + playlistBuilder.Append(",SUBTITLES=\"") + .Append(subtitleGroup) + .Append('"'); } - /// <summary> - /// Appends a VIDEO-RANGE field containing the range of the output video stream. - /// </summary> - /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/> - /// <param name="builder">StringBuilder to append the field to.</param> - /// <param name="state">StreamState of the current stream.</param> - private void AppendPlaylistVideoRangeField(StringBuilder builder, StreamState state) + playlistBuilder.Append(Environment.NewLine); + playlistBuilder.AppendLine(url); + builder.Append(playlistBuilder); + + return playlistBuilder; + } + + /// <summary> + /// Appends a VIDEO-RANGE field containing the range of the output video stream. + /// </summary> + /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/> + /// <param name="builder">StringBuilder to append the field to.</param> + /// <param name="state">StreamState of the current stream.</param> + private void AppendPlaylistVideoRangeField(StringBuilder builder, StreamState state) + { + if (state.VideoStream is not null && !string.IsNullOrEmpty(state.VideoStream.VideoRange)) { - if (state.VideoStream is not null && !string.IsNullOrEmpty(state.VideoStream.VideoRange)) + var videoRange = state.VideoStream.VideoRange; + if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) { - var videoRange = state.VideoStream.VideoRange; - if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) - { - if (string.Equals(videoRange, "SDR", StringComparison.OrdinalIgnoreCase)) - { - builder.Append(",VIDEO-RANGE=SDR"); - } - - if (string.Equals(videoRange, "HDR", StringComparison.OrdinalIgnoreCase)) - { - builder.Append(",VIDEO-RANGE=PQ"); - } - } - else + if (string.Equals(videoRange, "SDR", StringComparison.OrdinalIgnoreCase)) { - // Currently we only encode to SDR. builder.Append(",VIDEO-RANGE=SDR"); } - } - } - /// <summary> - /// Appends a CODECS field containing formatted strings of - /// the active streams output video and audio codecs. - /// </summary> - /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/> - /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/> - /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/> - /// <param name="builder">StringBuilder to append the field to.</param> - /// <param name="state">StreamState of the current stream.</param> - private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state) - { - // Video - string videoCodecs = string.Empty; - int? videoCodecLevel = GetOutputVideoCodecLevel(state); - if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue) - { - videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value); + if (string.Equals(videoRange, "HDR", StringComparison.OrdinalIgnoreCase)) + { + builder.Append(",VIDEO-RANGE=PQ"); + } } - - // Audio - string audioCodecs = string.Empty; - if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec)) + else { - audioCodecs = GetPlaylistAudioCodecs(state); + // Currently we only encode to SDR. + builder.Append(",VIDEO-RANGE=SDR"); } + } + } - StringBuilder codecs = new StringBuilder(); + /// <summary> + /// Appends a CODECS field containing formatted strings of + /// the active streams output video and audio codecs. + /// </summary> + /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/> + /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/> + /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/> + /// <param name="builder">StringBuilder to append the field to.</param> + /// <param name="state">StreamState of the current stream.</param> + private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state) + { + // Video + string videoCodecs = string.Empty; + int? videoCodecLevel = GetOutputVideoCodecLevel(state); + if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue) + { + videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value); + } - codecs.Append(videoCodecs); + // Audio + string audioCodecs = string.Empty; + if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec)) + { + audioCodecs = GetPlaylistAudioCodecs(state); + } - if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs)) - { - codecs.Append(','); - } + StringBuilder codecs = new StringBuilder(); - codecs.Append(audioCodecs); + codecs.Append(videoCodecs); - if (codecs.Length > 1) - { - builder.Append(",CODECS=\"") - .Append(codecs) - .Append('"'); - } + if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs)) + { + codecs.Append(','); } - /// <summary> - /// Appends a RESOLUTION field containing the resolution of the output stream. - /// </summary> - /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/> - /// <param name="builder">StringBuilder to append the field to.</param> - /// <param name="state">StreamState of the current stream.</param> - private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state) + codecs.Append(audioCodecs); + + if (codecs.Length > 1) { - if (state.OutputWidth.HasValue && state.OutputHeight.HasValue) - { - builder.Append(",RESOLUTION=") - .Append(state.OutputWidth.GetValueOrDefault()) - .Append('x') - .Append(state.OutputHeight.GetValueOrDefault()); - } + builder.Append(",CODECS=\"") + .Append(codecs) + .Append('"'); } + } - /// <summary> - /// Appends a FRAME-RATE field containing the framerate of the output stream. - /// </summary> - /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/> - /// <param name="builder">StringBuilder to append the field to.</param> - /// <param name="state">StreamState of the current stream.</param> - private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state) + /// <summary> + /// Appends a RESOLUTION field containing the resolution of the output stream. + /// </summary> + /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/> + /// <param name="builder">StringBuilder to append the field to.</param> + /// <param name="state">StreamState of the current stream.</param> + private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state) + { + if (state.OutputWidth.HasValue && state.OutputHeight.HasValue) { - double? framerate = null; - if (state.TargetFramerate.HasValue) - { - framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3); - } - else if (state.VideoStream?.RealFrameRate is not null) - { - framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3); - } + builder.Append(",RESOLUTION=") + .Append(state.OutputWidth.GetValueOrDefault()) + .Append('x') + .Append(state.OutputHeight.GetValueOrDefault()); + } + } - if (framerate.HasValue) - { - builder.Append(",FRAME-RATE=") - .Append(framerate.Value.ToString(CultureInfo.InvariantCulture)); - } + /// <summary> + /// Appends a FRAME-RATE field containing the framerate of the output stream. + /// </summary> + /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/> + /// <param name="builder">StringBuilder to append the field to.</param> + /// <param name="state">StreamState of the current stream.</param> + private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state) + { + double? framerate = null; + if (state.TargetFramerate.HasValue) + { + framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3); + } + else if (state.VideoStream?.RealFrameRate is not null) + { + framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3); } - private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, IPAddress ipAddress) + if (framerate.HasValue) { - // Within the local network this will likely do more harm than good. - if (_networkManager.IsInLocalNetwork(ipAddress)) - { - return false; - } + builder.Append(",FRAME-RATE=") + .Append(framerate.Value.ToString(CultureInfo.InvariantCulture)); + } + } - if (!enableAdaptiveBitrateStreaming) - { - return false; - } + private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, IPAddress ipAddress) + { + // Within the local network this will likely do more harm than good. + if (_networkManager.IsInLocalNetwork(ipAddress)) + { + return false; + } - if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath)) - { - // Opening live streams is so slow it's not even worth it - return false; - } + if (!enableAdaptiveBitrateStreaming) + { + return false; + } - if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) - { - return false; - } + if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath)) + { + // Opening live streams is so slow it's not even worth it + return false; + } - if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec)) - { - return false; - } + if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) + { + return false; + } - if (!state.IsOutputVideo) - { - return false; - } + if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec)) + { + return false; + } - // Having problems in android + if (!state.IsOutputVideo) + { return false; - // return state.VideoRequest.VideoBitRate.HasValue; } - private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder, ClaimsPrincipal user) + // Having problems in android + return false; + // return state.VideoRequest.VideoBitRate.HasValue; + } + + private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder, ClaimsPrincipal user) + { + if (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Drop) { - if (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Drop) - { - return; - } + return; + } - var selectedIndex = state.SubtitleStream is null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index; - const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\""; + var selectedIndex = state.SubtitleStream is null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index; + const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\""; - foreach (var stream in subtitles) - { - var name = stream.DisplayTitle; - - var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index; - var isForced = stream.IsForced; - - var url = string.Format( - CultureInfo.InvariantCulture, - "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}", - state.Request.MediaSourceId, - stream.Index.ToString(CultureInfo.InvariantCulture), - 30.ToString(CultureInfo.InvariantCulture), - user.GetToken()); - - var line = string.Format( - CultureInfo.InvariantCulture, - Format, - name, - isDefault ? "YES" : "NO", - isForced ? "YES" : "NO", - url, - stream.Language ?? "Unknown"); - - builder.AppendLine(line); - } + foreach (var stream in subtitles) + { + var name = stream.DisplayTitle; + + var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index; + var isForced = stream.IsForced; + + var url = string.Format( + CultureInfo.InvariantCulture, + "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}", + state.Request.MediaSourceId, + stream.Index.ToString(CultureInfo.InvariantCulture), + 30.ToString(CultureInfo.InvariantCulture), + user.GetToken()); + + var line = string.Format( + CultureInfo.InvariantCulture, + Format, + name, + isDefault ? "YES" : "NO", + isForced ? "YES" : "NO", + url, + stream.Language ?? "Unknown"); + + builder.AppendLine(line); } + } - /// <summary> - /// Get the H.26X level of the output video stream. - /// </summary> - /// <param name="state">StreamState of the current stream.</param> - /// <returns>H.26X level of the output video stream.</returns> - private int? GetOutputVideoCodecLevel(StreamState state) + /// <summary> + /// Get the H.26X level of the output video stream. + /// </summary> + /// <param name="state">StreamState of the current stream.</param> + /// <returns>H.26X level of the output video stream.</returns> + private int? GetOutputVideoCodecLevel(StreamState state) + { + string levelString = string.Empty; + if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + && state.VideoStream is not null + && state.VideoStream.Level.HasValue) { - string levelString = string.Empty; - if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) - && state.VideoStream is not null - && state.VideoStream.Level.HasValue) + levelString = state.VideoStream.Level.ToString() ?? string.Empty; + } + else + { + if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase)) { - levelString = state.VideoStream.Level.ToString() ?? string.Empty; + levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec) ?? "41"; + levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString); } - else - { - if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase)) - { - levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec) ?? "41"; - levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString); - } - if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) - { - levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120"; - levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString); - } - } - - if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel)) + if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) { - return parsedLevel; + levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120"; + levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString); } - - return null; } - /// <summary> - /// Get the H.26X profile of the output video stream. - /// </summary> - /// <param name="state">StreamState of the current stream.</param> - /// <param name="codec">Video codec.</param> - /// <returns>H.26X profile of the output video stream.</returns> - private string GetOutputVideoCodecProfile(StreamState state, string codec) + if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel)) { - string profileString = string.Empty; - if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) - && !string.IsNullOrEmpty(state.VideoStream.Profile)) - { - profileString = state.VideoStream.Profile; - } - else if (!string.IsNullOrEmpty(codec)) - { - profileString = state.GetRequestedProfiles(codec).FirstOrDefault() ?? string.Empty; - if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase)) - { - profileString ??= "high"; - } + return parsedLevel; + } - if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) - { - profileString ??= "main"; - } - } + return null; + } - return profileString; + /// <summary> + /// Get the H.26X profile of the output video stream. + /// </summary> + /// <param name="state">StreamState of the current stream.</param> + /// <param name="codec">Video codec.</param> + /// <returns>H.26X profile of the output video stream.</returns> + private string GetOutputVideoCodecProfile(StreamState state, string codec) + { + string profileString = string.Empty; + if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + && !string.IsNullOrEmpty(state.VideoStream.Profile)) + { + profileString = state.VideoStream.Profile; } - - /// <summary> - /// Gets a formatted string of the output audio codec, for use in the CODECS field. - /// </summary> - /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/> - /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/> - /// <param name="state">StreamState of the current stream.</param> - /// <returns>Formatted audio codec string.</returns> - private string GetPlaylistAudioCodecs(StreamState state) + else if (!string.IsNullOrEmpty(codec)) { - if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase)) + profileString = state.GetRequestedProfiles(codec).FirstOrDefault() ?? string.Empty; + if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase)) { - string? profile = state.GetRequestedProfiles("aac").FirstOrDefault(); - return HlsCodecStringHelpers.GetAACString(profile); + profileString ??= "high"; } - if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) { - return HlsCodecStringHelpers.GetMP3String(); + profileString ??= "main"; } + } - if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase)) - { - return HlsCodecStringHelpers.GetAC3String(); - } + return profileString; + } - if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase)) - { - return HlsCodecStringHelpers.GetEAC3String(); - } + /// <summary> + /// Gets a formatted string of the output audio codec, for use in the CODECS field. + /// </summary> + /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/> + /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/> + /// <param name="state">StreamState of the current stream.</param> + /// <returns>Formatted audio codec string.</returns> + private string GetPlaylistAudioCodecs(StreamState state) + { + if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase)) + { + string? profile = state.GetRequestedProfiles("aac").FirstOrDefault(); + return HlsCodecStringHelpers.GetAACString(profile); + } - if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)) - { - return HlsCodecStringHelpers.GetFLACString(); - } + if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) + { + return HlsCodecStringHelpers.GetMP3String(); + } - if (string.Equals(state.ActualOutputAudioCodec, "alac", StringComparison.OrdinalIgnoreCase)) - { - return HlsCodecStringHelpers.GetALACString(); - } + if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase)) + { + return HlsCodecStringHelpers.GetAC3String(); + } - if (string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)) - { - return HlsCodecStringHelpers.GetOPUSString(); - } + if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase)) + { + return HlsCodecStringHelpers.GetEAC3String(); + } - return string.Empty; + if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)) + { + return HlsCodecStringHelpers.GetFLACString(); } - /// <summary> - /// Gets a formatted string of the output video codec, for use in the CODECS field. - /// </summary> - /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/> - /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/> - /// <param name="state">StreamState of the current stream.</param> - /// <param name="codec">Video codec.</param> - /// <param name="level">Video level.</param> - /// <returns>Formatted video codec string.</returns> - private string GetPlaylistVideoCodecs(StreamState state, string codec, int level) + if (string.Equals(state.ActualOutputAudioCodec, "alac", StringComparison.OrdinalIgnoreCase)) { - if (level == 0) - { - // This is 0 when there's no requested H.26X level in the device profile - // and the source is not encoded in H.26X - _logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist"); - return string.Empty; - } + return HlsCodecStringHelpers.GetALACString(); + } - if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase)) - { - string profile = GetOutputVideoCodecProfile(state, "h264"); - return HlsCodecStringHelpers.GetH264String(profile, level); - } + if (string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)) + { + return HlsCodecStringHelpers.GetOPUSString(); + } - if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) - { - string profile = GetOutputVideoCodecProfile(state, "hevc"); - return HlsCodecStringHelpers.GetH265String(profile, level); - } + return string.Empty; + } + /// <summary> + /// Gets a formatted string of the output video codec, for use in the CODECS field. + /// </summary> + /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/> + /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/> + /// <param name="state">StreamState of the current stream.</param> + /// <param name="codec">Video codec.</param> + /// <param name="level">Video level.</param> + /// <returns>Formatted video codec string.</returns> + private string GetPlaylistVideoCodecs(StreamState state, string codec, int level) + { + if (level == 0) + { + // This is 0 when there's no requested H.26X level in the device profile + // and the source is not encoded in H.26X + _logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist"); return string.Empty; } - private int GetBitrateVariation(int bitrate) + if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase)) { - // By default, vary by just 50k - var variation = 50000; - - if (bitrate >= 10000000) - { - variation = 2000000; - } - else if (bitrate >= 5000000) - { - variation = 1500000; - } - else if (bitrate >= 3000000) - { - variation = 1000000; - } - else if (bitrate >= 2000000) - { - variation = 500000; - } - else if (bitrate >= 1000000) - { - variation = 300000; - } - else if (bitrate >= 600000) - { - variation = 200000; - } - else if (bitrate >= 400000) - { - variation = 100000; - } - - return variation; + string profile = GetOutputVideoCodecProfile(state, "h264"); + return HlsCodecStringHelpers.GetH264String(profile, level); } - private string ReplaceVideoBitrate(string url, int oldValue, int newValue) + if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) { - return url.Replace( - "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture), - "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture), - StringComparison.OrdinalIgnoreCase); + string profile = GetOutputVideoCodecProfile(state, "hevc"); + return HlsCodecStringHelpers.GetH265String(profile, level); } - private string ReplaceProfile(string url, string codec, string oldValue, string newValue) + return string.Empty; + } + + private int GetBitrateVariation(int bitrate) + { + // By default, vary by just 50k + var variation = 50000; + + if (bitrate >= 10000000) { - string profileStr = codec + "-profile="; - return url.Replace( - profileStr + oldValue, - profileStr + newValue, - StringComparison.OrdinalIgnoreCase); + variation = 2000000; } - - private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue) + else if (bitrate >= 5000000) { - var oldPlaylist = playlist.ToString(); - return oldPlaylist.Replace( - oldValue.ToString(), - newValue.ToString(), - StringComparison.Ordinal); + variation = 1500000; } - - private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist) + else if (bitrate >= 3000000) { - if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)) - { - return string.Empty; - } + variation = 1000000; + } + else if (bitrate >= 2000000) + { + variation = 500000; + } + else if (bitrate >= 1000000) + { + variation = 300000; + } + else if (bitrate >= 600000) + { + variation = 200000; + } + else if (bitrate >= 400000) + { + variation = 100000; + } - var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal); + 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); + } - return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty; + private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue) + { + var oldPlaylist = playlist.ToString(); + return oldPlaylist.Replace( + oldValue.ToString(), + newValue.ToString(), + StringComparison.Ordinal); + } + + private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist) + { + if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)) + { + return string.Empty; } + + var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal); + + return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty; } } |
