< Summary - Jellyfin

Information
Class: MediaBrowser.Model.Dlna.StreamBuilder
Assembly: MediaBrowser.Model
File(s): /srv/git/jellyfin/MediaBrowser.Model/Dlna/StreamBuilder.cs
Line coverage
68%
Covered lines: 724
Uncovered lines: 331
Coverable lines: 1055
Total lines: 2399
Line coverage: 68.6%
Branch coverage
58%
Covered branches: 553
Total branches: 942
Branch coverage: 58.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 10/18/2025 - 12:10:13 AM Line coverage: 68.6% (725/1056) Branch coverage: 58.7% (553/942) Total lines: 24001/19/2026 - 12:13:54 AM Line coverage: 68.6% (724/1055) Branch coverage: 58.7% (553/942) Total lines: 2399 10/18/2025 - 12:10:13 AM Line coverage: 68.6% (725/1056) Branch coverage: 58.7% (553/942) Total lines: 24001/19/2026 - 12:13:54 AM Line coverage: 68.6% (724/1055) Branch coverage: 58.7% (553/942) Total lines: 2399

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
GetOptimalAudioStream(...)0%110100%
GetOptimalAudioStream(...)0%4692680%
GetOptimalVideoStream(...)80%1010100%
GetOptimalStream(...)100%11100%
SortMediaSources(...)100%11100%
GetTranscodeReasonForFailedCondition(...)29.62%2432733.33%
NormalizeMediaSourceFormatIntoSingleContainer(...)100%1616100%
GetAudioDirectPlayProfile(...)0%600240%
GetTranscodeReasonsFromDirectPlayProfile(...)0%420200%
GetDefaultSubtitleStreamIndex(...)96.15%272688.23%
SetStreamInfoOptionsFromTranscodingProfile(...)87.5%8895%
SetStreamInfoOptionsFromDirectPlayProfile(...)0%620%
BuildVideoItem(...)66.96%14011286.91%
GetVideoTranscodeProfile(...)70%101096.96%
BuildStreamVideoItem(...)92.06%12612697.24%
GetDefaultAudioBitrate(...)68.18%332271.42%
GetAudioBitrate(...)87.5%323295.83%
GetMaxAudioBitrateForTotalBitrate(...)68.75%231670.58%
GetVideoDirectPlayProfile(...)63.63%222296.11%
AggregateFailureConditions(...)100%11100%
LogConditionFailure(...)66.66%66100%
GetSubtitleProfile(...)14.28%4974236.36%
IsSubtitleEmbedSupported(...)0%4260%
GetExternalSubtitleProfile(...)91.66%3636100%
IsBitrateLimitExceeded(...)66.66%6690.9%
ValidateMediaOptions(...)68.75%351658.33%
GetProfileConditionsForVideoAudio(...)100%11100%
GetProfileConditionsForAudio(...)0%620%
ApplyTranscodingConditions(...)63.8%714822147.85%
IsAudioContainerSupported(...)0%2040%
IsAudioDirectPlaySupported(...)0%4260%
IsAudioDirectStreamSupported(...)0%4260%
GetRank(...)100%44100%
CheckVideoConditions(...)50%3030100%
GetCompatibilityContainer(...)100%11100%
GetCompatibilityVideoCodec(...)100%11100%
GetCompatibilityAudioCodec(...)75%44100%
GetCompatibilityAudioCodecDirect(...)100%22100%

File(s)

