< 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: 258
Coverable lines: 258
Total lines: 860
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.Entities;
 12using Jellyfin.Data.Enums;
 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 is null ? 0 : 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                {
 0348                    if (videoRangeType == VideoRangeType.HLG)
 349                    {
 0350                        builder.Append(",VIDEO-RANGE=HLG");
 351                    }
 352                    else
 353                    {
 0354                        builder.Append(",VIDEO-RANGE=PQ");
 355                    }
 356                }
 357            }
 358            else
 359            {
 360                // Currently we only encode to SDR.
 0361                builder.Append(",VIDEO-RANGE=SDR");
 362            }
 363        }
 0364    }
 365
 366    /// <summary>
 367    /// Appends a CODECS field containing formatted strings of
 368    /// the active streams output video and audio codecs.
 369    /// </summary>
 370    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 371    /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
 372    /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
 373    /// <param name="builder">StringBuilder to append the field to.</param>
 374    /// <param name="state">StreamState of the current stream.</param>
 375    private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
 376    {
 377        // Video
 0378        string videoCodecs = string.Empty;
 0379        int? videoCodecLevel = GetOutputVideoCodecLevel(state);
 0380        if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue)
 381        {
 0382            videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
 383        }
 384
 385        // Audio
 0386        string audioCodecs = string.Empty;
 0387        if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
 388        {
 0389            audioCodecs = GetPlaylistAudioCodecs(state);
 390        }
 391
 0392        StringBuilder codecs = new StringBuilder();
 393
 0394        codecs.Append(videoCodecs);
 395
 0396        if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs))
 397        {
 0398            codecs.Append(',');
 399        }
 400
 0401        codecs.Append(audioCodecs);
 402
 0403        if (codecs.Length > 1)
 404        {
 0405            builder.Append(",CODECS=\"")
 0406                .Append(codecs)
 0407                .Append('"');
 408        }
 0409    }
 410
 411    /// <summary>
 412    /// Appends a SUPPLEMENTAL-CODECS field containing formatted strings of
 413    /// the active streams output Dolby Vision Videos.
 414    /// </summary>
 415    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 416    /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
 417    /// <param name="builder">StringBuilder to append the field to.</param>
 418    /// <param name="state">StreamState of the current stream.</param>
 419    private void AppendPlaylistSupplementalCodecsField(StringBuilder builder, StreamState state)
 420    {
 421        // Dolby Vision currently cannot exist when transcoding
 0422        if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
 423        {
 0424            return;
 425        }
 426
 0427        var dvProfile = state.VideoStream.DvProfile;
 0428        var dvLevel = state.VideoStream.DvLevel;
 0429        var dvRangeString = state.VideoStream.VideoRangeType switch
 0430        {
 0431            VideoRangeType.DOVIWithHDR10 => "db1p",
 0432            VideoRangeType.DOVIWithHLG => "db4h",
 0433            _ => string.Empty
 0434        };
 435
 0436        if (dvProfile is null || dvLevel is null || string.IsNullOrEmpty(dvRangeString))
 437        {
 0438            return;
 439        }
 440
 0441        var dvFourCc = string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase) ? "dav1" :
 0442        builder.Append(",SUPPLEMENTAL-CODECS=\"")
 0443            .Append(dvFourCc)
 0444            .Append('.')
 0445            .Append(dvProfile.Value.ToString("D2", CultureInfo.InvariantCulture))
 0446            .Append('.')
 0447            .Append(dvLevel.Value.ToString("D2", CultureInfo.InvariantCulture))
 0448            .Append('/')
 0449            .Append(dvRangeString)
 0450            .Append('"');
 0451    }
 452
 453    /// <summary>
 454    /// Appends a RESOLUTION field containing the resolution of the output stream.
 455    /// </summary>
 456    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 457    /// <param name="builder">StringBuilder to append the field to.</param>
 458    /// <param name="state">StreamState of the current stream.</param>
 459    private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state)
 460    {
 0461        if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
 462        {
 0463            builder.Append(",RESOLUTION=")
 0464                .Append(state.OutputWidth.GetValueOrDefault())
 0465                .Append('x')
 0466                .Append(state.OutputHeight.GetValueOrDefault());
 467        }
 0468    }
 469
 470    /// <summary>
 471    /// Appends a FRAME-RATE field containing the framerate of the output stream.
 472    /// </summary>
 473    /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
 474    /// <param name="builder">StringBuilder to append the field to.</param>
 475    /// <param name="state">StreamState of the current stream.</param>
 476    private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state)
 477    {
 0478        double? framerate = null;
 0479        if (state.TargetFramerate.HasValue)
 480        {
 0481            framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
 482        }
 0483        else if (state.VideoStream?.RealFrameRate is not null)
 484        {
 0485            framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
 486        }
 487
 0488        if (framerate.HasValue)
 489        {
 0490            builder.Append(",FRAME-RATE=")
 0491                .Append(framerate.Value.ToString(CultureInfo.InvariantCulture));
 492        }
 0493    }
 494
 495    private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreamin
 496    {
 497        // Within the local network this will likely do more harm than good.
 0498        if (_networkManager.IsInLocalNetwork(ipAddress))
 499        {
 0500            return false;
 501        }
 502
 0503        if (!enableAdaptiveBitrateStreaming)
 504        {
 0505            return false;
 506        }
 507
 0508        if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
 509        {
 510            // Opening live streams is so slow it's not even worth it
 0511            return false;
 512        }
 513
 0514        if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
 515        {
 0516            return false;
 517        }
 518
 0519        if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
 520        {
 0521            return false;
 522        }
 523
 0524        if (!state.IsOutputVideo)
 525        {
 0526            return false;
 527        }
 528
 529        // Having problems in android
 530        return false;
 531        // return state.VideoRequest.VideoBitRate.HasValue;
 532    }
 533
 534    private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder, ClaimsPrinci
 535    {
 0536        if (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Drop)
 537        {
 0538            return;
 539        }
 540
 0541        var selectedIndex = state.SubtitleStream is null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ?
 542        const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSEL
 543
 0544        foreach (var stream in subtitles)
 545        {
 0546            var name = stream.DisplayTitle;
 547
 0548            var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
 0549            var isForced = stream.IsForced;
 550
 0551            var url = string.Format(
 0552                CultureInfo.InvariantCulture,
 0553                "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
 0554                state.Request.MediaSourceId,
 0555                stream.Index.ToString(CultureInfo.InvariantCulture),
 0556                30.ToString(CultureInfo.InvariantCulture),
 0557                user.GetToken());
 558
 0559            var line = string.Format(
 0560                CultureInfo.InvariantCulture,
 0561                Format,
 0562                name,
 0563                isDefault ? "YES" : "NO",
 0564                isForced ? "YES" : "NO",
 0565                url,
 0566                stream.Language ?? "Unknown");
 567
 0568            builder.AppendLine(line);
 569        }
 0570    }
 571
 572    /// <summary>
 573    /// Appends EXT-X-IMAGE-STREAM-INF playlists for each available trickplay resolution.
 574    /// </summary>
 575    /// <param name="state">StreamState of the current stream.</param>
 576    /// <param name="trickplayResolutions">Dictionary of widths to corresponding tiles info.</param>
 577    /// <param name="builder">StringBuilder to append the field to.</param>
 578    /// <param name="user">Http user context.</param>
 579    private void AddTrickplay(StreamState state, Dictionary<int, TrickplayInfo> trickplayResolutions, StringBuilder buil
 580    {
 581        const string playlistFormat = "#EXT-X-IMAGE-STREAM-INF:BANDWIDTH={0},RESOLUTION={1}x{2},CODECS=\"jpeg\",URI=\"{3
 582
 0583        foreach (var resolution in trickplayResolutions)
 584        {
 0585            var width = resolution.Key;
 0586            var trickplayInfo = resolution.Value;
 587
 0588            var url = string.Format(
 0589                CultureInfo.InvariantCulture,
 0590                "Trickplay/{0}/tiles.m3u8?MediaSourceId={1}&api_key={2}",
 0591                width.ToString(CultureInfo.InvariantCulture),
 0592                state.Request.MediaSourceId,
 0593                user.GetToken());
 594
 0595            builder.AppendFormat(
 0596                CultureInfo.InvariantCulture,
 0597                playlistFormat,
 0598                trickplayInfo.Bandwidth.ToString(CultureInfo.InvariantCulture),
 0599                trickplayInfo.Width.ToString(CultureInfo.InvariantCulture),
 0600                trickplayInfo.Height.ToString(CultureInfo.InvariantCulture),
 0601                url);
 602
 0603            builder.AppendLine();
 604        }
 0605    }
 606
 607    /// <summary>
 608    /// Get the H.26X level of the output video stream.
 609    /// </summary>
 610    /// <param name="state">StreamState of the current stream.</param>
 611    /// <returns>H.26X level of the output video stream.</returns>
 612    private int? GetOutputVideoCodecLevel(StreamState state)
 613    {
 0614        string levelString = string.Empty;
 0615        if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 0616            && state.VideoStream is not null
 0617            && state.VideoStream.Level.HasValue)
 618        {
 0619            levelString = state.VideoStream.Level.Value.ToString(CultureInfo.InvariantCulture) ?? string.Empty;
 620        }
 621        else
 622        {
 0623            if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
 624            {
 0625                levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec) ?? "41";
 0626                levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
 627            }
 628
 0629            if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
 0630                || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
 631            {
 0632                levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120";
 0633                levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
 634            }
 635
 0636            if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
 637            {
 0638                levelString = state.GetRequestedLevel("av1") ?? "19";
 0639                levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
 640            }
 641        }
 642
 0643        if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
 644        {
 0645            return parsedLevel;
 646        }
 647
 0648        return null;
 649    }
 650
 651    /// <summary>
 652    /// Get the profile of the output video stream.
 653    /// </summary>
 654    /// <param name="state">StreamState of the current stream.</param>
 655    /// <param name="codec">Video codec.</param>
 656    /// <returns>Profile of the output video stream.</returns>
 657    private string GetOutputVideoCodecProfile(StreamState state, string codec)
 658    {
 0659        string profileString = string.Empty;
 0660        if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 0661            && !string.IsNullOrEmpty(state.VideoStream.Profile))
 662        {
 0663            profileString = state.VideoStream.Profile;
 664        }
 0665        else if (!string.IsNullOrEmpty(codec))
 666        {
 0667            profileString = state.GetRequestedProfiles(codec).FirstOrDefault() ?? string.Empty;
 0668            if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
 669            {
 0670                profileString ??= "high";
 671            }
 672
 0673            if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
 0674                || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
 0675                || string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
 676            {
 0677                profileString ??= "main";
 678            }
 679        }
 680
 0681        return profileString;
 682    }
 683
 684    /// <summary>
 685    /// Gets a formatted string of the output audio codec, for use in the CODECS field.
 686    /// </summary>
 687    /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
 688    /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
 689    /// <param name="state">StreamState of the current stream.</param>
 690    /// <returns>Formatted audio codec string.</returns>
 691    private string GetPlaylistAudioCodecs(StreamState state)
 692    {
 0693        if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
 694        {
 0695            string? profile = state.GetRequestedProfiles("aac").FirstOrDefault();
 0696            return HlsCodecStringHelpers.GetAACString(profile);
 697        }
 698
 0699        if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
 700        {
 0701            return HlsCodecStringHelpers.GetMP3String();
 702        }
 703
 0704        if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
 705        {
 0706            return HlsCodecStringHelpers.GetAC3String();
 707        }
 708
 0709        if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
 710        {
 0711            return HlsCodecStringHelpers.GetEAC3String();
 712        }
 713
 0714        if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
 715        {
 0716            return HlsCodecStringHelpers.GetFLACString();
 717        }
 718
 0719        if (string.Equals(state.ActualOutputAudioCodec, "alac", StringComparison.OrdinalIgnoreCase))
 720        {
 0721            return HlsCodecStringHelpers.GetALACString();
 722        }
 723
 0724        if (string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
 725        {
 0726            return HlsCodecStringHelpers.GetOPUSString();
 727        }
 728
 0729        return string.Empty;
 730    }
 731
 732    /// <summary>
 733    /// Gets a formatted string of the output video codec, for use in the CODECS field.
 734    /// </summary>
 735    /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
 736    /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
 737    /// <param name="state">StreamState of the current stream.</param>
 738    /// <param name="codec">Video codec.</param>
 739    /// <param name="level">Video level.</param>
 740    /// <returns>Formatted video codec string.</returns>
 741    private string GetPlaylistVideoCodecs(StreamState state, string codec, int level)
 742    {
 0743        if (level == 0)
 744        {
 745            // This is 0 when there's no requested level in the device profile
 746            // and the source is not encoded in H.26X or AV1
 0747            _logger.LogError("Got invalid level when building CODECS field for HLS master playlist");
 0748            return string.Empty;
 749        }
 750
 0751        if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
 752        {
 0753            string profile = GetOutputVideoCodecProfile(state, "h264");
 0754            return HlsCodecStringHelpers.GetH264String(profile, level);
 755        }
 756
 0757        if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
 0758            || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
 759        {
 0760            string profile = GetOutputVideoCodecProfile(state, "hevc");
 0761            return HlsCodecStringHelpers.GetH265String(profile, level);
 762        }
 763
 0764        if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
 765        {
 0766            string profile = GetOutputVideoCodecProfile(state, "av1");
 767
 768            // Currently we only transcode to 8 bits AV1
 0769            int bitDepth = 8;
 0770            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
 0771                && state.VideoStream is not null
 0772                && state.VideoStream.BitDepth.HasValue)
 773            {
 0774                bitDepth = state.VideoStream.BitDepth.Value;
 775            }
 776
 0777            return HlsCodecStringHelpers.GetAv1String(profile, level, false, bitDepth);
 778        }
 779
 780        // VP9 HLS is for video remuxing only, everything is probed from the original video
 0781        if (string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
 782        {
 0783            var width = state.VideoStream.Width ?? 0;
 0784            var height = state.VideoStream.Height ?? 0;
 0785            var framerate = state.VideoStream.ReferenceFrameRate ?? 30;
 0786            var bitDepth = state.VideoStream.BitDepth ?? 8;
 0787            return HlsCodecStringHelpers.GetVp9String(
 0788                width,
 0789                height,
 0790                state.VideoStream.PixelFormat,
 0791                framerate,
 0792                bitDepth);
 793        }
 794
 0795        return string.Empty;
 796    }
 797
 798    private int GetBitrateVariation(int bitrate)
 799    {
 800        // By default, vary by just 50k
 0801        var variation = 50000;
 802
 0803        if (bitrate >= 10000000)
 804        {
 0805            variation = 2000000;
 806        }
 0807        else if (bitrate >= 5000000)
 808        {
 0809            variation = 1500000;
 810        }
 0811        else if (bitrate >= 3000000)
 812        {
 0813            variation = 1000000;
 814        }
 0815        else if (bitrate >= 2000000)
 816        {
 0817            variation = 500000;
 818        }
 0819        else if (bitrate >= 1000000)
 820        {
 0821            variation = 300000;
 822        }
 0823        else if (bitrate >= 600000)
 824        {
 0825            variation = 200000;
 826        }
 0827        else if (bitrate >= 400000)
 828        {
 0829            variation = 100000;
 830        }
 831
 0832        return variation;
 833    }
 834
 835    private string ReplaceVideoBitrate(string url, int oldValue, int newValue)
 836    {
 0837        return url.Replace(
 0838            "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
 0839            "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
 0840            StringComparison.OrdinalIgnoreCase);
 841    }
 842
 843    private string ReplaceProfile(string url, string codec, string oldValue, string newValue)
 844    {
 0845        string profileStr = codec + "-profile=";
 0846        return url.Replace(
 0847            profileStr + oldValue,
 0848            profileStr + newValue,
 0849            StringComparison.OrdinalIgnoreCase);
 850    }
 851
 852    private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
 853    {
 0854        var oldPlaylist = playlist.ToString();
 0855        return oldPlaylist.Replace(
 0856            oldValue.ToString(),
 0857            newValue.ToString(),
 0858            StringComparison.Ordinal);
 859    }
 860}

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.Data.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)