< 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: 270
Coverable lines: 270
Total lines: 998
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 196
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 12/27/2025 - 12:11:51 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: 9203/29/2026 - 12:13:48 AM Line coverage: 0% (0/270) Branch coverage: 0% (0/196) Total lines: 998 12/27/2025 - 12:11:51 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: 9203/29/2026 - 12:13:48 AM Line coverage: 0% (0/270) Branch coverage: 0% (0/196) Total lines: 998

Coverage delta

Coverage delta 1 -1

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        // For DoVi profiles without a compatible base layer (P5 HEVC, P10/bl0 AV1),
 213        // add a spec-compliant dvh1/dav1 variant before the hvc1 hack variant.
 214        // SUPPLEMENTAL-CODECS cannot be used for these profiles (no compatible BL to supplement).
 215        // The DoVi variant is listed first so spec-compliant clients (Apple TV, webOS 24+)
 216        // select it over the fallback when both have identical BANDWIDTH.
 217        // Only emit for clients that explicitly declared DOVI support to avoid breaking
 218        // non-compliant players that don't recognize dvh1/dav1 CODECS strings.
 219        if (state.VideoStream is not null
 220            && state.VideoRequest is not null
 221            && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 222            && state.VideoStream.VideoRangeType == VideoRangeType.DOVI
 223            && state.VideoStream.DvProfile.HasValue
 224            && state.VideoStream.DvLevel.HasValue
 225            && state.GetRequestedRangeTypes(state.VideoStream.Codec)
 226                .Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase))
 227        {
 228            AppendDoviPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
 229        }
 230
 231        var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
 232
 233        if (state.VideoStream is not null && state.VideoRequest is not null)
 234        {
 235            var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
 236
 237            // Provide AV1 and HEVC SDR entrances for backward compatibility.
 238            foreach (var sdrVideoCodec in new[] { "av1", "hevc" })
 239            {
 240                var isAv1EncodingAllowed = encodingOptions.AllowAv1Encoding
 241                    && string.Equals(sdrVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)
 242                    && string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase);
 243                var isHevcEncodingAllowed = encodingOptions.AllowHevcEncoding
 244                    && string.Equals(sdrVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
 245                    && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase);
 246                var isEncodingAllowed = isAv1EncodingAllowed || isHevcEncodingAllowed;
 247
 248                if (isEncodingAllowed
 249                    && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 250                    && state.VideoStream.VideoRange == VideoRange.HDR)
 251                {
 252                    // Force AV1 and HEVC Main Profile and disable video stream copy.
 253                    state.OutputVideoCodec = sdrVideoCodec;
 254
 255                    var sdrPlaylistQuery = playlistQuery;
 256                    sdrPlaylistQuery["VideoCodec"] = sdrVideoCodec;
 257                    sdrPlaylistQuery[sdrVideoCodec + "-profile"] = "main";
 258                    sdrPlaylistQuery["AllowVideoStreamCopy"] = "false";
 259
 260                    var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylist
 261
 262                    // HACK: Use the same bitrate so that the client can choose by other attributes, such as color range
 263                    AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
 264
 265                    // Restore the video codec
 266                    state.OutputVideoCodec = "copy";
 267                }
 268            }
 269
 270            // Provide H.264 SDR entrance for backward compatibility.
 271            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 272                && state.VideoStream.VideoRange == VideoRange.HDR)
 273            {
 274                // Force H.264 and disable video stream copy.
 275                state.OutputVideoCodec = "h264";
 276
 277                var sdrPlaylistQuery = playlistQuery;
 278                sdrPlaylistQuery["VideoCodec"] = "h264";
 279                sdrPlaylistQuery["AllowVideoStreamCopy"] = "false";
 280
 281                var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylistQuer
 282
 283                // HACK: Use the same bitrate so that the client can choose by other attributes, such as color range.
 284                AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
 285
 286                // Restore the video codec
 287                state.OutputVideoCodec = "copy";
 288            }
 289
 290            // Provide Level 5.0 entrance for backward compatibility.
 291            // e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
 292            // but in fact it is capable of playing videos up to Level 6.1.
 293            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 294                && state.VideoStream.Level.HasValue
 295                && state.VideoStream.Level > 150
 296                && state.VideoStream.VideoRange == VideoRange.SDR
 297                && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
 298            {
 299                var playlistCodecsField = new StringBuilder();
 300                AppendPlaylistCodecsField(playlistCodecsField, state);
 301
 302                // Force the video level to 5.0.
 303                var originalLevel = state.VideoStream.Level;
 304                state.VideoStream.Level = 150;
 305                var newPlaylistCodecsField = new StringBuilder();
 306                AppendPlaylistCodecsField(newPlaylistCodecsField, state);
 307
 308                // Restore the video level.
 309                state.VideoStream.Level = originalLevel;
 310                var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField)
 311                builder.Append(newPlaylist);
 312            }
 313        }
 314
 315        if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.Htt
 316        {
 317            var requestedVideoBitrate = state.VideoRequest?.VideoBitRate ?? 0;
 318
 319            // By default, vary by just 200k
 320            var variation = GetBitrateVariation(totalBitrate);
 321
 322            var newBitrate = totalBitrate - variation;
 323            var variantQuery = playlistQuery;
 324            variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture);
 325            var variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery);
 326            AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
 327
 328            variation *= 2;
 329            newBitrate = totalBitrate - variation;
 330            variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture);
 331            variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery);
 332            AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
 333        }
 334
 335        if (!isLiveStream && (state.VideoRequest?.EnableTrickplay ?? false))
 336        {
 337            var sourceId = Guid.Parse(state.Request.MediaSourceId);
 338            var trickplayResolutions = await _trickplayManager.GetTrickplayResolutions(sourceId).ConfigureAwait(false);
 339            AddTrickplay(state, trickplayResolutions, builder, _httpContextAccessor.HttpContext.User);
 340        }
 341
 342        return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"))
 343    }
 344
 345    private StringBuilder AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subt
 346    {
 0347        var playlistBuilder = new StringBuilder();
 0348        playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
 0349            .Append(bitrate.ToString(CultureInfo.InvariantCulture))
 0350            .Append(",AVERAGE-BANDWIDTH=")
 0351            .Append(bitrate.ToString(CultureInfo.InvariantCulture));
 352
 0353        AppendPlaylistVideoRangeField(playlistBuilder, state);
 354
 0355        AppendPlaylistCodecsField(playlistBuilder, state);
 356
 0357        AppendPlaylistSupplementalCodecsField(playlistBuilder, state);
 358
 0359        AppendPlaylistResolutionField(playlistBuilder, state);
 360
 0361        AppendPlaylistFramerateField(playlistBuilder, state);
 362
 0363        if (!string.IsNullOrWhiteSpace(subtitleGroup))
 364        {
 0365            playlistBuilder.Append(",SUBTITLES=\"")
 0366                .Append(subtitleGroup)
 0367                .Append('"');
 368        }
 369
 0370        playlistBuilder.Append(Environment.NewLine);
 0371        playlistBuilder.AppendLine(url);
 0372        builder.Append(playlistBuilder);
 373
 0374        return playlistBuilder;
 375    }
 376
 377    /// <summary>
 378    /// Appends a Dolby Vision variant with dvh1/dav1 CODECS for profiles without a compatible
 379    /// base layer (P5 HEVC, P10/bl0 AV1). This enables spec-compliant HLS clients to detect
 380    /// DoVi from the manifest rather than relying on init segment inspection.
 381    /// </summary>
 382    /// <param name="builder">StringBuilder for the master playlist.</param>
 383    /// <param name="state">StreamState of the current stream.</param>
 384    /// <param name="url">Playlist URL for this variant.</param>
 385    /// <param name="bitrate">Bitrate for the BANDWIDTH field.</param>
 386    /// <param name="subtitleGroup">Subtitle group identifier, or null.</param>
 387    private void AppendDoviPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleG
 388    {
 0389        var dvProfile = state.VideoStream.DvProfile;
 0390        var dvLevel = state.VideoStream.DvLevel;
 0391        if (dvProfile is null || dvLevel is null)
 392        {
 0393            return;
 394        }
 395
 0396        var playlistBuilder = new StringBuilder();
 0397        playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
 0398            .Append(bitrate.ToString(CultureInfo.InvariantCulture))
 0399            .Append(",AVERAGE-BANDWIDTH=")
 0400            .Append(bitrate.ToString(CultureInfo.InvariantCulture));
 401
 0402        playlistBuilder.Append(",VIDEO-RANGE=PQ");
 403
 0404        var dvCodec = HlsCodecStringHelpers.GetDoviString(dvProfile.Value, dvLevel.Value, state.ActualOutputVideoCodec);
 405
 0406        string audioCodecs = string.Empty;
 0407        if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
 408        {
 0409            audioCodecs = GetPlaylistAudioCodecs(state);
 410        }
 411
 0412        playlistBuilder.Append(",CODECS=\"")
 0413            .Append(dvCodec);
 0414        if (!string.IsNullOrEmpty(audioCodecs))
 415        {
 0416            playlistBuilder.Append(',').Append(audioCodecs);
 417        }
 418
 0419        playlistBuilder.Append('"');
 420
 0421        AppendPlaylistResolutionField(playlistBuilder, state);
 0422        AppendPlaylistFramerateField(playlistBuilder, state);
 423
 0424        if (!string.IsNullOrWhiteSpace(subtitleGroup))
 425        {
 0426            playlistBuilder.Append(",SUBTITLES=\"")
 0427                .Append(subtitleGroup)
 0428                .Append('"');
 429        }
 430
 0431        playlistBuilder.AppendLine();
 0432        playlistBuilder.AppendLine(url);
 0433        builder.Append(playlistBuilder);
 0434    }
 435
 436    /// <summary>
 437    /// Appends a VIDEO-RANGE field containing the range of the output video stream.
 438    /// </summary>
 439    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 440    /// <param name="builder">StringBuilder to append the field to.</param>
 441    /// <param name="state">StreamState of the current stream.</param>
 442    private void AppendPlaylistVideoRangeField(StringBuilder builder, StreamState state)
 443    {
 0444        if (state.VideoStream is not null && state.VideoStream.VideoRange != VideoRange.Unknown)
 445        {
 0446            var videoRange = state.VideoStream.VideoRange;
 0447            var videoRangeType = state.VideoStream.VideoRangeType;
 0448            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
 449            {
 0450                if (videoRange == VideoRange.SDR)
 451                {
 0452                    builder.Append(",VIDEO-RANGE=SDR");
 453                }
 454
 0455                if (videoRange == VideoRange.HDR)
 456                {
 457                    switch (videoRangeType)
 458                    {
 459                        case VideoRangeType.HLG:
 460                        case VideoRangeType.DOVIWithHLG:
 0461                            builder.Append(",VIDEO-RANGE=HLG");
 0462                            break;
 463                        default:
 0464                            builder.Append(",VIDEO-RANGE=PQ");
 0465                            break;
 466                    }
 467                }
 468            }
 469            else
 470            {
 471                // Currently we only encode to SDR.
 0472                builder.Append(",VIDEO-RANGE=SDR");
 473            }
 474        }
 0475    }
 476
 477    /// <summary>
 478    /// Appends a CODECS field containing formatted strings of
 479    /// the active streams output video and audio codecs.
 480    /// </summary>
 481    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 482    /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
 483    /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
 484    /// <param name="builder">StringBuilder to append the field to.</param>
 485    /// <param name="state">StreamState of the current stream.</param>
 486    private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
 487    {
 488        // Video
 0489        string videoCodecs = string.Empty;
 0490        int? videoCodecLevel = GetOutputVideoCodecLevel(state);
 0491        if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue)
 492        {
 0493            videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
 494        }
 495
 496        // Audio
 0497        string audioCodecs = string.Empty;
 0498        if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
 499        {
 0500            audioCodecs = GetPlaylistAudioCodecs(state);
 501        }
 502
 0503        StringBuilder codecs = new StringBuilder();
 504
 0505        codecs.Append(videoCodecs);
 506
 0507        if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs))
 508        {
 0509            codecs.Append(',');
 510        }
 511
 0512        codecs.Append(audioCodecs);
 513
 0514        if (codecs.Length > 1)
 515        {
 0516            builder.Append(",CODECS=\"")
 0517                .Append(codecs)
 0518                .Append('"');
 519        }
 0520    }
 521
 522    /// <summary>
 523    /// Appends a SUPPLEMENTAL-CODECS field containing formatted strings of
 524    /// the active streams output Dolby Vision Videos.
 525    /// </summary>
 526    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 527    /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
 528    /// <param name="builder">StringBuilder to append the field to.</param>
 529    /// <param name="state">StreamState of the current stream.</param>
 530    private void AppendPlaylistSupplementalCodecsField(StringBuilder builder, StreamState state)
 531    {
 532        // HDR dynamic metadata currently cannot exist when transcoding
 0533        if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
 534        {
 0535            return;
 536        }
 537
 0538        if (EncodingHelper.IsDovi(state.VideoStream) && !_encodingHelper.IsDoviRemoved(state))
 539        {
 0540            AppendDvString();
 541        }
 0542        else if (EncodingHelper.IsHdr10Plus(state.VideoStream) && !_encodingHelper.IsHdr10PlusRemoved(state))
 543        {
 0544            AppendHdr10PlusString();
 545        }
 546
 0547        return;
 548
 549        void AppendDvString()
 550        {
 551            var dvProfile = state.VideoStream.DvProfile;
 552            var dvLevel = state.VideoStream.DvLevel;
 553            var dvRangeString = state.VideoStream.VideoRangeType switch
 554            {
 555                VideoRangeType.DOVIWithHDR10 => "db1p",
 556                VideoRangeType.DOVIWithHLG => "db4h",
 557                VideoRangeType.DOVIWithHDR10Plus => "db1p", // The HDR10+ metadata would be removed if Dovi metadata is 
 558                _ => string.Empty // Don't label Dovi with EL and SDR due to compatability issues, ignore invalid config
 559            };
 560
 561            if (dvProfile is null || dvLevel is null || string.IsNullOrEmpty(dvRangeString))
 562            {
 563                return;
 564            }
 565
 566            var dvFourCc = string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase) ? "dav
 567            builder.Append(",SUPPLEMENTAL-CODECS=\"")
 568                .Append(dvFourCc)
 569                .Append('.')
 570                .Append(dvProfile.Value.ToString("D2", CultureInfo.InvariantCulture))
 571                .Append('.')
 572                .Append(dvLevel.Value.ToString("D2", CultureInfo.InvariantCulture))
 573                .Append('/')
 574                .Append(dvRangeString)
 575                .Append('"');
 576        }
 577
 578        void AppendHdr10PlusString()
 579        {
 580            var videoCodecLevel = GetOutputVideoCodecLevel(state);
 581            if (string.IsNullOrEmpty(state.ActualOutputVideoCodec) || videoCodecLevel is null)
 582            {
 583                return;
 584            }
 585
 586            var videoCodecString = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
 587            builder.Append(",SUPPLEMENTAL-CODECS=\"")
 588                .Append(videoCodecString)
 589                .Append('/')
 590                .Append("cdm4")
 591                .Append('"');
 592        }
 593    }
 594
 595    /// <summary>
 596    /// Appends a RESOLUTION field containing the resolution of the output stream.
 597    /// </summary>
 598    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 599    /// <param name="builder">StringBuilder to append the field to.</param>
 600    /// <param name="state">StreamState of the current stream.</param>
 601    private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state)
 602    {
 0603        if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
 604        {
 0605            builder.Append(",RESOLUTION=")
 0606                .Append(state.OutputWidth.GetValueOrDefault())
 0607                .Append('x')
 0608                .Append(state.OutputHeight.GetValueOrDefault());
 609        }
 0610    }
 611
 612    /// <summary>
 613    /// Appends a FRAME-RATE field containing the framerate of the output stream.
 614    /// </summary>
 615    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 616    /// <param name="builder">StringBuilder to append the field to.</param>
 617    /// <param name="state">StreamState of the current stream.</param>
 618    private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state)
 619    {
 0620        double? framerate = null;
 0621        if (state.TargetFramerate.HasValue)
 622        {
 0623            framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
 624        }
 0625        else if (state.VideoStream?.RealFrameRate is not null)
 626        {
 0627            framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
 628        }
 629
 0630        if (framerate.HasValue)
 631        {
 0632            builder.Append(",FRAME-RATE=")
 0633                .Append(framerate.Value.ToString(CultureInfo.InvariantCulture));
 634        }
 0635    }
 636
 637    private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreamin
 638    {
 639        // Within the local network this will likely do more harm than good.
 0640        if (_networkManager.IsInLocalNetwork(ipAddress))
 641        {
 0642            return false;
 643        }
 644
 0645        if (!enableAdaptiveBitrateStreaming)
 646        {
 0647            return false;
 648        }
 649
 0650        if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
 651        {
 652            // Opening live streams is so slow it's not even worth it
 0653            return false;
 654        }
 655
 0656        if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
 657        {
 0658            return false;
 659        }
 660
 0661        if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
 662        {
 0663            return false;
 664        }
 665
 0666        if (!state.IsOutputVideo)
 667        {
 0668            return false;
 669        }
 670
 0671        return state.VideoRequest?.VideoBitRate.HasValue ?? false;
 672    }
 673
 674    private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder, ClaimsPrinci
 675    {
 0676        if (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Drop)
 677        {
 0678            return;
 679        }
 680
 0681        var selectedIndex = state.SubtitleStream is null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ?
 682        const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSEL
 683
 0684        foreach (var stream in subtitles)
 685        {
 0686            var name = stream.DisplayTitle;
 687
 0688            var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
 0689            var isForced = stream.IsForced;
 690
 0691            var url = string.Format(
 0692                CultureInfo.InvariantCulture,
 0693                "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&ApiKey={3}",
 0694                state.Request.MediaSourceId,
 0695                stream.Index.ToString(CultureInfo.InvariantCulture),
 0696                30.ToString(CultureInfo.InvariantCulture),
 0697                user.GetToken());
 698
 0699            var line = string.Format(
 0700                CultureInfo.InvariantCulture,
 0701                Format,
 0702                name,
 0703                isDefault ? "YES" : "NO",
 0704                isForced ? "YES" : "NO",
 0705                url,
 0706                stream.Language ?? "Unknown");
 707
 0708            builder.AppendLine(line);
 709        }
 0710    }
 711
 712    /// <summary>
 713    /// Appends EXT-X-IMAGE-STREAM-INF playlists for each available trickplay resolution.
 714    /// </summary>
 715    /// <param name="state">StreamState of the current stream.</param>
 716    /// <param name="trickplayResolutions">Dictionary of widths to corresponding tiles info.</param>
 717    /// <param name="builder">StringBuilder to append the field to.</param>
 718    /// <param name="user">Http user context.</param>
 719    private void AddTrickplay(StreamState state, Dictionary<int, TrickplayInfo> trickplayResolutions, StringBuilder buil
 720    {
 721        const string playlistFormat = "#EXT-X-IMAGE-STREAM-INF:BANDWIDTH={0},RESOLUTION={1}x{2},CODECS=\"jpeg\",URI=\"{3
 722
 0723        foreach (var resolution in trickplayResolutions)
 724        {
 0725            var width = resolution.Key;
 0726            var trickplayInfo = resolution.Value;
 727
 0728            var url = string.Format(
 0729                CultureInfo.InvariantCulture,
 0730                "Trickplay/{0}/tiles.m3u8?MediaSourceId={1}&ApiKey={2}",
 0731                width.ToString(CultureInfo.InvariantCulture),
 0732                state.Request.MediaSourceId,
 0733                user.GetToken());
 734
 0735            builder.AppendFormat(
 0736                CultureInfo.InvariantCulture,
 0737                playlistFormat,
 0738                trickplayInfo.Bandwidth.ToString(CultureInfo.InvariantCulture),
 0739                trickplayInfo.Width.ToString(CultureInfo.InvariantCulture),
 0740                trickplayInfo.Height.ToString(CultureInfo.InvariantCulture),
 0741                url);
 742
 0743            builder.AppendLine();
 744        }
 0745    }
 746
 747    /// <summary>
 748    /// Get the H.26X level of the output video stream.
 749    /// </summary>
 750    /// <param name="state">StreamState of the current stream.</param>
 751    /// <returns>H.26X level of the output video stream.</returns>
 752    private int? GetOutputVideoCodecLevel(StreamState state)
 753    {
 0754        string levelString = string.Empty;
 0755        if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 0756            && state.VideoStream is not null
 0757            && state.VideoStream.Level.HasValue)
 758        {
 0759            levelString = state.VideoStream.Level.Value.ToString(CultureInfo.InvariantCulture);
 760        }
 761        else
 762        {
 0763            if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
 764            {
 0765                levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec) ?? "41";
 0766                levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
 767            }
 768
 0769            if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
 0770                || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
 771            {
 0772                levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120";
 0773                levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
 774            }
 775
 0776            if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
 777            {
 0778                levelString = state.GetRequestedLevel("av1") ?? "19";
 0779                levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
 780            }
 781        }
 782
 0783        if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
 784        {
 0785            return parsedLevel;
 786        }
 787
 0788        return null;
 789    }
 790
 791    /// <summary>
 792    /// Get the profile of the output video stream.
 793    /// </summary>
 794    /// <param name="state">StreamState of the current stream.</param>
 795    /// <param name="codec">Video codec.</param>
 796    /// <returns>Profile of the output video stream.</returns>
 797    private string GetOutputVideoCodecProfile(StreamState state, string codec)
 798    {
 0799        string profileString = string.Empty;
 0800        if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 0801            && !string.IsNullOrEmpty(state.VideoStream.Profile))
 802        {
 0803            profileString = state.VideoStream.Profile;
 804        }
 0805        else if (!string.IsNullOrEmpty(codec))
 806        {
 0807            profileString = state.GetRequestedProfiles(codec).FirstOrDefault() ?? string.Empty;
 0808            if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
 809            {
 0810                profileString ??= "high";
 811            }
 812
 0813            if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
 0814                || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
 0815                || string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
 816            {
 0817                profileString ??= "main";
 818            }
 819        }
 820
 0821        return profileString;
 822    }
 823
 824    /// <summary>
 825    /// Gets a formatted string of the output audio codec, for use in the CODECS field.
 826    /// </summary>
 827    /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
 828    /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
 829    /// <param name="state">StreamState of the current stream.</param>
 830    /// <returns>Formatted audio codec string.</returns>
 831    private string GetPlaylistAudioCodecs(StreamState state)
 832    {
 0833        if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
 834        {
 0835            string? profile = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
 0836                ? state.AudioStream?.Profile : state.GetRequestedProfiles("aac").FirstOrDefault();
 837
 0838            return HlsCodecStringHelpers.GetAACString(profile);
 839        }
 840
 0841        if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
 842        {
 0843            return HlsCodecStringHelpers.GetMP3String();
 844        }
 845
 0846        if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
 847        {
 0848            return HlsCodecStringHelpers.GetAC3String();
 849        }
 850
 0851        if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
 852        {
 0853            return HlsCodecStringHelpers.GetEAC3String();
 854        }
 855
 0856        if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
 857        {
 0858            return HlsCodecStringHelpers.GetFLACString();
 859        }
 860
 0861        if (string.Equals(state.ActualOutputAudioCodec, "alac", StringComparison.OrdinalIgnoreCase))
 862        {
 0863            return HlsCodecStringHelpers.GetALACString();
 864        }
 865
 0866        if (string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
 867        {
 0868            return HlsCodecStringHelpers.GetOPUSString();
 869        }
 870
 0871        if (string.Equals(state.ActualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
 872        {
 0873            return HlsCodecStringHelpers.GetTRUEHDString();
 874        }
 875
 0876        if (string.Equals(state.ActualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase))
 877        {
 878            // lavc only support encoding DTS core profile
 0879            string? profile = EncodingHelper.IsCopyCodec(state.OutputAudioCodec) ? state.AudioStream?.Profile : "DTS";
 880
 0881            return HlsCodecStringHelpers.GetDTSString(profile);
 882        }
 883
 0884        return string.Empty;
 885    }
 886
 887    /// <summary>
 888    /// Gets a formatted string of the output video codec, for use in the CODECS field.
 889    /// </summary>
 890    /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
 891    /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
 892    /// <param name="state">StreamState of the current stream.</param>
 893    /// <param name="codec">Video codec.</param>
 894    /// <param name="level">Video level.</param>
 895    /// <returns>Formatted video codec string.</returns>
 896    private string GetPlaylistVideoCodecs(StreamState state, string codec, int level)
 897    {
 0898        if (level == 0)
 899        {
 900            // This is 0 when there's no requested level in the device profile
 901            // and the source is not encoded in H.26X or AV1
 0902            _logger.LogError("Got invalid level when building CODECS field for HLS master playlist");
 0903            return string.Empty;
 904        }
 905
 0906        if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
 907        {
 0908            string profile = GetOutputVideoCodecProfile(state, "h264");
 0909            return HlsCodecStringHelpers.GetH264String(profile, level);
 910        }
 911
 0912        if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
 0913            || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
 914        {
 0915            string profile = GetOutputVideoCodecProfile(state, "hevc");
 0916            return HlsCodecStringHelpers.GetH265String(profile, level);
 917        }
 918
 0919        if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
 920        {
 0921            string profile = GetOutputVideoCodecProfile(state, "av1");
 922
 923            // Currently we only transcode to 8 bits AV1
 0924            int bitDepth = 8;
 0925            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 0926                && state.VideoStream is not null
 0927                && state.VideoStream.BitDepth.HasValue)
 928            {
 0929                bitDepth = state.VideoStream.BitDepth.Value;
 930            }
 931
 0932            return HlsCodecStringHelpers.GetAv1String(profile, level, false, bitDepth);
 933        }
 934
 935        // VP9 HLS is for video remuxing only, everything is probed from the original video
 0936        if (string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
 937        {
 0938            var width = state.VideoStream.Width ?? 0;
 0939            var height = state.VideoStream.Height ?? 0;
 0940            var framerate = state.VideoStream.ReferenceFrameRate ?? 30;
 0941            var bitDepth = state.VideoStream.BitDepth ?? 8;
 0942            return HlsCodecStringHelpers.GetVp9String(
 0943                width,
 0944                height,
 0945                state.VideoStream.PixelFormat,
 0946                framerate,
 0947                bitDepth);
 948        }
 949
 0950        return string.Empty;
 951    }
 952
 953    private int GetBitrateVariation(int bitrate)
 954    {
 955        // By default, vary by just 50k
 0956        var variation = 50000;
 957
 0958        if (bitrate >= 10000000)
 959        {
 0960            variation = 2000000;
 961        }
 0962        else if (bitrate >= 5000000)
 963        {
 0964            variation = 1500000;
 965        }
 0966        else if (bitrate >= 3000000)
 967        {
 0968            variation = 1000000;
 969        }
 0970        else if (bitrate >= 2000000)
 971        {
 0972            variation = 500000;
 973        }
 0974        else if (bitrate >= 1000000)
 975        {
 0976            variation = 300000;
 977        }
 0978        else if (bitrate >= 600000)
 979        {
 0980            variation = 200000;
 981        }
 0982        else if (bitrate >= 400000)
 983        {
 0984            variation = 100000;
 985        }
 986
 0987        return variation;
 988    }
 989
 990    private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
 991    {
 0992        var oldPlaylist = playlist.ToString();
 0993        return oldPlaylist.Replace(
 0994            oldValue.ToString(),
 0995            newValue.ToString(),
 0996            StringComparison.Ordinal);
 997    }
 998}

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)
AppendDoviPlaylist(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)