/srv/git/jellyfin/MediaBrowser.Model/Dlna/StreamBuilder.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Globalization;
 4using System.Linq;
 5using Jellyfin.Data.Enums;
 6using Jellyfin.Extensions;
 7using MediaBrowser.Model.Dto;
 8using MediaBrowser.Model.Entities;
 9using MediaBrowser.Model.Extensions;
 10using MediaBrowser.Model.MediaInfo;
 11using MediaBrowser.Model.Session;
 12using Microsoft.Extensions.Logging;
 13
 14namespace MediaBrowser.Model.Dlna
 15{
 16    /// <summary>
 17    /// Class StreamBuilder.
 18    /// </summary>
 19    public class StreamBuilder
 20    {
 21        // Aliases
 22        internal const TranscodeReason ContainerReasons = TranscodeReason.ContainerNotSupported | TranscodeReason.Contai
 23        internal const TranscodeReason AudioCodecReasons = TranscodeReason.AudioBitrateNotSupported | TranscodeReason.Au
 24        internal const TranscodeReason AudioReasons = TranscodeReason.AudioCodecNotSupported | AudioCodecReasons;
 25        internal const TranscodeReason VideoCodecReasons = TranscodeReason.VideoResolutionNotSupported | TranscodeReason
 26        internal const TranscodeReason VideoReasons = TranscodeReason.VideoCodecNotSupported | VideoCodecReasons;
 27        internal const TranscodeReason DirectStreamReasons = AudioReasons | TranscodeReason.ContainerNotSupported | Tran
 28
 29        private readonly ILogger _logger;
 30        private readonly ITranscoderSupport _transcoderSupport;
 131        private static readonly string[] _supportedHlsVideoCodecs = ["h264", "hevc", "vp9", "av1"];
 132        private static readonly string[] _supportedHlsAudioCodecsTs = ["aac", "ac3", "eac3", "mp3"];
 133        private static readonly string[] _supportedHlsAudioCodecsMp4 = ["aac", "ac3", "eac3", "mp3", "alac", "flac", "op
 34
 35        /// <summary>
 36        /// Initializes a new instance of the <see cref="StreamBuilder"/> class.
 37        /// </summary>
 38        /// <param name="transcoderSupport">The <see cref="ITranscoderSupport"/> object.</param>
 39        /// <param name="logger">The <see cref="ILogger"/> object.</param>
 40        public StreamBuilder(ITranscoderSupport transcoderSupport, ILogger logger)
 41        {
 27842            _transcoderSupport = transcoderSupport;
 27843            _logger = logger;
 27844        }
 45
 46        /// <summary>
 47        /// Gets the optimal audio stream.
 48        /// </summary>
 49        /// <param name="options">The <see cref="MediaOptions"/> object to get the audio stream from.</param>
 50        /// <returns>The <see cref="StreamInfo"/> of the optimal audio stream.</returns>
 51        public StreamInfo? GetOptimalAudioStream(MediaOptions options)
 52        {
 053            ValidateMediaOptions(options, false);
 54
 055            List<StreamInfo> streams = [];
 056            foreach (var mediaSource in options.MediaSources)
 57            {
 058                if (!(string.IsNullOrEmpty(options.MediaSourceId)
 059                    || string.Equals(mediaSource.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)))
 60                {
 61                    continue;
 62                }
 63
 064                StreamInfo? streamInfo = GetOptimalAudioStream(mediaSource, options);
 065                if (streamInfo is not null)
 66                {
 067                    streamInfo.DeviceId = options.DeviceId;
 068                    streamInfo.DeviceProfileId = options.Profile.Id?.ToString("N", CultureInfo.InvariantCulture);
 069                    streams.Add(streamInfo);
 70                }
 71            }
 72
 073            return GetOptimalStream(streams, options.GetMaxBitrate(true) ?? 0);
 74        }
 75
 76        private StreamInfo? GetOptimalAudioStream(MediaSourceInfo item, MediaOptions options)
 77        {
 078            var playlistItem = new StreamInfo
 079            {
 080                ItemId = options.ItemId,
 081                MediaType = DlnaProfileType.Audio,
 082                MediaSource = item,
 083                RunTimeTicks = item.RunTimeTicks,
 084                Context = options.Context,
 085                DeviceProfile = options.Profile
 086            };
 87
 088            if (options.ForceDirectPlay)
 89            {
 090                playlistItem.PlayMethod = PlayMethod.DirectPlay;
 091                playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, 
 092                return playlistItem;
 93            }
 94
 095            if (options.ForceDirectStream)
 96            {
 097                playlistItem.PlayMethod = PlayMethod.DirectStream;
 098                playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, 
 099                return playlistItem;
 100            }
 101
 0102            MediaStream audioStream = item.GetDefaultAudioStream(null);
 103
 0104            ArgumentNullException.ThrowIfNull(audioStream);
 105
 0106            var directPlayInfo = GetAudioDirectPlayProfile(item, audioStream, options);
 107
 0108            var directPlayMethod = directPlayInfo.PlayMethod;
 0109            var transcodeReasons = directPlayInfo.TranscodeReasons;
 110
 0111            if (directPlayMethod is PlayMethod.DirectPlay)
 112            {
 0113                var audioFailureReasons = GetCompatibilityAudioCodec(options, item, item.Container, audioStream, null, f
 0114                transcodeReasons |= audioFailureReasons;
 115
 0116                if (audioFailureReasons == 0)
 117                {
 0118                    playlistItem.PlayMethod = directPlayMethod.Value;
 0119                    playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profi
 120
 0121                    return playlistItem;
 122                }
 123            }
 124
 0125            if (directPlayMethod is PlayMethod.DirectStream)
 126            {
 0127                var remuxContainer = item.TranscodingContainer ?? "ts";
 0128                string[] supportedHlsContainers = ["ts", "mp4"];
 129                // If the container specified for the profile is an HLS supported container, use that container instead,
 130                // The client should be responsible to ensure this container is compatible
 0131                remuxContainer = Array.Exists(supportedHlsContainers, element => string.Equals(element, directPlayInfo.P
 132                bool codeIsSupported;
 0133                if (item.TranscodingSubProtocol == MediaStreamProtocol.hls)
 134                {
 135                    // Enforce HLS audio codec restrictions
 0136                    if (string.Equals(remuxContainer, "mp4", StringComparison.OrdinalIgnoreCase))
 137                    {
 0138                        codeIsSupported = _supportedHlsAudioCodecsMp4.Contains(directPlayInfo.Profile?.AudioCodec ?? dir
 139                    }
 140                    else
 141                    {
 0142                        codeIsSupported = _supportedHlsAudioCodecsTs.Contains(directPlayInfo.Profile?.AudioCodec ?? dire
 143                    }
 144                }
 145                else
 146                {
 147                    // Let's assume the client has given a correct container for http
 0148                    codeIsSupported = true;
 149                }
 150
 0151                if (codeIsSupported)
 152                {
 0153                    playlistItem.PlayMethod = directPlayMethod.Value;
 0154                    playlistItem.Container = remuxContainer;
 0155                    playlistItem.TranscodeReasons = transcodeReasons;
 0156                    playlistItem.SubProtocol = item.TranscodingSubProtocol;
 0157                    item.TranscodingContainer = remuxContainer;
 0158                    return playlistItem;
 159                }
 160
 0161                transcodeReasons |= TranscodeReason.AudioCodecNotSupported;
 0162                playlistItem.TranscodeReasons = transcodeReasons;
 163            }
 164
 0165            TranscodingProfile? transcodingProfile = null;
 0166            foreach (var tcProfile in options.Profile.TranscodingProfiles)
 167            {
 0168                if (tcProfile.Type == playlistItem.MediaType
 0169                    && tcProfile.Context == options.Context
 0170                    && _transcoderSupport.CanEncodeToAudioCodec(tcProfile.AudioCodec ?? tcProfile.Container))
 171                {
 0172                    transcodingProfile = tcProfile;
 0173                    break;
 174                }
 175            }
 176
 0177            if (transcodingProfile is not null)
 178            {
 0179                if (!item.SupportsTranscoding)
 180                {
 0181                    return null;
 182                }
 183
 0184                SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile);
 185
 0186                var inputAudioChannels = audioStream.Channels;
 0187                var inputAudioBitrate = audioStream.BitRate;
 0188                var inputAudioSampleRate = audioStream.SampleRate;
 0189                var inputAudioBitDepth = audioStream.BitDepth;
 190
 0191                var audioTranscodingConditions = GetProfileConditionsForAudio(options.Profile.CodecProfiles, transcoding
 0192                ApplyTranscodingConditions(playlistItem, audioTranscodingConditions, null, true, true);
 193
 194                // Honor requested max channels
 0195                playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels;
 196
 0197                var configuredBitrate = options.GetMaxBitrate(true);
 198
 0199                long transcodingBitrate = options.AudioTranscodingBitrate
 0200                    ?? (options.Context == EncodingContext.Streaming ? options.Profile.MusicStreamingTranscodingBitrate 
 0201                    ?? configuredBitrate
 0202                    ?? 128000;
 203
 0204                if (configuredBitrate.HasValue)
 205                {
 0206                    transcodingBitrate = Math.Min(configuredBitrate.Value, transcodingBitrate);
 207                }
 208
 0209                var longBitrate = Math.Min(transcodingBitrate, playlistItem.AudioBitrate ?? transcodingBitrate);
 0210                playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate);
 211
 212                // Pure audio transcoding does not support comma separated list of transcoding codec at the moment.
 213                // So just use the AudioCodec as is would be safe enough as the _transcoderSupport.CanEncodeToAudioCodec
 214                // would fail so this profile will not even be picked up.
 0215                if (playlistItem.AudioCodecs.Count == 0 && !string.IsNullOrWhiteSpace(transcodingProfile.AudioCodec))
 216                {
 0217                    playlistItem.AudioCodecs = [transcodingProfile.AudioCodec];
 218                }
 219            }
 220
 0221            playlistItem.TranscodeReasons = transcodeReasons;
 0222            return playlistItem;
 223        }
 224
 225        /// <summary>
 226        /// Gets the optimal video stream.
 227        /// </summary>
 228        /// <param name="options">The <see cref="MediaOptions"/> object to get the video stream from.</param>
 229        /// <returns>The <see cref="StreamInfo"/> of the optimal video stream.</returns>
 230        public StreamInfo? GetOptimalVideoStream(MediaOptions options)
 231        {
 278232            ValidateMediaOptions(options, true);
 233
 278234            var mediaSources = string.IsNullOrEmpty(options.MediaSourceId)
 278235                ? options.MediaSources
 278236                : options.MediaSources.Where(x => string.Equals(x.Id, options.MediaSourceId, StringComparison.OrdinalIgn
 237
 278238            List<StreamInfo> streams = [];
 1112239            foreach (var mediaSourceInfo in mediaSources)
 240            {
 278241                var streamInfo = BuildVideoItem(mediaSourceInfo, options);
 278242                if (streamInfo is not null)
 243                {
 278244                    streams.Add(streamInfo);
 245                }
 246            }
 247
 1112248            foreach (var stream in streams)
 249            {
 278250                stream.DeviceId = options.DeviceId;
 278251                stream.DeviceProfileId = options.Profile.Id?.ToString("N", CultureInfo.InvariantCulture);
 252            }
 253
 278254            return GetOptimalStream(streams, options.GetMaxBitrate(false) ?? 0);
 255        }
 256
 257        private static StreamInfo? GetOptimalStream(List<StreamInfo> streams, long maxBitrate)
 278258            => SortMediaSources(streams, maxBitrate).FirstOrDefault();
 259
 260        private static IOrderedEnumerable<StreamInfo> SortMediaSources(List<StreamInfo> streams, long maxBitrate)
 261        {
 278262            return streams.OrderBy(i =>
 278263            {
 278264                // Nothing beats direct playing a file
 278265                if (i.PlayMethod == PlayMethod.DirectPlay && i.MediaSource?.Protocol == MediaProtocol.File)
 278266                {
 278267                    return 0;
 278268                }
 278269
 278270                return 1;
 278271            }).ThenBy(i =>
 278272            {
 278273                switch (i.PlayMethod)
 278274                {
 278275                    // Let's assume direct streaming a file is just as desirable as direct playing a remote url
 278276                    case PlayMethod.DirectStream:
 278277                    case PlayMethod.DirectPlay:
 278278                        return 0;
 278279                    default:
 278280                        return 1;
 278281                }
 278282            }).ThenBy(i =>
 278283            {
 278284                switch (i.MediaSource?.Protocol)
 278285                {
 278286                    case MediaProtocol.File:
 278287                        return 0;
 278288                    default:
 278289                        return 1;
 278290                }
 278291            }).ThenBy(i =>
 278292            {
 278293                if (maxBitrate > 0)
 278294                {
 278295                    if (i.MediaSource?.Bitrate is not null)
 278296                    {
 278297                        return Math.Abs(i.MediaSource.Bitrate.Value - maxBitrate);
 278298                    }
 278299                }
 278300
 278301                return 0;
 278302            }).ThenBy(streams.IndexOf);
 303        }
 304
 305        private static TranscodeReason GetTranscodeReasonForFailedCondition(ProfileCondition condition)
 306        {
 247307            switch (condition.Property)
 308            {
 309                case ProfileConditionValue.AudioBitrate:
 0310                    return TranscodeReason.AudioBitrateNotSupported;
 311
 312                case ProfileConditionValue.AudioChannels:
 122313                    return TranscodeReason.AudioChannelsNotSupported;
 314
 315                case ProfileConditionValue.AudioProfile:
 0316                    return TranscodeReason.AudioProfileNotSupported;
 317
 318                case ProfileConditionValue.AudioSampleRate:
 0319                    return TranscodeReason.AudioSampleRateNotSupported;
 320
 321                case ProfileConditionValue.Has64BitOffsets:
 322                    // TODO
 0323                    return 0;
 324
 325                case ProfileConditionValue.Height:
 0326                    return TranscodeReason.VideoResolutionNotSupported;
 327
 328                case ProfileConditionValue.IsAnamorphic:
 0329                    return TranscodeReason.AnamorphicVideoNotSupported;
 330
 331                case ProfileConditionValue.IsAvc:
 332                    // TODO
 0333                    return 0;
 334
 335                case ProfileConditionValue.IsInterlaced:
 0336                    return TranscodeReason.InterlacedVideoNotSupported;
 337
 338                case ProfileConditionValue.IsSecondaryAudio:
 35339                    return TranscodeReason.SecondaryAudioNotSupported;
 340
 341                case ProfileConditionValue.NumStreams:
 2342                    return TranscodeReason.StreamCountExceedsLimit;
 343
 344                case ProfileConditionValue.NumAudioStreams:
 345                    // TODO
 0346                    return 0;
 347
 348                case ProfileConditionValue.NumVideoStreams:
 349                    // TODO
 0350                    return 0;
 351
 352                case ProfileConditionValue.PacketLength:
 353                    // TODO
 0354                    return 0;
 355
 356                case ProfileConditionValue.RefFrames:
 0357                    return TranscodeReason.RefFramesNotSupported;
 358
 359                case ProfileConditionValue.VideoBitDepth:
 0360                    return TranscodeReason.VideoBitDepthNotSupported;
 361
 362                case ProfileConditionValue.AudioBitDepth:
 0363                    return TranscodeReason.AudioBitDepthNotSupported;
 364
 365                case ProfileConditionValue.VideoBitrate:
 4366                    return TranscodeReason.VideoBitrateNotSupported;
 367
 368                case ProfileConditionValue.VideoCodecTag:
 18369                    return TranscodeReason.VideoCodecTagNotSupported;
 370
 371                case ProfileConditionValue.VideoFramerate:
 0372                    return TranscodeReason.VideoFramerateNotSupported;
 373
 374                case ProfileConditionValue.VideoLevel:
 13375                    return TranscodeReason.VideoLevelNotSupported;
 376
 377                case ProfileConditionValue.VideoProfile:
 25378                    return TranscodeReason.VideoProfileNotSupported;
 379
 380                case ProfileConditionValue.VideoRangeType:
 28381                    return TranscodeReason.VideoRangeTypeNotSupported;
 382
 383                case ProfileConditionValue.VideoTimestamp:
 384                    // TODO
 0385                    return 0;
 386
 387                case ProfileConditionValue.Width:
 0388                    return TranscodeReason.VideoResolutionNotSupported;
 389
 390                default:
 0391                    return 0;
 392            }
 393        }
 394
 395        /// <summary>
 396        /// Normalizes input container.
 397        /// </summary>
 398        /// <param name="inputContainer">The input container.</param>
 399        /// <param name="profile">The <see cref="DeviceProfile"/>.</param>
 400        /// <param name="type">The <see cref="DlnaProfileType"/>.</param>
 401        /// <param name="playProfile">The <see cref="DirectPlayProfile"/> object to get the video stream from.</param>
 402        /// <returns>The normalized input container.</returns>
 403        public static string? NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile? profil
 404        {
 405            // If the source is Live TV the inputContainer will be null until the mediasource is probed on first access
 402406            if (profile is null || string.IsNullOrEmpty(inputContainer) || !inputContainer.Contains(',', StringCompariso
 407            {
 94408                return inputContainer;
 409            }
 410
 308411            var formats = ContainerHelper.Split(inputContainer);
 308412            var playProfiles = playProfile is null ? profile.DirectPlayProfiles : [playProfile];
 1361413            foreach (var format in formats)
 414            {
 2593415                foreach (var directPlayProfile in playProfiles)
 416                {
 924417                    if (directPlayProfile.Type != type)
 418                    {
 419                        continue;
 420                    }
 421
 696422                    if (directPlayProfile.SupportsContainer(format))
 423                    {
 291424                        return format;
 425                    }
 426                }
 427            }
 428
 17429            return inputContainer;
 430        }
 431
 432        private (DirectPlayProfile? Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPla
 433        {
 0434            var directPlayProfile = options.Profile.DirectPlayProfiles
 0435                .FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream)
 436
 0437            TranscodeReason transcodeReasons = 0;
 0438            if (directPlayProfile is null)
 439            {
 0440                _logger.LogDebug(
 0441                    "Profile: {0}, No audio direct play profiles found for {1} with codec {2}",
 0442                    options.Profile.Name ?? "Unknown Profile",
 0443                    item.Path ?? "Unknown path",
 0444                    audioStream.Codec ?? "Unknown codec");
 445
 0446                var directStreamProfile = options.Profile.DirectPlayProfiles
 0447                    .FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectStreamSupported(x, item, audioS
 448
 0449                if (directStreamProfile is not null)
 450                {
 0451                    directPlayProfile = directStreamProfile;
 0452                    transcodeReasons |= TranscodeReason.ContainerNotSupported;
 453                }
 454                else
 455                {
 0456                    return (null, null, GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profil
 457                }
 458            }
 459
 460            // The profile describes what the device supports
 461            // If device requirements are satisfied then allow both direct stream and direct play
 462            // Note: As of 10.10 codebase, SupportsDirectPlay is always true because the MediaSourceInfo initializes thi
 463            // Need to check additionally for current transcode reasons
 0464            if (item.SupportsDirectPlay && transcodeReasons == 0)
 465            {
 0466                if (!IsBitrateLimitExceeded(item, options.GetMaxBitrate(true) ?? 0))
 467                {
 0468                    if (options.EnableDirectPlay)
 469                    {
 0470                        return (directPlayProfile, PlayMethod.DirectPlay, 0);
 471                    }
 472                }
 473                else
 474                {
 0475                    transcodeReasons |= TranscodeReason.ContainerBitrateExceedsLimit;
 476                }
 477            }
 478
 479            // While options takes the network and other factors into account. Only applies to direct stream
 0480            if (item.SupportsDirectStream)
 481            {
 0482                if (!IsBitrateLimitExceeded(item, options.GetMaxBitrate(true) ?? 0))
 483                {
 484                    // Note: as of 10.10 codebase, the options.EnableDirectStream is always false due to
 485                    // "direct-stream http streaming is currently broken"
 486                    // Don't check that option for audio as we always assume that is supported
 0487                    if (transcodeReasons == TranscodeReason.ContainerNotSupported)
 488                    {
 0489                        return (directPlayProfile, PlayMethod.DirectStream, transcodeReasons);
 490                    }
 491                }
 492                else
 493                {
 0494                    transcodeReasons |= TranscodeReason.ContainerBitrateExceedsLimit;
 495                }
 496            }
 497
 0498            return (directPlayProfile, null, transcodeReasons);
 499        }
 500
 501        private static TranscodeReason GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream? video
 502        {
 0503            var mediaType = videoStream is null ? DlnaProfileType.Audio : DlnaProfileType.Video;
 504
 0505            var containerSupported = false;
 0506            var audioSupported = false;
 0507            var videoSupported = false;
 508
 0509            foreach (var profile in directPlayProfiles)
 510            {
 511                // Check container type
 0512                if (profile.Type == mediaType && profile.SupportsContainer(item.Container))
 513                {
 0514                    containerSupported = true;
 515
 0516                    videoSupported = videoStream is null || profile.SupportsVideoCodec(videoStream.Codec);
 517
 0518                    audioSupported = audioStream is null || profile.SupportsAudioCodec(audioStream.Codec);
 519
 0520                    if (videoSupported && audioSupported)
 521                    {
 0522                        break;
 523                    }
 524                }
 525            }
 526
 0527            TranscodeReason reasons = 0;
 0528            if (!containerSupported)
 529            {
 0530                reasons |= TranscodeReason.ContainerNotSupported;
 531            }
 532
 0533            if (!videoSupported)
 534            {
 0535                reasons |= TranscodeReason.VideoCodecNotSupported;
 536            }
 537
 0538            if (!audioSupported)
 539            {
 0540                reasons |= TranscodeReason.AudioCodecNotSupported;
 541            }
 542
 0543            return reasons;
 544        }
 545
 546        private static int? GetDefaultSubtitleStreamIndex(MediaSourceInfo item, SubtitleProfile[] subtitleProfiles)
 547        {
 175548            int highestScore = -1;
 1788549            foreach (var stream in item.MediaStreams)
 550            {
 719551                if (stream.Type == MediaStreamType.Subtitle
 719552                    && stream.Score.HasValue
 719553                    && stream.Score.Value > highestScore)
 554                {
 144555                    highestScore = stream.Score.Value;
 556                }
 557            }
 558
 175559            List<MediaStream> topStreams = [];
 1788560            foreach (var stream in item.MediaStreams)
 561            {
 719562                if (stream.Type == MediaStreamType.Subtitle && stream.Score.HasValue && stream.Score.Value == highestSco
 563                {
 321564                    topStreams.Add(stream);
 565                }
 566            }
 567
 568            // If multiple streams have an equal score, try to pick the most efficient one
 175569            if (topStreams.Count > 1)
 570            {
 378571                foreach (var stream in topStreams)
 572                {
 1464573                    foreach (var profile in subtitleProfiles)
 574                    {
 549575                        if (profile.Method == SubtitleDeliveryMethod.External && string.Equals(profile.Format, stream.Co
 576                        {
 0577                            return stream.Index;
 578                        }
 579                    }
 580                }
 581            }
 582
 583            // If no optimization panned out, just use the original default
 175584            return item.DefaultSubtitleStreamIndex;
 0585        }
 586
 587        private static void SetStreamInfoOptionsFromTranscodingProfile(MediaSourceInfo item, StreamInfo playlistItem, Tr
 588        {
 146589            var container = transcodingProfile.Container;
 146590            var protocol = transcodingProfile.Protocol;
 591
 146592            item.TranscodingContainer = container;
 146593            item.TranscodingSubProtocol = protocol;
 594
 146595            if (playlistItem.PlayMethod == PlayMethod.Transcode)
 596            {
 146597                playlistItem.Container = container;
 146598                playlistItem.SubProtocol = protocol;
 599            }
 600
 146601            playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
 146602            if (int.TryParse(transcodingProfile.MaxAudioChannels, CultureInfo.InvariantCulture, out int transcodingMaxAu
 603            {
 133604                playlistItem.TranscodingMaxAudioChannels = transcodingMaxAudioChannels;
 605            }
 606
 146607            playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength;
 608
 146609            playlistItem.CopyTimestamps = transcodingProfile.CopyTimestamps;
 146610            playlistItem.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
 146611            playlistItem.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
 612
 146613            playlistItem.EnableAudioVbrEncoding = transcodingProfile.EnableAudioVbrEncoding;
 614
 146615            if (transcodingProfile.MinSegments > 0)
 616            {
 102617                playlistItem.MinSegments = transcodingProfile.MinSegments;
 618            }
 619
 146620            if (transcodingProfile.SegmentLength > 0)
 621            {
 0622                playlistItem.SegmentLength = transcodingProfile.SegmentLength;
 623            }
 146624        }
 625
 626        private static void SetStreamInfoOptionsFromDirectPlayProfile(MediaOptions options, MediaSourceInfo item, Stream
 627        {
 0628            var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileTy
 0629            var protocol = MediaStreamProtocol.http;
 630
 0631            item.TranscodingContainer = container;
 0632            item.TranscodingSubProtocol = protocol;
 633
 0634            playlistItem.Container = container;
 0635            playlistItem.SubProtocol = protocol;
 636
 0637            playlistItem.VideoCodecs = [item.VideoStream.Codec];
 0638            playlistItem.AudioCodecs = ContainerHelper.Split(directPlayProfile?.AudioCodec);
 0639        }
 640
 641        private StreamInfo BuildVideoItem(MediaSourceInfo item, MediaOptions options)
 642        {
 278643            ArgumentNullException.ThrowIfNull(item);
 644
 278645            StreamInfo playlistItem = new StreamInfo
 278646            {
 278647                ItemId = options.ItemId,
 278648                MediaType = DlnaProfileType.Video,
 278649                MediaSource = item,
 278650                RunTimeTicks = item.RunTimeTicks,
 278651                Context = options.Context,
 278652                DeviceProfile = options.Profile,
 278653                SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile
 278654                AlwaysBurnInSubtitleWhenTranscoding = options.AlwaysBurnInSubtitleWhenTranscoding
 278655            };
 656
 278657            var subtitleStream = playlistItem.SubtitleStreamIndex.HasValue ? item.GetMediaStream(MediaStreamType.Subtitl
 658
 278659            var audioStream = item.GetDefaultAudioStream(options.AudioStreamIndex ?? item.DefaultAudioStreamIndex);
 278660            if (audioStream is not null)
 661            {
 277662                playlistItem.AudioStreamIndex = audioStream.Index;
 663            }
 664
 665            // Collect candidate audio streams
 278666            ICollection<MediaStream> candidateAudioStreams = audioStream is null ? [] : [audioStream];
 667            // When the index is explicitly required by client or the default is specified by user, don't do any stream 
 278668            if (!item.DefaultAudioIndexSource.HasFlag(AudioIndexSource.User) && (options.AudioStreamIndex is null or < 0
 669            {
 670                // When user has no preferences allow stream selection on all streams.
 175671                if (item.DefaultAudioIndexSource == AudioIndexSource.None && audioStream is not null)
 672                {
 174673                    candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio).ToAr
 174674                    if (audioStream.IsDefault)
 675                    {
 676                        // If default is picked, only allow selection within default streams.
 174677                        candidateAudioStreams = candidateAudioStreams.Where(stream => stream.IsDefault).ToArray();
 678                    }
 679                }
 680
 175681                if (item.DefaultAudioIndexSource.HasFlag(AudioIndexSource.Language))
 682                {
 683                    // If user has language preference, only allow stream selection within the same language.
 0684                    candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && st
 0685                    if (item.DefaultAudioIndexSource.HasFlag(AudioIndexSource.Default))
 686                    {
 0687                        var defaultStreamsInPreferredLanguage = candidateAudioStreams.Where(stream => stream.IsDefault).
 688
 689                        // If the user also prefers default streams, try limit selection within default tracks in the sa
 690                        // If there is no default stream in the preferred language, allow selection on all default strea
 0691                        candidateAudioStreams = defaultStreamsInPreferredLanguage.Length > 0
 0692                            ? defaultStreamsInPreferredLanguage
 0693                            : item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault
 694                    }
 695                }
 175696                else if (item.DefaultAudioIndexSource.HasFlag(AudioIndexSource.Default))
 697                {
 698                    // If user prefers default streams, only allow stream selection on default streams.
 0699                    candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && st
 700                }
 701            }
 702
 278703            var videoStream = item.VideoStream;
 704
 278705            var bitrateLimitExceeded = IsBitrateLimitExceeded(item, options.GetMaxBitrate(false) ?? 0);
 278706            var isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || !bitrateLimitExceeded)
 278707            var isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || !bitrateLimitExc
 278708            TranscodeReason transcodeReasons = 0;
 709
 710            // Force transcode or remux for BD/DVD folders
 278711            if (item.VideoType == VideoType.Dvd || item.VideoType == VideoType.BluRay)
 712            {
 0713                isEligibleForDirectPlay = false;
 714            }
 715
 278716            if (bitrateLimitExceeded)
 717            {
 24718                transcodeReasons = TranscodeReason.ContainerBitrateExceedsLimit;
 719            }
 720
 278721            _logger.LogDebug(
 278722                "Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
 278723                options.Profile.Name ?? "Unknown Profile",
 278724                item.Path ?? "Unknown path",
 278725                isEligibleForDirectPlay,
 278726                isEligibleForDirectStream);
 727
 278728            DirectPlayProfile? directPlayProfile = null;
 278729            if (isEligibleForDirectPlay || isEligibleForDirectStream)
 730            {
 731                // See if it can be direct played
 254732                var directPlayInfo = GetVideoDirectPlayProfile(options, item, videoStream, audioStream, candidateAudioSt
 254733                var directPlay = directPlayInfo.PlayMethod;
 254734                transcodeReasons |= directPlayInfo.TranscodeReasons;
 735
 254736                if (directPlay.HasValue)
 737                {
 124738                    directPlayProfile = directPlayInfo.Profile;
 124739                    playlistItem.PlayMethod = directPlay.Value;
 124740                    playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profi
 124741                    var videoCodec = videoStream?.Codec;
 124742                    playlistItem.VideoCodecs = videoCodec is null ? [] : [videoCodec];
 743
 124744                    if (directPlay == PlayMethod.DirectPlay)
 745                    {
 124746                        playlistItem.SubProtocol = MediaStreamProtocol.http;
 747
 124748                        var audioStreamIndex = directPlayInfo.AudioStreamIndex ?? audioStream?.Index;
 124749                        if (audioStreamIndex.HasValue)
 750                        {
 124751                            playlistItem.AudioStreamIndex = audioStreamIndex;
 124752                            var audioCodec = item.GetMediaStream(MediaStreamType.Audio, audioStreamIndex.Value)?.Codec;
 124753                            playlistItem.AudioCodecs = audioCodec is null ? [] : [audioCodec];
 754                        }
 755                    }
 0756                    else if (directPlay == PlayMethod.DirectStream)
 757                    {
 0758                        playlistItem.AudioStreamIndex = audioStream?.Index;
 0759                        if (audioStream is not null)
 760                        {
 0761                            playlistItem.AudioCodecs = ContainerHelper.Split(directPlayProfile?.AudioCodec);
 762                        }
 763
 0764                        SetStreamInfoOptionsFromDirectPlayProfile(options, item, playlistItem, directPlayProfile);
 0765                        BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStream
 766                    }
 767
 124768                    if (subtitleStream is not null)
 769                    {
 116770                        var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles,
 771
 116772                        playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
 116773                        playlistItem.SubtitleFormat = subtitleProfile.Format;
 774                    }
 775                }
 776
 254777                _logger.LogDebug(
 254778                    "DirectPlay Result for Profile: {0}, Path: {1}, PlayMethod: {2}, AudioStreamIndex: {3}, SubtitleStre
 254779                    options.Profile.Name ?? "Anonymous Profile",
 254780                    item.Path ?? "Unknown path",
 254781                    directPlayInfo.PlayMethod,
 254782                    directPlayInfo.AudioStreamIndex ?? audioStream?.Index,
 254783                    playlistItem.SubtitleStreamIndex,
 254784                    directPlayInfo.TranscodeReasons);
 785            }
 786
 278787            playlistItem.TranscodeReasons = transcodeReasons;
 788
 278789            if (playlistItem.PlayMethod != PlayMethod.DirectStream && playlistItem.PlayMethod != PlayMethod.DirectPlay)
 790            {
 791                // Can't direct play, find the transcoding profile
 792                // If we do this for direct-stream we will overwrite the info
 154793                var (transcodingProfile, playMethod) = GetVideoTranscodeProfile(item, options, videoStream, audioStream,
 794
 154795                if (transcodingProfile is not null && playMethod.HasValue)
 796                {
 146797                    SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile);
 798
 146799                    BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, t
 800
 146801                    playlistItem.PlayMethod = PlayMethod.Transcode;
 802
 146803                    if (subtitleStream is not null)
 804                    {
 123805                        var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles,
 123806                        playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
 123807                        playlistItem.SubtitleFormat = subtitleProfile.Format;
 123808                        playlistItem.SubtitleCodecs = [subtitleProfile.Format];
 809                    }
 810
 146811                    if ((playlistItem.TranscodeReasons & (VideoReasons | TranscodeReason.ContainerBitrateExceedsLimit)) 
 812                    {
 44813                        ApplyTranscodingConditions(playlistItem, transcodingProfile.Conditions, null, true, true);
 814                    }
 815                }
 816            }
 817
 278818            _logger.LogDebug(
 278819                "StreamBuilder.BuildVideoItem( Profile={0}, Path={1}, AudioStreamIndex={2}, SubtitleStreamIndex={3} ) =>
 278820                options.Profile.Name ?? "Anonymous Profile",
 278821                item.Path ?? "Unknown path",
 278822                options.AudioStreamIndex,
 278823                options.SubtitleStreamIndex,
 278824                playlistItem.PlayMethod,
 278825                playlistItem.TranscodeReasons,
 278826                playlistItem.ToUrl("media:", "<token>", null));
 827
 278828            item.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileT
 278829            return playlistItem;
 830        }
 831
 832        private (TranscodingProfile? Profile, PlayMethod? PlayMethod) GetVideoTranscodeProfile(
 833            MediaSourceInfo item,
 834            MediaOptions options,
 835            MediaStream? videoStream,
 836            MediaStream? audioStream,
 837            StreamInfo playlistItem)
 838        {
 154839            var mediaSource = playlistItem.MediaSource;
 840
 154841            ArgumentNullException.ThrowIfNull(mediaSource);
 842
 154843            if (!(item.SupportsTranscoding || item.SupportsDirectStream))
 844            {
 0845                return (null, null);
 846            }
 847
 154848            var transcodingProfiles = options.Profile.TranscodingProfiles
 154849                .Where(i => i.Type == playlistItem.MediaType && i.Context == options.Context);
 850
 154851            if (item.UseMostCompatibleTranscodingProfile)
 852            {
 0853                transcodingProfiles = transcodingProfiles.Where(i => string.Equals(i.Container, "ts", StringComparison.O
 854            }
 855
 154856            var videoCodec = videoStream?.Codec;
 154857            var audioCodec = audioStream?.Codec;
 858
 154859            var analyzedProfiles = transcodingProfiles
 154860                .Select(transcodingProfile =>
 154861                {
 154862                    var rank = (Video: 3, Audio: 3);
 154863
 154864                    var container = transcodingProfile.Container;
 154865
 154866                    if (videoStream is not null
 154867                        && options.AllowVideoStreamCopy
 154868                        && ContainerHelper.ContainsContainer(transcodingProfile.VideoCodec, videoCodec))
 154869                    {
 154870                        var failures = GetCompatibilityVideoCodec(options, mediaSource, container, videoStream);
 154871                        rank.Video = failures == 0 ? 1 : 2;
 154872                    }
 154873
 154874                    if (audioStream is not null
 154875                        && options.AllowAudioStreamCopy)
 154876                    {
 154877                        // For Audio stream, we prefer the audio codec that can be directly copied, then the codec that 
 154878                        // the transcoding conditions, then the one does not satisfy the transcoding conditions.
 154879                        // For example: A client can support both aac and flac, but flac only supports 2 channels while 
 154880                        // When the source audio is 6 channel flac, we should transcode to 6 channel aac, instead of dow
 154881                        var transcodingAudioCodecs = ContainerHelper.Split(transcodingProfile.AudioCodec);
 154882
 154883                        foreach (var transcodingAudioCodec in transcodingAudioCodecs)
 154884                        {
 154885                            var failures = GetCompatibilityAudioCodec(options, mediaSource, container, audioStream, tran
 154886
 154887                            var rankAudio = 3;
 154888
 154889                            if (failures == 0)
 154890                            {
 154891                                rankAudio = string.Equals(transcodingAudioCodec, audioCodec, StringComparison.OrdinalIgn
 154892                            }
 154893
 154894                            rank.Audio = Math.Min(rank.Audio, rankAudio);
 154895
 154896                            if (rank.Audio == 1)
 154897                            {
 154898                                break;
 154899                            }
 154900                        }
 154901                    }
 154902
 154903                    PlayMethod playMethod = PlayMethod.Transcode;
 154904
 154905                    if (rank.Video == 1)
 154906                    {
 154907                        playMethod = PlayMethod.DirectStream;
 154908                    }
 154909
 154910                    return (Profile: transcodingProfile, PlayMethod: playMethod, Rank: rank);
 154911                })
 154912                .OrderBy(analysis => analysis.Rank);
 913
 154914            var profileMatch = analyzedProfiles.FirstOrDefault();
 915
 154916            return (profileMatch.Profile, profileMatch.PlayMethod);
 917        }
 918
 919        private void BuildStreamVideoItem(
 920            StreamInfo playlistItem,
 921            MediaOptions options,
 922            MediaSourceInfo item,
 923            MediaStream? videoStream,
 924            MediaStream? audioStream,
 925            IEnumerable<MediaStream> candidateAudioStreams,
 926            string? container,
 927            string? videoCodec,
 928            string? audioCodec)
 929        {
 930            // Prefer matching video codecs
 146931            var videoCodecs = ContainerHelper.Split(videoCodec).ToList();
 932
 146933            if (videoCodecs.Count == 0 && videoStream is not null)
 934            {
 935                // Add the original codec if no codec is specified
 0936                videoCodecs.Add(videoStream.Codec);
 937            }
 938
 939            // Enforce HLS video codec restrictions
 146940            if (playlistItem.SubProtocol == MediaStreamProtocol.hls)
 941            {
 132942                videoCodecs = videoCodecs.Where(codec => _supportedHlsVideoCodecs.Contains(codec)).ToList();
 943            }
 944
 146945            playlistItem.VideoCodecs = videoCodecs;
 946
 947            // Copy video codec options as a starting point, this applies to transcode and direct-stream
 146948            playlistItem.MaxFramerate = videoStream?.ReferenceFrameRate;
 146949            var qualifier = videoStream?.Codec;
 146950            if (videoStream?.Level is not null)
 951            {
 145952                playlistItem.SetOption(qualifier, "level", videoStream.Level.Value.ToString(CultureInfo.InvariantCulture
 953            }
 954
 146955            if (videoStream?.BitDepth is not null)
 956            {
 145957                playlistItem.SetOption(qualifier, "videobitdepth", videoStream.BitDepth.Value.ToString(CultureInfo.Invar
 958            }
 959
 146960            if (!string.IsNullOrEmpty(videoStream?.Profile))
 961            {
 145962                playlistItem.SetOption(qualifier, "profile", videoStream.Profile.ToLowerInvariant());
 963            }
 964
 965            // Prefer matching audio codecs, could do better here
 146966            var audioCodecs = ContainerHelper.Split(audioCodec).ToList();
 967
 146968            if (audioCodecs.Count == 0 && audioStream is not null)
 969            {
 970                // Add the original codec if no codec is specified
 0971                audioCodecs.Add(audioStream.Codec);
 972            }
 973
 974            // Enforce HLS audio codec restrictions
 146975            if (playlistItem.SubProtocol == MediaStreamProtocol.hls)
 976            {
 132977                if (string.Equals(playlistItem.Container, "mp4", StringComparison.OrdinalIgnoreCase))
 978                {
 72979                    audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsMp4.Contains(codec)).ToList();
 980                }
 981                else
 982                {
 60983                    audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsTs.Contains(codec)).ToList();
 984                }
 985            }
 986
 146987            var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerHelper.ContainsContainer(
 988
 146989            var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && audioStreamWithSupportedCodec.Channe
 990
 146991            var directAudioFailures = audioStreamWithSupportedCodec is null ? default : GetCompatibilityAudioCodec(optio
 992
 146993            playlistItem.TranscodeReasons |= directAudioFailures;
 994
 146995            var directAudioStreamSatisfied = audioStreamWithSupportedCodec is not null && !channelsExceedsLimit
 146996                && directAudioFailures == 0;
 997
 146998            directAudioStreamSatisfied = directAudioStreamSatisfied && !playlistItem.TranscodeReasons.HasFlag(TranscodeR
 999
 1461000            var directAudioStream = directAudioStreamSatisfied ? audioStreamWithSupportedCodec : null;
 1001
 1461002            if (channelsExceedsLimit && playlistItem.TargetAudioStream is not null)
 1003            {
 101004                playlistItem.TranscodeReasons |= TranscodeReason.AudioChannelsNotSupported;
 101005                playlistItem.TargetAudioStream.Channels = playlistItem.TranscodingMaxAudioChannels;
 1006            }
 1007
 1461008            playlistItem.AudioCodecs = audioCodecs;
 1461009            if (directAudioStream is not null)
 1010            {
 551011                audioStream = directAudioStream;
 551012                playlistItem.AudioStreamIndex = audioStream.Index;
 551013                audioCodecs = [audioStream.Codec];
 551014                playlistItem.AudioCodecs = audioCodecs;
 1015
 1016                // Copy matching audio codec options
 551017                playlistItem.AudioSampleRate = audioStream.SampleRate;
 551018                playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels?.ToString(CultureInfo.InvariantC
 1019
 551020                if (!string.IsNullOrEmpty(audioStream.Profile))
 1021                {
 531022                    playlistItem.SetOption(audioStream.Codec, "profile", audioStream.Profile.ToLowerInvariant());
 1023                }
 1024
 551025                if (audioStream.Level.HasValue && audioStream.Level.Value != 0)
 1026                {
 01027                    playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.Value.ToString(CultureInfo.Inva
 1028                }
 1029            }
 1030
 1461031            int? width = videoStream?.Width;
 1461032            int? height = videoStream?.Height;
 1461033            int? bitDepth = videoStream?.BitDepth;
 1461034            int? videoBitrate = videoStream?.BitRate;
 1461035            double? videoLevel = videoStream?.Level;
 1461036            string? videoProfile = videoStream?.Profile;
 1461037            VideoRangeType? videoRangeType = videoStream?.VideoRangeType;
 1461038            float videoFramerate = videoStream is null ? 0 : videoStream.ReferenceFrameRate ?? 0;
 1461039            bool? isAnamorphic = videoStream?.IsAnamorphic;
 1461040            bool? isInterlaced = videoStream?.IsInterlaced;
 1461041            string? videoCodecTag = videoStream?.CodecTag;
 1461042            bool? isAvc = videoStream?.IsAVC;
 1043
 1461044            TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp;
 1461045            int? packetLength = videoStream?.PacketLength;
 1461046            int? refFrames = videoStream?.RefFrames;
 1047
 1461048            int numStreams = item.MediaStreams.Count;
 1461049            int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
 1461050            int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
 1051
 1461052            var useSubContainer = playlistItem.SubProtocol == MediaStreamProtocol.hls;
 1053
 1461054            var appliedVideoConditions = options.Profile.CodecProfiles
 1461055                .Where(i => i.Type == CodecType.Video &&
 1461056                    i.ContainsAnyCodec(playlistItem.VideoCodecs, container, useSubContainer) &&
 1461057                    i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition,
 1461058                // Reverse codec profiles for backward compatibility - first codec profile has higher priority
 1461059                .Reverse();
 8881060            foreach (var condition in appliedVideoConditions)
 1061            {
 24161062                foreach (var transcodingVideoCodec in playlistItem.VideoCodecs)
 1063                {
 9101064                    if (condition.ContainsAnyCodec(transcodingVideoCodec, container, useSubContainer))
 1065                    {
 3061066                        ApplyTranscodingConditions(playlistItem, condition.Conditions, transcodingVideoCodec, true, true
 1067                        continue;
 1068                    }
 1069                }
 1070            }
 1071
 1072            // Honor requested max channels
 1461073            playlistItem.GlobalMaxAudioChannels = channelsExceedsLimit ? playlistItem.TranscodingMaxAudioChannels : opti
 1074
 1461075            int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(true) ?? 0, playlistItem.TargetAudioCodec, audioStr
 1461076            playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate);
 1077
 1461078            bool? isSecondaryAudio = audioStream is null ? null : item.IsSecondaryAudio(audioStream);
 1461079            int? inputAudioBitrate = audioStream?.BitRate;
 1461080            int? audioChannels = audioStream?.Channels;
 1461081            string? audioProfile = audioStream?.Profile;
 1461082            int? inputAudioSampleRate = audioStream?.SampleRate;
 1461083            int? inputAudioBitDepth = audioStream?.BitDepth;
 1084
 1461085            var appliedAudioConditions = options.Profile.CodecProfiles
 1461086                .Where(i => i.Type == CodecType.VideoAudio &&
 1461087                    i.ContainsAnyCodec(playlistItem.AudioCodecs, container) &&
 1461088                    i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondi
 1461089                // Reverse codec profiles for backward compatibility - first codec profile has higher priority
 1461090                .Reverse();
 1091
 5961092            foreach (var codecProfile in appliedAudioConditions)
 1093            {
 4561094                foreach (var transcodingAudioCodec in playlistItem.AudioCodecs)
 1095                {
 1521096                    if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container))
 1097                    {
 1521098                        ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, t
 1521099                        break;
 1100                    }
 1101                }
 1102            }
 1103
 1461104            var maxBitrateSetting = options.GetMaxBitrate(false);
 1105            // Honor max rate
 1461106            if (maxBitrateSetting.HasValue)
 1107            {
 1461108                var availableBitrateForVideo = maxBitrateSetting.Value;
 1109
 1461110                if (playlistItem.AudioBitrate.HasValue)
 1111                {
 1461112                    availableBitrateForVideo -= playlistItem.AudioBitrate.Value;
 1113                }
 1114
 1115                // Make sure the video bitrate is lower than bitrate settings but at least 64k
 1116                // Don't use Math.Clamp as availableBitrateForVideo can be lower then 64k.
 1461117                var currentValue = playlistItem.VideoBitrate ?? availableBitrateForVideo;
 1461118                playlistItem.VideoBitrate = Math.Max(Math.Min(availableBitrateForVideo, currentValue), 64_000);
 1119            }
 1120
 1461121            _logger.LogDebug(
 1461122                "Transcode Result for Profile: {Profile}, Path: {Path}, PlayMethod: {PlayMethod}, AudioStreamIndex: {Aud
 1461123                options.Profile.Name ?? "Anonymous Profile",
 1461124                item.Path ?? "Unknown path",
 1461125                playlistItem.PlayMethod,
 1461126                audioStream?.Index,
 1461127                playlistItem.SubtitleStreamIndex,
 1461128                playlistItem.TranscodeReasons);
 1461129        }
 1130
 1131        private static int GetDefaultAudioBitrate(string? audioCodec, int? audioChannels)
 1132        {
 601133            if (!string.IsNullOrEmpty(audioCodec))
 1134            {
 1135                // Default to a higher bitrate for stream copy
 601136                if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
 601137                    || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
 601138                    || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
 601139                    || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
 1140                {
 551141                    if ((audioChannels ?? 0) < 2)
 1142                    {
 01143                        return 128000;
 1144                    }
 1145
 551146                    return (audioChannels ?? 0) >= 6 ? 640000 : 384000;
 1147                }
 1148
 51149                if (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase)
 51150                    || string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase))
 1151                {
 01152                    if ((audioChannels ?? 0) < 2)
 1153                    {
 01154                        return 768000;
 1155                    }
 1156
 01157                    return (audioChannels ?? 0) >= 6 ? 3584000 : 1536000;
 1158                }
 1159            }
 1160
 51161            return 192000;
 1162        }
 1163
 1164        private static int GetAudioBitrate(long maxTotalBitrate, IReadOnlyList<string> targetAudioCodecs, MediaStream? a
 1165        {
 1461166            string? targetAudioCodec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0];
 1167
 1461168            int? targetAudioChannels = item.GetTargetAudioChannels(targetAudioCodec);
 1169
 1170            int defaultBitrate;
 1461171            int encoderAudioBitrateLimit = int.MaxValue;
 1172
 1461173            if (audioStream is null)
 1174            {
 11175                defaultBitrate = 192000;
 1176            }
 1177            else
 1178            {
 1451179                if (targetAudioChannels.HasValue
 1451180                    && audioStream.Channels.HasValue
 1451181                    && audioStream.Channels.Value > targetAudioChannels.Value)
 1182                {
 1183                    // Reduce the bitrate if we're down mixing.
 461184                    defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, targetAudioChannels);
 1185                }
 991186                else if (targetAudioChannels.HasValue
 991187                         && audioStream.Channels.HasValue
 991188                         && audioStream.Channels.Value <= targetAudioChannels.Value
 991189                         && !string.IsNullOrEmpty(audioStream.Codec)
 991190                         && targetAudioCodecs is not null
 991191                         && targetAudioCodecs.Count > 0
 991192                         && !targetAudioCodecs.Any(elem => string.Equals(audioStream.Codec, elem, StringComparison.Ordin
 1193                {
 1194                    // Shift the bitrate if we're transcoding to a different audio codec.
 141195                    defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, audioStream.Channels.Value);
 1196                }
 1197                else
 1198                {
 851199                    defaultBitrate = audioStream.BitRate ?? GetDefaultAudioBitrate(targetAudioCodec, targetAudioChannels
 1200                }
 1201
 1202                // Seeing webm encoding failures when source has 1 audio channel and 22k bitrate.
 1203                // Any attempts to transcode over 64k will fail
 1451204                if (audioStream.Channels == 1
 1451205                    && (audioStream.BitRate ?? 0) < 64000)
 1206                {
 01207                    encoderAudioBitrateLimit = 64000;
 1208                }
 1209            }
 1210
 1461211            if (maxTotalBitrate > 0)
 1212            {
 1461213                defaultBitrate = Math.Min(GetMaxAudioBitrateForTotalBitrate(maxTotalBitrate), defaultBitrate);
 1214            }
 1215
 1461216            return Math.Min(defaultBitrate, encoderAudioBitrateLimit);
 1217        }
 1218
 1219        private static int GetMaxAudioBitrateForTotalBitrate(long totalBitrate)
 1220        {
 1461221            if (totalBitrate <= 640000)
 1222            {
 81223                return 128000;
 1224            }
 1225
 1381226            if (totalBitrate <= 2000000)
 1227            {
 01228                return 384000;
 1229            }
 1230
 1381231            if (totalBitrate <= 3000000)
 1232            {
 01233                return 448000;
 1234            }
 1235
 1381236            if (totalBitrate <= 4000000)
 1237            {
 01238                return 640000;
 1239            }
 1240
 1381241            if (totalBitrate <= 5000000)
 1242            {
 01243                return 768000;
 1244            }
 1245
 1381246            if (totalBitrate <= 10000000)
 1247            {
 81248                return 1536000;
 1249            }
 1250
 1301251            if (totalBitrate <= 15000000)
 1252            {
 01253                return 2304000;
 1254            }
 1255
 1301256            if (totalBitrate <= 20000000)
 1257            {
 141258                return 3584000;
 1259            }
 1260
 1161261            return 7168000;
 1262        }
 1263
 1264        private (DirectPlayProfile? Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeRea
 1265            MediaOptions options,
 1266            MediaSourceInfo mediaSource,
 1267            MediaStream? videoStream,
 1268            MediaStream? audioStream,
 1269            ICollection<MediaStream> candidateAudioStreams,
 1270            MediaStream? subtitleStream,
 1271            bool isEligibleForDirectPlay,
 1272            bool isEligibleForDirectStream)
 1273        {
 2541274            if (options.ForceDirectPlay)
 1275            {
 01276                return (null, PlayMethod.DirectPlay, audioStream?.Index, 0);
 1277            }
 1278
 2541279            if (options.ForceDirectStream)
 1280            {
 01281                return (null, PlayMethod.DirectStream, audioStream?.Index, 0);
 1282            }
 1283
 2541284            DeviceProfile profile = options.Profile;
 2541285            string container = mediaSource.Container;
 1286
 1287            // Check container conditions
 2541288            var containerProfileReasons = GetCompatibilityContainer(options, mediaSource, container, videoStream);
 1289
 1290            // Check video conditions
 2541291            var videoCodecProfileReasons = videoStream is null ? default : GetCompatibilityVideoCodec(options, mediaSour
 1292
 1293            // Check audio candidates profile conditions
 2541294            var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => GetCompatibilityAudioCode
 1295
 2541296            TranscodeReason subtitleProfileReasons = 0;
 2541297            if (subtitleStream is not null)
 1298            {
 2231299                var subtitleProfile = GetSubtitleProfile(mediaSource, subtitleStream, options.Profile.SubtitleProfiles, 
 1300
 2231301                if (subtitleProfile.Method != SubtitleDeliveryMethod.Drop
 2231302                    && subtitleProfile.Method != SubtitleDeliveryMethod.External
 2231303                    && subtitleProfile.Method != SubtitleDeliveryMethod.Embed)
 1304                {
 01305                    _logger.LogDebug("Not eligible for {0} due to unsupported subtitles", PlayMethod.DirectPlay);
 01306                    subtitleProfileReasons |= TranscodeReason.SubtitleCodecNotSupported;
 1307                }
 1308            }
 1309
 2541310            var containerSupported = false;
 2541311            TranscodeReason[] rankings = [TranscodeReason.VideoCodecNotSupported, VideoCodecReasons, TranscodeReason.Aud
 1312
 1313            // Check DirectPlay profiles to see if it can be direct played
 2541314            var analyzedProfiles = profile.DirectPlayProfiles
 2541315                .Where(directPlayProfile => directPlayProfile.Type == DlnaProfileType.Video)
 2541316                .Select((directPlayProfile, order) =>
 2541317                {
 2541318                    TranscodeReason directPlayProfileReasons = 0;
 2541319                    TranscodeReason audioCodecProfileReasons = 0;
 2541320
 2541321                    // Check container type
 2541322                    if (!directPlayProfile.SupportsContainer(container))
 2541323                    {
 2541324                        directPlayProfileReasons |= TranscodeReason.ContainerNotSupported;
 2541325                    }
 2541326                    else
 2541327                    {
 2541328                        containerSupported = true;
 2541329                    }
 2541330
 2541331                    // Check video codec
 2541332                    string? videoCodec = videoStream?.Codec;
 2541333                    if (!directPlayProfile.SupportsVideoCodec(videoCodec))
 2541334                    {
 2541335                        directPlayProfileReasons |= TranscodeReason.VideoCodecNotSupported;
 2541336                    }
 2541337
 2541338                    // Check audio codec
 2541339                    MediaStream? selectedAudioStream = null;
 2541340                    if (candidateAudioStreams.Count != 0)
 2541341                    {
 2541342                        selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.Supp
 2541343                        if (selectedAudioStream is null)
 2541344                        {
 2541345                            directPlayProfileReasons |= TranscodeReason.AudioCodecNotSupported;
 2541346                        }
 2541347                        else
 2541348                        {
 2541349                            audioCodecProfileReasons = audioStreamMatches.GetValueOrDefault(selectedAudioStream);
 2541350                        }
 2541351                    }
 2541352
 2541353                    var failureReasons = directPlayProfileReasons | containerProfileReasons | subtitleProfileReasons;
 2541354
 2541355                    if ((failureReasons & TranscodeReason.VideoCodecNotSupported) == 0)
 2541356                    {
 2541357                        failureReasons |= videoCodecProfileReasons;
 2541358                    }
 2541359
 2541360                    if ((failureReasons & TranscodeReason.AudioCodecNotSupported) == 0)
 2541361                    {
 2541362                        failureReasons |= audioCodecProfileReasons;
 2541363                    }
 2541364
 2541365                    var directStreamFailureReasons = failureReasons & (~DirectStreamReasons);
 2541366
 2541367                    PlayMethod? playMethod = null;
 2541368                    if (failureReasons == 0 && isEligibleForDirectPlay && mediaSource.SupportsDirectPlay)
 2541369                    {
 2541370                        playMethod = PlayMethod.DirectPlay;
 2541371                    }
 2541372                    else if (directStreamFailureReasons == 0 && isEligibleForDirectStream && mediaSource.SupportsDirectS
 2541373                    {
 2541374                        playMethod = PlayMethod.DirectStream;
 2541375                    }
 2541376
 2541377                    var ranked = GetRank(ref failureReasons, rankings);
 2541378
 2541379                    return (Result: (Profile: directPlayProfile, PlayMethod: playMethod, AudioStreamIndex: selectedAudio
 2541380                })
 2541381                .OrderByDescending(analysis => analysis.Result.PlayMethod)
 2541382                .ThenByDescending(analysis => analysis.Rank)
 2541383                .ThenBy(analysis => analysis.Order)
 2541384                .ToArray()
 2541385                .ToLookup(analysis => analysis.Result.PlayMethod is not null);
 1386
 2541387            var profileMatch = analyzedProfiles[true]
 2541388                .Select(analysis => analysis.Result)
 2541389                .FirstOrDefault();
 2541390            if (profileMatch.Profile is not null)
 1391            {
 1241392                return profileMatch;
 1393            }
 1394
 1301395            var failureReasons = analyzedProfiles[false]
 1301396                .Select(analysis => analysis.Result)
 1301397                .Where(result => !containerSupported || !result.TranscodeReason.HasFlag(TranscodeReason.ContainerNotSupp
 1301398                .FirstOrDefault().TranscodeReason;
 1301399            if (failureReasons == 0)
 1400            {
 141401                failureReasons = TranscodeReason.DirectPlayError;
 1402            }
 1403
 1301404            return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons);
 1405        }
 1406
 1407        private TranscodeReason AggregateFailureConditions(MediaSourceInfo mediaSource, DeviceProfile profile, string ty
 1408        {
 16611409            return conditions.Aggregate<ProfileCondition, TranscodeReason>(0, (reasons, i) =>
 16611410            {
 16611411                LogConditionFailure(profile, type, i, mediaSource);
 16611412                var transcodeReasons = GetTranscodeReasonForFailedCondition(i);
 16611413                return reasons | transcodeReasons;
 16611414            });
 1415        }
 1416
 1417        private void LogConditionFailure(DeviceProfile profile, string type, ProfileCondition condition, MediaSourceInfo
 1418        {
 2471419            _logger.LogDebug(
 2471420                "Profile: {0}, DirectPlay=false. Reason={1}.{2} Condition: {3}. ConditionValue: {4}. IsRequired: {5}. Pa
 2471421                type,
 2471422                profile.Name ?? "Unknown Profile",
 2471423                condition.Property,
 2471424                condition.Condition,
 2471425                condition.Value ?? string.Empty,
 2471426                condition.IsRequired,
 2471427                mediaSource.Path ?? "Unknown path");
 2471428        }
 1429
 1430        /// <summary>
 1431        /// Normalizes input container.
 1432        /// </summary>
 1433        /// <param name="mediaSource">The <see cref="MediaSourceInfo"/>.</param>
 1434        /// <param name="subtitleStream">The <see cref="MediaStream"/> of the subtitle stream.</param>
 1435        /// <param name="subtitleProfiles">The list of supported <see cref="SubtitleProfile"/>s.</param>
 1436        /// <param name="playMethod">The <see cref="PlayMethod"/>.</param>
 1437        /// <param name="transcoderSupport">The <see cref="ITranscoderSupport"/>.</param>
 1438        /// <param name="outputContainer">The output container.</param>
 1439        /// <param name="transcodingSubProtocol">The subtitle transcoding protocol.</param>
 1440        /// <returns>The normalized input container.</returns>
 1441        public static SubtitleProfile GetSubtitleProfile(
 1442            MediaSourceInfo mediaSource,
 1443            MediaStream subtitleStream,
 1444            SubtitleProfile[] subtitleProfiles,
 1445            PlayMethod playMethod,
 1446            ITranscoderSupport transcoderSupport,
 1447            string? outputContainer,
 1448            MediaStreamProtocol? transcodingSubProtocol)
 1449        {
 4621450            if (!subtitleStream.IsExternal && (playMethod != PlayMethod.Transcode || transcodingSubProtocol != MediaStre
 1451            {
 1452                // Look for supported embedded subs of the same format
 01453                foreach (var profile in subtitleProfiles)
 1454                {
 01455                    if (!profile.SupportsLanguage(subtitleStream.Language))
 1456                    {
 1457                        continue;
 1458                    }
 1459
 01460                    if (profile.Method != SubtitleDeliveryMethod.Embed)
 1461                    {
 1462                        continue;
 1463                    }
 1464
 01465                    if (!ContainerHelper.ContainsContainer(profile.Container, outputContainer))
 1466                    {
 1467                        continue;
 1468                    }
 1469
 01470                    if (playMethod == PlayMethod.Transcode && !IsSubtitleEmbedSupported(outputContainer))
 1471                    {
 1472                        continue;
 1473                    }
 1474
 01475                    if (subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format) && string.Equals
 1476                    {
 01477                        return profile;
 1478                    }
 1479                }
 1480
 1481                // Look for supported embedded subs of a convertible format
 01482                foreach (var profile in subtitleProfiles)
 1483                {
 01484                    if (!profile.SupportsLanguage(subtitleStream.Language))
 1485                    {
 1486                        continue;
 1487                    }
 1488
 01489                    if (profile.Method != SubtitleDeliveryMethod.Embed)
 1490                    {
 1491                        continue;
 1492                    }
 1493
 01494                    if (!ContainerHelper.ContainsContainer(profile.Container, outputContainer))
 1495                    {
 1496                        continue;
 1497                    }
 1498
 01499                    if (playMethod == PlayMethod.Transcode && !IsSubtitleEmbedSupported(outputContainer))
 1500                    {
 1501                        continue;
 1502                    }
 1503
 01504                    if (subtitleStream.IsTextSubtitleStream && subtitleStream.SupportsSubtitleConversionTo(profile.Forma
 1505                    {
 01506                        return profile;
 1507                    }
 1508                }
 1509            }
 1510
 1511            // Look for an external or hls profile that matches the stream type (text/graphical) and doesn't require con
 4621512            return GetExternalSubtitleProfile(mediaSource, subtitleStream, subtitleProfiles, playMethod, transcoderSuppo
 4621513                GetExternalSubtitleProfile(mediaSource, subtitleStream, subtitleProfiles, playMethod, transcoderSupport,
 4621514                new SubtitleProfile
 4621515                {
 4621516                    Method = SubtitleDeliveryMethod.Encode,
 4621517                    Format = subtitleStream.Codec
 4621518                };
 1519        }
 1520
 1521        private static bool IsSubtitleEmbedSupported(string? transcodingContainer)
 1522        {
 01523            if (!string.IsNullOrEmpty(transcodingContainer))
 1524            {
 01525                if (ContainerHelper.ContainsContainer(transcodingContainer, "ts,mpegts,mp4"))
 1526                {
 01527                    return false;
 1528                }
 1529
 01530                if (ContainerHelper.ContainsContainer(transcodingContainer, "mkv,matroska"))
 1531                {
 01532                    return true;
 1533                }
 1534            }
 1535
 01536            return false;
 1537        }
 1538
 1539        private static SubtitleProfile? GetExternalSubtitleProfile(MediaSourceInfo mediaSource, MediaStream subtitleStre
 1540        {
 50661541            foreach (var profile in subtitleProfiles)
 1542            {
 20161543                if (profile.Method != SubtitleDeliveryMethod.External && profile.Method != SubtitleDeliveryMethod.Hls)
 1544                {
 1545                    continue;
 1546                }
 1547
 13821548                if (profile.Method == SubtitleDeliveryMethod.Hls && playMethod != PlayMethod.Transcode)
 1549                {
 1550                    continue;
 1551                }
 1552
 13821553                if (!profile.SupportsLanguage(subtitleStream.Language))
 1554                {
 1555                    continue;
 1556                }
 1557
 13821558                if (!subtitleStream.IsExternal && !transcoderSupport.CanExtractSubtitles(subtitleStream.Codec))
 1559                {
 1560                    continue;
 1561                }
 1562
 13821563                if ((profile.Method == SubtitleDeliveryMethod.External && subtitleStream.IsTextSubtitleStream == MediaSt
 13821564                    (profile.Method == SubtitleDeliveryMethod.Hls && subtitleStream.IsTextSubtitleStream))
 1565                {
 13641566                    bool requiresConversion = !string.Equals(subtitleStream.Codec, profile.Format, StringComparison.Ordi
 1567
 13641568                    if (!requiresConversion)
 1569                    {
 1761570                        return profile;
 1571                    }
 1572
 11881573                    if (!allowConversion)
 1574                    {
 1575                        continue;
 1576                    }
 1577
 1578                    // TODO: Build this into subtitleStream.SupportsExternalStream
 2861579                    if (mediaSource.IsInfiniteStream)
 1580                    {
 1581                        continue;
 1582                    }
 1583
 2861584                    if (subtitleStream.IsTextSubtitleStream && subtitleStream.SupportsExternalStream && subtitleStream.S
 1585                    {
 2861586                        return profile;
 1587                    }
 1588                }
 1589            }
 1590
 2861591            return null;
 1592        }
 1593
 1594        private bool IsBitrateLimitExceeded(MediaSourceInfo item, long maxBitrate)
 1595        {
 1596            // Don't restrict bitrate if item is remote.
 2781597            if (item.IsRemote)
 1598            {
 01599                return false;
 1600            }
 1601
 1602            // If no maximum bitrate is set, default to no maximum bitrate.
 2781603            long requestedMaxBitrate = maxBitrate > 0 ? maxBitrate : int.MaxValue;
 1604
 1605            // If we don't know the item bitrate, then force a transcode if requested max bitrate is under 40 mbps
 2781606            int itemBitrate = item.Bitrate ?? 40000000;
 1607
 2781608            if (itemBitrate > requestedMaxBitrate)
 1609            {
 241610                _logger.LogDebug(
 241611                    "Bitrate exceeds limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}",
 241612                    itemBitrate,
 241613                    requestedMaxBitrate);
 241614                return true;
 1615            }
 1616
 2541617            return false;
 1618        }
 1619
 1620        private static void ValidateMediaOptions(MediaOptions options, bool isMediaSource)
 1621        {
 2781622            if (options.ItemId.IsEmpty())
 1623            {
 01624                ArgumentException.ThrowIfNullOrEmpty(options.DeviceId);
 1625            }
 1626
 2781627            if (options.Profile is null)
 1628            {
 01629                throw new ArgumentException("Profile is required");
 1630            }
 1631
 2781632            if (options.MediaSources is null)
 1633            {
 01634                throw new ArgumentException("MediaSources is required");
 1635            }
 1636
 2781637            if (isMediaSource)
 1638            {
 2781639                if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
 1640                {
 01641                    throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested");
 1642                }
 1643
 2781644                if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
 1645                {
 01646                    throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested"
 1647                }
 1648            }
 2781649        }
 1650
 1651        private static IEnumerable<ProfileCondition> GetProfileConditionsForVideoAudio(
 1652            IEnumerable<CodecProfile> codecProfiles,
 1653            string container,
 1654            string codec,
 1655            int? audioChannels,
 1656            int? audioBitrate,
 1657            int? audioSampleRate,
 1658            int? audioBitDepth,
 1659            string audioProfile,
 1660            bool? isSecondaryAudio)
 1661        {
 10051662            return codecProfiles
 10051663                .Where(profile => profile.Type == CodecType.VideoAudio &&
 10051664                    profile.ContainsAnyCodec(codec, container) &&
 10051665                    profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(appl
 10051666                .SelectMany(profile => profile.Conditions)
 10051667                .Where(condition => !ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBi
 1668        }
 1669
 1670        private static IEnumerable<ProfileCondition> GetProfileConditionsForAudio(
 1671            IEnumerable<CodecProfile> codecProfiles,
 1672            string container,
 1673            string? codec,
 1674            int? audioChannels,
 1675            int? audioBitrate,
 1676            int? audioSampleRate,
 1677            int? audioBitDepth,
 1678            bool checkConditions)
 1679        {
 01680            var conditions = codecProfiles
 01681                .Where(profile => profile.Type == CodecType.Audio &&
 01682                    profile.ContainsAnyCodec(codec, container) &&
 01683                    profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsAudioConditionSatisfied(applyCond
 01684                .SelectMany(profile => profile.Conditions);
 1685
 01686            if (!checkConditions)
 1687            {
 01688                return conditions;
 1689            }
 1690
 01691            return conditions.Where(condition => !ConditionProcessor.IsAudioConditionSatisfied(condition, audioChannels,
 1692        }
 1693
 1694        private void ApplyTranscodingConditions(StreamInfo item, IEnumerable<ProfileCondition> conditions, string? quali
 1695        {
 34921696            foreach (ProfileCondition condition in conditions)
 1697            {
 12441698                string value = condition.Value;
 1699
 12441700                if (string.IsNullOrEmpty(value))
 1701                {
 1702                    continue;
 1703                }
 1704
 1705                // No way to express this
 12441706                if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 1707                {
 1708                    continue;
 1709                }
 1710
 12441711                switch (condition.Property)
 1712                {
 1713                    case ProfileConditionValue.AudioBitrate:
 1714                        {
 01715                            if (!enableNonQualifiedConditions)
 1716                            {
 1717                                continue;
 1718                            }
 1719
 01720                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 1721                            {
 01722                                if (condition.Condition == ProfileConditionType.Equals)
 1723                                {
 01724                                    item.AudioBitrate = num;
 1725                                }
 01726                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 1727                                {
 01728                                    item.AudioBitrate = Math.Min(num, item.AudioBitrate ?? num);
 1729                                }
 01730                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 1731                                {
 01732                                    item.AudioBitrate = Math.Max(num, item.AudioBitrate ?? num);
 1733                                }
 1734                            }
 1735
 01736                            break;
 1737                        }
 1738
 1739                    case ProfileConditionValue.AudioSampleRate:
 1740                        {
 01741                            if (!enableNonQualifiedConditions)
 1742                            {
 1743                                continue;
 1744                            }
 1745
 01746                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 1747                            {
 01748                                if (condition.Condition == ProfileConditionType.Equals)
 1749                                {
 01750                                    item.AudioSampleRate = num;
 1751                                }
 01752                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 1753                                {
 01754                                    item.AudioSampleRate = Math.Min(num, item.AudioSampleRate ?? num);
 1755                                }
 01756                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 1757                                {
 01758                                    item.AudioSampleRate = Math.Max(num, item.AudioSampleRate ?? num);
 1759                                }
 1760                            }
 1761
 01762                            break;
 1763                        }
 1764
 1765                    case ProfileConditionValue.AudioChannels:
 1766                        {
 281767                            if (string.IsNullOrEmpty(qualifier))
 1768                            {
 01769                                if (!enableNonQualifiedConditions)
 1770                                {
 01771                                    continue;
 1772                                }
 1773                            }
 1774                            else
 1775                            {
 281776                                if (!enableQualifiedConditions)
 1777                                {
 1778                                    continue;
 1779                                }
 1780                            }
 1781
 281782                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 1783                            {
 281784                                if (condition.Condition == ProfileConditionType.Equals)
 1785                                {
 01786                                    item.SetOption(qualifier, "audiochannels", num.ToString(CultureInfo.InvariantCulture
 1787                                }
 281788                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 1789                                {
 281790                                    item.SetOption(qualifier, "audiochannels", Math.Min(num, item.GetTargetAudioChannels
 1791                                }
 01792                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 1793                                {
 01794                                    item.SetOption(qualifier, "audiochannels", Math.Max(num, item.GetTargetAudioChannels
 1795                                }
 1796                            }
 1797
 01798                            break;
 1799                        }
 1800
 1801                    case ProfileConditionValue.IsAvc:
 1802                        {
 01803                            if (!enableNonQualifiedConditions)
 1804                            {
 1805                                continue;
 1806                            }
 1807
 01808                            if (bool.TryParse(value, out var isAvc))
 1809                            {
 01810                                if (isAvc && condition.Condition == ProfileConditionType.Equals)
 1811                                {
 01812                                    item.RequireAvc = true;
 1813                                }
 01814                                else if (!isAvc && condition.Condition == ProfileConditionType.NotEquals)
 1815                                {
 01816                                    item.RequireAvc = true;
 1817                                }
 1818                            }
 1819
 01820                            break;
 1821                        }
 1822
 1823                    case ProfileConditionValue.IsAnamorphic:
 1824                        {
 2011825                            if (!enableNonQualifiedConditions)
 1826                            {
 1827                                continue;
 1828                            }
 1829
 2011830                            if (bool.TryParse(value, out var isAnamorphic))
 1831                            {
 2011832                                if (isAnamorphic && condition.Condition == ProfileConditionType.Equals)
 1833                                {
 01834                                    item.RequireNonAnamorphic = true;
 1835                                }
 2011836                                else if (!isAnamorphic && condition.Condition == ProfileConditionType.NotEquals)
 1837                                {
 01838                                    item.RequireNonAnamorphic = true;
 1839                                }
 1840                            }
 1841
 01842                            break;
 1843                        }
 1844
 1845                    case ProfileConditionValue.IsInterlaced:
 1846                        {
 1091847                            if (string.IsNullOrEmpty(qualifier))
 1848                            {
 01849                                if (!enableNonQualifiedConditions)
 1850                                {
 01851                                    continue;
 1852                                }
 1853                            }
 1854                            else
 1855                            {
 1091856                                if (!enableQualifiedConditions)
 1857                                {
 1858                                    continue;
 1859                                }
 1860                            }
 1861
 1091862                            if (bool.TryParse(value, out var isInterlaced))
 1863                            {
 1091864                                if (!isInterlaced && condition.Condition == ProfileConditionType.Equals)
 1865                                {
 01866                                    item.SetOption(qualifier, "deinterlace", "true");
 1867                                }
 1091868                                else if (isInterlaced && condition.Condition == ProfileConditionType.NotEquals)
 1869                                {
 1091870                                    item.SetOption(qualifier, "deinterlace", "true");
 1871                                }
 1872                            }
 1873
 1091874                            break;
 1875                        }
 1876
 1877                    case ProfileConditionValue.AudioProfile:
 1878                    case ProfileConditionValue.Has64BitOffsets:
 1879                    case ProfileConditionValue.PacketLength:
 1880                    case ProfileConditionValue.NumStreams:
 1881                    case ProfileConditionValue.NumAudioStreams:
 1882                    case ProfileConditionValue.NumVideoStreams:
 1883                    case ProfileConditionValue.IsSecondaryAudio:
 1884                    case ProfileConditionValue.VideoTimestamp:
 1885                        {
 1886                            // Not supported yet
 1887                            break;
 1888                        }
 1889
 1890                    case ProfileConditionValue.RefFrames:
 1891                        {
 21892                            if (string.IsNullOrEmpty(qualifier))
 1893                            {
 01894                                if (!enableNonQualifiedConditions)
 1895                                {
 01896                                    continue;
 1897                                }
 1898                            }
 1899                            else
 1900                            {
 21901                                if (!enableQualifiedConditions)
 1902                                {
 1903                                    continue;
 1904                                }
 1905                            }
 1906
 21907                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 1908                            {
 21909                                if (condition.Condition == ProfileConditionType.Equals)
 1910                                {
 01911                                    item.SetOption(qualifier, "maxrefframes", num.ToString(CultureInfo.InvariantCulture)
 1912                                }
 21913                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 1914                                {
 21915                                    item.SetOption(qualifier, "maxrefframes", Math.Min(num, item.GetTargetRefFrames(qual
 1916                                }
 01917                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 1918                                {
 01919                                    item.SetOption(qualifier, "maxrefframes", Math.Max(num, item.GetTargetRefFrames(qual
 1920                                }
 1921                            }
 1922
 01923                            break;
 1924                        }
 1925
 1926                    case ProfileConditionValue.VideoBitDepth:
 1927                        {
 01928                            if (string.IsNullOrEmpty(qualifier))
 1929                            {
 01930                                if (!enableNonQualifiedConditions)
 1931                                {
 01932                                    continue;
 1933                                }
 1934                            }
 1935                            else
 1936                            {
 01937                                if (!enableQualifiedConditions)
 1938                                {
 1939                                    continue;
 1940                                }
 1941                            }
 1942
 01943                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 1944                            {
 01945                                if (condition.Condition == ProfileConditionType.Equals)
 1946                                {
 01947                                    item.SetOption(qualifier, "videobitdepth", num.ToString(CultureInfo.InvariantCulture
 1948                                }
 01949                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 1950                                {
 01951                                    item.SetOption(qualifier, "videobitdepth", Math.Min(num, item.GetTargetVideoBitDepth
 1952                                }
 01953                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 1954                                {
 01955                                    item.SetOption(qualifier, "videobitdepth", Math.Max(num, item.GetTargetVideoBitDepth
 1956                                }
 1957                            }
 1958
 01959                            break;
 1960                        }
 1961
 1962                    case ProfileConditionValue.VideoProfile:
 1963                        {
 2241964                            if (string.IsNullOrEmpty(qualifier))
 1965                            {
 1966                                continue;
 1967                            }
 1968
 1969                            // Change from split by | to comma
 1970                            // Strip spaces to avoid having to encode
 2241971                            var values = value
 2241972                                .Split('|', StringSplitOptions.RemoveEmptyEntries);
 1973
 2241974                            if (condition.Condition == ProfileConditionType.Equals)
 1975                            {
 01976                                item.SetOption(qualifier, "profile", string.Join(',', values));
 1977                            }
 2241978                            else if (condition.Condition == ProfileConditionType.EqualsAny)
 1979                            {
 2241980                                var currentValue = item.GetOption(qualifier, "profile");
 2241981                                if (!string.IsNullOrEmpty(currentValue) && values.Any(value => value == currentValue))
 1982                                {
 711983                                    item.SetOption(qualifier, "profile", currentValue);
 1984                                }
 1985                                else
 1986                                {
 1531987                                    item.SetOption(qualifier, "profile", string.Join(',', values));
 1988                                }
 1989                            }
 1990
 1531991                            break;
 1992                        }
 1993
 1994                    case ProfileConditionValue.VideoRangeType:
 1995                        {
 2581996                            if (string.IsNullOrEmpty(qualifier))
 1997                            {
 1998                                continue;
 1999                            }
 2000
 2001                            // change from split by | to comma
 2002                            // strip spaces to avoid having to encode
 2582003                            var values = value
 2582004                                .Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
 2005
 2582006                            if (condition.Condition == ProfileConditionType.Equals)
 2007                            {
 02008                                item.SetOption(qualifier, "rangetype", string.Join(',', values));
 2009                            }
 2582010                            else if (condition.Condition == ProfileConditionType.NotEquals)
 2011                            {
 02012                                item.SetOption(qualifier, "rangetype", string.Join(',', Enum.GetNames(typeof(VideoRangeT
 2013                            }
 2582014                            else if (condition.Condition == ProfileConditionType.EqualsAny)
 2015                            {
 2582016                                var currentValue = item.GetOption(qualifier, "rangetype");
 2582017                                if (!string.IsNullOrEmpty(currentValue) && values.Any(v => string.Equals(v, currentValue
 2018                                {
 02019                                    item.SetOption(qualifier, "rangetype", currentValue);
 2020                                }
 2021                                else
 2022                                {
 2582023                                    item.SetOption(qualifier, "rangetype", string.Join(',', values));
 2024                                }
 2025                            }
 2026
 2582027                            break;
 2028                        }
 2029
 2030                    case ProfileConditionValue.VideoCodecTag:
 2031                        {
 122032                            if (string.IsNullOrEmpty(qualifier))
 2033                            {
 2034                                continue;
 2035                            }
 2036
 2037                            // change from split by | to comma
 2038                            // strip spaces to avoid having to encode
 122039                            var values = value
 122040                                .Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
 2041
 122042                            if (condition.Condition == ProfileConditionType.Equals)
 2043                            {
 02044                                item.SetOption(qualifier, "codectag", string.Join(',', values));
 2045                            }
 122046                            else if (condition.Condition == ProfileConditionType.EqualsAny)
 2047                            {
 122048                                var currentValue = item.GetOption(qualifier, "codectag");
 122049                                if (!string.IsNullOrEmpty(currentValue) && values.Any(v => string.Equals(v, currentValue
 2050                                {
 02051                                    item.SetOption(qualifier, "codectag", currentValue);
 2052                                }
 2053                                else
 2054                                {
 122055                                    item.SetOption(qualifier, "codectag", string.Join(',', values));
 2056                                }
 2057                            }
 2058
 122059                            break;
 2060                        }
 2061
 2062                    case ProfileConditionValue.Height:
 2063                        {
 02064                            if (!enableNonQualifiedConditions)
 2065                            {
 2066                                continue;
 2067                            }
 2068
 02069                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 2070                            {
 02071                                if (condition.Condition == ProfileConditionType.Equals)
 2072                                {
 02073                                    item.MaxHeight = num;
 2074                                }
 02075                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 2076                                {
 02077                                    item.MaxHeight = Math.Min(num, item.MaxHeight ?? num);
 2078                                }
 02079                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 2080                                {
 02081                                    item.MaxHeight = Math.Max(num, item.MaxHeight ?? num);
 2082                                }
 2083                            }
 2084
 02085                            break;
 2086                        }
 2087
 2088                    case ProfileConditionValue.VideoBitrate:
 2089                        {
 322090                            if (!enableNonQualifiedConditions)
 2091                            {
 2092                                continue;
 2093                            }
 2094
 322095                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 2096                            {
 322097                                if (condition.Condition == ProfileConditionType.Equals)
 2098                                {
 02099                                    item.VideoBitrate = num;
 2100                                }
 322101                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 2102                                {
 322103                                    item.VideoBitrate = Math.Min(num, item.VideoBitrate ?? num);
 2104                                }
 02105                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 2106                                {
 02107                                    item.VideoBitrate = Math.Max(num, item.VideoBitrate ?? num);
 2108                                }
 2109                            }
 2110
 02111                            break;
 2112                        }
 2113
 2114                    case ProfileConditionValue.VideoFramerate:
 2115                        {
 122116                            if (!enableNonQualifiedConditions)
 2117                            {
 2118                                continue;
 2119                            }
 2120
 122121                            if (float.TryParse(value, CultureInfo.InvariantCulture, out var num))
 2122                            {
 122123                                if (condition.Condition == ProfileConditionType.Equals)
 2124                                {
 02125                                    item.MaxFramerate = num;
 2126                                }
 122127                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 2128                                {
 122129                                    item.MaxFramerate = Math.Min(num, item.MaxFramerate ?? num);
 2130                                }
 02131                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 2132                                {
 02133                                    item.MaxFramerate = Math.Max(num, item.MaxFramerate ?? num);
 2134                                }
 2135                            }
 2136
 02137                            break;
 2138                        }
 2139
 2140                    case ProfileConditionValue.VideoLevel:
 2141                        {
 2122142                            if (string.IsNullOrEmpty(qualifier))
 2143                            {
 2144                                continue;
 2145                            }
 2146
 2122147                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 2148                            {
 2122149                                if (condition.Condition == ProfileConditionType.Equals)
 2150                                {
 02151                                    item.SetOption(qualifier, "level", num.ToString(CultureInfo.InvariantCulture));
 2152                                }
 2122153                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 2154                                {
 2122155                                    item.SetOption(qualifier, "level", Math.Min(num, item.GetTargetVideoLevel(qualifier)
 2156                                }
 02157                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 2158                                {
 02159                                    item.SetOption(qualifier, "level", Math.Max(num, item.GetTargetVideoLevel(qualifier)
 2160                                }
 2161                            }
 2162
 02163                            break;
 2164                        }
 2165
 2166                    case ProfileConditionValue.Width:
 2167                        {
 42168                            if (!enableNonQualifiedConditions)
 2169                            {
 2170                                continue;
 2171                            }
 2172
 42173                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 2174                            {
 42175                                if (condition.Condition == ProfileConditionType.Equals)
 2176                                {
 02177                                    item.MaxWidth = num;
 2178                                }
 42179                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 2180                                {
 42181                                    item.MaxWidth = Math.Min(num, item.MaxWidth ?? num);
 2182                                }
 02183                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 2184                                {
 02185                                    item.MaxWidth = Math.Max(num, item.MaxWidth ?? num);
 2186                                }
 2187                            }
 2188
 2189                            break;
 2190                        }
 2191
 2192                    default:
 2193                        break;
 2194                }
 2195            }
 5022196        }
 2197
 2198        private static bool IsAudioContainerSupported(DirectPlayProfile profile, MediaSourceInfo item)
 2199        {
 2200            // Check container type
 02201            if (!profile.SupportsContainer(item.Container))
 2202            {
 02203                return false;
 2204            }
 2205
 2206            // Never direct play audio in matroska when the device only declare support for webm.
 2207            // The first check is not enough because mkv is assumed can be webm.
 2208            // See https://github.com/jellyfin/jellyfin/issues/13344
 02209            return !ContainerHelper.ContainsContainer("mkv", item.Container)
 02210                   || profile.SupportsContainer("mkv");
 2211        }
 2212
 2213        private static bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audi
 2214        {
 02215            if (!IsAudioContainerSupported(profile, item))
 2216            {
 02217                return false;
 2218            }
 2219
 2220            // Check audio codec
 02221            string? audioCodec = audioStream?.Codec;
 02222            if (!profile.SupportsAudioCodec(audioCodec))
 2223            {
 02224                return false;
 2225            }
 2226
 02227            return true;
 2228        }
 2229
 2230        private static bool IsAudioDirectStreamSupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream au
 2231        {
 2232            // Check container type, this should NOT be supported
 2233            // If the container is supported, the file should be directly played
 02234            if (IsAudioContainerSupported(profile, item))
 2235            {
 02236                return false;
 2237            }
 2238
 2239            // Check audio codec, we cannot use the SupportsAudioCodec here
 2240            // Because that one assumes empty container supports all codec, which is just useless
 02241            string? audioCodec = audioStream?.Codec;
 02242            return string.Equals(profile.AudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase)
 02243                   || string.Equals(profile.Container, audioCodec, StringComparison.OrdinalIgnoreCase);
 2244        }
 2245
 2246        private int GetRank(ref TranscodeReason a, TranscodeReason[] rankings)
 2247        {
 12092248            var index = 1;
 87402249            foreach (var flag in rankings)
 2250            {
 36662251                var reason = a & flag;
 36662252                if (reason != 0)
 2253                {
 10102254                    return index;
 2255                }
 2256
 26562257                index++;
 2258            }
 2259
 1992260            return index;
 2261        }
 2262
 2263        /// <summary>
 2264        /// Check the profile conditions.
 2265        /// </summary>
 2266        /// <param name="conditions">Profile conditions.</param>
 2267        /// <param name="mediaSource">Media source.</param>
 2268        /// <param name="videoStream">Video stream.</param>
 2269        /// <returns>Failed profile conditions.</returns>
 2270        private IEnumerable<ProfileCondition> CheckVideoConditions(ProfileCondition[] conditions, MediaSourceInfo mediaS
 2271        {
 7292272            int? width = videoStream?.Width;
 7292273            int? height = videoStream?.Height;
 7292274            int? bitDepth = videoStream?.BitDepth;
 7292275            int? videoBitrate = videoStream?.BitRate;
 7292276            double? videoLevel = videoStream?.Level;
 7292277            string? videoProfile = videoStream?.Profile;
 7292278            VideoRangeType? videoRangeType = videoStream?.VideoRangeType;
 7292279            float videoFramerate = videoStream is null ? 0 : videoStream.ReferenceFrameRate ?? 0;
 7292280            bool? isAnamorphic = videoStream?.IsAnamorphic;
 7292281            bool? isInterlaced = videoStream?.IsInterlaced;
 7292282            string? videoCodecTag = videoStream?.CodecTag;
 7292283            bool? isAvc = videoStream?.IsAVC;
 2284
 7292285            TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : mediaSource.Time
 7292286            int? packetLength = videoStream?.PacketLength;
 7292287            int? refFrames = videoStream?.RefFrames;
 2288
 7292289            int numStreams = mediaSource.MediaStreams.Count;
 7292290            int? numAudioStreams = mediaSource.GetStreamCount(MediaStreamType.Audio);
 7292291            int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video);
 2292
 7292293            return conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, widt
 2294        }
 2295
 2296        /// <summary>
 2297        /// Check the compatibility of the container.
 2298        /// </summary>
 2299        /// <param name="options">Media options.</param>
 2300        /// <param name="mediaSource">Media source.</param>
 2301        /// <param name="container">Container.</param>
 2302        /// <param name="videoStream">Video stream.</param>
 2303        /// <returns>Transcode reasons if the container is not fully compatible.</returns>
 2304        private TranscodeReason GetCompatibilityContainer(MediaOptions options, MediaSourceInfo mediaSource, string cont
 2305        {
 2542306            var profile = options.Profile;
 2307
 2542308            var failures = AggregateFailureConditions(
 2542309                mediaSource,
 2542310                profile,
 2542311                "VideoCodecProfile",
 2542312                profile.ContainerProfiles
 2542313                    .Where(containerProfile => containerProfile.Type == DlnaProfileType.Video && containerProfile.Contai
 2542314                    .SelectMany(containerProfile => CheckVideoConditions(containerProfile.Conditions, mediaSource, video
 2315
 2542316            return failures;
 2317        }
 2318
 2319        /// <summary>
 2320        /// Check the compatibility of the video codec.
 2321        /// </summary>
 2322        /// <param name="options">Media options.</param>
 2323        /// <param name="mediaSource">Media source.</param>
 2324        /// <param name="container">Container.</param>
 2325        /// <param name="videoStream">Video stream.</param>
 2326        /// <returns>Transcode reasons if the video stream is not fully compatible.</returns>
 2327        private TranscodeReason GetCompatibilityVideoCodec(MediaOptions options, MediaSourceInfo mediaSource, string con
 2328        {
 4022329            var profile = options.Profile;
 2330
 4022331            string videoCodec = videoStream.Codec;
 2332
 4022333            var failures = AggregateFailureConditions(
 4022334                mediaSource,
 4022335                profile,
 4022336                "VideoCodecProfile",
 4022337                profile.CodecProfiles
 4022338                    .Where(codecProfile => codecProfile.Type == CodecType.Video &&
 4022339                        codecProfile.ContainsAnyCodec(videoCodec, container) &&
 4022340                        !CheckVideoConditions(codecProfile.ApplyConditions, mediaSource, videoStream).Any())
 4022341                    .SelectMany(codecProfile => CheckVideoConditions(codecProfile.Conditions, mediaSource, videoStream))
 2342
 4022343            return failures;
 2344        }
 2345
 2346        /// <summary>
 2347        /// Check the compatibility of the audio codec.
 2348        /// </summary>
 2349        /// <param name="options">Media options.</param>
 2350        /// <param name="mediaSource">Media source.</param>
 2351        /// <param name="container">Container.</param>
 2352        /// <param name="audioStream">Audio stream.</param>
 2353        /// <param name="transcodingAudioCodec">Override audio codec.</param>
 2354        /// <param name="isVideo">The media source is video.</param>
 2355        /// <param name="isSecondaryAudio">The audio stream is secondary.</param>
 2356        /// <returns>Transcode reasons if the audio stream is not fully compatible.</returns>
 2357        private TranscodeReason GetCompatibilityAudioCodec(MediaOptions options, MediaSourceInfo mediaSource, string con
 2358        {
 10052359            var profile = options.Profile;
 2360
 10052361            var audioCodec = transcodingAudioCodec ?? audioStream.Codec;
 10052362            var audioProfile = audioStream.Profile;
 10052363            var audioChannels = audioStream.Channels;
 10052364            var audioBitrate = audioStream.BitRate;
 10052365            var audioSampleRate = audioStream.SampleRate;
 10052366            var audioBitDepth = audioStream.BitDepth;
 2367
 10052368            var audioFailureConditions = isVideo
 10052369                ? GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioCodec, audioChannels, audioBi
 10052370                : GetProfileConditionsForAudio(profile.CodecProfiles, container, audioCodec, audioChannels, audioBitrate
 2371
 10052372            var failures = AggregateFailureConditions(mediaSource, profile, "AudioCodecProfile", audioFailureConditions)
 2373
 10052374            return failures;
 2375        }
 2376
 2377        /// <summary>
 2378        /// Check the compatibility of the audio codec for direct playback.
 2379        /// </summary>
 2380        /// <param name="options">Media options.</param>
 2381        /// <param name="mediaSource">Media source.</param>
 2382        /// <param name="container">Container.</param>
 2383        /// <param name="audioStream">Audio stream.</param>
 2384        /// <param name="isVideo">The media source is video.</param>
 2385        /// <param name="isSecondaryAudio">The audio stream is secondary.</param>
 2386        /// <returns>Transcode reasons if the audio stream is not fully compatible for direct playback.</returns>
 2387        private TranscodeReason GetCompatibilityAudioCodecDirect(MediaOptions options, MediaSourceInfo mediaSource, stri
 2388        {
 2732389            var failures = GetCompatibilityAudioCodec(options, mediaSource, container, audioStream, null, isVideo, isSec
 2390
 2732391            if (audioStream.IsExternal)
 2392            {
 62393                failures |= TranscodeReason.AudioIsExternal;
 2394            }
 2395
 2732396            return failures;
 2397        }
 2398    }
 2399}

Methods/Properties

.cctor()
.ctor(MediaBrowser.Model.Dlna.ITranscoderSupport,Microsoft.Extensions.Logging.ILogger)
GetOptimalAudioStream(MediaBrowser.Model.Dlna.MediaOptions)
GetOptimalAudioStream(MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Dlna.MediaOptions)
GetOptimalVideoStream(MediaBrowser.Model.Dlna.MediaOptions)
GetOptimalStream(System.Collections.Generic.List`1<MediaBrowser.Model.Dlna.StreamInfo>,System.Int64)
SortMediaSources(System.Collections.Generic.List`1<MediaBrowser.Model.Dlna.StreamInfo>,System.Int64)
GetTranscodeReasonForFailedCondition(MediaBrowser.Model.Dlna.ProfileCondition)
NormalizeMediaSourceFormatIntoSingleContainer(System.String,MediaBrowser.Model.Dlna.DeviceProfile,MediaBrowser.Model.Dlna.DlnaProfileType,MediaBrowser.Model.Dlna.DirectPlayProfile)
GetAudioDirectPlayProfile(MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Entities.MediaStream,MediaBrowser.Model.Dlna.MediaOptions)
GetTranscodeReasonsFromDirectPlayProfile(MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Entities.MediaStream,MediaBrowser.Model.Entities.MediaStream,System.Collections.Generic.IEnumerable`1<MediaBrowser.Model.Dlna.DirectPlayProfile>)
GetDefaultSubtitleStreamIndex(MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Dlna.SubtitleProfile[])
SetStreamInfoOptionsFromTranscodingProfile(MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Dlna.StreamInfo,MediaBrowser.Model.Dlna.TranscodingProfile)
SetStreamInfoOptionsFromDirectPlayProfile(MediaBrowser.Model.Dlna.MediaOptions,MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Dlna.StreamInfo,MediaBrowser.Model.Dlna.DirectPlayProfile)
BuildVideoItem(MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Dlna.MediaOptions)
GetVideoTranscodeProfile(MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Dlna.MediaOptions,MediaBrowser.Model.Entities.MediaStream,MediaBrowser.Model.Entities.MediaStream,MediaBrowser.Model.Dlna.StreamInfo)
BuildStreamVideoItem(MediaBrowser.Model.Dlna.StreamInfo,MediaBrowser.Model.Dlna.MediaOptions,MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Entities.MediaStream,MediaBrowser.Model.Entities.MediaStream,System.Collections.Generic.IEnumerable`1<MediaBrowser.Model.Entities.MediaStream>,System.String,System.String,System.String)
GetDefaultAudioBitrate(System.String,System.Nullable`1<System.Int32>)
GetAudioBitrate(System.Int64,System.Collections.Generic.IReadOnlyList`1<System.String>,MediaBrowser.Model.Entities.MediaStream,MediaBrowser.Model.Dlna.StreamInfo)
GetMaxAudioBitrateForTotalBitrate(System.Int64)
GetVideoDirectPlayProfile(MediaBrowser.Model.Dlna.MediaOptions,MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Entities.MediaStream,MediaBrowser.Model.Entities.MediaStream,System.Collections.Generic.ICollection`1<MediaBrowser.Model.Entities.MediaStream>,MediaBrowser.Model.Entities.MediaStream,System.Boolean,System.Boolean)
AggregateFailureConditions(MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Dlna.DeviceProfile,System.String,System.Collections.Generic.IEnumerable`1<MediaBrowser.Model.Dlna.ProfileCondition>)
LogConditionFailure(MediaBrowser.Model.Dlna.DeviceProfile,System.String,MediaBrowser.Model.Dlna.ProfileCondition,MediaBrowser.Model.Dto.MediaSourceInfo)
GetSubtitleProfile(MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Entities.MediaStream,MediaBrowser.Model.Dlna.SubtitleProfile[],MediaBrowser.Model.Session.PlayMethod,MediaBrowser.Model.Dlna.ITranscoderSupport,System.String,System.Nullable`1<Jellyfin.Data.Enums.MediaStreamProtocol>)
IsSubtitleEmbedSupported(System.String)
GetExternalSubtitleProfile(MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Entities.MediaStream,MediaBrowser.Model.Dlna.SubtitleProfile[],MediaBrowser.Model.Session.PlayMethod,MediaBrowser.Model.Dlna.ITranscoderSupport,System.Boolean)
IsBitrateLimitExceeded(MediaBrowser.Model.Dto.MediaSourceInfo,System.Int64)
ValidateMediaOptions(MediaBrowser.Model.Dlna.MediaOptions,System.Boolean)
GetProfileConditionsForVideoAudio(System.Collections.Generic.IEnumerable`1<MediaBrowser.Model.Dlna.CodecProfile>,System.String,System.String,System.Nullable`1<System.Int32>,System.Nullable`1<System.Int32>,System.Nullable`1<System.Int32>,System.Nullable`1<System.Int32>,System.String,System.Nullable`1<System.Boolean>)
GetProfileConditionsForAudio(System.Collections.Generic.IEnumerable`1<MediaBrowser.Model.Dlna.CodecProfile>,System.String,System.String,System.Nullable`1<System.Int32>,System.Nullable`1<System.Int32>,System.Nullable`1<System.Int32>,System.Nullable`1<System.Int32>,System.Boolean)
ApplyTranscodingConditions(MediaBrowser.Model.Dlna.StreamInfo,System.Collections.Generic.IEnumerable`1<MediaBrowser.Model.Dlna.ProfileCondition>,System.String,System.Boolean,System.Boolean)
IsAudioContainerSupported(MediaBrowser.Model.Dlna.DirectPlayProfile,MediaBrowser.Model.Dto.MediaSourceInfo)
IsAudioDirectPlaySupported(MediaBrowser.Model.Dlna.DirectPlayProfile,MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Entities.MediaStream)
IsAudioDirectStreamSupported(MediaBrowser.Model.Dlna.DirectPlayProfile,MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Entities.MediaStream)
GetRank(MediaBrowser.Model.Session.TranscodeReason&,MediaBrowser.Model.Session.TranscodeReason[])
CheckVideoConditions(MediaBrowser.Model.Dlna.ProfileCondition[],MediaBrowser.Model.Dto.MediaSourceInfo,MediaBrowser.Model.Entities.MediaStream)
GetCompatibilityContainer(MediaBrowser.Model.Dlna.MediaOptions,MediaBrowser.Model.Dto.MediaSourceInfo,System.String,MediaBrowser.Model.Entities.MediaStream)
GetCompatibilityVideoCodec(MediaBrowser.Model.Dlna.MediaOptions,MediaBrowser.Model.Dto.MediaSourceInfo,System.String,MediaBrowser.Model.Entities.MediaStream)
GetCompatibilityAudioCodec(MediaBrowser.Model.Dlna.MediaOptions,MediaBrowser.Model.Dto.MediaSourceInfo,System.String,MediaBrowser.Model.Entities.MediaStream,System.String,System.Boolean,System.Boolean)
GetCompatibilityAudioCodecDirect(MediaBrowser.Model.Dlna.MediaOptions,MediaBrowser.Model.Dto.MediaSourceInfo,System.String,MediaBrowser.Model.Entities.MediaStream,System.Boolean,System.Boolean)