< Summary - Jellyfin

Information
Class: Jellyfin.Api.Helpers.StreamingHelpers
Assembly: Jellyfin.Api
File(s): /srv/git/jellyfin/Jellyfin.Api/Helpers/StreamingHelpers.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 131
Coverable lines: 131
Total lines: 589
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 121
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

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ParseStreamOptions(...)0%2040%
GetOutputFileExtension(...)0%1190340%
GetOutputFilePath(...)100%210%
ParseParams(...)0%6320790%
GetContainerFileExtension(...)0%2040%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Globalization;
 4using System.IO;
 5using System.Linq;
 6using System.Threading;
 7using System.Threading.Tasks;
 8using Jellyfin.Api.Extensions;
 9using Jellyfin.Data.Enums;
 10using Jellyfin.Extensions;
 11using MediaBrowser.Common.Configuration;
 12using MediaBrowser.Common.Extensions;
 13using MediaBrowser.Controller.Configuration;
 14using MediaBrowser.Controller.Entities;
 15using MediaBrowser.Controller.Library;
 16using MediaBrowser.Controller.MediaEncoding;
 17using MediaBrowser.Controller.Streaming;
 18using MediaBrowser.Model.Dlna;
 19using MediaBrowser.Model.Dto;
 20using MediaBrowser.Model.Entities;
 21using Microsoft.AspNetCore.Http;
 22using Microsoft.AspNetCore.Http.HttpResults;
 23using Microsoft.Net.Http.Headers;
 24
 25namespace Jellyfin.Api.Helpers;
 26
 27/// <summary>
 28/// The streaming helpers.
 29/// </summary>
 30public static class StreamingHelpers
 31{
 32    /// <summary>
 33    /// Gets the current streaming state.
 34    /// </summary>
 35    /// <param name="streamingRequest">The <see cref="StreamingRequestDto"/>.</param>
 36    /// <param name="httpContext">The <see cref="HttpContext"/>.</param>
 37    /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
 38    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
 39    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 40    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</p
 41    /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
 42    /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
 43    /// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
 44    /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
 45    /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
 46    /// <returns>A <see cref="Task"/> containing the current <see cref="StreamState"/>.</returns>
 47    public static async Task<StreamState> GetStreamingState(
 48        StreamingRequestDto streamingRequest,
 49        HttpContext httpContext,
 50        IMediaSourceManager mediaSourceManager,
 51        IUserManager userManager,
 52        ILibraryManager libraryManager,
 53        IServerConfigurationManager serverConfigurationManager,
 54        IMediaEncoder mediaEncoder,
 55        EncodingHelper encodingHelper,
 56        ITranscodeManager transcodeManager,
 57        TranscodingJobType transcodingJobType,
 58        CancellationToken cancellationToken)
 59    {
 60        var httpRequest = httpContext.Request;
 61        if (!string.IsNullOrWhiteSpace(streamingRequest.Params))
 62        {
 63            ParseParams(streamingRequest);
 64        }
 65
 66        streamingRequest.StreamOptions = ParseStreamOptions(httpRequest.Query);
 67        if (httpRequest.Path.Value is null)
 68        {
 69            throw new ResourceNotFoundException(nameof(httpRequest.Path));
 70        }
 71
 72        var url = httpRequest.Path.Value.AsSpan().RightPart('.').ToString();
 73
 74        if (string.IsNullOrEmpty(streamingRequest.AudioCodec))
 75        {
 76            streamingRequest.AudioCodec = encodingHelper.InferAudioCodec(url);
 77        }
 78
 79        var state = new StreamState(mediaSourceManager, transcodingJobType, transcodeManager)
 80        {
 81            Request = streamingRequest,
 82            RequestedUrl = url,
 83            UserAgent = httpRequest.Headers[HeaderNames.UserAgent]
 84        };
 85
 86        var userId = httpContext.User.GetUserId();
 87        if (!userId.IsEmpty())
 88        {
 89            state.User = userManager.GetUserById(userId);
 90        }
 91
 92        if (state.IsVideoRequest && !string.IsNullOrWhiteSpace(state.Request.VideoCodec))
 93        {
 94            state.SupportedVideoCodecs = state.Request.VideoCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
 95            state.Request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
 96        }
 97
 98        if (!string.IsNullOrWhiteSpace(streamingRequest.AudioCodec))
 99        {
 100            state.SupportedAudioCodecs = streamingRequest.AudioCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
 101            state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(mediaEncoder.CanEncodeToAudioCodec)
 102                                       ?? state.SupportedAudioCodecs.FirstOrDefault();
 103        }
 104
 105        if (!string.IsNullOrWhiteSpace(streamingRequest.SubtitleCodec))
 106        {
 107            state.SupportedSubtitleCodecs = streamingRequest.SubtitleCodec.Split(',', StringSplitOptions.RemoveEmptyEntr
 108            state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(mediaEncoder.CanEncodeToSubtitleC
 109                                          ?? state.SupportedSubtitleCodecs.FirstOrDefault();
 110        }
 111
 112        var item = libraryManager.GetItemById<BaseItem>(streamingRequest.Id)
 113            ?? throw new ResourceNotFoundException();
 114
 115        state.IsInputVideo = item.MediaType == MediaType.Video;
 116
 117        MediaSourceInfo? mediaSource = null;
 118        if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId))
 119        {
 120            var currentJob = !string.IsNullOrWhiteSpace(streamingRequest.PlaySessionId)
 121                ? transcodeManager.GetTranscodingJob(streamingRequest.PlaySessionId)
 122                : null;
 123
 124            if (currentJob is not null)
 125            {
 126                mediaSource = currentJob.MediaSource;
 127            }
 128
 129            if (mediaSource is null)
 130            {
 131                var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById<BaseItem>
 132
 133                mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId)
 134                    ? mediaSources[0]
 135                    : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordina
 136
 137                if (mediaSource is null && Guid.Parse(streamingRequest.MediaSourceId).Equals(streamingRequest.Id))
 138                {
 139                    mediaSource = mediaSources[0];
 140                }
 141            }
 142        }
 143        else
 144        {
 145            var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStr
 146            mediaSource = liveStreamInfo.Item1;
 147            state.DirectStreamProvider = liveStreamInfo.Item2;
 148
 149            // Cap the max bitrate when it is too high. This is usually due to ffmpeg is unable to probe the source live
 150            if (mediaSource.FallbackMaxStreamingBitrate is not null && streamingRequest.VideoBitRate is not null)
 151            {
 152                streamingRequest.VideoBitRate = Math.Min(streamingRequest.VideoBitRate.Value, mediaSource.FallbackMaxStr
 153            }
 154        }
 155
 156        var encodingOptions = serverConfigurationManager.GetEncodingOptions();
 157
 158        encodingHelper.AttachMediaSourceInfo(state, encodingOptions, mediaSource, url);
 159
 160        string? containerInternal = Path.GetExtension(state.RequestedUrl);
 161
 162        if (!string.IsNullOrEmpty(streamingRequest.Container))
 163        {
 164            containerInternal = streamingRequest.Container;
 165        }
 166
 167        if (string.IsNullOrEmpty(containerInternal))
 168        {
 169            containerInternal = streamingRequest.Static ?
 170                StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, null, DlnaProfileType.
 171                : GetOutputFileExtension(state, mediaSource);
 172        }
 173
 174        var outputAudioCodec = streamingRequest.AudioCodec;
 175        state.OutputAudioCodec = outputAudioCodec;
 176        state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
 177        state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioC
 178        if (EncodingHelper.LosslessAudioCodecs.Contains(outputAudioCodec))
 179        {
 180            state.OutputAudioBitrate = state.AudioStream.BitRate ?? 0;
 181        }
 182        else
 183        {
 184            state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingReque
 185        }
 186
 187        if (outputAudioCodec.StartsWith("pcm_", StringComparison.Ordinal))
 188        {
 189            containerInternal = ".pcm";
 190        }
 191
 192        if (state.VideoRequest is not null)
 193        {
 194            state.OutputVideoCodec = state.Request.VideoCodec;
 195            state.OutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, s
 196
 197            encodingHelper.TryStreamCopy(state);
 198
 199            if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.OutputVideoBitrate.HasValue)
 200            {
 201                var isVideoResolutionNotRequested = !state.VideoRequest.Width.HasValue
 202                    && !state.VideoRequest.Height.HasValue
 203                    && !state.VideoRequest.MaxWidth.HasValue
 204                    && !state.VideoRequest.MaxHeight.HasValue;
 205
 206                if (isVideoResolutionNotRequested
 207                    && state.VideoStream is not null
 208                    && state.VideoRequest.VideoBitRate.HasValue
 209                    && state.VideoStream.BitRate.HasValue
 210                    && state.VideoRequest.VideoBitRate.Value >= state.VideoStream.BitRate.Value)
 211                {
 212                    // Don't downscale the resolution if the width/height/MaxWidth/MaxHeight is not requested,
 213                    // and the requested video bitrate is higher than source video bitrate.
 214                    if (state.VideoStream.Width.HasValue || state.VideoStream.Height.HasValue)
 215                    {
 216                        state.VideoRequest.MaxWidth = state.VideoStream?.Width;
 217                        state.VideoRequest.MaxHeight = state.VideoStream?.Height;
 218                    }
 219                }
 220                else
 221                {
 222                    var resolution = ResolutionNormalizer.Normalize(
 223                        state.VideoStream?.BitRate,
 224                        state.OutputVideoBitrate.Value,
 225                        state.VideoRequest.MaxWidth,
 226                        state.VideoRequest.MaxHeight);
 227
 228                    state.VideoRequest.MaxWidth = resolution.MaxWidth;
 229                    state.VideoRequest.MaxHeight = resolution.MaxHeight;
 230                }
 231            }
 232        }
 233
 234        var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
 235            ? GetOutputFileExtension(state, mediaSource)
 236            : ("." + GetContainerFileExtension(state.OutputContainer));
 237
 238        state.OutputFilePath = GetOutputFilePath(state, ext, serverConfigurationManager, streamingRequest.DeviceId, stre
 239
 240        return state;
 241    }
 242
 243    /// <summary>
 244    /// Parses query parameters as StreamOptions.
 245    /// </summary>
 246    /// <param name="queryString">The query string.</param>
 247    /// <returns>A <see cref="Dictionary{String,String}"/> containing the stream options.</returns>
 248    private static Dictionary<string, string?> ParseStreamOptions(IQueryCollection queryString)
 249    {
 0250        Dictionary<string, string?> streamOptions = new Dictionary<string, string?>();
 0251        foreach (var param in queryString)
 252        {
 0253            if (char.IsLower(param.Key[0]))
 254            {
 255                // This was probably not parsed initially and should be a StreamOptions
 256                // or the generated URL should correctly serialize it
 257                // TODO: This should be incorporated either in the lower framework for parsing requests
 0258                streamOptions[param.Key] = param.Value;
 259            }
 260        }
 261
 0262        return streamOptions;
 263    }
 264
 265    /// <summary>
 266    /// Gets the output file extension.
 267    /// </summary>
 268    /// <param name="state">The state.</param>
 269    /// <param name="mediaSource">The mediaSource.</param>
 270    /// <returns>System.String.</returns>
 271    private static string GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
 272    {
 0273        var ext = Path.GetExtension(state.RequestedUrl);
 0274        if (!string.IsNullOrEmpty(ext))
 275        {
 0276            return ext;
 277        }
 278
 279        // Try to infer based on the desired video codec
 0280        if (state.IsVideoRequest)
 281        {
 0282            var videoCodec = state.Request.VideoCodec;
 283
 0284            if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
 285            {
 0286                return ".ts";
 287            }
 288
 0289            if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
 0290                || string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase))
 291            {
 0292                return ".mp4";
 293            }
 294
 0295            if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
 296            {
 0297                return ".ogv";
 298            }
 299
 0300            if (string.Equals(videoCodec, "vp8", StringComparison.OrdinalIgnoreCase)
 0301                || string.Equals(videoCodec, "vp9", StringComparison.OrdinalIgnoreCase)
 0302                || string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
 303            {
 0304                return ".webm";
 305            }
 306
 0307            if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
 308            {
 0309                return ".asf";
 310            }
 311        }
 312        else
 313        {
 314            // Try to infer based on the desired audio codec
 0315            var audioCodec = state.Request.AudioCodec;
 316
 0317            if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
 318            {
 0319                return ".aac";
 320            }
 321
 0322            if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase))
 323            {
 0324                return ".mp3";
 325            }
 326
 0327            if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase))
 328            {
 0329                return ".ogg";
 330            }
 331
 0332            if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase))
 333            {
 0334                return ".wma";
 335            }
 336        }
 337
 338        // Fallback to the container of mediaSource
 0339        if (!string.IsNullOrEmpty(mediaSource?.Container))
 340        {
 0341            var idx = mediaSource.Container.IndexOf(',', StringComparison.OrdinalIgnoreCase);
 0342            return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim();
 343        }
 344
 0345        throw new InvalidOperationException("Failed to find an appropriate file extension");
 346    }
 347
 348    /// <summary>
 349    /// Gets the output file path for transcoding.
 350    /// </summary>
 351    /// <param name="state">The current <see cref="StreamState"/>.</param>
 352    /// <param name="outputFileExtension">The file extension of the output file.</param>
 353    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</p
 354    /// <param name="deviceId">The device id.</param>
 355    /// <param name="playSessionId">The play session id.</param>
 356    /// <returns>The complete file path, including the folder, for the transcoding file.</returns>
 357    private static string GetOutputFilePath(StreamState state, string outputFileExtension, IServerConfigurationManager s
 358    {
 0359        var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
 360
 0361        var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
 0362        var ext = outputFileExtension.ToLowerInvariant();
 0363        var folder = serverConfigurationManager.GetTranscodePath();
 364
 0365        return Path.Combine(folder, filename + ext);
 366    }
 367
 368    /// <summary>
 369    /// Parses the parameters.
 370    /// </summary>
 371    /// <param name="request">The request.</param>
 372    private static void ParseParams(StreamingRequestDto request)
 373    {
 0374        if (string.IsNullOrEmpty(request.Params))
 375        {
 0376            return;
 377        }
 378
 0379        var vals = request.Params.Split(';');
 380
 0381        var videoRequest = request as VideoRequestDto;
 382
 0383        for (var i = 0; i < vals.Length; i++)
 384        {
 0385            var val = vals[i];
 386
 0387            if (string.IsNullOrWhiteSpace(val))
 388            {
 389                continue;
 390            }
 391
 392            switch (i)
 393            {
 394                case 0:
 395                    // DeviceProfileId
 396                    break;
 397                case 1:
 0398                    request.DeviceId = val;
 0399                    break;
 400                case 2:
 0401                    request.MediaSourceId = val;
 0402                    break;
 403                case 3:
 0404                    request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
 0405                    break;
 406                case 4:
 0407                    if (videoRequest is not null)
 408                    {
 0409                        videoRequest.VideoCodec = val;
 410                    }
 411
 0412                    break;
 413                case 5:
 0414                    request.AudioCodec = val;
 0415                    break;
 416                case 6:
 0417                    if (videoRequest is not null)
 418                    {
 0419                        videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
 420                    }
 421
 0422                    break;
 423                case 7:
 0424                    if (videoRequest is not null)
 425                    {
 0426                        videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
 427                    }
 428
 0429                    break;
 430                case 8:
 0431                    if (videoRequest is not null)
 432                    {
 0433                        videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture);
 434                    }
 435
 0436                    break;
 437                case 9:
 0438                    request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture);
 0439                    break;
 440                case 10:
 0441                    request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
 0442                    break;
 443                case 11:
 0444                    if (videoRequest is not null)
 445                    {
 0446                        videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture);
 447                    }
 448
 0449                    break;
 450                case 12:
 0451                    if (videoRequest is not null)
 452                    {
 0453                        videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture);
 454                    }
 455
 0456                    break;
 457                case 13:
 0458                    if (videoRequest is not null)
 459                    {
 0460                        videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture);
 461                    }
 462
 0463                    break;
 464                case 14:
 0465                    request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
 0466                    break;
 467                case 15:
 0468                    if (videoRequest is not null)
 469                    {
 0470                        videoRequest.Level = val;
 471                    }
 472
 0473                    break;
 474                case 16:
 0475                    if (videoRequest is not null)
 476                    {
 0477                        videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture);
 478                    }
 479
 0480                    break;
 481                case 17:
 0482                    if (videoRequest is not null)
 483                    {
 0484                        videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture);
 485                    }
 486
 0487                    break;
 488                case 18:
 0489                    if (videoRequest is not null)
 490                    {
 0491                        videoRequest.Profile = val;
 492                    }
 493
 0494                    break;
 495                case 19:
 496                    // cabac no longer used
 497                    break;
 498                case 20:
 0499                    request.PlaySessionId = val;
 0500                    break;
 501                case 21:
 502                    // api_key
 503                    break;
 504                case 22:
 0505                    request.LiveStreamId = val;
 0506                    break;
 507                case 23:
 508                    // Duplicating ItemId because of MediaMonkey
 509                    break;
 510                case 24:
 0511                    if (videoRequest is not null)
 512                    {
 0513                        videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
 514                    }
 515
 0516                    break;
 517                case 25:
 0518                    if (!string.IsNullOrWhiteSpace(val) && videoRequest is not null)
 519                    {
 0520                        if (Enum.TryParse(val, out SubtitleDeliveryMethod method))
 521                        {
 0522                            videoRequest.SubtitleMethod = method;
 523                        }
 524                    }
 525
 0526                    break;
 527                case 26:
 0528                    request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
 0529                    break;
 530                case 27:
 0531                    if (videoRequest is not null)
 532                    {
 0533                        videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgno
 534                    }
 535
 0536                    break;
 537                case 28:
 0538                    request.Tag = val;
 0539                    break;
 540                case 29:
 0541                    if (videoRequest is not null)
 542                    {
 0543                        videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
 544                    }
 545
 0546                    break;
 547                case 30:
 0548                    request.SubtitleCodec = val;
 0549                    break;
 550                case 31:
 0551                    if (videoRequest is not null)
 552                    {
 0553                        videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCas
 554                    }
 555
 0556                    break;
 557                case 32:
 0558                    if (videoRequest is not null)
 559                    {
 0560                        videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
 561                    }
 562
 0563                    break;
 564                case 33:
 0565                    request.TranscodeReasons = val;
 566                    break;
 567            }
 568        }
 0569    }
 570
 571    /// <summary>
 572    /// Parses the container into its file extension.
 573    /// </summary>
 574    /// <param name="container">The container.</param>
 575    private static string? GetContainerFileExtension(string? container)
 576    {
 0577        if (string.Equals(container, "mpegts", StringComparison.OrdinalIgnoreCase))
 578        {
 0579            return "ts";
 580        }
 581
 0582        if (string.Equals(container, "matroska", StringComparison.OrdinalIgnoreCase))
 583        {
 0584            return "mkv";
 585        }
 586
 0587        return container;
 588    }
 589}