< 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: 4
Uncovered lines: 409
Coverable lines: 413
Total lines: 1015
Line coverage: 0.9%
Branch coverage
0%
Covered branches: 2
Total branches: 304
Branch coverage: 0.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/12/2026 - 12:15:57 AM Line coverage: 0% (0/406) Branch coverage: 0% (0/298) Total lines: 99810/29/2026 - 12:17:11 AM Line coverage: 0.9% (4/413) Branch coverage: 0.6% (2/304) Total lines: 1015 5/12/2026 - 12:15:57 AM Line coverage: 0% (0/406) Branch coverage: 0% (0/298) Total lines: 99810/29/2026 - 12:17:11 AM Line coverage: 0.9% (4/413) Branch coverage: 0.6% (2/304) Total lines: 1015

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.Controllers;
 11using Jellyfin.Api.Extensions;
 12using Jellyfin.Data.Enums;
 13using Jellyfin.Database.Implementations.Entities;
 14using Jellyfin.Extensions;
 15using MediaBrowser.Common.Configuration;
 16using MediaBrowser.Common.Extensions;
 17using MediaBrowser.Common.Net;
 18using MediaBrowser.Controller.Configuration;
 19using MediaBrowser.Controller.Library;
 20using MediaBrowser.Controller.MediaEncoding;
 21using MediaBrowser.Controller.Streaming;
 22using MediaBrowser.Controller.Trickplay;
 23using MediaBrowser.Model.Dlna;
 24using MediaBrowser.Model.Entities;
 25using MediaBrowser.Model.Net;
 26using Microsoft.AspNetCore.Http;
 27using Microsoft.AspNetCore.Mvc;
 28using Microsoft.Extensions.Logging;
 29using Microsoft.Net.Http.Headers;
 30
 31namespace Jellyfin.Api.Helpers;
 32
 33/// <summary>
 34/// Dynamic hls helper.
 35/// </summary>
 36public class DynamicHlsHelper
 37{
 38    private readonly ILibraryManager _libraryManager;
 39    private readonly IUserManager _userManager;
 40    private readonly IMediaSourceManager _mediaSourceManager;
 41    private readonly IServerConfigurationManager _serverConfigurationManager;
 42    private readonly IMediaEncoder _mediaEncoder;
 43    private readonly ITranscodeManager _transcodeManager;
 44    private readonly INetworkManager _networkManager;
 45    private readonly ILogger<DynamicHlsHelper> _logger;
 46    private readonly IHttpContextAccessor _httpContextAccessor;
 47    private readonly EncodingHelper _encodingHelper;
 48    private readonly ITrickplayManager _trickplayManager;
 49
 50    /// <summary>
 51    /// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class.
 52    /// </summary>
 53    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 54    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
 55    /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
 56    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</p
 57    /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
 58    /// <param name="transcodeManager">Instance of <see cref="ITranscodeManager"/>.</param>
 59    /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
 60    /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param>
 61    /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
 62    /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
 63    /// <param name="trickplayManager">Instance of <see cref="ITrickplayManager"/>.</param>
 64    public DynamicHlsHelper(
 65        ILibraryManager libraryManager,
 66        IUserManager userManager,
 67        IMediaSourceManager mediaSourceManager,
 68        IServerConfigurationManager serverConfigurationManager,
 69        IMediaEncoder mediaEncoder,
 70        ITranscodeManager transcodeManager,
 71        INetworkManager networkManager,
 72        ILogger<DynamicHlsHelper> logger,
 73        IHttpContextAccessor httpContextAccessor,
 74        EncodingHelper encodingHelper,
 75        ITrickplayManager trickplayManager)
 76    {
 077        _libraryManager = libraryManager;
 078        _userManager = userManager;
 079        _mediaSourceManager = mediaSourceManager;
 080        _serverConfigurationManager = serverConfigurationManager;
 081        _mediaEncoder = mediaEncoder;
 082        _transcodeManager = transcodeManager;
 083        _networkManager = networkManager;
 084        _logger = logger;
 085        _httpContextAccessor = httpContextAccessor;
 086        _encodingHelper = encodingHelper;
 087        _trickplayManager = trickplayManager;
 088    }
 89
 90    /// <summary>
 91    /// Get master hls playlist.
 92    /// </summary>
 93    /// <param name="transcodingJobType">Transcoding job type.</param>
 94    /// <param name="streamingRequest">Streaming request dto.</param>
 95    /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
 96    /// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns>
 97    public async Task<ActionResult> GetMasterHlsPlaylist(
 98        TranscodingJobType transcodingJobType,
 99        StreamingRequestDto streamingRequest,
 100        bool enableAdaptiveBitrateStreaming)
 101    {
 0102        var isHeadRequest = _httpContextAccessor.HttpContext?.Request.Method == WebRequestMethods.Http.Head;
 103        // CTS lifecycle is managed internally.
 0104        var cancellationTokenSource = new CancellationTokenSource();
 0105        return await GetMasterPlaylistInternal(
 0106            streamingRequest,
 0107            isHeadRequest,
 0108            enableAdaptiveBitrateStreaming,
 0109            transcodingJobType,
 0110            cancellationTokenSource).ConfigureAwait(false);
 0111    }
 112
 113    private async Task<ActionResult> GetMasterPlaylistInternal(
 114        StreamingRequestDto streamingRequest,
 115        bool isHeadRequest,
 116        bool enableAdaptiveBitrateStreaming,
 117        TranscodingJobType transcodingJobType,
 118        CancellationTokenSource cancellationTokenSource)
 119    {
 0120        if (_httpContextAccessor.HttpContext is null)
 121        {
 0122            throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
 123        }
 124
 0125        using var state = await StreamingHelpers.GetStreamingState(
 0126                streamingRequest,
 0127                _httpContextAccessor.HttpContext,
 0128                _mediaSourceManager,
 0129                _userManager,
 0130                _libraryManager,
 0131                _serverConfigurationManager,
 0132                _mediaEncoder,
 0133                _encodingHelper,
 0134                _transcodeManager,
 0135                transcodingJobType,
 0136                cancellationTokenSource.Token)
 0137            .ConfigureAwait(false);
 138
 0139        _httpContextAccessor.HttpContext.Response.Headers.Append(HeaderNames.Expires, "0");
 0140        if (isHeadRequest)
 141        {
 0142            return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8"));
 143        }
 144
 0145        var totalBitrate = (state.OutputAudioBitrate ?? 0) + (state.OutputVideoBitrate ?? 0);
 146
 0147        var builder = new StringBuilder();
 148
 0149        builder.AppendLine("#EXTM3U");
 150
 0151        var isLiveStream = state.IsSegmentedLiveStream;
 152
 0153        var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString();
 154
 155        // from universal audio service, need to override the AudioCodec when the actual request differs from original q
 0156        if (!string.Equals(state.OutputAudioCodec, _httpContextAccessor.HttpContext.Request.Query["AudioCodec"].ToString
 157        {
 0158            var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString);
 0159            newQuery["AudioCodec"] = state.OutputAudioCodec;
 0160            queryString = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(string.Empty, newQuery);
 161        }
 162
 163        // from universal audio service
 0164        if (!string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
 0165            && !queryString.Contains("SegmentContainer", StringComparison.OrdinalIgnoreCase))
 166        {
 0167            queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
 168        }
 169
 170        // from universal audio service
 0171        if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons)
 0172            && !queryString.Contains("TranscodeReasons=", StringComparison.OrdinalIgnoreCase))
 173        {
 0174            queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
 175        }
 176
 177        // Video rotation metadata is only supported in fMP4 remuxing
 0178        if (state.VideoStream is not null
 0179            && state.VideoRequest is not null
 0180            && (state.VideoStream?.Rotation ?? 0) != 0
 0181            && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 0182            && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
 0183            && !string.Equals(state.Request.SegmentContainer, "mp4", StringComparison.OrdinalIgnoreCase))
 184        {
 0185            queryString += "&AllowVideoStreamCopy=false";
 186        }
 187
 188        // Main stream
 0189        var baseUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
 0190        var playlistUrl = baseUrl + queryString;
 0191        var playlistQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString);
 192
 0193        var subtitleStreams = state.MediaSource
 0194            .MediaStreams
 0195            .Where(i => i.IsTextSubtitleStream)
 0196            .ToList();
 197
 0198        var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || 
 0199            ? "subs"
 0200            : null;
 201
 202        // If we're burning in subtitles then don't add additional subs to the manifest
 0203        if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
 204        {
 0205            subtitleGroup = null;
 206        }
 207
 0208        if (!string.IsNullOrWhiteSpace(subtitleGroup))
 209        {
 0210            AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
 211        }
 212
 213        // For DoVi profiles without a compatible base layer (P5 HEVC, P10/bl0 AV1),
 214        // add a spec-compliant dvh1/dav1 variant before the hvc1 hack variant.
 215        // SUPPLEMENTAL-CODECS cannot be used for these profiles (no compatible BL to supplement).
 216        // The DoVi variant is listed first so spec-compliant clients (Apple TV, webOS 24+)
 217        // select it over the fallback when both have identical BANDWIDTH.
 218        // Only emit for clients that explicitly declared DOVI support to avoid breaking
 219        // non-compliant players that don't recognize dvh1/dav1 CODECS strings.
 0220        if (state.VideoStream is not null
 0221            && state.VideoRequest is not null
 0222            && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 0223            && state.VideoStream.VideoRangeType == VideoRangeType.DOVI
 0224            && state.VideoStream.DvProfile.HasValue
 0225            && state.VideoStream.DvLevel.HasValue
 0226            && state.GetRequestedRangeTypes(state.VideoStream.Codec)
 0227                .Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase))
 228        {
 0229            AppendDoviPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
 230        }
 231
 0232        var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
 233
 0234        if (state.VideoStream is not null && state.VideoRequest is not null)
 235        {
 0236            var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
 237
 238            // Provide AV1 and HEVC SDR entrances for backward compatibility.
 0239            foreach (var sdrVideoCodec in new[] { "av1", "hevc" })
 240            {
 0241                var isAv1EncodingAllowed = encodingOptions.AllowAv1Encoding
 0242                    && string.Equals(sdrVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)
 0243                    && string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase);
 0244                var isHevcEncodingAllowed = encodingOptions.AllowHevcEncoding
 0245                    && string.Equals(sdrVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
 0246                    && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase);
 0247                var isEncodingAllowed = isAv1EncodingAllowed || isHevcEncodingAllowed;
 248
 0249                if (isEncodingAllowed
 0250                    && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 0251                    && state.VideoStream.VideoRange == VideoRange.HDR)
 252                {
 253                    // Force AV1 and HEVC Main Profile and disable video stream copy.
 0254                    state.OutputVideoCodec = sdrVideoCodec;
 255
 0256                    var sdrPlaylistQuery = playlistQuery;
 0257                    sdrPlaylistQuery["VideoCodec"] = sdrVideoCodec;
 0258                    sdrPlaylistQuery[sdrVideoCodec + "-profile"] = "main";
 0259                    sdrPlaylistQuery["AllowVideoStreamCopy"] = "false";
 260
 0261                    var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylist
 262
 263                    // HACK: Use the same bitrate so that the client can choose by other attributes, such as color range
 0264                    AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
 265
 266                    // Restore the video codec
 0267                    state.OutputVideoCodec = "copy";
 268                }
 269            }
 270
 271            // Provide H.264 SDR entrance for backward compatibility.
 0272            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 0273                && state.VideoStream.VideoRange == VideoRange.HDR)
 274            {
 275                // Force H.264 and disable video stream copy.
 0276                state.OutputVideoCodec = "h264";
 277
 0278                var sdrPlaylistQuery = playlistQuery;
 0279                sdrPlaylistQuery["VideoCodec"] = "h264";
 0280                sdrPlaylistQuery["AllowVideoStreamCopy"] = "false";
 281
 0282                var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylistQuer
 283
 284                // HACK: Use the same bitrate so that the client can choose by other attributes, such as color range.
 0285                AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
 286
 287                // Restore the video codec
 0288                state.OutputVideoCodec = "copy";
 289            }
 290
 291            // Provide Level 5.0 entrance for backward compatibility.
 292            // e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
 293            // but in fact it is capable of playing videos up to Level 6.1.
 0294            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 0295                && state.VideoStream.Level.HasValue
 0296                && state.VideoStream.Level > 150
 0297                && state.VideoStream.VideoRange == VideoRange.SDR
 0298                && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
 299            {
 0300                var playlistCodecsField = new StringBuilder();
 0301                AppendPlaylistCodecsField(playlistCodecsField, state);
 302
 303                // Force the video level to 5.0.
 0304                var originalLevel = state.VideoStream.Level;
 0305                state.VideoStream.Level = 150;
 0306                var newPlaylistCodecsField = new StringBuilder();
 0307                AppendPlaylistCodecsField(newPlaylistCodecsField, state);
 308
 309                // Restore the video level.
 0310                state.VideoStream.Level = originalLevel;
 0311                var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField)
 0312                builder.Append(newPlaylist);
 313            }
 314        }
 315
 0316        if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.Htt
 317        {
 0318            var requestedVideoBitrate = state.VideoRequest?.VideoBitRate ?? 0;
 319
 320            // By default, vary by just 200k
 0321            var variation = GetBitrateVariation(totalBitrate);
 322
 0323            var newBitrate = totalBitrate - variation;
 0324            var variantQuery = playlistQuery;
 0325            variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture);
 0326            var variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery);
 0327            AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
 328
 0329            variation *= 2;
 0330            newBitrate = totalBitrate - variation;
 0331            variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture);
 0332            variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery);
 0333            AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
 334        }
 335
 0336        if (!isLiveStream && (state.VideoRequest?.EnableTrickplay ?? false))
 337        {
 0338            var sourceId = Guid.Parse(state.Request.MediaSourceId);
 0339            var trickplayResolutions = await _trickplayManager.GetTrickplayResolutions(sourceId).ConfigureAwait(false);
 0340            AddTrickplay(state, trickplayResolutions, builder, _httpContextAccessor.HttpContext.User);
 341        }
 342
 0343        return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"))
 0344    }
 345
 346    private StringBuilder AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subt
 347    {
 0348        var playlistBuilder = new StringBuilder();
 0349        playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
 0350            .Append(bitrate.ToString(CultureInfo.InvariantCulture))
 0351            .Append(",AVERAGE-BANDWIDTH=")
 0352            .Append(bitrate.ToString(CultureInfo.InvariantCulture));
 353
 0354        AppendPlaylistVideoRangeField(playlistBuilder, state);
 355
 0356        AppendPlaylistCodecsField(playlistBuilder, state);
 357
 0358        AppendPlaylistSupplementalCodecsField(playlistBuilder, state);
 359
 0360        AppendPlaylistResolutionField(playlistBuilder, state);
 361
 0362        AppendPlaylistFramerateField(playlistBuilder, state);
 363
 0364        if (!string.IsNullOrWhiteSpace(subtitleGroup))
 365        {
 0366            playlistBuilder.Append(",SUBTITLES=\"")
 0367                .Append(subtitleGroup)
 0368                .Append('"');
 369        }
 370
 0371        playlistBuilder.Append(Environment.NewLine);
 0372        playlistBuilder.AppendLine(url);
 0373        builder.Append(playlistBuilder);
 374
 0375        return playlistBuilder;
 376    }
 377
 378    /// <summary>
 379    /// Appends a Dolby Vision variant with dvh1/dav1 CODECS for profiles without a compatible
 380    /// base layer (P5 HEVC, P10/bl0 AV1). This enables spec-compliant HLS clients to detect
 381    /// DoVi from the manifest rather than relying on init segment inspection.
 382    /// </summary>
 383    /// <param name="builder">StringBuilder for the master playlist.</param>
 384    /// <param name="state">StreamState of the current stream.</param>
 385    /// <param name="url">Playlist URL for this variant.</param>
 386    /// <param name="bitrate">Bitrate for the BANDWIDTH field.</param>
 387    /// <param name="subtitleGroup">Subtitle group identifier, or null.</param>
 388    private void AppendDoviPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleG
 389    {
 0390        var dvProfile = state.VideoStream.DvProfile;
 0391        var dvLevel = state.VideoStream.DvLevel;
 0392        if (dvProfile is null || dvLevel is null)
 393        {
 0394            return;
 395        }
 396
 0397        var playlistBuilder = new StringBuilder();
 0398        playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
 0399            .Append(bitrate.ToString(CultureInfo.InvariantCulture))
 0400            .Append(",AVERAGE-BANDWIDTH=")
 0401            .Append(bitrate.ToString(CultureInfo.InvariantCulture));
 402
 0403        playlistBuilder.Append(",VIDEO-RANGE=PQ");
 404
 0405        var dvCodec = HlsCodecStringHelpers.GetDoviString(dvProfile.Value, dvLevel.Value, state.ActualOutputVideoCodec);
 406
 0407        string audioCodecs = string.Empty;
 0408        if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
 409        {
 0410            audioCodecs = GetPlaylistAudioCodecs(state);
 411        }
 412
 0413        playlistBuilder.Append(",CODECS=\"")
 0414            .Append(dvCodec);
 0415        if (!string.IsNullOrEmpty(audioCodecs))
 416        {
 0417            playlistBuilder.Append(',').Append(audioCodecs);
 418        }
 419
 0420        playlistBuilder.Append('"');
 421
 0422        AppendPlaylistResolutionField(playlistBuilder, state);
 0423        AppendPlaylistFramerateField(playlistBuilder, state);
 424
 0425        if (!string.IsNullOrWhiteSpace(subtitleGroup))
 426        {
 0427            playlistBuilder.Append(",SUBTITLES=\"")
 0428                .Append(subtitleGroup)
 0429                .Append('"');
 430        }
 431
 0432        playlistBuilder.AppendLine();
 0433        playlistBuilder.AppendLine(url);
 0434        builder.Append(playlistBuilder);
 0435    }
 436
 437    /// <summary>
 438    /// Appends a VIDEO-RANGE field containing the range of the output video stream.
 439    /// </summary>
 440    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 441    /// <param name="builder">StringBuilder to append the field to.</param>
 442    /// <param name="state">StreamState of the current stream.</param>
 443    private void AppendPlaylistVideoRangeField(StringBuilder builder, StreamState state)
 444    {
 0445        if (state.VideoStream is not null && state.VideoStream.VideoRange != VideoRange.Unknown)
 446        {
 0447            var videoRange = state.VideoStream.VideoRange;
 0448            var videoRangeType = state.VideoStream.VideoRangeType;
 0449            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
 450            {
 0451                if (videoRange == VideoRange.SDR)
 452                {
 0453                    builder.Append(",VIDEO-RANGE=SDR");
 454                }
 455
 0456                if (videoRange == VideoRange.HDR)
 457                {
 0458                    switch (videoRangeType)
 459                    {
 460                        case VideoRangeType.HLG:
 461                        case VideoRangeType.DOVIWithHLG:
 0462                        case VideoRangeType.DOVIInvalid when string.Equals(state.VideoStream.ColorTransfer, "arib-std-b6
 0463                            builder.Append(",VIDEO-RANGE=HLG");
 0464                            break;
 465                        default:
 0466                            builder.Append(",VIDEO-RANGE=PQ");
 0467                            break;
 468                    }
 469                }
 470            }
 471            else
 472            {
 473                // Currently we only encode to SDR.
 0474                builder.Append(",VIDEO-RANGE=SDR");
 475            }
 476        }
 0477    }
 478
 479    /// <summary>
 480    /// Appends a CODECS field containing formatted strings of
 481    /// the active streams output video and audio codecs.
 482    /// </summary>
 483    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 484    /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
 485    /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
 486    /// <param name="builder">StringBuilder to append the field to.</param>
 487    /// <param name="state">StreamState of the current stream.</param>
 488    private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
 489    {
 490        // Video
 0491        string videoCodecs = string.Empty;
 0492        int? videoCodecLevel = GetOutputVideoCodecLevel(state);
 0493        if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue)
 494        {
 0495            videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
 496        }
 497
 498        // Audio
 0499        string audioCodecs = string.Empty;
 0500        if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
 501        {
 0502            audioCodecs = GetPlaylistAudioCodecs(state);
 503        }
 504
 0505        StringBuilder codecs = new StringBuilder();
 506
 0507        codecs.Append(videoCodecs);
 508
 0509        if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs))
 510        {
 0511            codecs.Append(',');
 512        }
 513
 0514        codecs.Append(audioCodecs);
 515
 0516        if (codecs.Length > 1)
 517        {
 0518            builder.Append(",CODECS=\"")
 0519                .Append(codecs)
 0520                .Append('"');
 521        }
 0522    }
 523
 524    /// <summary>
 525    /// Appends a SUPPLEMENTAL-CODECS field containing formatted strings of
 526    /// the active streams output Dolby Vision Videos.
 527    /// </summary>
 528    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 529    /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
 530    /// <param name="builder">StringBuilder to append the field to.</param>
 531    /// <param name="state">StreamState of the current stream.</param>
 532    private void AppendPlaylistSupplementalCodecsField(StringBuilder builder, StreamState state)
 533    {
 534        // HDR dynamic metadata currently cannot exist when transcoding
 0535        if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
 536        {
 0537            return;
 538        }
 539
 0540        if (EncodingHelper.IsDovi(state.VideoStream) && !_encodingHelper.IsDoviRemoved(state))
 541        {
 0542            AppendDvString();
 543        }
 0544        else if (EncodingHelper.IsHdr10Plus(state.VideoStream) && !_encodingHelper.IsHdr10PlusRemoved(state))
 545        {
 0546            AppendHdr10PlusString();
 547        }
 548
 0549        return;
 550
 551        void AppendDvString()
 552        {
 553            var dvProfile = state.VideoStream.DvProfile;
 554            var dvLevel = state.VideoStream.DvLevel;
 555            var dvRangeString = state.VideoStream.VideoRangeType switch
 556            {
 557                VideoRangeType.DOVIWithHDR10 => "db1p",
 558                VideoRangeType.DOVIWithHLG => "db4h",
 559                VideoRangeType.DOVIWithHDR10Plus => "db1p", // The HDR10+ metadata would be removed if Dovi metadata is 
 560                _ => string.Empty // Don't label Dovi with EL and SDR due to compatability issues, ignore invalid config
 561            };
 562
 563            if (dvProfile is null || dvLevel is null || string.IsNullOrEmpty(dvRangeString))
 564            {
 565                return;
 566            }
 567
 568            var dvFourCc = string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase) ? "dav
 569            builder.Append(",SUPPLEMENTAL-CODECS=\"")
 570                .Append(dvFourCc)
 571                .Append('.')
 572                .Append(dvProfile.Value.ToString("D2", CultureInfo.InvariantCulture))
 573                .Append('.')
 574                .Append(dvLevel.Value.ToString("D2", CultureInfo.InvariantCulture))
 575                .Append('/')
 576                .Append(dvRangeString)
 577                .Append('"');
 578        }
 579
 580        void AppendHdr10PlusString()
 581        {
 582            var videoCodecLevel = GetOutputVideoCodecLevel(state);
 583            if (string.IsNullOrEmpty(state.ActualOutputVideoCodec) || videoCodecLevel is null)
 584            {
 585                return;
 586            }
 587
 588            var videoCodecString = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
 589            builder.Append(",SUPPLEMENTAL-CODECS=\"")
 590                .Append(videoCodecString)
 591                .Append('/')
 592                .Append("cdm4")
 593                .Append('"');
 594        }
 595    }
 596
 597    /// <summary>
 598    /// Appends a RESOLUTION field containing the resolution of the output stream.
 599    /// </summary>
 600    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 601    /// <param name="builder">StringBuilder to append the field to.</param>
 602    /// <param name="state">StreamState of the current stream.</param>
 603    private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state)
 604    {
 0605        if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
 606        {
 0607            builder.Append(",RESOLUTION=")
 0608                .Append(state.OutputWidth.GetValueOrDefault())
 0609                .Append('x')
 0610                .Append(state.OutputHeight.GetValueOrDefault());
 611        }
 0612    }
 613
 614    /// <summary>
 615    /// Appends a FRAME-RATE field containing the framerate of the output stream.
 616    /// </summary>
 617    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 618    /// <param name="builder">StringBuilder to append the field to.</param>
 619    /// <param name="state">StreamState of the current stream.</param>
 620    private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state)
 621    {
 0622        double? framerate = null;
 0623        if (state.TargetFramerate.HasValue)
 624        {
 0625            framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
 626        }
 0627        else if (state.VideoStream?.RealFrameRate is not null)
 628        {
 0629            framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
 630        }
 631
 0632        if (framerate.HasValue)
 633        {
 0634            builder.Append(",FRAME-RATE=")
 0635                .Append(framerate.Value.ToString(CultureInfo.InvariantCulture));
 636        }
 0637    }
 638
 639    private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreamin
 640    {
 641        // Within the local network this will likely do more harm than good.
 0642        if (_networkManager.IsInLocalNetwork(ipAddress))
 643        {
 0644            return false;
 645        }
 646
 0647        if (!enableAdaptiveBitrateStreaming)
 648        {
 0649            return false;
 650        }
 651
 0652        if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
 653        {
 654            // Opening live streams is so slow it's not even worth it
 0655            return false;
 656        }
 657
 0658        if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
 659        {
 0660            return false;
 661        }
 662
 0663        if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
 664        {
 0665            return false;
 666        }
 667
 0668        if (!state.IsOutputVideo)
 669        {
 0670            return false;
 671        }
 672
 0673        return state.VideoRequest?.VideoBitRate.HasValue ?? false;
 674    }
 675
 676    private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder, ClaimsPrinci
 677    {
 0678        if (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Drop)
 679        {
 0680            return;
 681        }
 682
 0683        var selectedIndex = state.SubtitleStream is null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ?
 684        const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSEL
 685
 0686        foreach (var stream in subtitles)
 687        {
 0688            var name = stream.DisplayTitle;
 689
 0690            var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
 0691            var isForced = stream.IsForced;
 692
 0693            var url = string.Format(
 0694                CultureInfo.InvariantCulture,
 0695                "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&ApiKey={3}&VttTimestampMapMpegts={4}",
 0696                state.Request.MediaSourceId,
 0697                stream.Index.ToString(CultureInfo.InvariantCulture),
 0698                30.ToString(CultureInfo.InvariantCulture),
 0699                user.GetToken(),
 0700                GetVttTimestampMapMpegts(state.Request.SegmentContainer).ToString(CultureInfo.InvariantCulture));
 701
 0702            var line = string.Format(
 0703                CultureInfo.InvariantCulture,
 0704                Format,
 0705                name,
 0706                isDefault ? "YES" : "NO",
 0707                isForced ? "YES" : "NO",
 0708                url,
 0709                stream.Language ?? "Unknown");
 710
 0711            builder.AppendLine(line);
 712        }
 0713    }
 714
 715    /// <summary>
 716    /// Gets the WebVTT X-TIMESTAMP-MAP MPEGTS offset appropriate for the given HLS segment container.
 717    /// fMP4 segments start at PTS 0, unlike legacy MPEG-TS segments which start at the historical 10 second offset.
 718    /// </summary>
 719    /// <param name="segmentContainer">The HLS segment container, e.g. "ts" or "mp4".</param>
 720    /// <returns>The MPEGTS offset to embed in the WebVTT X-TIMESTAMP-MAP header.</returns>
 721    internal static long GetVttTimestampMapMpegts(string? segmentContainer)
 722    {
 5723        var segmentExtension = EncodingHelper.GetSegmentFileExtension(segmentContainer);
 5724        return string.Equals(segmentExtension, ".mp4", StringComparison.OrdinalIgnoreCase)
 5725            ? 0
 5726            : SubtitleController.DefaultVttTimestampMapMpegts;
 727    }
 728
 729    /// <summary>
 730    /// Appends EXT-X-IMAGE-STREAM-INF playlists for each available trickplay resolution.
 731    /// </summary>
 732    /// <param name="state">StreamState of the current stream.</param>
 733    /// <param name="trickplayResolutions">Dictionary of widths to corresponding tiles info.</param>
 734    /// <param name="builder">StringBuilder to append the field to.</param>
 735    /// <param name="user">Http user context.</param>
 736    private void AddTrickplay(StreamState state, Dictionary<int, TrickplayInfo> trickplayResolutions, StringBuilder buil
 737    {
 738        const string playlistFormat = "#EXT-X-IMAGE-STREAM-INF:BANDWIDTH={0},RESOLUTION={1}x{2},CODECS=\"jpeg\",URI=\"{3
 739
 0740        foreach (var resolution in trickplayResolutions)
 741        {
 0742            var width = resolution.Key;
 0743            var trickplayInfo = resolution.Value;
 744
 0745            var url = string.Format(
 0746                CultureInfo.InvariantCulture,
 0747                "Trickplay/{0}/tiles.m3u8?MediaSourceId={1}&ApiKey={2}",
 0748                width.ToString(CultureInfo.InvariantCulture),
 0749                state.Request.MediaSourceId,
 0750                user.GetToken());
 751
 0752            builder.AppendFormat(
 0753                CultureInfo.InvariantCulture,
 0754                playlistFormat,
 0755                trickplayInfo.Bandwidth.ToString(CultureInfo.InvariantCulture),
 0756                trickplayInfo.Width.ToString(CultureInfo.InvariantCulture),
 0757                trickplayInfo.Height.ToString(CultureInfo.InvariantCulture),
 0758                url);
 759
 0760            builder.AppendLine();
 761        }
 0762    }
 763
 764    /// <summary>
 765    /// Get the H.26X level of the output video stream.
 766    /// </summary>
 767    /// <param name="state">StreamState of the current stream.</param>
 768    /// <returns>H.26X level of the output video stream.</returns>
 769    private int? GetOutputVideoCodecLevel(StreamState state)
 770    {
 0771        string levelString = string.Empty;
 0772        if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 0773            && state.VideoStream is not null
 0774            && state.VideoStream.Level.HasValue)
 775        {
 0776            levelString = state.VideoStream.Level.Value.ToString(CultureInfo.InvariantCulture);
 777        }
 778        else
 779        {
 0780            if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
 781            {
 0782                levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec) ?? "41";
 0783                levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
 784            }
 785
 0786            if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
 0787                || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
 788            {
 0789                levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120";
 0790                levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
 791            }
 792
 0793            if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
 794            {
 0795                levelString = state.GetRequestedLevel("av1") ?? "19";
 0796                levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
 797            }
 798        }
 799
 0800        if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
 801        {
 0802            return parsedLevel;
 803        }
 804
 0805        return null;
 806    }
 807
 808    /// <summary>
 809    /// Get the profile of the output video stream.
 810    /// </summary>
 811    /// <param name="state">StreamState of the current stream.</param>
 812    /// <param name="codec">Video codec.</param>
 813    /// <returns>Profile of the output video stream.</returns>
 814    private string GetOutputVideoCodecProfile(StreamState state, string codec)
 815    {
 0816        string profileString = string.Empty;
 0817        if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 0818            && !string.IsNullOrEmpty(state.VideoStream.Profile))
 819        {
 0820            profileString = state.VideoStream.Profile;
 821        }
 0822        else if (!string.IsNullOrEmpty(codec))
 823        {
 0824            profileString = state.GetRequestedProfiles(codec).FirstOrDefault() ?? string.Empty;
 0825            if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
 826            {
 0827                profileString ??= "high";
 828            }
 829
 0830            if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
 0831                || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
 0832                || string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
 833            {
 0834                profileString ??= "main";
 835            }
 836        }
 837
 0838        return profileString;
 839    }
 840
 841    /// <summary>
 842    /// Gets a formatted string of the output audio codec, for use in the CODECS field.
 843    /// </summary>
 844    /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
 845    /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
 846    /// <param name="state">StreamState of the current stream.</param>
 847    /// <returns>Formatted audio codec string.</returns>
 848    private string GetPlaylistAudioCodecs(StreamState state)
 849    {
 0850        if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
 851        {
 0852            string? profile = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
 0853                ? state.AudioStream?.Profile : state.GetRequestedProfiles("aac").FirstOrDefault();
 854
 0855            return HlsCodecStringHelpers.GetAACString(profile);
 856        }
 857
 0858        if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
 859        {
 0860            return HlsCodecStringHelpers.GetMP3String();
 861        }
 862
 0863        if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
 864        {
 0865            return HlsCodecStringHelpers.GetAC3String();
 866        }
 867
 0868        if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
 869        {
 0870            return HlsCodecStringHelpers.GetEAC3String();
 871        }
 872
 0873        if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
 874        {
 0875            return HlsCodecStringHelpers.GetFLACString();
 876        }
 877
 0878        if (string.Equals(state.ActualOutputAudioCodec, "alac", StringComparison.OrdinalIgnoreCase))
 879        {
 0880            return HlsCodecStringHelpers.GetALACString();
 881        }
 882
 0883        if (string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
 884        {
 0885            return HlsCodecStringHelpers.GetOPUSString();
 886        }
 887
 0888        if (string.Equals(state.ActualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
 889        {
 0890            return HlsCodecStringHelpers.GetTRUEHDString();
 891        }
 892
 0893        if (string.Equals(state.ActualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase))
 894        {
 895            // lavc only support encoding DTS core profile
 0896            string? profile = EncodingHelper.IsCopyCodec(state.OutputAudioCodec) ? state.AudioStream?.Profile : "DTS";
 897
 0898            return HlsCodecStringHelpers.GetDTSString(profile);
 899        }
 900
 0901        return string.Empty;
 902    }
 903
 904    /// <summary>
 905    /// Gets a formatted string of the output video codec, for use in the CODECS field.
 906    /// </summary>
 907    /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
 908    /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
 909    /// <param name="state">StreamState of the current stream.</param>
 910    /// <param name="codec">Video codec.</param>
 911    /// <param name="level">Video level.</param>
 912    /// <returns>Formatted video codec string.</returns>
 913    private string GetPlaylistVideoCodecs(StreamState state, string codec, int level)
 914    {
 0915        if (level == 0)
 916        {
 917            // This is 0 when there's no requested level in the device profile
 918            // and the source is not encoded in H.26X or AV1
 0919            _logger.LogError("Got invalid level when building CODECS field for HLS master playlist");
 0920            return string.Empty;
 921        }
 922
 0923        if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
 924        {
 0925            string profile = GetOutputVideoCodecProfile(state, "h264");
 0926            return HlsCodecStringHelpers.GetH264String(profile, level);
 927        }
 928
 0929        if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
 0930            || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
 931        {
 0932            string profile = GetOutputVideoCodecProfile(state, "hevc");
 0933            return HlsCodecStringHelpers.GetH265String(profile, level);
 934        }
 935
 0936        if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
 937        {
 0938            string profile = GetOutputVideoCodecProfile(state, "av1");
 939
 940            // Currently we only transcode to 8 bits AV1
 0941            int bitDepth = 8;
 0942            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 0943                && state.VideoStream is not null
 0944                && state.VideoStream.BitDepth.HasValue)
 945            {
 0946                bitDepth = state.VideoStream.BitDepth.Value;
 947            }
 948
 0949            return HlsCodecStringHelpers.GetAv1String(profile, level, false, bitDepth);
 950        }
 951
 952        // VP9 HLS is for video remuxing only, everything is probed from the original video
 0953        if (string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
 954        {
 0955            var width = state.VideoStream.Width ?? 0;
 0956            var height = state.VideoStream.Height ?? 0;
 0957            var framerate = state.VideoStream.ReferenceFrameRate ?? 30;
 0958            var bitDepth = state.VideoStream.BitDepth ?? 8;
 0959            return HlsCodecStringHelpers.GetVp9String(
 0960                width,
 0961                height,
 0962                state.VideoStream.PixelFormat,
 0963                framerate,
 0964                bitDepth);
 965        }
 966
 0967        return string.Empty;
 968    }
 969
 970    private int GetBitrateVariation(int bitrate)
 971    {
 972        // By default, vary by just 50k
 0973        var variation = 50000;
 974
 0975        if (bitrate >= 10000000)
 976        {
 0977            variation = 2000000;
 978        }
 0979        else if (bitrate >= 5000000)
 980        {
 0981            variation = 1500000;
 982        }
 0983        else if (bitrate >= 3000000)
 984        {
 0985            variation = 1000000;
 986        }
 0987        else if (bitrate >= 2000000)
 988        {
 0989            variation = 500000;
 990        }
 0991        else if (bitrate >= 1000000)
 992        {
 0993            variation = 300000;
 994        }
 0995        else if (bitrate >= 600000)
 996        {
 0997            variation = 200000;
 998        }
 0999        else if (bitrate >= 400000)
 1000        {
 01001            variation = 100000;
 1002        }
 1003
 01004        return variation;
 1005    }
 1006
 1007    private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
 1008    {
 01009        var oldPlaylist = playlist.ToString();
 01010        return oldPlaylist.Replace(
 01011            oldValue.ToString(),
 01012            newValue.ToString(),
 01013            StringComparison.Ordinal);
 1014    }
 1015}

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)
GetMasterHlsPlaylist()
GetMasterPlaylistInternal()
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)
GetVttTimestampMapMpegts(System.String)
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)