< 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: 244
Coverable lines: 244
Total lines: 891
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 174
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

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(_httpContextAccessor.HttpContext.Re
 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        // Main stream
 177        var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
 178
 179        playlistUrl += queryString;
 180
 181        var subtitleStreams = state.MediaSource
 182            .MediaStreams
 183            .Where(i => i.IsTextSubtitleStream)
 184            .ToList();
 185
 186        var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || 
 187            ? "subs"
 188            : null;
 189
 190        // If we're burning in subtitles then don't add additional subs to the manifest
 191        if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
 192        {
 193            subtitleGroup = null;
 194        }
 195
 196        if (!string.IsNullOrWhiteSpace(subtitleGroup))
 197        {
 198            AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
 199        }
 200
 201        // Video rotation metadata is only supported in fMP4 remuxing
 202        if (state.VideoStream is not null
 203            && state.VideoRequest is not null
 204            && (state.VideoStream?.Rotation ?? 0) != 0
 205            && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 206            && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
 207            && !string.Equals(state.Request.SegmentContainer, "mp4", StringComparison.OrdinalIgnoreCase))
 208        {
 209            playlistUrl += "&AllowVideoStreamCopy=false";
 210        }
 211
 212        var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
 213
 214        if (state.VideoStream is not null && state.VideoRequest is not null)
 215        {
 216            var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
 217
 218            // Provide SDR HEVC entrance for backward compatibility.
 219            if (encodingOptions.AllowHevcEncoding
 220                && !encodingOptions.AllowAv1Encoding
 221                && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 222                && state.VideoStream.VideoRange == VideoRange.HDR
 223                && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
 224            {
 225                var requestedVideoProfiles = state.GetRequestedProfiles("hevc");
 226                if (requestedVideoProfiles is not null && requestedVideoProfiles.Length > 0)
 227                {
 228                    // Force HEVC Main Profile and disable video stream copy.
 229                    state.OutputVideoCodec = "hevc";
 230                    var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "mai
 231                    sdrVideoUrl += "&AllowVideoStreamCopy=false";
 232
 233                    // HACK: Use the same bitrate so that the client can choose by other attributes, such as color range
 234                    AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
 235
 236                    // Restore the video codec
 237                    state.OutputVideoCodec = "copy";
 238                }
 239            }
 240
 241            // Provide Level 5.0 entrance for backward compatibility.
 242            // e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
 243            // but in fact it is capable of playing videos up to Level 6.1.
 244            if (encodingOptions.AllowHevcEncoding
 245                && !encodingOptions.AllowAv1Encoding
 246                && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 247                && state.VideoStream.Level.HasValue
 248                && state.VideoStream.Level > 150
 249                && state.VideoStream.VideoRange == VideoRange.SDR
 250                && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
 251            {
 252                var playlistCodecsField = new StringBuilder();
 253                AppendPlaylistCodecsField(playlistCodecsField, state);
 254
 255                // Force the video level to 5.0.
 256                var originalLevel = state.VideoStream.Level;
 257                state.VideoStream.Level = 150;
 258                var newPlaylistCodecsField = new StringBuilder();
 259                AppendPlaylistCodecsField(newPlaylistCodecsField, state);
 260
 261                // Restore the video level.
 262                state.VideoStream.Level = originalLevel;
 263                var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField)
 264                builder.Append(newPlaylist);
 265            }
 266        }
 267
 268        if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.Htt
 269        {
 270            var requestedVideoBitrate = state.VideoRequest?.VideoBitRate ?? 0;
 271
 272            // By default, vary by just 200k
 273            var variation = GetBitrateVariation(totalBitrate);
 274
 275            var newBitrate = totalBitrate - variation;
 276            var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
 277            AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
 278
 279            variation *= 2;
 280            newBitrate = totalBitrate - variation;
 281            variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
 282            AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
 283        }
 284
 285        if (!isLiveStream && (state.VideoRequest?.EnableTrickplay ?? false))
 286        {
 287            var sourceId = Guid.Parse(state.Request.MediaSourceId);
 288            var trickplayResolutions = await _trickplayManager.GetTrickplayResolutions(sourceId).ConfigureAwait(false);
 289            AddTrickplay(state, trickplayResolutions, builder, _httpContextAccessor.HttpContext.User);
 290        }
 291
 292        return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"))
 293    }
 294
 295    private StringBuilder AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subt
 296    {
 0297        var playlistBuilder = new StringBuilder();
 0298        playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
 0299            .Append(bitrate.ToString(CultureInfo.InvariantCulture))
 0300            .Append(",AVERAGE-BANDWIDTH=")
 0301            .Append(bitrate.ToString(CultureInfo.InvariantCulture));
 302
 0303        AppendPlaylistVideoRangeField(playlistBuilder, state);
 304
 0305        AppendPlaylistCodecsField(playlistBuilder, state);
 306
 0307        AppendPlaylistSupplementalCodecsField(playlistBuilder, state);
 308
 0309        AppendPlaylistResolutionField(playlistBuilder, state);
 310
 0311        AppendPlaylistFramerateField(playlistBuilder, state);
 312
 0313        if (!string.IsNullOrWhiteSpace(subtitleGroup))
 314        {
 0315            playlistBuilder.Append(",SUBTITLES=\"")
 0316                .Append(subtitleGroup)
 0317                .Append('"');
 318        }
 319
 0320        playlistBuilder.Append(Environment.NewLine);
 0321        playlistBuilder.AppendLine(url);
 0322        builder.Append(playlistBuilder);
 323
 0324        return playlistBuilder;
 325    }
 326
 327    /// <summary>
 328    /// Appends a VIDEO-RANGE field containing the range of the output video stream.
 329    /// </summary>
 330    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 331    /// <param name="builder">StringBuilder to append the field to.</param>
 332    /// <param name="state">StreamState of the current stream.</param>
 333    private void AppendPlaylistVideoRangeField(StringBuilder builder, StreamState state)
 334    {
 0335        if (state.VideoStream is not null && state.VideoStream.VideoRange != VideoRange.Unknown)
 336        {
 0337            var videoRange = state.VideoStream.VideoRange;
 0338            var videoRangeType = state.VideoStream.VideoRangeType;
 0339            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
 340            {
 0341                if (videoRange == VideoRange.SDR)
 342                {
 0343                    builder.Append(",VIDEO-RANGE=SDR");
 344                }
 345
 0346                if (videoRange == VideoRange.HDR)
 347                {
 348                    switch (videoRangeType)
 349                    {
 350                        case VideoRangeType.HLG:
 351                        case VideoRangeType.DOVIWithHLG:
 0352                            builder.Append(",VIDEO-RANGE=HLG");
 0353                            break;
 354                        default:
 0355                            builder.Append(",VIDEO-RANGE=PQ");
 0356                            break;
 357                    }
 358                }
 359            }
 360            else
 361            {
 362                // Currently we only encode to SDR.
 0363                builder.Append(",VIDEO-RANGE=SDR");
 364            }
 365        }
 0366    }
 367
 368    /// <summary>
 369    /// Appends a CODECS field containing formatted strings of
 370    /// the active streams output video and audio codecs.
 371    /// </summary>
 372    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 373    /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
 374    /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
 375    /// <param name="builder">StringBuilder to append the field to.</param>
 376    /// <param name="state">StreamState of the current stream.</param>
 377    private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
 378    {
 379        // Video
 0380        string videoCodecs = string.Empty;
 0381        int? videoCodecLevel = GetOutputVideoCodecLevel(state);
 0382        if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue)
 383        {
 0384            videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
 385        }
 386
 387        // Audio
 0388        string audioCodecs = string.Empty;
 0389        if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
 390        {
 0391            audioCodecs = GetPlaylistAudioCodecs(state);
 392        }
 393
 0394        StringBuilder codecs = new StringBuilder();
 395
 0396        codecs.Append(videoCodecs);
 397
 0398        if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs))
 399        {
 0400            codecs.Append(',');
 401        }
 402
 0403        codecs.Append(audioCodecs);
 404
 0405        if (codecs.Length > 1)
 406        {
 0407            builder.Append(",CODECS=\"")
 0408                .Append(codecs)
 0409                .Append('"');
 410        }
 0411    }
 412
 413    /// <summary>
 414    /// Appends a SUPPLEMENTAL-CODECS field containing formatted strings of
 415    /// the active streams output Dolby Vision Videos.
 416    /// </summary>
 417    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 418    /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
 419    /// <param name="builder">StringBuilder to append the field to.</param>
 420    /// <param name="state">StreamState of the current stream.</param>
 421    private void AppendPlaylistSupplementalCodecsField(StringBuilder builder, StreamState state)
 422    {
 423        // HDR dynamic metadata currently cannot exist when transcoding
 0424        if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
 425        {
 0426            return;
 427        }
 428
 0429        if (EncodingHelper.IsDovi(state.VideoStream) && !_encodingHelper.IsDoviRemoved(state))
 430        {
 0431            AppendDvString();
 432        }
 0433        else if (EncodingHelper.IsHdr10Plus(state.VideoStream) && !_encodingHelper.IsHdr10PlusRemoved(state))
 434        {
 0435            AppendHdr10PlusString();
 436        }
 437
 0438        return;
 439
 440        void AppendDvString()
 441        {
 442            var dvProfile = state.VideoStream.DvProfile;
 443            var dvLevel = state.VideoStream.DvLevel;
 444            var dvRangeString = state.VideoStream.VideoRangeType switch
 445            {
 446                VideoRangeType.DOVIWithHDR10 => "db1p",
 447                VideoRangeType.DOVIWithHLG => "db4h",
 448                VideoRangeType.DOVIWithHDR10Plus => "db1p", // The HDR10+ metadata would be removed if Dovi metadata is 
 449                _ => string.Empty // Don't label Dovi with EL and SDR due to compatability issues, ignore invalid config
 450            };
 451
 452            if (dvProfile is null || dvLevel is null || string.IsNullOrEmpty(dvRangeString))
 453            {
 454                return;
 455            }
 456
 457            var dvFourCc = string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase) ? "dav
 458            builder.Append(",SUPPLEMENTAL-CODECS=\"")
 459                .Append(dvFourCc)
 460                .Append('.')
 461                .Append(dvProfile.Value.ToString("D2", CultureInfo.InvariantCulture))
 462                .Append('.')
 463                .Append(dvLevel.Value.ToString("D2", CultureInfo.InvariantCulture))
 464                .Append('/')
 465                .Append(dvRangeString)
 466                .Append('"');
 467        }
 468
 469        void AppendHdr10PlusString()
 470        {
 471            var videoCodecLevel = GetOutputVideoCodecLevel(state);
 472            if (string.IsNullOrEmpty(state.ActualOutputVideoCodec) || videoCodecLevel is null)
 473            {
 474                return;
 475            }
 476
 477            var videoCodecString = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
 478            builder.Append(",SUPPLEMENTAL-CODECS=\"")
 479                .Append(videoCodecString)
 480                .Append('/')
 481                .Append("cdm4")
 482                .Append('"');
 483        }
 484    }
 485
 486    /// <summary>
 487    /// Appends a RESOLUTION field containing the resolution of the output stream.
 488    /// </summary>
 489    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 490    /// <param name="builder">StringBuilder to append the field to.</param>
 491    /// <param name="state">StreamState of the current stream.</param>
 492    private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state)
 493    {
 0494        if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
 495        {
 0496            builder.Append(",RESOLUTION=")
 0497                .Append(state.OutputWidth.GetValueOrDefault())
 0498                .Append('x')
 0499                .Append(state.OutputHeight.GetValueOrDefault());
 500        }
 0501    }
 502
 503    /// <summary>
 504    /// Appends a FRAME-RATE field containing the framerate of the output stream.
 505    /// </summary>
 506    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 507    /// <param name="builder">StringBuilder to append the field to.</param>
 508    /// <param name="state">StreamState of the current stream.</param>
 509    private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state)
 510    {
 0511        double? framerate = null;
 0512        if (state.TargetFramerate.HasValue)
 513        {
 0514            framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
 515        }
 0516        else if (state.VideoStream?.RealFrameRate is not null)
 517        {
 0518            framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
 519        }
 520
 0521        if (framerate.HasValue)
 522        {
 0523            builder.Append(",FRAME-RATE=")
 0524                .Append(framerate.Value.ToString(CultureInfo.InvariantCulture));
 525        }
 0526    }
 527
 528    private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreamin
 529    {
 530        // Within the local network this will likely do more harm than good.
 0531        if (_networkManager.IsInLocalNetwork(ipAddress))
 532        {
 0533            return false;
 534        }
 535
 0536        if (!enableAdaptiveBitrateStreaming)
 537        {
 0538            return false;
 539        }
 540
 0541        if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
 542        {
 543            // Opening live streams is so slow it's not even worth it
 0544            return false;
 545        }
 546
 0547        if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
 548        {
 0549            return false;
 550        }
 551
 0552        if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
 553        {
 0554            return false;
 555        }
 556
 0557        if (!state.IsOutputVideo)
 558        {
 0559            return false;
 560        }
 561
 0562        return state.VideoRequest?.VideoBitRate.HasValue ?? false;
 563    }
 564
 565    private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder, ClaimsPrinci
 566    {
 0567        if (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Drop)
 568        {
 0569            return;
 570        }
 571
 0572        var selectedIndex = state.SubtitleStream is null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ?
 573        const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSEL
 574
 0575        foreach (var stream in subtitles)
 576        {
 0577            var name = stream.DisplayTitle;
 578
 0579            var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
 0580            var isForced = stream.IsForced;
 581
 0582            var url = string.Format(
 0583                CultureInfo.InvariantCulture,
 0584                "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&ApiKey={3}",
 0585                state.Request.MediaSourceId,
 0586                stream.Index.ToString(CultureInfo.InvariantCulture),
 0587                30.ToString(CultureInfo.InvariantCulture),
 0588                user.GetToken());
 589
 0590            var line = string.Format(
 0591                CultureInfo.InvariantCulture,
 0592                Format,
 0593                name,
 0594                isDefault ? "YES" : "NO",
 0595                isForced ? "YES" : "NO",
 0596                url,
 0597                stream.Language ?? "Unknown");
 598
 0599            builder.AppendLine(line);
 600        }
 0601    }
 602
 603    /// <summary>
 604    /// Appends EXT-X-IMAGE-STREAM-INF playlists for each available trickplay resolution.
 605    /// </summary>
 606    /// <param name="state">StreamState of the current stream.</param>
 607    /// <param name="trickplayResolutions">Dictionary of widths to corresponding tiles info.</param>
 608    /// <param name="builder">StringBuilder to append the field to.</param>
 609    /// <param name="user">Http user context.</param>
 610    private void AddTrickplay(StreamState state, Dictionary<int, TrickplayInfo> trickplayResolutions, StringBuilder buil
 611    {
 612        const string playlistFormat = "#EXT-X-IMAGE-STREAM-INF:BANDWIDTH={0},RESOLUTION={1}x{2},CODECS=\"jpeg\",URI=\"{3
 613
 0614        foreach (var resolution in trickplayResolutions)
 615        {
 0616            var width = resolution.Key;
 0617            var trickplayInfo = resolution.Value;
 618
 0619            var url = string.Format(
 0620                CultureInfo.InvariantCulture,
 0621                "Trickplay/{0}/tiles.m3u8?MediaSourceId={1}&ApiKey={2}",
 0622                width.ToString(CultureInfo.InvariantCulture),
 0623                state.Request.MediaSourceId,
 0624                user.GetToken());
 625
 0626            builder.AppendFormat(
 0627                CultureInfo.InvariantCulture,
 0628                playlistFormat,
 0629                trickplayInfo.Bandwidth.ToString(CultureInfo.InvariantCulture),
 0630                trickplayInfo.Width.ToString(CultureInfo.InvariantCulture),
 0631                trickplayInfo.Height.ToString(CultureInfo.InvariantCulture),
 0632                url);
 633
 0634            builder.AppendLine();
 635        }
 0636    }
 637
 638    /// <summary>
 639    /// Get the H.26X level of the output video stream.
 640    /// </summary>
 641    /// <param name="state">StreamState of the current stream.</param>
 642    /// <returns>H.26X level of the output video stream.</returns>
 643    private int? GetOutputVideoCodecLevel(StreamState state)
 644    {
 0645        string levelString = string.Empty;
 0646        if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 0647            && state.VideoStream is not null
 0648            && state.VideoStream.Level.HasValue)
 649        {
 0650            levelString = state.VideoStream.Level.Value.ToString(CultureInfo.InvariantCulture);
 651        }
 652        else
 653        {
 0654            if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
 655            {
 0656                levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec) ?? "41";
 0657                levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
 658            }
 659
 0660            if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
 0661                || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
 662            {
 0663                levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120";
 0664                levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
 665            }
 666
 0667            if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
 668            {
 0669                levelString = state.GetRequestedLevel("av1") ?? "19";
 0670                levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
 671            }
 672        }
 673
 0674        if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
 675        {
 0676            return parsedLevel;
 677        }
 678
 0679        return null;
 680    }
 681
 682    /// <summary>
 683    /// Get the profile of the output video stream.
 684    /// </summary>
 685    /// <param name="state">StreamState of the current stream.</param>
 686    /// <param name="codec">Video codec.</param>
 687    /// <returns>Profile of the output video stream.</returns>
 688    private string GetOutputVideoCodecProfile(StreamState state, string codec)
 689    {
 0690        string profileString = string.Empty;
 0691        if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 0692            && !string.IsNullOrEmpty(state.VideoStream.Profile))
 693        {
 0694            profileString = state.VideoStream.Profile;
 695        }
 0696        else if (!string.IsNullOrEmpty(codec))
 697        {
 0698            profileString = state.GetRequestedProfiles(codec).FirstOrDefault() ?? string.Empty;
 0699            if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
 700            {
 0701                profileString ??= "high";
 702            }
 703
 0704            if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
 0705                || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
 0706                || string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
 707            {
 0708                profileString ??= "main";
 709            }
 710        }
 711
 0712        return profileString;
 713    }
 714
 715    /// <summary>
 716    /// Gets a formatted string of the output audio codec, for use in the CODECS field.
 717    /// </summary>
 718    /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
 719    /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
 720    /// <param name="state">StreamState of the current stream.</param>
 721    /// <returns>Formatted audio codec string.</returns>
 722    private string GetPlaylistAudioCodecs(StreamState state)
 723    {
 0724        if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
 725        {
 0726            string? profile = state.GetRequestedProfiles("aac").FirstOrDefault();
 0727            return HlsCodecStringHelpers.GetAACString(profile);
 728        }
 729
 0730        if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
 731        {
 0732            return HlsCodecStringHelpers.GetMP3String();
 733        }
 734
 0735        if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
 736        {
 0737            return HlsCodecStringHelpers.GetAC3String();
 738        }
 739
 0740        if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
 741        {
 0742            return HlsCodecStringHelpers.GetEAC3String();
 743        }
 744
 0745        if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
 746        {
 0747            return HlsCodecStringHelpers.GetFLACString();
 748        }
 749
 0750        if (string.Equals(state.ActualOutputAudioCodec, "alac", StringComparison.OrdinalIgnoreCase))
 751        {
 0752            return HlsCodecStringHelpers.GetALACString();
 753        }
 754
 0755        if (string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
 756        {
 0757            return HlsCodecStringHelpers.GetOPUSString();
 758        }
 759
 0760        return string.Empty;
 761    }
 762
 763    /// <summary>
 764    /// Gets a formatted string of the output video codec, for use in the CODECS field.
 765    /// </summary>
 766    /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
 767    /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
 768    /// <param name="state">StreamState of the current stream.</param>
 769    /// <param name="codec">Video codec.</param>
 770    /// <param name="level">Video level.</param>
 771    /// <returns>Formatted video codec string.</returns>
 772    private string GetPlaylistVideoCodecs(StreamState state, string codec, int level)
 773    {
 0774        if (level == 0)
 775        {
 776            // This is 0 when there's no requested level in the device profile
 777            // and the source is not encoded in H.26X or AV1
 0778            _logger.LogError("Got invalid level when building CODECS field for HLS master playlist");
 0779            return string.Empty;
 780        }
 781
 0782        if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
 783        {
 0784            string profile = GetOutputVideoCodecProfile(state, "h264");
 0785            return HlsCodecStringHelpers.GetH264String(profile, level);
 786        }
 787
 0788        if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
 0789            || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
 790        {
 0791            string profile = GetOutputVideoCodecProfile(state, "hevc");
 0792            return HlsCodecStringHelpers.GetH265String(profile, level);
 793        }
 794
 0795        if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
 796        {
 0797            string profile = GetOutputVideoCodecProfile(state, "av1");
 798
 799            // Currently we only transcode to 8 bits AV1
 0800            int bitDepth = 8;
 0801            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 0802                && state.VideoStream is not null
 0803                && state.VideoStream.BitDepth.HasValue)
 804            {
 0805                bitDepth = state.VideoStream.BitDepth.Value;
 806            }
 807
 0808            return HlsCodecStringHelpers.GetAv1String(profile, level, false, bitDepth);
 809        }
 810
 811        // VP9 HLS is for video remuxing only, everything is probed from the original video
 0812        if (string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
 813        {
 0814            var width = state.VideoStream.Width ?? 0;
 0815            var height = state.VideoStream.Height ?? 0;
 0816            var framerate = state.VideoStream.ReferenceFrameRate ?? 30;
 0817            var bitDepth = state.VideoStream.BitDepth ?? 8;
 0818            return HlsCodecStringHelpers.GetVp9String(
 0819                width,
 0820                height,
 0821                state.VideoStream.PixelFormat,
 0822                framerate,
 0823                bitDepth);
 824        }
 825
 0826        return string.Empty;
 827    }
 828
 829    private int GetBitrateVariation(int bitrate)
 830    {
 831        // By default, vary by just 50k
 0832        var variation = 50000;
 833
 0834        if (bitrate >= 10000000)
 835        {
 0836            variation = 2000000;
 837        }
 0838        else if (bitrate >= 5000000)
 839        {
 0840            variation = 1500000;
 841        }
 0842        else if (bitrate >= 3000000)
 843        {
 0844            variation = 1000000;
 845        }
 0846        else if (bitrate >= 2000000)
 847        {
 0848            variation = 500000;
 849        }
 0850        else if (bitrate >= 1000000)
 851        {
 0852            variation = 300000;
 853        }
 0854        else if (bitrate >= 600000)
 855        {
 0856            variation = 200000;
 857        }
 0858        else if (bitrate >= 400000)
 859        {
 0860            variation = 100000;
 861        }
 862
 0863        return variation;
 864    }
 865
 866    private string ReplaceVideoBitrate(string url, int oldValue, int newValue)
 867    {
 0868        return url.Replace(
 0869            "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
 0870            "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
 0871            StringComparison.OrdinalIgnoreCase);
 872    }
 873
 874    private string ReplaceProfile(string url, string codec, string oldValue, string newValue)
 875    {
 0876        string profileStr = codec + "-profile=";
 0877        return url.Replace(
 0878            profileStr + oldValue,
 0879            profileStr + newValue,
 0880            StringComparison.OrdinalIgnoreCase);
 881    }
 882
 883    private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
 884    {
 0885        var oldPlaylist = playlist.ToString();
 0886        return oldPlaylist.Replace(
 0887            oldValue.ToString(),
 0888            newValue.ToString(),
 0889            StringComparison.Ordinal);
 890    }
 891}

Methods/Properties

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