< Summary - Jellyfin

Information
Class: Jellyfin.Api.Helpers.DynamicHlsHelper
Assembly: Jellyfin.Api
File(s): /srv/git/jellyfin/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 241
Coverable lines: 241
Total lines: 920
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 186
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 10/18/2025 - 12:10:13 AM Line coverage: 0% (0/244) Branch coverage: 0% (0/174) Total lines: 89112/29/2025 - 12:13:19 AM Line coverage: 0% (0/235) Branch coverage: 0% (0/174) Total lines: 9051/10/2026 - 12:12:36 AM Line coverage: 0% (0/241) Branch coverage: 0% (0/186) Total lines: 920 10/18/2025 - 12:10:13 AM Line coverage: 0% (0/244) Branch coverage: 0% (0/174) Total lines: 89112/29/2025 - 12:13:19 AM Line coverage: 0% (0/235) Branch coverage: 0% (0/174) Total lines: 9051/10/2026 - 12:12:36 AM Line coverage: 0% (0/241) Branch coverage: 0% (0/186) Total lines: 920

Metrics

File(s)

/srv/git/jellyfin/Jellyfin.Api/Helpers/DynamicHlsHelper.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Globalization;
 4using System.Linq;
 5using System.Net;
 6using System.Security.Claims;
 7using System.Text;
 8using System.Threading;
 9using System.Threading.Tasks;
 10using Jellyfin.Api.Extensions;
 11using Jellyfin.Data.Enums;
 12using Jellyfin.Database.Implementations.Entities;
 13using Jellyfin.Extensions;
 14using MediaBrowser.Common.Configuration;
 15using MediaBrowser.Common.Extensions;
 16using MediaBrowser.Common.Net;
 17using MediaBrowser.Controller.Configuration;
 18using MediaBrowser.Controller.Library;
 19using MediaBrowser.Controller.MediaEncoding;
 20using MediaBrowser.Controller.Streaming;
 21using MediaBrowser.Controller.Trickplay;
 22using MediaBrowser.Model.Dlna;
 23using MediaBrowser.Model.Entities;
 24using MediaBrowser.Model.Net;
 25using Microsoft.AspNetCore.Http;
 26using Microsoft.AspNetCore.Mvc;
 27using Microsoft.Extensions.Logging;
 28using Microsoft.Net.Http.Headers;
 29
 30namespace Jellyfin.Api.Helpers;
 31
 32/// <summary>
 33/// Dynamic hls helper.
 34/// </summary>
 35public class DynamicHlsHelper
 36{
 37    private readonly ILibraryManager _libraryManager;
 38    private readonly IUserManager _userManager;
 39    private readonly IMediaSourceManager _mediaSourceManager;
 40    private readonly IServerConfigurationManager _serverConfigurationManager;
 41    private readonly IMediaEncoder _mediaEncoder;
 42    private readonly ITranscodeManager _transcodeManager;
 43    private readonly INetworkManager _networkManager;
 44    private readonly ILogger<DynamicHlsHelper> _logger;
 45    private readonly IHttpContextAccessor _httpContextAccessor;
 46    private readonly EncodingHelper _encodingHelper;
 47    private readonly ITrickplayManager _trickplayManager;
 48
 49    /// <summary>
 50    /// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class.
 51    /// </summary>
 52    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 53    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
 54    /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
 55    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</p
 56    /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
 57    /// <param name="transcodeManager">Instance of <see cref="ITranscodeManager"/>.</param>
 58    /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
 59    /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param>
 60    /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
 61    /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
 62    /// <param name="trickplayManager">Instance of <see cref="ITrickplayManager"/>.</param>
 63    public DynamicHlsHelper(
 64        ILibraryManager libraryManager,
 65        IUserManager userManager,
 66        IMediaSourceManager mediaSourceManager,
 67        IServerConfigurationManager serverConfigurationManager,
 68        IMediaEncoder mediaEncoder,
 69        ITranscodeManager transcodeManager,
 70        INetworkManager networkManager,
 71        ILogger<DynamicHlsHelper> logger,
 72        IHttpContextAccessor httpContextAccessor,
 73        EncodingHelper encodingHelper,
 74        ITrickplayManager trickplayManager)
 75    {
 076        _libraryManager = libraryManager;
 077        _userManager = userManager;
 078        _mediaSourceManager = mediaSourceManager;
 079        _serverConfigurationManager = serverConfigurationManager;
 080        _mediaEncoder = mediaEncoder;
 081        _transcodeManager = transcodeManager;
 082        _networkManager = networkManager;
 083        _logger = logger;
 084        _httpContextAccessor = httpContextAccessor;
 085        _encodingHelper = encodingHelper;
 086        _trickplayManager = trickplayManager;
 087    }
 88
 89    /// <summary>
 90    /// Get master hls playlist.
 91    /// </summary>
 92    /// <param name="transcodingJobType">Transcoding job type.</param>
 93    /// <param name="streamingRequest">Streaming request dto.</param>
 94    /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
 95    /// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns>
 96    public async Task<ActionResult> GetMasterHlsPlaylist(
 97        TranscodingJobType transcodingJobType,
 98        StreamingRequestDto streamingRequest,
 99        bool enableAdaptiveBitrateStreaming)
 100    {
 101        var isHeadRequest = _httpContextAccessor.HttpContext?.Request.Method == WebRequestMethods.Http.Head;
 102        // CTS lifecycle is managed internally.
 103        var cancellationTokenSource = new CancellationTokenSource();
 104        return await GetMasterPlaylistInternal(
 105            streamingRequest,
 106            isHeadRequest,
 107            enableAdaptiveBitrateStreaming,
 108            transcodingJobType,
 109            cancellationTokenSource).ConfigureAwait(false);
 110    }
 111
 112    private async Task<ActionResult> GetMasterPlaylistInternal(
 113        StreamingRequestDto streamingRequest,
 114        bool isHeadRequest,
 115        bool enableAdaptiveBitrateStreaming,
 116        TranscodingJobType transcodingJobType,
 117        CancellationTokenSource cancellationTokenSource)
 118    {
 119        if (_httpContextAccessor.HttpContext is null)
 120        {
 121            throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
 122        }
 123
 124        using var state = await StreamingHelpers.GetStreamingState(
 125                streamingRequest,
 126                _httpContextAccessor.HttpContext,
 127                _mediaSourceManager,
 128                _userManager,
 129                _libraryManager,
 130                _serverConfigurationManager,
 131                _mediaEncoder,
 132                _encodingHelper,
 133                _transcodeManager,
 134                transcodingJobType,
 135                cancellationTokenSource.Token)
 136            .ConfigureAwait(false);
 137
 138        _httpContextAccessor.HttpContext.Response.Headers.Append(HeaderNames.Expires, "0");
 139        if (isHeadRequest)
 140        {
 141            return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8"));
 142        }
 143
 144        var totalBitrate = (state.OutputAudioBitrate ?? 0) + (state.OutputVideoBitrate ?? 0);
 145
 146        var builder = new StringBuilder();
 147
 148        builder.AppendLine("#EXTM3U");
 149
 150        var isLiveStream = state.IsSegmentedLiveStream;
 151
 152        var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString();
 153
 154        // from universal audio service, need to override the AudioCodec when the actual request differs from original q
 155        if (!string.Equals(state.OutputAudioCodec, _httpContextAccessor.HttpContext.Request.Query["AudioCodec"].ToString
 156        {
 157            var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString);
 158            newQuery["AudioCodec"] = state.OutputAudioCodec;
 159            queryString = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(string.Empty, newQuery);
 160        }
 161
 162        // from universal audio service
 163        if (!string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
 164            && !queryString.Contains("SegmentContainer", StringComparison.OrdinalIgnoreCase))
 165        {
 166            queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
 167        }
 168
 169        // from universal audio service
 170        if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons)
 171            && !queryString.Contains("TranscodeReasons=", StringComparison.OrdinalIgnoreCase))
 172        {
 173            queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
 174        }
 175
 176        // Video rotation metadata is only supported in fMP4 remuxing
 177        if (state.VideoStream is not null
 178            && state.VideoRequest is not null
 179            && (state.VideoStream?.Rotation ?? 0) != 0
 180            && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 181            && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
 182            && !string.Equals(state.Request.SegmentContainer, "mp4", StringComparison.OrdinalIgnoreCase))
 183        {
 184            queryString += "&AllowVideoStreamCopy=false";
 185        }
 186
 187        // Main stream
 188        var baseUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
 189        var playlistUrl = baseUrl + queryString;
 190        var playlistQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString);
 191
 192        var subtitleStreams = state.MediaSource
 193            .MediaStreams
 194            .Where(i => i.IsTextSubtitleStream)
 195            .ToList();
 196
 197        var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || 
 198            ? "subs"
 199            : null;
 200
 201        // If we're burning in subtitles then don't add additional subs to the manifest
 202        if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
 203        {
 204            subtitleGroup = null;
 205        }
 206
 207        if (!string.IsNullOrWhiteSpace(subtitleGroup))
 208        {
 209            AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
 210        }
 211
 212        var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
 213
 214        if (state.VideoStream is not null && state.VideoRequest is not null)
 215        {
 216            var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
 217
 218            // Provide AV1 and HEVC SDR entrances for backward compatibility.
 219            foreach (var sdrVideoCodec in new[] { "av1", "hevc" })
 220            {
 221                var isAv1EncodingAllowed = encodingOptions.AllowAv1Encoding
 222                    && string.Equals(sdrVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)
 223                    && string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase);
 224                var isHevcEncodingAllowed = encodingOptions.AllowHevcEncoding
 225                    && string.Equals(sdrVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
 226                    && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase);
 227                var isEncodingAllowed = isAv1EncodingAllowed || isHevcEncodingAllowed;
 228
 229                if (isEncodingAllowed
 230                    && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 231                    && state.VideoStream.VideoRange == VideoRange.HDR)
 232                {
 233                    // Force AV1 and HEVC Main Profile and disable video stream copy.
 234                    state.OutputVideoCodec = sdrVideoCodec;
 235
 236                    var sdrPlaylistQuery = playlistQuery;
 237                    sdrPlaylistQuery["VideoCodec"] = sdrVideoCodec;
 238                    sdrPlaylistQuery[sdrVideoCodec + "-profile"] = "main";
 239                    sdrPlaylistQuery["AllowVideoStreamCopy"] = "false";
 240
 241                    var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylist
 242
 243                    // HACK: Use the same bitrate so that the client can choose by other attributes, such as color range
 244                    AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
 245
 246                    // Restore the video codec
 247                    state.OutputVideoCodec = "copy";
 248                }
 249            }
 250
 251            // Provide H.264 SDR entrance for backward compatibility.
 252            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 253                && state.VideoStream.VideoRange == VideoRange.HDR)
 254            {
 255                // Force H.264 and disable video stream copy.
 256                state.OutputVideoCodec = "h264";
 257
 258                var sdrPlaylistQuery = playlistQuery;
 259                sdrPlaylistQuery["VideoCodec"] = "h264";
 260                sdrPlaylistQuery["AllowVideoStreamCopy"] = "false";
 261
 262                var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylistQuer
 263
 264                // HACK: Use the same bitrate so that the client can choose by other attributes, such as color range.
 265                AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
 266
 267                // Restore the video codec
 268                state.OutputVideoCodec = "copy";
 269            }
 270
 271            // Provide Level 5.0 entrance for backward compatibility.
 272            // e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
 273            // but in fact it is capable of playing videos up to Level 6.1.
 274            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 275                && state.VideoStream.Level.HasValue
 276                && state.VideoStream.Level > 150
 277                && state.VideoStream.VideoRange == VideoRange.SDR
 278                && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
 279            {
 280                var playlistCodecsField = new StringBuilder();
 281                AppendPlaylistCodecsField(playlistCodecsField, state);
 282
 283                // Force the video level to 5.0.
 284                var originalLevel = state.VideoStream.Level;
 285                state.VideoStream.Level = 150;
 286                var newPlaylistCodecsField = new StringBuilder();
 287                AppendPlaylistCodecsField(newPlaylistCodecsField, state);
 288
 289                // Restore the video level.
 290                state.VideoStream.Level = originalLevel;
 291                var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField)
 292                builder.Append(newPlaylist);
 293            }
 294        }
 295
 296        if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.Htt
 297        {
 298            var requestedVideoBitrate = state.VideoRequest?.VideoBitRate ?? 0;
 299
 300            // By default, vary by just 200k
 301            var variation = GetBitrateVariation(totalBitrate);
 302
 303            var newBitrate = totalBitrate - variation;
 304            var variantQuery = playlistQuery;
 305            variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture);
 306            var variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery);
 307            AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
 308
 309            variation *= 2;
 310            newBitrate = totalBitrate - variation;
 311            variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture);
 312            variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery);
 313            AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
 314        }
 315
 316        if (!isLiveStream && (state.VideoRequest?.EnableTrickplay ?? false))
 317        {
 318            var sourceId = Guid.Parse(state.Request.MediaSourceId);
 319            var trickplayResolutions = await _trickplayManager.GetTrickplayResolutions(sourceId).ConfigureAwait(false);
 320            AddTrickplay(state, trickplayResolutions, builder, _httpContextAccessor.HttpContext.User);
 321        }
 322
 323        return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"))
 324    }
 325
 326    private StringBuilder AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subt
 327    {
 0328        var playlistBuilder = new StringBuilder();
 0329        playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
 0330            .Append(bitrate.ToString(CultureInfo.InvariantCulture))
 0331            .Append(",AVERAGE-BANDWIDTH=")
 0332            .Append(bitrate.ToString(CultureInfo.InvariantCulture));
 333
 0334        AppendPlaylistVideoRangeField(playlistBuilder, state);
 335
 0336        AppendPlaylistCodecsField(playlistBuilder, state);
 337
 0338        AppendPlaylistSupplementalCodecsField(playlistBuilder, state);
 339
 0340        AppendPlaylistResolutionField(playlistBuilder, state);
 341
 0342        AppendPlaylistFramerateField(playlistBuilder, state);
 343
 0344        if (!string.IsNullOrWhiteSpace(subtitleGroup))
 345        {
 0346            playlistBuilder.Append(",SUBTITLES=\"")
 0347                .Append(subtitleGroup)
 0348                .Append('"');
 349        }
 350
 0351        playlistBuilder.Append(Environment.NewLine);
 0352        playlistBuilder.AppendLine(url);
 0353        builder.Append(playlistBuilder);
 354
 0355        return playlistBuilder;
 356    }
 357
 358    /// <summary>
 359    /// Appends a VIDEO-RANGE field containing the range of the output video stream.
 360    /// </summary>
 361    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 362    /// <param name="builder">StringBuilder to append the field to.</param>
 363    /// <param name="state">StreamState of the current stream.</param>
 364    private void AppendPlaylistVideoRangeField(StringBuilder builder, StreamState state)
 365    {
 0366        if (state.VideoStream is not null && state.VideoStream.VideoRange != VideoRange.Unknown)
 367        {
 0368            var videoRange = state.VideoStream.VideoRange;
 0369            var videoRangeType = state.VideoStream.VideoRangeType;
 0370            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
 371            {
 0372                if (videoRange == VideoRange.SDR)
 373                {
 0374                    builder.Append(",VIDEO-RANGE=SDR");
 375                }
 376
 0377                if (videoRange == VideoRange.HDR)
 378                {
 379                    switch (videoRangeType)
 380                    {
 381                        case VideoRangeType.HLG:
 382                        case VideoRangeType.DOVIWithHLG:
 0383                            builder.Append(",VIDEO-RANGE=HLG");
 0384                            break;
 385                        default:
 0386                            builder.Append(",VIDEO-RANGE=PQ");
 0387                            break;
 388                    }
 389                }
 390            }
 391            else
 392            {
 393                // Currently we only encode to SDR.
 0394                builder.Append(",VIDEO-RANGE=SDR");
 395            }
 396        }
 0397    }
 398
 399    /// <summary>
 400    /// Appends a CODECS field containing formatted strings of
 401    /// the active streams output video and audio codecs.
 402    /// </summary>
 403    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 404    /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
 405    /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
 406    /// <param name="builder">StringBuilder to append the field to.</param>
 407    /// <param name="state">StreamState of the current stream.</param>
 408    private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
 409    {
 410        // Video
 0411        string videoCodecs = string.Empty;
 0412        int? videoCodecLevel = GetOutputVideoCodecLevel(state);
 0413        if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue)
 414        {
 0415            videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
 416        }
 417
 418        // Audio
 0419        string audioCodecs = string.Empty;
 0420        if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
 421        {
 0422            audioCodecs = GetPlaylistAudioCodecs(state);
 423        }
 424
 0425        StringBuilder codecs = new StringBuilder();
 426
 0427        codecs.Append(videoCodecs);
 428
 0429        if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs))
 430        {
 0431            codecs.Append(',');
 432        }
 433
 0434        codecs.Append(audioCodecs);
 435
 0436        if (codecs.Length > 1)
 437        {
 0438            builder.Append(",CODECS=\"")
 0439                .Append(codecs)
 0440                .Append('"');
 441        }
 0442    }
 443
 444    /// <summary>
 445    /// Appends a SUPPLEMENTAL-CODECS field containing formatted strings of
 446    /// the active streams output Dolby Vision Videos.
 447    /// </summary>
 448    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 449    /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
 450    /// <param name="builder">StringBuilder to append the field to.</param>
 451    /// <param name="state">StreamState of the current stream.</param>
 452    private void AppendPlaylistSupplementalCodecsField(StringBuilder builder, StreamState state)
 453    {
 454        // HDR dynamic metadata currently cannot exist when transcoding
 0455        if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
 456        {
 0457            return;
 458        }
 459
 0460        if (EncodingHelper.IsDovi(state.VideoStream) && !_encodingHelper.IsDoviRemoved(state))
 461        {
 0462            AppendDvString();
 463        }
 0464        else if (EncodingHelper.IsHdr10Plus(state.VideoStream) && !_encodingHelper.IsHdr10PlusRemoved(state))
 465        {
 0466            AppendHdr10PlusString();
 467        }
 468
 0469        return;
 470
 471        void AppendDvString()
 472        {
 473            var dvProfile = state.VideoStream.DvProfile;
 474            var dvLevel = state.VideoStream.DvLevel;
 475            var dvRangeString = state.VideoStream.VideoRangeType switch
 476            {
 477                VideoRangeType.DOVIWithHDR10 => "db1p",
 478                VideoRangeType.DOVIWithHLG => "db4h",
 479                VideoRangeType.DOVIWithHDR10Plus => "db1p", // The HDR10+ metadata would be removed if Dovi metadata is 
 480                _ => string.Empty // Don't label Dovi with EL and SDR due to compatability issues, ignore invalid config
 481            };
 482
 483            if (dvProfile is null || dvLevel is null || string.IsNullOrEmpty(dvRangeString))
 484            {
 485                return;
 486            }
 487
 488            var dvFourCc = string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase) ? "dav
 489            builder.Append(",SUPPLEMENTAL-CODECS=\"")
 490                .Append(dvFourCc)
 491                .Append('.')
 492                .Append(dvProfile.Value.ToString("D2", CultureInfo.InvariantCulture))
 493                .Append('.')
 494                .Append(dvLevel.Value.ToString("D2", CultureInfo.InvariantCulture))
 495                .Append('/')
 496                .Append(dvRangeString)
 497                .Append('"');
 498        }
 499
 500        void AppendHdr10PlusString()
 501        {
 502            var videoCodecLevel = GetOutputVideoCodecLevel(state);
 503            if (string.IsNullOrEmpty(state.ActualOutputVideoCodec) || videoCodecLevel is null)
 504            {
 505                return;
 506            }
 507
 508            var videoCodecString = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
 509            builder.Append(",SUPPLEMENTAL-CODECS=\"")
 510                .Append(videoCodecString)
 511                .Append('/')
 512                .Append("cdm4")
 513                .Append('"');
 514        }
 515    }
 516
 517    /// <summary>
 518    /// Appends a RESOLUTION field containing the resolution of the output stream.
 519    /// </summary>
 520    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 521    /// <param name="builder">StringBuilder to append the field to.</param>
 522    /// <param name="state">StreamState of the current stream.</param>
 523    private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state)
 524    {
 0525        if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
 526        {
 0527            builder.Append(",RESOLUTION=")
 0528                .Append(state.OutputWidth.GetValueOrDefault())
 0529                .Append('x')
 0530                .Append(state.OutputHeight.GetValueOrDefault());
 531        }
 0532    }
 533
 534    /// <summary>
 535    /// Appends a FRAME-RATE field containing the framerate of the output stream.
 536    /// </summary>
 537    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 538    /// <param name="builder">StringBuilder to append the field to.</param>
 539    /// <param name="state">StreamState of the current stream.</param>
 540    private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state)
 541    {
 0542        double? framerate = null;
 0543        if (state.TargetFramerate.HasValue)
 544        {
 0545            framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
 546        }
 0547        else if (state.VideoStream?.RealFrameRate is not null)
 548        {
 0549            framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
 550        }
 551
 0552        if (framerate.HasValue)
 553        {
 0554            builder.Append(",FRAME-RATE=")
 0555                .Append(framerate.Value.ToString(CultureInfo.InvariantCulture));
 556        }
 0557    }
 558
 559    private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreamin
 560    {
 561        // Within the local network this will likely do more harm than good.
 0562        if (_networkManager.IsInLocalNetwork(ipAddress))
 563        {
 0564            return false;
 565        }
 566
 0567        if (!enableAdaptiveBitrateStreaming)
 568        {
 0569            return false;
 570        }
 571
 0572        if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
 573        {
 574            // Opening live streams is so slow it's not even worth it
 0575            return false;
 576        }
 577
 0578        if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
 579        {
 0580            return false;
 581        }
 582
 0583        if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
 584        {
 0585            return false;
 586        }
 587
 0588        if (!state.IsOutputVideo)
 589        {
 0590            return false;
 591        }
 592
 0593        return state.VideoRequest?.VideoBitRate.HasValue ?? false;
 594    }
 595
 596    private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder, ClaimsPrinci
 597    {
 0598        if (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Drop)
 599        {
 0600            return;
 601        }
 602
 0603        var selectedIndex = state.SubtitleStream is null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ?
 604        const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSEL
 605
 0606        foreach (var stream in subtitles)
 607        {
 0608            var name = stream.DisplayTitle;
 609
 0610            var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
 0611            var isForced = stream.IsForced;
 612
 0613            var url = string.Format(
 0614                CultureInfo.InvariantCulture,
 0615                "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&ApiKey={3}",
 0616                state.Request.MediaSourceId,
 0617                stream.Index.ToString(CultureInfo.InvariantCulture),
 0618                30.ToString(CultureInfo.InvariantCulture),
 0619                user.GetToken());
 620
 0621            var line = string.Format(
 0622                CultureInfo.InvariantCulture,
 0623                Format,
 0624                name,
 0625                isDefault ? "YES" : "NO",
 0626                isForced ? "YES" : "NO",
 0627                url,
 0628                stream.Language ?? "Unknown");
 629
 0630            builder.AppendLine(line);
 631        }
 0632    }
 633
 634    /// <summary>
 635    /// Appends EXT-X-IMAGE-STREAM-INF playlists for each available trickplay resolution.
 636    /// </summary>
 637    /// <param name="state">StreamState of the current stream.</param>
 638    /// <param name="trickplayResolutions">Dictionary of widths to corresponding tiles info.</param>
 639    /// <param name="builder">StringBuilder to append the field to.</param>
 640    /// <param name="user">Http user context.</param>
 641    private void AddTrickplay(StreamState state, Dictionary<int, TrickplayInfo> trickplayResolutions, StringBuilder buil
 642    {
 643        const string playlistFormat = "#EXT-X-IMAGE-STREAM-INF:BANDWIDTH={0},RESOLUTION={1}x{2},CODECS=\"jpeg\",URI=\"{3
 644
 0645        foreach (var resolution in trickplayResolutions)
 646        {
 0647            var width = resolution.Key;
 0648            var trickplayInfo = resolution.Value;
 649
 0650            var url = string.Format(
 0651                CultureInfo.InvariantCulture,
 0652                "Trickplay/{0}/tiles.m3u8?MediaSourceId={1}&ApiKey={2}",
 0653                width.ToString(CultureInfo.InvariantCulture),
 0654                state.Request.MediaSourceId,
 0655                user.GetToken());
 656
 0657            builder.AppendFormat(
 0658                CultureInfo.InvariantCulture,
 0659                playlistFormat,
 0660                trickplayInfo.Bandwidth.ToString(CultureInfo.InvariantCulture),
 0661                trickplayInfo.Width.ToString(CultureInfo.InvariantCulture),
 0662                trickplayInfo.Height.ToString(CultureInfo.InvariantCulture),
 0663                url);
 664
 0665            builder.AppendLine();
 666        }
 0667    }
 668
 669    /// <summary>
 670    /// Get the H.26X level of the output video stream.
 671    /// </summary>
 672    /// <param name="state">StreamState of the current stream.</param>
 673    /// <returns>H.26X level of the output video stream.</returns>
 674    private int? GetOutputVideoCodecLevel(StreamState state)
 675    {
 0676        string levelString = string.Empty;
 0677        if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 0678            && state.VideoStream is not null
 0679            && state.VideoStream.Level.HasValue)
 680        {
 0681            levelString = state.VideoStream.Level.Value.ToString(CultureInfo.InvariantCulture);
 682        }
 683        else
 684        {
 0685            if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
 686            {
 0687                levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec) ?? "41";
 0688                levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
 689            }
 690
 0691            if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
 0692                || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
 693            {
 0694                levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120";
 0695                levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
 696            }
 697
 0698            if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
 699            {
 0700                levelString = state.GetRequestedLevel("av1") ?? "19";
 0701                levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
 702            }
 703        }
 704
 0705        if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
 706        {
 0707            return parsedLevel;
 708        }
 709
 0710        return null;
 711    }
 712
 713    /// <summary>
 714    /// Get the profile of the output video stream.
 715    /// </summary>
 716    /// <param name="state">StreamState of the current stream.</param>
 717    /// <param name="codec">Video codec.</param>
 718    /// <returns>Profile of the output video stream.</returns>
 719    private string GetOutputVideoCodecProfile(StreamState state, string codec)
 720    {
 0721        string profileString = string.Empty;
 0722        if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 0723            && !string.IsNullOrEmpty(state.VideoStream.Profile))
 724        {
 0725            profileString = state.VideoStream.Profile;
 726        }
 0727        else if (!string.IsNullOrEmpty(codec))
 728        {
 0729            profileString = state.GetRequestedProfiles(codec).FirstOrDefault() ?? string.Empty;
 0730            if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
 731            {
 0732                profileString ??= "high";
 733            }
 734
 0735            if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
 0736                || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
 0737                || string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
 738            {
 0739                profileString ??= "main";
 740            }
 741        }
 742
 0743        return profileString;
 744    }
 745
 746    /// <summary>
 747    /// Gets a formatted string of the output audio codec, for use in the CODECS field.
 748    /// </summary>
 749    /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
 750    /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
 751    /// <param name="state">StreamState of the current stream.</param>
 752    /// <returns>Formatted audio codec string.</returns>
 753    private string GetPlaylistAudioCodecs(StreamState state)
 754    {
 0755        if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
 756        {
 0757            string? profile = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
 0758                ? state.AudioStream?.Profile : state.GetRequestedProfiles("aac").FirstOrDefault();
 759
 0760            return HlsCodecStringHelpers.GetAACString(profile);
 761        }
 762
 0763        if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
 764        {
 0765            return HlsCodecStringHelpers.GetMP3String();
 766        }
 767
 0768        if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
 769        {
 0770            return HlsCodecStringHelpers.GetAC3String();
 771        }
 772
 0773        if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
 774        {
 0775            return HlsCodecStringHelpers.GetEAC3String();
 776        }
 777
 0778        if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
 779        {
 0780            return HlsCodecStringHelpers.GetFLACString();
 781        }
 782
 0783        if (string.Equals(state.ActualOutputAudioCodec, "alac", StringComparison.OrdinalIgnoreCase))
 784        {
 0785            return HlsCodecStringHelpers.GetALACString();
 786        }
 787
 0788        if (string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
 789        {
 0790            return HlsCodecStringHelpers.GetOPUSString();
 791        }
 792
 0793        if (string.Equals(state.ActualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
 794        {
 0795            return HlsCodecStringHelpers.GetTRUEHDString();
 796        }
 797
 0798        if (string.Equals(state.ActualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase))
 799        {
 800            // lavc only support encoding DTS core profile
 0801            string? profile = EncodingHelper.IsCopyCodec(state.OutputAudioCodec) ? state.AudioStream?.Profile : "DTS";
 802
 0803            return HlsCodecStringHelpers.GetDTSString(profile);
 804        }
 805
 0806        return string.Empty;
 807    }
 808
 809    /// <summary>
 810    /// Gets a formatted string of the output video codec, for use in the CODECS field.
 811    /// </summary>
 812    /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
 813    /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
 814    /// <param name="state">StreamState of the current stream.</param>
 815    /// <param name="codec">Video codec.</param>
 816    /// <param name="level">Video level.</param>
 817    /// <returns>Formatted video codec string.</returns>
 818    private string GetPlaylistVideoCodecs(StreamState state, string codec, int level)
 819    {
 0820        if (level == 0)
 821        {
 822            // This is 0 when there's no requested level in the device profile
 823            // and the source is not encoded in H.26X or AV1
 0824            _logger.LogError("Got invalid level when building CODECS field for HLS master playlist");
 0825            return string.Empty;
 826        }
 827
 0828        if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
 829        {
 0830            string profile = GetOutputVideoCodecProfile(state, "h264");
 0831            return HlsCodecStringHelpers.GetH264String(profile, level);
 832        }
 833
 0834        if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
 0835            || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
 836        {
 0837            string profile = GetOutputVideoCodecProfile(state, "hevc");
 0838            return HlsCodecStringHelpers.GetH265String(profile, level);
 839        }
 840
 0841        if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
 842        {
 0843            string profile = GetOutputVideoCodecProfile(state, "av1");
 844
 845            // Currently we only transcode to 8 bits AV1
 0846            int bitDepth = 8;
 0847            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 0848                && state.VideoStream is not null
 0849                && state.VideoStream.BitDepth.HasValue)
 850            {
 0851                bitDepth = state.VideoStream.BitDepth.Value;
 852            }
 853
 0854            return HlsCodecStringHelpers.GetAv1String(profile, level, false, bitDepth);
 855        }
 856
 857        // VP9 HLS is for video remuxing only, everything is probed from the original video
 0858        if (string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
 859        {
 0860            var width = state.VideoStream.Width ?? 0;
 0861            var height = state.VideoStream.Height ?? 0;
 0862            var framerate = state.VideoStream.ReferenceFrameRate ?? 30;
 0863            var bitDepth = state.VideoStream.BitDepth ?? 8;
 0864            return HlsCodecStringHelpers.GetVp9String(
 0865                width,
 0866                height,
 0867                state.VideoStream.PixelFormat,
 0868                framerate,
 0869                bitDepth);
 870        }
 871
 0872        return string.Empty;
 873    }
 874
 875    private int GetBitrateVariation(int bitrate)
 876    {
 877        // By default, vary by just 50k
 0878        var variation = 50000;
 879
 0880        if (bitrate >= 10000000)
 881        {
 0882            variation = 2000000;
 883        }
 0884        else if (bitrate >= 5000000)
 885        {
 0886            variation = 1500000;
 887        }
 0888        else if (bitrate >= 3000000)
 889        {
 0890            variation = 1000000;
 891        }
 0892        else if (bitrate >= 2000000)
 893        {
 0894            variation = 500000;
 895        }
 0896        else if (bitrate >= 1000000)
 897        {
 0898            variation = 300000;
 899        }
 0900        else if (bitrate >= 600000)
 901        {
 0902            variation = 200000;
 903        }
 0904        else if (bitrate >= 400000)
 905        {
 0906            variation = 100000;
 907        }
 908
 0909        return variation;
 910    }
 911
 912    private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
 913    {
 0914        var oldPlaylist = playlist.ToString();
 0915        return oldPlaylist.Replace(
 0916            oldValue.ToString(),
 0917            newValue.ToString(),
 0918            StringComparison.Ordinal);
 919    }
 920}

Methods/Properties

.ctor(MediaBrowser.Controller.Library.ILibraryManager,MediaBrowser.Controller.Library.IUserManager,MediaBrowser.Controller.Library.IMediaSourceManager,MediaBrowser.Controller.Configuration.IServerConfigurationManager,MediaBrowser.Controller.MediaEncoding.IMediaEncoder,MediaBrowser.Controller.MediaEncoding.ITranscodeManager,MediaBrowser.Common.Net.INetworkManager,Microsoft.Extensions.Logging.ILogger`1<Jellyfin.Api.Helpers.DynamicHlsHelper>,Microsoft.AspNetCore.Http.IHttpContextAccessor,MediaBrowser.Controller.MediaEncoding.EncodingHelper,MediaBrowser.Controller.Trickplay.ITrickplayManager)
AppendPlaylist(System.Text.StringBuilder,MediaBrowser.Controller.Streaming.StreamState,System.String,System.Int32,System.String)
AppendPlaylistVideoRangeField(System.Text.StringBuilder,MediaBrowser.Controller.Streaming.StreamState)
AppendPlaylistCodecsField(System.Text.StringBuilder,MediaBrowser.Controller.Streaming.StreamState)
AppendPlaylistSupplementalCodecsField(System.Text.StringBuilder,MediaBrowser.Controller.Streaming.StreamState)
AppendPlaylistResolutionField(System.Text.StringBuilder,MediaBrowser.Controller.Streaming.StreamState)
AppendPlaylistFramerateField(System.Text.StringBuilder,MediaBrowser.Controller.Streaming.StreamState)
EnableAdaptiveBitrateStreaming(MediaBrowser.Controller.Streaming.StreamState,System.Boolean,System.Boolean,System.Net.IPAddress)
AddSubtitles(MediaBrowser.Controller.Streaming.StreamState,System.Collections.Generic.IEnumerable`1<MediaBrowser.Model.Entities.MediaStream>,System.Text.StringBuilder,System.Security.Claims.ClaimsPrincipal)
AddTrickplay(MediaBrowser.Controller.Streaming.StreamState,System.Collections.Generic.Dictionary`2<System.Int32,Jellyfin.Database.Implementations.Entities.TrickplayInfo>,System.Text.StringBuilder,System.Security.Claims.ClaimsPrincipal)
GetOutputVideoCodecLevel(MediaBrowser.Controller.Streaming.StreamState)
GetOutputVideoCodecProfile(MediaBrowser.Controller.Streaming.StreamState,System.String)
GetPlaylistAudioCodecs(MediaBrowser.Controller.Streaming.StreamState)
GetPlaylistVideoCodecs(MediaBrowser.Controller.Streaming.StreamState,System.String,System.Int32)
GetBitrateVariation(System.Int32)
ReplacePlaylistCodecsField(System.Text.StringBuilder,System.Text.StringBuilder,System.Text.StringBuilder)