| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784 | using System;using System.Collections.Generic;using System.Globalization;using System.IO;using System.Linq;using System.Threading;using System.Threading.Tasks;using Jellyfin.Api.Models.StreamingDtos;using Jellyfin.Extensions;using MediaBrowser.Common.Configuration;using MediaBrowser.Common.Extensions;using MediaBrowser.Controller.Configuration;using MediaBrowser.Controller.Devices;using MediaBrowser.Controller.Dlna;using MediaBrowser.Controller.Library;using MediaBrowser.Controller.MediaEncoding;using MediaBrowser.Controller.Net;using MediaBrowser.Model.Dlna;using MediaBrowser.Model.Dto;using MediaBrowser.Model.Entities;using Microsoft.AspNetCore.Http;using Microsoft.Extensions.Primitives;using Microsoft.Net.Http.Headers;namespace Jellyfin.Api.Helpers{    /// <summary>    /// The streaming helpers.    /// </summary>    public static class StreamingHelpers    {        /// <summary>        /// Gets the current streaming state.        /// </summary>        /// <param name="streamingRequest">The <see cref="StreamingRequestDto"/>.</param>        /// <param name="httpRequest">The <see cref="HttpRequest"/>.</param>        /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>        /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>        /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>        /// <param name="transcodingJobHelper">Initialized <see cref="TranscodingJobHelper"/>.</param>        /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>        /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>        /// <returns>A <see cref="Task"/> containing the current <see cref="StreamState"/>.</returns>        public static async Task<StreamState> GetStreamingState(            StreamingRequestDto streamingRequest,            HttpRequest httpRequest,            IAuthorizationContext authorizationContext,            IMediaSourceManager mediaSourceManager,            IUserManager userManager,            ILibraryManager libraryManager,            IServerConfigurationManager serverConfigurationManager,            IMediaEncoder mediaEncoder,            EncodingHelper encodingHelper,            IDlnaManager dlnaManager,            IDeviceManager deviceManager,            TranscodingJobHelper transcodingJobHelper,            TranscodingJobType transcodingJobType,            CancellationToken cancellationToken)        {            // Parse the DLNA time seek header            if (!streamingRequest.StartTimeTicks.HasValue)            {                var timeSeek = httpRequest.Headers["TimeSeekRange.dlna.org"];                streamingRequest.StartTimeTicks = ParseTimeSeekHeader(timeSeek.ToString());            }            if (!string.IsNullOrWhiteSpace(streamingRequest.Params))            {                ParseParams(streamingRequest);            }            streamingRequest.StreamOptions = ParseStreamOptions(httpRequest.Query);            if (httpRequest.Path.Value == null)            {                throw new ResourceNotFoundException(nameof(httpRequest.Path));            }            var url = httpRequest.Path.Value.AsSpan().RightPart('.').ToString();            if (string.IsNullOrEmpty(streamingRequest.AudioCodec))            {                streamingRequest.AudioCodec = encodingHelper.InferAudioCodec(url);            }            var enableDlnaHeaders = !string.IsNullOrWhiteSpace(streamingRequest.Params) ||                                    streamingRequest.StreamOptions.ContainsKey("dlnaheaders") ||                                    string.Equals(httpRequest.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase);            var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper)            {                Request = streamingRequest,                RequestedUrl = url,                UserAgent = httpRequest.Headers[HeaderNames.UserAgent],                EnableDlnaHeaders = enableDlnaHeaders            };            var auth = await authorizationContext.GetAuthorizationInfo(httpRequest).ConfigureAwait(false);            if (!auth.UserId.Equals(default))            {                state.User = userManager.GetUserById(auth.UserId);            }            if (state.IsVideoRequest && !string.IsNullOrWhiteSpace(state.Request.VideoCodec))            {                state.SupportedVideoCodecs = state.Request.VideoCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);                state.Request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();            }            if (!string.IsNullOrWhiteSpace(streamingRequest.AudioCodec))            {                state.SupportedAudioCodecs = streamingRequest.AudioCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);                state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(mediaEncoder.CanEncodeToAudioCodec)                                           ?? state.SupportedAudioCodecs.FirstOrDefault();            }            if (!string.IsNullOrWhiteSpace(streamingRequest.SubtitleCodec))            {                state.SupportedSubtitleCodecs = streamingRequest.SubtitleCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);                state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(mediaEncoder.CanEncodeToSubtitleCodec)                                              ?? state.SupportedSubtitleCodecs.FirstOrDefault();            }            var item = libraryManager.GetItemById(streamingRequest.Id);            state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);            MediaSourceInfo? mediaSource = null;            if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId))            {                var currentJob = !string.IsNullOrWhiteSpace(streamingRequest.PlaySessionId)                    ? transcodingJobHelper.GetTranscodingJob(streamingRequest.PlaySessionId)                    : null;                if (currentJob != null)                {                    mediaSource = currentJob.MediaSource;                }                if (mediaSource == null)                {                    var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById(streamingRequest.Id), null, false, false, cancellationToken).ConfigureAwait(false);                    mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId)                        ? mediaSources[0]                        : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordinal));                    if (mediaSource == null && Guid.Parse(streamingRequest.MediaSourceId).Equals(streamingRequest.Id))                    {                        mediaSource = mediaSources[0];                    }                }            }            else            {                var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);                mediaSource = liveStreamInfo.Item1;                state.DirectStreamProvider = liveStreamInfo.Item2;            }            var encodingOptions = serverConfigurationManager.GetEncodingOptions();            encodingHelper.AttachMediaSourceInfo(state, encodingOptions, mediaSource, url);            string? containerInternal = Path.GetExtension(state.RequestedUrl);            if (!string.IsNullOrEmpty(streamingRequest.Container))            {                containerInternal = streamingRequest.Container;            }            if (string.IsNullOrEmpty(containerInternal))            {                containerInternal = streamingRequest.Static ?                    StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, null, DlnaProfileType.Audio)                    : GetOutputFileExtension(state, mediaSource);            }            state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');            state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream);            state.OutputAudioCodec = streamingRequest.AudioCodec;            state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);            if (state.VideoRequest != null)            {                state.OutputVideoCodec = state.Request.VideoCodec;                state.OutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);                encodingHelper.TryStreamCopy(state);                if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.OutputVideoBitrate.HasValue)                {                    var isVideoResolutionNotRequested = !state.VideoRequest.Width.HasValue                        && !state.VideoRequest.Height.HasValue                        && !state.VideoRequest.MaxWidth.HasValue                        && !state.VideoRequest.MaxHeight.HasValue;                    if (isVideoResolutionNotRequested                        && state.VideoStream != null                        && state.VideoRequest.VideoBitRate.HasValue                        && state.VideoStream.BitRate.HasValue                        && state.VideoRequest.VideoBitRate.Value >= state.VideoStream.BitRate.Value)                    {                        // Don't downscale the resolution if the width/height/MaxWidth/MaxHeight is not requested,                        // and the requested video bitrate is higher than source video bitrate.                        if (state.VideoStream.Width.HasValue || state.VideoStream.Height.HasValue)                        {                            state.VideoRequest.MaxWidth = state.VideoStream?.Width;                            state.VideoRequest.MaxHeight = state.VideoStream?.Height;                        }                    }                    else                    {                        var resolution = ResolutionNormalizer.Normalize(                            state.VideoStream?.BitRate,                            state.OutputVideoBitrate.Value,                            state.VideoRequest.MaxWidth,                            state.VideoRequest.MaxHeight);                        state.VideoRequest.MaxWidth = resolution.MaxWidth;                        state.VideoRequest.MaxHeight = resolution.MaxHeight;                    }                }            }            ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, httpRequest, streamingRequest.DeviceProfileId, streamingRequest.Static);            var ext = string.IsNullOrWhiteSpace(state.OutputContainer)                ? GetOutputFileExtension(state, mediaSource)                : ("." + state.OutputContainer);            state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);            return state;        }        /// <summary>        /// Adds the dlna headers.        /// </summary>        /// <param name="state">The state.</param>        /// <param name="responseHeaders">The response headers.</param>        /// <param name="isStaticallyStreamed">if set to <c>true</c> [is statically streamed].</param>        /// <param name="startTimeTicks">The start time in ticks.</param>        /// <param name="request">The <see cref="HttpRequest"/>.</param>        /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>        public static void AddDlnaHeaders(            StreamState state,            IHeaderDictionary responseHeaders,            bool isStaticallyStreamed,            long? startTimeTicks,            HttpRequest request,            IDlnaManager dlnaManager)        {            if (!state.EnableDlnaHeaders)            {                return;            }            var profile = state.DeviceProfile;            StringValues transferMode = request.Headers["transferMode.dlna.org"];            responseHeaders.Add("transferMode.dlna.org", string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode.ToString());            responseHeaders.Add("realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*");            if (state.RunTimeTicks.HasValue)            {                if (string.Equals(request.Headers["getMediaInfo.sec"], "1", StringComparison.OrdinalIgnoreCase))                {                    var ms = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds;                    responseHeaders.Add("MediaInfo.sec", string.Format(                        CultureInfo.InvariantCulture,                        "SEC_Duration={0};",                        Convert.ToInt32(ms)));                }                if (!isStaticallyStreamed && profile != null)                {                    AddTimeSeekResponseHeaders(state, responseHeaders, startTimeTicks);                }            }            profile ??= dlnaManager.GetDefaultProfile();            var audioCodec = state.ActualOutputAudioCodec;            if (!state.IsVideoRequest)            {                responseHeaders.Add("contentFeatures.dlna.org", ContentFeatureBuilder.BuildAudioHeader(                    profile,                    state.OutputContainer,                    audioCodec,                    state.OutputAudioBitrate,                    state.OutputAudioSampleRate,                    state.OutputAudioChannels,                    state.OutputAudioBitDepth,                    isStaticallyStreamed,                    state.RunTimeTicks,                    state.TranscodeSeekInfo));            }            else            {                var videoCodec = state.ActualOutputVideoCodec;                responseHeaders.Add(                    "contentFeatures.dlna.org",                    ContentFeatureBuilder.BuildVideoHeader(profile, state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoRangeType, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty);            }        }        /// <summary>        /// Parses the time seek header.        /// </summary>        /// <param name="value">The time seek header string.</param>        /// <returns>A nullable <see cref="long"/> representing the seek time in ticks.</returns>        private static long? ParseTimeSeekHeader(ReadOnlySpan<char> value)        {            if (value.IsEmpty)            {                return null;            }            const string npt = "npt=";            if (!value.StartsWith(npt, StringComparison.OrdinalIgnoreCase))            {                throw new ArgumentException("Invalid timeseek header");            }            var index = value.IndexOf('-');            value = index == -1                ? value.Slice(npt.Length)                : value.Slice(npt.Length, index - npt.Length);            if (value.IndexOf(':') == -1)            {                // Parses npt times in the format of '417.33'                if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds))                {                    return TimeSpan.FromSeconds(seconds).Ticks;                }                throw new ArgumentException("Invalid timeseek header");            }            try            {                // Parses npt times in the format of '10:19:25.7'                return TimeSpan.Parse(value).Ticks;            }            catch            {                throw new ArgumentException("Invalid timeseek header");            }        }        /// <summary>        /// Parses query parameters as StreamOptions.        /// </summary>        /// <param name="queryString">The query string.</param>        /// <returns>A <see cref="Dictionary{String,String}"/> containing the stream options.</returns>        private static Dictionary<string, string> ParseStreamOptions(IQueryCollection queryString)        {            Dictionary<string, string> streamOptions = new Dictionary<string, string>();            foreach (var param in queryString)            {                if (char.IsLower(param.Key[0]))                {                    // This was probably not parsed initially and should be a StreamOptions                    // or the generated URL should correctly serialize it                    // TODO: This should be incorporated either in the lower framework for parsing requests                    streamOptions[param.Key] = param.Value;                }            }            return streamOptions;        }        /// <summary>        /// Adds the dlna time seek headers to the response.        /// </summary>        /// <param name="state">The current <see cref="StreamState"/>.</param>        /// <param name="responseHeaders">The <see cref="IHeaderDictionary"/> of the response.</param>        /// <param name="startTimeTicks">The start time in ticks.</param>        private static void AddTimeSeekResponseHeaders(StreamState state, IHeaderDictionary responseHeaders, long? startTimeTicks)        {            var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks!.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture);            var startSeconds = TimeSpan.FromTicks(startTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);            responseHeaders.Add("TimeSeekRange.dlna.org", string.Format(                CultureInfo.InvariantCulture,                "npt={0}-{1}/{1}",                startSeconds,                runtimeSeconds));            responseHeaders.Add("X-AvailableSeekRange", string.Format(                CultureInfo.InvariantCulture,                "1 npt={0}-{1}",                startSeconds,                runtimeSeconds));        }        /// <summary>        /// Gets the output file extension.        /// </summary>        /// <param name="state">The state.</param>        /// <param name="mediaSource">The mediaSource.</param>        /// <returns>System.String.</returns>        private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)        {            var ext = Path.GetExtension(state.RequestedUrl);            if (!string.IsNullOrEmpty(ext))            {                return ext;            }            // Try to infer based on the desired video codec            if (state.IsVideoRequest)            {                var videoCodec = state.Request.VideoCodec;                if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||                    string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase))                {                    return ".ts";                }                if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))                {                    return ".ogv";                }                if (string.Equals(videoCodec, "vp8", StringComparison.OrdinalIgnoreCase)                    || string.Equals(videoCodec, "vp9", StringComparison.OrdinalIgnoreCase)                    || string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))                {                    return ".webm";                }                if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))                {                    return ".asf";                }            }            // Try to infer based on the desired audio codec            if (!state.IsVideoRequest)            {                var audioCodec = state.Request.AudioCodec;                if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))                {                    return ".aac";                }                if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase))                {                    return ".mp3";                }                if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase))                {                    return ".ogg";                }                if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase))                {                    return ".wma";                }            }            // Fallback to the container of mediaSource            if (!string.IsNullOrEmpty(mediaSource?.Container))            {                var idx = mediaSource.Container.IndexOf(',', StringComparison.OrdinalIgnoreCase);                return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim();            }            return null;        }        /// <summary>        /// Gets the output file path for transcoding.        /// </summary>        /// <param name="state">The current <see cref="StreamState"/>.</param>        /// <param name="outputFileExtension">The file extension of the output file.</param>        /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>        /// <param name="deviceId">The device id.</param>        /// <param name="playSessionId">The play session id.</param>        /// <returns>The complete file path, including the folder, for the transcoding file.</returns>        private static string GetOutputFilePath(StreamState state, string outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId)        {            var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";            var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);            var ext = outputFileExtension?.ToLowerInvariant();            var folder = serverConfigurationManager.GetTranscodePath();            return Path.Combine(folder, filename + ext);        }        private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static)        {            if (!string.IsNullOrWhiteSpace(deviceProfileId))            {                state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId);                if (state.DeviceProfile == null)                {                    var caps = deviceManager.GetCapabilities(deviceProfileId);                    state.DeviceProfile = caps == null ? dlnaManager.GetProfile(request.Headers) : caps.DeviceProfile;                }            }            var profile = state.DeviceProfile;            if (profile == null)            {                // Don't use settings from the default profile.                // Only use a specific profile if it was requested.                return;            }            var audioCodec = state.ActualOutputAudioCodec;            var videoCodec = state.ActualOutputVideoCodec;            var mediaProfile = !state.IsVideoRequest                ? profile.GetAudioMediaProfile(state.OutputContainer, audioCodec, state.OutputAudioChannels, state.OutputAudioBitrate, state.OutputAudioSampleRate, state.OutputAudioBitDepth)                : profile.GetVideoMediaProfile(                    state.OutputContainer,                    audioCodec,                    videoCodec,                    state.OutputWidth,                    state.OutputHeight,                    state.TargetVideoBitDepth,                    state.OutputVideoBitrate,                    state.TargetVideoProfile,                    state.TargetVideoRangeType,                    state.TargetVideoLevel,                    state.TargetFramerate,                    state.TargetPacketLength,                    state.TargetTimestamp,                    state.IsTargetAnamorphic,                    state.IsTargetInterlaced,                    state.TargetRefFrames,                    state.TargetVideoStreamCount,                    state.TargetAudioStreamCount,                    state.TargetVideoCodecTag,                    state.IsTargetAVC);            if (mediaProfile != null)            {                state.MimeType = mediaProfile.MimeType;            }            if (!(@static.HasValue && @static.Value))            {                var transcodingProfile = !state.IsVideoRequest ? profile.GetAudioTranscodingProfile(state.OutputContainer, audioCodec) : profile.GetVideoTranscodingProfile(state.OutputContainer, audioCodec, videoCodec);                if (transcodingProfile != null)                {                    state.EstimateContentLength = transcodingProfile.EstimateContentLength;                    // state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;                    state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;                    if (state.VideoRequest != null)                    {                        state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps;                        state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;                    }                }            }        }        /// <summary>        /// Parses the parameters.        /// </summary>        /// <param name="request">The request.</param>        private static void ParseParams(StreamingRequestDto request)        {            if (string.IsNullOrEmpty(request.Params))            {                return;            }            var vals = request.Params.Split(';');            var videoRequest = request as VideoRequestDto;            for (var i = 0; i < vals.Length; i++)            {                var val = vals[i];                if (string.IsNullOrWhiteSpace(val))                {                    continue;                }                switch (i)                {                    case 0:                        request.DeviceProfileId = val;                        break;                    case 1:                        request.DeviceId = val;                        break;                    case 2:                        request.MediaSourceId = val;                        break;                    case 3:                        request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);                        break;                    case 4:                        if (videoRequest != null)                        {                            videoRequest.VideoCodec = val;                        }                        break;                    case 5:                        request.AudioCodec = val;                        break;                    case 6:                        if (videoRequest != null)                        {                            videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);                        }                        break;                    case 7:                        if (videoRequest != null)                        {                            videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);                        }                        break;                    case 8:                        if (videoRequest != null)                        {                            videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture);                        }                        break;                    case 9:                        request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture);                        break;                    case 10:                        request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);                        break;                    case 11:                        if (videoRequest != null)                        {                            videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture);                        }                        break;                    case 12:                        if (videoRequest != null)                        {                            videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture);                        }                        break;                    case 13:                        if (videoRequest != null)                        {                            videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture);                        }                        break;                    case 14:                        request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);                        break;                    case 15:                        if (videoRequest != null)                        {                            videoRequest.Level = val;                        }                        break;                    case 16:                        if (videoRequest != null)                        {                            videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture);                        }                        break;                    case 17:                        if (videoRequest != null)                        {                            videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture);                        }                        break;                    case 18:                        if (videoRequest != null)                        {                            videoRequest.Profile = val;                        }                        break;                    case 19:                        // cabac no longer used                        break;                    case 20:                        request.PlaySessionId = val;                        break;                    case 21:                        // api_key                        break;                    case 22:                        request.LiveStreamId = val;                        break;                    case 23:                        // Duplicating ItemId because of MediaMonkey                        break;                    case 24:                        if (videoRequest != null)                        {                            videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);                        }                        break;                    case 25:                        if (!string.IsNullOrWhiteSpace(val) && videoRequest != null)                        {                            if (Enum.TryParse(val, out SubtitleDeliveryMethod method))                            {                                videoRequest.SubtitleMethod = method;                            }                        }                        break;                    case 26:                        request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);                        break;                    case 27:                        if (videoRequest != null)                        {                            videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);                        }                        break;                    case 28:                        request.Tag = val;                        break;                    case 29:                        if (videoRequest != null)                        {                            videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);                        }                        break;                    case 30:                        request.SubtitleCodec = val;                        break;                    case 31:                        if (videoRequest != null)                        {                            videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);                        }                        break;                    case 32:                        if (videoRequest != null)                        {                            videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);                        }                        break;                    case 33:                        request.TranscodeReasons = val;                        break;                }            }        }    }}
 |