< Summary - Jellyfin

Information
Class: MediaBrowser.Model.Dlna.StreamBuilder
Assembly: MediaBrowser.Model
File(s): /srv/git/jellyfin/MediaBrowser.Model/Dlna/StreamBuilder.cs
Line coverage
70%
Covered lines: 763
Uncovered lines: 326
Coverable lines: 1089
Total lines: 2469
Line coverage: 70%
Branch coverage
59%
Covered branches: 591
Total branches: 990
Branch coverage: 59.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 3/26/2026 - 12:14:14 AM Line coverage: 68.6% (724/1055) Branch coverage: 58.7% (553/942) Total lines: 23994/12/2026 - 12:13:54 AM Line coverage: 69.1% (730/1055) Branch coverage: 61.2% (578/944) Total lines: 23995/11/2026 - 12:15:59 AM Line coverage: 68.5% (733/1069) Branch coverage: 60.8% (584/960) Total lines: 24365/20/2026 - 12:15:44 AM Line coverage: 69.6% (749/1075) Branch coverage: 59.7% (576/964) Total lines: 24496/1/2026 - 12:16:05 AM Line coverage: 69.9% (759/1085) Branch coverage: 59.3% (583/982) Total lines: 24616/18/2026 - 12:16:24 AM Line coverage: 70% (763/1089) Branch coverage: 59.6% (591/990) Total lines: 2469 3/26/2026 - 12:14:14 AM Line coverage: 68.6% (724/1055) Branch coverage: 58.7% (553/942) Total lines: 23994/12/2026 - 12:13:54 AM Line coverage: 69.1% (730/1055) Branch coverage: 61.2% (578/944) Total lines: 23995/11/2026 - 12:15:59 AM Line coverage: 68.5% (733/1069) Branch coverage: 60.8% (584/960) Total lines: 24365/20/2026 - 12:15:44 AM Line coverage: 69.6% (749/1075) Branch coverage: 59.7% (576/964) Total lines: 24496/1/2026 - 12:16:05 AM Line coverage: 69.9% (759/1085) Branch coverage: 59.3% (583/982) Total lines: 24616/18/2026 - 12:16:24 AM Line coverage: 70% (763/1089) Branch coverage: 59.6% (591/990) Total lines: 2469

Coverage delta

Coverage delta 3 -3

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(...)32.14%2362835.71%
NormalizeMediaSourceFormatIntoSingleContainer(...)87.5%1616100%
GetAudioDirectPlayProfile(...)0%600240%
GetTranscodeReasonsFromDirectPlayProfile(...)0%420200%
GetDefaultSubtitleStreamIndex(...)82.35%353490.9%
SetStreamInfoOptionsFromTranscodingProfile(...)75%8895%
SetStreamInfoOptionsFromDirectPlayProfile(...)0%620%
BuildVideoItem(...)58.92%14011286.91%
GetVideoTranscodeProfile(...)70%101096.96%
BuildStreamVideoItem(...)88.97%13613697.36%
GetDefaultAudioBitrate(...)59.09%332271.42%
GetAudioBitrate(...)81.25%323295.83%
GetMaxAudioBitrateForTotalBitrate(...)68.75%231670.58%
GetVideoDirectPlayProfile(...)54.54%222296.11%
AggregateFailureConditions(...)100%11100%
LogConditionFailure(...)66.66%66100%
GetSubtitleProfile(...)78.94%473881.81%
IsSubtitleEmbedSupported(...)100%66100%
CanConsiderEmbedSubtitle(...)100%88100%
GetExternalSubtitleProfile(...)81.25%4848100%
IsBitrateLimitExceeded(...)66.66%6690.9%
ValidateMediaOptions(...)68.75%351658.33%
GetProfileConditionsForVideoAudio(...)100%11100%
GetProfileConditionsForAudio(...)0%620%
ApplyTranscodingConditions(...)61.63%927523244.82%
IsAudioContainerSupported(...)0%2040%
IsAudioDirectPlaySupported(...)0%4260%
IsAudioDirectStreamSupported(...)0%4260%
GetRank(...)100%44100%
CheckVideoConditions(...)50%3232100%
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        {
 28042            _transcoderSupport = transcoderSupport;
 28043            _logger = logger;
 28044        }
 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        {
 280232            ValidateMediaOptions(options, true);
 233
 280234            var mediaSources = string.IsNullOrEmpty(options.MediaSourceId)
 280235                ? options.MediaSources
 280236                : options.MediaSources.Where(x => string.Equals(x.Id, options.MediaSourceId, StringComparison.OrdinalIgn
 237
 280238            List<StreamInfo> streams = [];
 1120239            foreach (var mediaSourceInfo in mediaSources)
 240            {
 280241                var streamInfo = BuildVideoItem(mediaSourceInfo, options);
 280242                if (streamInfo is not null)
 243                {
 280244                    streams.Add(streamInfo);
 245                }
 246            }
 247
 1120248            foreach (var stream in streams)
 249            {
 280250                stream.DeviceId = options.DeviceId;
 280251                stream.DeviceProfileId = options.Profile.Id?.ToString("N", CultureInfo.InvariantCulture);
 252            }
 253
 280254            return GetOptimalStream(streams, options.GetMaxBitrate(false) ?? 0);
 255        }
 256
 257        private static StreamInfo? GetOptimalStream(List<StreamInfo> streams, long maxBitrate)
 280258            => SortMediaSources(streams, maxBitrate).FirstOrDefault();
 259
 260        private static IOrderedEnumerable<StreamInfo> SortMediaSources(List<StreamInfo> streams, long maxBitrate)
 261        {
 280262            return streams.OrderBy(i =>
 280263            {
 280264                // Nothing beats direct playing a file
 280265                if (i.PlayMethod == PlayMethod.DirectPlay && i.MediaSource?.Protocol == MediaProtocol.File)
 280266                {
 280267                    return 0;
 280268                }
 280269
 280270                return 1;
 280271            }).ThenBy(i =>
 280272            {
 280273                switch (i.PlayMethod)
 280274                {
 280275                    // Let's assume direct streaming a file is just as desirable as direct playing a remote url
 280276                    case PlayMethod.DirectStream:
 280277                    case PlayMethod.DirectPlay:
 280278                        return 0;
 280279                    default:
 280280                        return 1;
 280281                }
 280282            }).ThenBy(i =>
 280283            {
 280284                switch (i.MediaSource?.Protocol)
 280285                {
 280286                    case MediaProtocol.File:
 280287                        return 0;
 280288                    default:
 280289                        return 1;
 280290                }
 280291            }).ThenBy(i =>
 280292            {
 280293                if (maxBitrate > 0)
 280294                {
 280295                    if (i.MediaSource?.Bitrate is not null)
 280296                    {
 280297                        return Math.Abs(i.MediaSource.Bitrate.Value - maxBitrate);
 280298                    }
 280299                }
 280300
 280301                return 0;
 280302            }).ThenBy(streams.IndexOf);
 303        }
 304
 305        private static TranscodeReason GetTranscodeReasonForFailedCondition(ProfileCondition condition)
 306        {
 248307            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.VideoRotation:
 1384                    return TranscodeReason.VideoRotationNotSupported;
 385
 386                case ProfileConditionValue.VideoTimestamp:
 387                    // TODO
 0388                    return 0;
 389
 390                case ProfileConditionValue.Width:
 0391                    return TranscodeReason.VideoResolutionNotSupported;
 392
 393                default:
 0394                    return 0;
 395            }
 396        }
 397
 398        /// <summary>
 399        /// Normalizes input container.
 400        /// </summary>
 401        /// <param name="inputContainer">The input container.</param>
 402        /// <param name="profile">The <see cref="DeviceProfile"/>.</param>
 403        /// <param name="type">The <see cref="DlnaProfileType"/>.</param>
 404        /// <param name="playProfile">The <see cref="DirectPlayProfile"/> object to get the video stream from.</param>
 405        /// <returns>The normalized input container.</returns>
 406        public static string? NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile? profil
 407        {
 408            // If the source is Live TV the inputContainer will be null until the mediasource is probed on first access
 405409            if (profile is null || string.IsNullOrEmpty(inputContainer) || !inputContainer.Contains(',', StringCompariso
 410            {
 94411                return inputContainer;
 412            }
 413
 311414            var formats = ContainerHelper.Split(inputContainer);
 311415            var playProfiles = playProfile is null ? profile.DirectPlayProfiles : [playProfile];
 1370416            foreach (var format in formats)
 417            {
 2602418                foreach (var directPlayProfile in playProfiles)
 419                {
 927420                    if (directPlayProfile.Type != type)
 421                    {
 422                        continue;
 423                    }
 424
 699425                    if (directPlayProfile.SupportsContainer(format))
 426                    {
 294427                        return format;
 428                    }
 429                }
 430            }
 431
 17432            return inputContainer;
 433        }
 434
 435        private (DirectPlayProfile? Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPla
 436        {
 0437            var directPlayProfile = options.Profile.DirectPlayProfiles
 0438                .FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream)
 439
 0440            TranscodeReason transcodeReasons = 0;
 0441            if (directPlayProfile is null)
 442            {
 0443                _logger.LogDebug(
 0444                    "Profile: {0}, No audio direct play profiles found for {1} with codec {2}",
 0445                    options.Profile.Name ?? "Unknown Profile",
 0446                    item.Path ?? "Unknown path",
 0447                    audioStream.Codec ?? "Unknown codec");
 448
 0449                var directStreamProfile = options.Profile.DirectPlayProfiles
 0450                    .FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectStreamSupported(x, item, audioS
 451
 0452                if (directStreamProfile is not null)
 453                {
 0454                    directPlayProfile = directStreamProfile;
 0455                    transcodeReasons |= TranscodeReason.ContainerNotSupported;
 456                }
 457                else
 458                {
 0459                    return (null, null, GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profil
 460                }
 461            }
 462
 463            // The profile describes what the device supports
 464            // If device requirements are satisfied then allow both direct stream and direct play
 465            // Note: As of 10.10 codebase, SupportsDirectPlay is always true because the MediaSourceInfo initializes thi
 466            // Need to check additionally for current transcode reasons
 0467            if (item.SupportsDirectPlay && transcodeReasons == 0)
 468            {
 0469                if (!IsBitrateLimitExceeded(item, options.GetMaxBitrate(true) ?? 0))
 470                {
 0471                    if (options.EnableDirectPlay)
 472                    {
 0473                        return (directPlayProfile, PlayMethod.DirectPlay, 0);
 474                    }
 475                }
 476                else
 477                {
 0478                    transcodeReasons |= TranscodeReason.ContainerBitrateExceedsLimit;
 479                }
 480            }
 481
 482            // While options takes the network and other factors into account. Only applies to direct stream
 0483            if (item.SupportsDirectStream)
 484            {
 0485                if (!IsBitrateLimitExceeded(item, options.GetMaxBitrate(true) ?? 0))
 486                {
 487                    // Note: as of 10.10 codebase, the options.EnableDirectStream is always false due to
 488                    // "direct-stream http streaming is currently broken"
 489                    // Don't check that option for audio as we always assume that is supported
 0490                    if (transcodeReasons == TranscodeReason.ContainerNotSupported)
 491                    {
 0492                        return (directPlayProfile, PlayMethod.DirectStream, transcodeReasons);
 493                    }
 494                }
 495                else
 496                {
 0497                    transcodeReasons |= TranscodeReason.ContainerBitrateExceedsLimit;
 498                }
 499            }
 500
 0501            return (directPlayProfile, null, transcodeReasons);
 502        }
 503
 504        private static TranscodeReason GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream? video
 505        {
 0506            var mediaType = videoStream is null ? DlnaProfileType.Audio : DlnaProfileType.Video;
 507
 0508            var containerSupported = false;
 0509            var audioSupported = false;
 0510            var videoSupported = false;
 511
 0512            foreach (var profile in directPlayProfiles)
 513            {
 514                // Check container type
 0515                if (profile.Type == mediaType && profile.SupportsContainer(item.Container))
 516                {
 0517                    containerSupported = true;
 518
 0519                    videoSupported = videoStream is null || profile.SupportsVideoCodec(videoStream.Codec);
 520
 0521                    audioSupported = audioStream is null || profile.SupportsAudioCodec(audioStream.Codec);
 522
 0523                    if (videoSupported && audioSupported)
 524                    {
 0525                        break;
 526                    }
 527                }
 528            }
 529
 0530            TranscodeReason reasons = 0;
 0531            if (!containerSupported)
 532            {
 0533                reasons |= TranscodeReason.ContainerNotSupported;
 534            }
 535
 0536            if (!videoSupported)
 537            {
 0538                reasons |= TranscodeReason.VideoCodecNotSupported;
 539            }
 540
 0541            if (!audioSupported)
 542            {
 0543                reasons |= TranscodeReason.AudioCodecNotSupported;
 544            }
 545
 0546            return reasons;
 547        }
 548
 549        private static int? GetDefaultSubtitleStreamIndex(MediaSourceInfo item, SubtitleProfile[] subtitleProfiles)
 550        {
 177551            int highestScore = -1;
 1800552            foreach (var stream in item.MediaStreams)
 553            {
 723554                if (stream.Type == MediaStreamType.Subtitle
 723555                    && stream.Score.HasValue
 723556                    && stream.Score.Value > highestScore)
 557                {
 144558                    highestScore = stream.Score.Value;
 559                }
 560            }
 561
 177562            List<MediaStream> topStreams = [];
 1800563            foreach (var stream in item.MediaStreams)
 564            {
 723565                if (stream.Type == MediaStreamType.Subtitle && stream.Score.HasValue && stream.Score.Value == highestSco
 566                {
 321567                    topStreams.Add(stream);
 568                }
 569            }
 570
 571            // If multiple streams have an equal score, try to pick the most efficient one
 177572            if (topStreams.Count > 1)
 573            {
 378574                foreach (var stream in topStreams)
 575                {
 1464576                    foreach (var profile in subtitleProfiles)
 577                    {
 549578                        if (profile.Method == SubtitleDeliveryMethod.External
 549579                            && (string.Equals(profile.Format, stream.Codec, StringComparison.OrdinalIgnoreCase)
 549580                                // FFmpeg cannot mux VobSub back into an .idx/.sub pair, so extracted VobSub streams are
 549581                                || (string.Equals(profile.Format, "mks", StringComparison.OrdinalIgnoreCase)
 549582                                    && stream.IsVobSubSubtitleStream
 549583                                    && (!stream.IsExternal || stream.Path.EndsWith(".mks", StringComparison.OrdinalIgnor
 584                        {
 0585                            return stream.Index;
 586                        }
 587                    }
 588                }
 589            }
 590
 591            // If no optimization panned out, just use the original default
 177592            return item.DefaultSubtitleStreamIndex;
 0593        }
 594
 595        private static void SetStreamInfoOptionsFromTranscodingProfile(MediaSourceInfo item, StreamInfo playlistItem, Tr
 596        {
 147597            var container = transcodingProfile.Container;
 147598            var protocol = transcodingProfile.Protocol;
 599
 147600            item.TranscodingContainer = container;
 147601            item.TranscodingSubProtocol = protocol;
 602
 147603            if (playlistItem.PlayMethod == PlayMethod.Transcode)
 604            {
 147605                playlistItem.Container = container;
 147606                playlistItem.SubProtocol = protocol;
 607            }
 608
 147609            playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
 147610            if (int.TryParse(transcodingProfile.MaxAudioChannels, CultureInfo.InvariantCulture, out int transcodingMaxAu
 611            {
 133612                playlistItem.TranscodingMaxAudioChannels = transcodingMaxAudioChannels;
 613            }
 614
 147615            playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength;
 616
 147617            playlistItem.CopyTimestamps = transcodingProfile.CopyTimestamps;
 147618            playlistItem.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
 147619            playlistItem.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
 620
 147621            playlistItem.EnableAudioVbrEncoding = transcodingProfile.EnableAudioVbrEncoding;
 622
 147623            if (transcodingProfile.MinSegments > 0)
 624            {
 102625                playlistItem.MinSegments = transcodingProfile.MinSegments;
 626            }
 627
 147628            if (transcodingProfile.SegmentLength > 0)
 629            {
 0630                playlistItem.SegmentLength = transcodingProfile.SegmentLength;
 631            }
 147632        }
 633
 634        private static void SetStreamInfoOptionsFromDirectPlayProfile(MediaOptions options, MediaSourceInfo item, Stream
 635        {
 0636            var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileTy
 0637            var protocol = MediaStreamProtocol.http;
 638
 0639            item.TranscodingContainer = container;
 0640            item.TranscodingSubProtocol = protocol;
 641
 0642            playlistItem.Container = container;
 0643            playlistItem.SubProtocol = protocol;
 644
 0645            playlistItem.VideoCodecs = [item.VideoStream.Codec];
 0646            playlistItem.AudioCodecs = ContainerHelper.Split(directPlayProfile?.AudioCodec);
 0647        }
 648
 649        private StreamInfo BuildVideoItem(MediaSourceInfo item, MediaOptions options)
 650        {
 280651            ArgumentNullException.ThrowIfNull(item);
 652
 280653            StreamInfo playlistItem = new StreamInfo
 280654            {
 280655                ItemId = options.ItemId,
 280656                MediaType = DlnaProfileType.Video,
 280657                MediaSource = item,
 280658                RunTimeTicks = item.RunTimeTicks,
 280659                Context = options.Context,
 280660                DeviceProfile = options.Profile,
 280661                SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile
 280662                AlwaysBurnInSubtitleWhenTranscoding = options.AlwaysBurnInSubtitleWhenTranscoding
 280663            };
 664
 280665            var subtitleStream = playlistItem.SubtitleStreamIndex.HasValue ? item.GetMediaStream(MediaStreamType.Subtitl
 666
 280667            var audioStream = item.GetDefaultAudioStream(options.AudioStreamIndex ?? item.DefaultAudioStreamIndex);
 280668            if (audioStream is not null)
 669            {
 279670                playlistItem.AudioStreamIndex = audioStream.Index;
 671            }
 672
 673            // Collect candidate audio streams
 280674            ICollection<MediaStream> candidateAudioStreams = audioStream is null ? [] : [audioStream];
 675            // When the index is explicitly required by client or the default is specified by user, don't do any stream 
 280676            if (!item.DefaultAudioIndexSource.HasFlag(AudioIndexSource.User) && (options.AudioStreamIndex is null or < 0
 677            {
 678                // When user has no preferences allow stream selection on all streams.
 177679                if (item.DefaultAudioIndexSource == AudioIndexSource.None && audioStream is not null)
 680                {
 176681                    candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio).ToAr
 176682                    if (audioStream.IsDefault)
 683                    {
 684                        // If default is picked, only allow selection within default streams.
 176685                        candidateAudioStreams = candidateAudioStreams.Where(stream => stream.IsDefault).ToArray();
 686                    }
 687                }
 688
 177689                if (item.DefaultAudioIndexSource.HasFlag(AudioIndexSource.Language))
 690                {
 691                    // If user has language preference, only allow stream selection within the same language.
 0692                    candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && st
 0693                    if (item.DefaultAudioIndexSource.HasFlag(AudioIndexSource.Default))
 694                    {
 0695                        var defaultStreamsInPreferredLanguage = candidateAudioStreams.Where(stream => stream.IsDefault).
 696
 697                        // If the user also prefers default streams, try limit selection within default tracks in the sa
 698                        // If there is no default stream in the preferred language, allow selection on all default strea
 0699                        candidateAudioStreams = defaultStreamsInPreferredLanguage.Length > 0
 0700                            ? defaultStreamsInPreferredLanguage
 0701                            : item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault
 702                    }
 703                }
 177704                else if (item.DefaultAudioIndexSource.HasFlag(AudioIndexSource.Default))
 705                {
 706                    // If user prefers default streams, only allow stream selection on default streams.
 0707                    candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && st
 708                }
 709            }
 710
 280711            var videoStream = item.VideoStream;
 712
 280713            var bitrateLimitExceeded = IsBitrateLimitExceeded(item, options.GetMaxBitrate(false) ?? 0);
 280714            var isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || !bitrateLimitExceeded)
 280715            var isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || !bitrateLimitExc
 280716            TranscodeReason transcodeReasons = 0;
 717
 718            // Force transcode or remux for BD/DVD folders
 280719            if (item.VideoType == VideoType.Dvd || item.VideoType == VideoType.BluRay)
 720            {
 0721                isEligibleForDirectPlay = false;
 722            }
 723
 280724            if (bitrateLimitExceeded)
 725            {
 24726                transcodeReasons = TranscodeReason.ContainerBitrateExceedsLimit;
 727            }
 728
 280729            _logger.LogDebug(
 280730                "Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
 280731                options.Profile.Name ?? "Unknown Profile",
 280732                item.Path ?? "Unknown path",
 280733                isEligibleForDirectPlay,
 280734                isEligibleForDirectStream);
 735
 280736            DirectPlayProfile? directPlayProfile = null;
 280737            if (isEligibleForDirectPlay || isEligibleForDirectStream)
 738            {
 739                // See if it can be direct played
 256740                var directPlayInfo = GetVideoDirectPlayProfile(options, item, videoStream, audioStream, candidateAudioSt
 256741                var directPlay = directPlayInfo.PlayMethod;
 256742                transcodeReasons |= directPlayInfo.TranscodeReasons;
 743
 256744                if (directPlay.HasValue)
 745                {
 125746                    directPlayProfile = directPlayInfo.Profile;
 125747                    playlistItem.PlayMethod = directPlay.Value;
 125748                    playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profi
 125749                    var videoCodec = videoStream?.Codec;
 125750                    playlistItem.VideoCodecs = videoCodec is null ? [] : [videoCodec];
 751
 125752                    if (directPlay == PlayMethod.DirectPlay)
 753                    {
 125754                        playlistItem.SubProtocol = MediaStreamProtocol.http;
 755
 125756                        var audioStreamIndex = directPlayInfo.AudioStreamIndex ?? audioStream?.Index;
 125757                        if (audioStreamIndex.HasValue)
 758                        {
 125759                            playlistItem.AudioStreamIndex = audioStreamIndex;
 125760                            var audioCodec = item.GetMediaStream(MediaStreamType.Audio, audioStreamIndex.Value)?.Codec;
 125761                            playlistItem.AudioCodecs = audioCodec is null ? [] : [audioCodec];
 762                        }
 763                    }
 0764                    else if (directPlay == PlayMethod.DirectStream)
 765                    {
 0766                        playlistItem.AudioStreamIndex = audioStream?.Index;
 0767                        if (audioStream is not null)
 768                        {
 0769                            playlistItem.AudioCodecs = ContainerHelper.Split(directPlayProfile?.AudioCodec);
 770                        }
 771
 0772                        SetStreamInfoOptionsFromDirectPlayProfile(options, item, playlistItem, directPlayProfile);
 0773                        BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStream
 774                    }
 775
 125776                    if (subtitleStream is not null)
 777                    {
 116778                        var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles,
 779
 116780                        playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
 116781                        playlistItem.SubtitleFormat = subtitleProfile.Format;
 782                    }
 783                }
 784
 256785                _logger.LogDebug(
 256786                    "DirectPlay Result for Profile: {0}, Path: {1}, PlayMethod: {2}, AudioStreamIndex: {3}, SubtitleStre
 256787                    options.Profile.Name ?? "Anonymous Profile",
 256788                    item.Path ?? "Unknown path",
 256789                    directPlayInfo.PlayMethod,
 256790                    directPlayInfo.AudioStreamIndex ?? audioStream?.Index,
 256791                    playlistItem.SubtitleStreamIndex,
 256792                    directPlayInfo.TranscodeReasons);
 793            }
 794
 280795            playlistItem.TranscodeReasons = transcodeReasons;
 796
 280797            if (playlistItem.PlayMethod != PlayMethod.DirectStream && playlistItem.PlayMethod != PlayMethod.DirectPlay)
 798            {
 799                // Can't direct play, find the transcoding profile
 800                // If we do this for direct-stream we will overwrite the info
 155801                var (transcodingProfile, playMethod) = GetVideoTranscodeProfile(item, options, videoStream, audioStream,
 802
 155803                if (transcodingProfile is not null && playMethod.HasValue)
 804                {
 147805                    SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile);
 806
 147807                    BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, t
 808
 147809                    playlistItem.PlayMethod = PlayMethod.Transcode;
 810
 147811                    if (subtitleStream is not null)
 812                    {
 123813                        var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles,
 123814                        playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
 123815                        playlistItem.SubtitleFormat = subtitleProfile.Format;
 123816                        playlistItem.SubtitleCodecs = [subtitleProfile.Format];
 817                    }
 818
 147819                    if ((playlistItem.TranscodeReasons & (VideoReasons | TranscodeReason.ContainerBitrateExceedsLimit)) 
 820                    {
 55821                        ApplyTranscodingConditions(playlistItem, transcodingProfile.Conditions, null, true, true);
 822                    }
 823                }
 824            }
 825
 280826            _logger.LogDebug(
 280827                "StreamBuilder.BuildVideoItem( Profile={0}, Path={1}, AudioStreamIndex={2}, SubtitleStreamIndex={3} ) =>
 280828                options.Profile.Name ?? "Anonymous Profile",
 280829                item.Path ?? "Unknown path",
 280830                options.AudioStreamIndex,
 280831                options.SubtitleStreamIndex,
 280832                playlistItem.PlayMethod,
 280833                playlistItem.TranscodeReasons,
 280834                playlistItem.ToUrl("media:", "<token>", null));
 835
 280836            item.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileT
 280837            return playlistItem;
 838        }
 839
 840        private (TranscodingProfile? Profile, PlayMethod? PlayMethod) GetVideoTranscodeProfile(
 841            MediaSourceInfo item,
 842            MediaOptions options,
 843            MediaStream? videoStream,
 844            MediaStream? audioStream,
 845            StreamInfo playlistItem)
 846        {
 155847            var mediaSource = playlistItem.MediaSource;
 848
 155849            ArgumentNullException.ThrowIfNull(mediaSource);
 850
 155851            if (!(item.SupportsTranscoding || item.SupportsDirectStream))
 852            {
 0853                return (null, null);
 854            }
 855
 155856            var transcodingProfiles = options.Profile.TranscodingProfiles
 155857                .Where(i => i.Type == playlistItem.MediaType && i.Context == options.Context);
 858
 155859            if (item.UseMostCompatibleTranscodingProfile)
 860            {
 0861                transcodingProfiles = transcodingProfiles.Where(i => string.Equals(i.Container, "ts", StringComparison.O
 862            }
 863
 155864            var videoCodec = videoStream?.Codec;
 155865            var audioCodec = audioStream?.Codec;
 866
 155867            var analyzedProfiles = transcodingProfiles
 155868                .Select(transcodingProfile =>
 155869                {
 155870                    var rank = (Video: 3, Audio: 3);
 155871
 155872                    var container = transcodingProfile.Container;
 155873
 155874                    if (videoStream is not null
 155875                        && options.AllowVideoStreamCopy
 155876                        && ContainerHelper.ContainsContainer(transcodingProfile.VideoCodec, videoCodec))
 155877                    {
 155878                        var failures = GetCompatibilityVideoCodec(options, mediaSource, container, videoStream);
 155879                        rank.Video = failures == 0 ? 1 : 2;
 155880                    }
 155881
 155882                    if (audioStream is not null
 155883                        && options.AllowAudioStreamCopy)
 155884                    {
 155885                        // For Audio stream, we prefer the audio codec that can be directly copied, then the codec that 
 155886                        // the transcoding conditions, then the one does not satisfy the transcoding conditions.
 155887                        // For example: A client can support both aac and flac, but flac only supports 2 channels while 
 155888                        // When the source audio is 6 channel flac, we should transcode to 6 channel aac, instead of dow
 155889                        var transcodingAudioCodecs = ContainerHelper.Split(transcodingProfile.AudioCodec);
 155890
 155891                        foreach (var transcodingAudioCodec in transcodingAudioCodecs)
 155892                        {
 155893                            var failures = GetCompatibilityAudioCodec(options, mediaSource, container, audioStream, tran
 155894
 155895                            var rankAudio = 3;
 155896
 155897                            if (failures == 0)
 155898                            {
 155899                                rankAudio = string.Equals(transcodingAudioCodec, audioCodec, StringComparison.OrdinalIgn
 155900                            }
 155901
 155902                            rank.Audio = Math.Min(rank.Audio, rankAudio);
 155903
 155904                            if (rank.Audio == 1)
 155905                            {
 155906                                break;
 155907                            }
 155908                        }
 155909                    }
 155910
 155911                    PlayMethod playMethod = PlayMethod.Transcode;
 155912
 155913                    if (rank.Video == 1)
 155914                    {
 155915                        playMethod = PlayMethod.DirectStream;
 155916                    }
 155917
 155918                    return (Profile: transcodingProfile, PlayMethod: playMethod, Rank: rank);
 155919                })
 155920                .OrderBy(analysis => analysis.Rank);
 921
 155922            var profileMatch = analyzedProfiles.FirstOrDefault();
 923
 155924            return (profileMatch.Profile, profileMatch.PlayMethod);
 925        }
 926
 927        private void BuildStreamVideoItem(
 928            StreamInfo playlistItem,
 929            MediaOptions options,
 930            MediaSourceInfo item,
 931            MediaStream? videoStream,
 932            MediaStream? audioStream,
 933            IEnumerable<MediaStream> candidateAudioStreams,
 934            string? container,
 935            string? videoCodec,
 936            string? audioCodec)
 937        {
 938            // Prefer matching video codecs
 147939            var videoCodecs = ContainerHelper.Split(videoCodec).ToList();
 940
 147941            if (videoCodecs.Count == 0 && videoStream is not null)
 942            {
 943                // Add the original codec if no codec is specified
 0944                videoCodecs.Add(videoStream.Codec);
 945            }
 946
 947            // Enforce HLS video codec restrictions
 147948            if (playlistItem.SubProtocol == MediaStreamProtocol.hls)
 949            {
 133950                videoCodecs = videoCodecs.Where(codec => _supportedHlsVideoCodecs.Contains(codec)).ToList();
 951            }
 952
 147953            playlistItem.VideoCodecs = videoCodecs;
 147954            if (videoStream is not null && !ContainerHelper.ContainsContainer(videoCodecs, false, videoStream.Codec))
 955            {
 31956                playlistItem.TranscodeReasons |= TranscodeReason.VideoCodecNotSupported;
 957            }
 958
 959            // Copy video codec options as a starting point, this applies to transcode and direct-stream
 147960            playlistItem.MaxFramerate = videoStream?.ReferenceFrameRate;
 147961            var qualifier = videoStream?.Codec;
 147962            if (videoStream?.Level is not null)
 963            {
 146964                playlistItem.SetOption(qualifier, "level", videoStream.Level.Value.ToString(CultureInfo.InvariantCulture
 965            }
 966
 147967            if (videoStream?.BitDepth is not null)
 968            {
 146969                playlistItem.SetOption(qualifier, "videobitdepth", videoStream.BitDepth.Value.ToString(CultureInfo.Invar
 970            }
 971
 147972            if (!string.IsNullOrEmpty(videoStream?.Profile))
 973            {
 146974                playlistItem.SetOption(qualifier, "profile", videoStream.Profile.ToLowerInvariant());
 975            }
 976
 977            // Prefer matching audio codecs, could do better here
 147978            var audioCodecs = ContainerHelper.Split(audioCodec).ToList();
 979
 147980            if (audioCodecs.Count == 0 && audioStream is not null)
 981            {
 982                // Add the original codec if no codec is specified
 0983                audioCodecs.Add(audioStream.Codec);
 984            }
 985
 986            // Enforce HLS audio codec restrictions
 147987            if (playlistItem.SubProtocol == MediaStreamProtocol.hls)
 988            {
 133989                if (string.Equals(playlistItem.Container, "mp4", StringComparison.OrdinalIgnoreCase))
 990                {
 72991                    audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsMp4.Contains(codec)).ToList();
 992                }
 993                else
 994                {
 61995                    audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsTs.Contains(codec)).ToList();
 996                }
 997            }
 998
 147999            var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerHelper.ContainsContainer(
 1000
 1471001            var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && audioStreamWithSupportedCodec.Channe
 1002
 1471003            var directAudioFailures = audioStreamWithSupportedCodec is null ? default : GetCompatibilityAudioCodec(optio
 1004
 1471005            playlistItem.TranscodeReasons |= directAudioFailures;
 1471006            if (audioStream is not null && audioStreamWithSupportedCodec is null)
 1007            {
 651008                playlistItem.TranscodeReasons |= TranscodeReason.AudioCodecNotSupported;
 1009            }
 1010
 1471011            var directAudioStreamSatisfied = audioStreamWithSupportedCodec is not null && !channelsExceedsLimit
 1471012                && directAudioFailures == 0;
 1013
 1471014            directAudioStreamSatisfied = directAudioStreamSatisfied && !playlistItem.TranscodeReasons.HasFlag(TranscodeR
 1015
 1471016            var directAudioStream = directAudioStreamSatisfied ? audioStreamWithSupportedCodec : null;
 1017
 1471018            if (channelsExceedsLimit && playlistItem.TargetAudioStream is not null)
 1019            {
 101020                playlistItem.TranscodeReasons |= TranscodeReason.AudioChannelsNotSupported;
 101021                playlistItem.TargetAudioStream.Channels = playlistItem.TranscodingMaxAudioChannels;
 1022            }
 1023
 1471024            playlistItem.AudioCodecs = audioCodecs;
 1471025            if (directAudioStream is not null)
 1026            {
 561027                audioStream = directAudioStream;
 561028                playlistItem.AudioStreamIndex = audioStream.Index;
 561029                audioCodecs = [audioStream.Codec];
 561030                playlistItem.AudioCodecs = audioCodecs;
 1031
 1032                // Copy matching audio codec options
 561033                playlistItem.AudioSampleRate = audioStream.SampleRate;
 561034                playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels?.ToString(CultureInfo.InvariantC
 1035
 561036                if (!string.IsNullOrEmpty(audioStream.Profile))
 1037                {
 541038                    playlistItem.SetOption(audioStream.Codec, "profile", audioStream.Profile.ToLowerInvariant());
 1039                }
 1040
 561041                if (audioStream.Level.HasValue && audioStream.Level.Value != 0)
 1042                {
 01043                    playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.Value.ToString(CultureInfo.Inva
 1044                }
 1045            }
 1046
 1471047            int? width = videoStream?.Width;
 1471048            int? height = videoStream?.Height;
 1471049            int? bitDepth = videoStream?.BitDepth;
 1471050            int? videoBitrate = videoStream?.BitRate;
 1471051            double? videoLevel = videoStream?.Level;
 1471052            string? videoProfile = videoStream?.Profile;
 1471053            VideoRangeType? videoRangeType = videoStream?.VideoRangeType;
 1471054            float videoFramerate = videoStream is null ? 0 : videoStream.ReferenceFrameRate ?? 0;
 1471055            bool? isAnamorphic = videoStream?.IsAnamorphic;
 1471056            bool? isInterlaced = videoStream?.IsInterlaced;
 1471057            string? videoCodecTag = videoStream?.CodecTag;
 1471058            bool? isAvc = videoStream?.IsAVC;
 1471059            int? videoRotation = videoStream?.Rotation;
 1060
 1471061            TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp;
 1471062            int? packetLength = videoStream?.PacketLength;
 1471063            int? refFrames = videoStream?.RefFrames;
 1064
 1471065            int numStreams = item.MediaStreams.Count;
 1471066            int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
 1471067            int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
 1068
 1471069            var useSubContainer = playlistItem.SubProtocol == MediaStreamProtocol.hls;
 1070
 1471071            var appliedVideoConditions = options.Profile.CodecProfiles
 1471072                .Where(i => i.Type == CodecType.Video &&
 1471073                    i.ContainsAnyCodec(playlistItem.VideoCodecs, container, useSubContainer) &&
 1471074                    i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition,
 1471075                // Reverse codec profiles for backward compatibility - first codec profile has higher priority
 1471076                .Reverse();
 8901077            foreach (var condition in appliedVideoConditions)
 1078            {
 24161079                foreach (var transcodingVideoCodec in playlistItem.VideoCodecs)
 1080                {
 9101081                    if (condition.ContainsAnyCodec(transcodingVideoCodec, container, useSubContainer))
 1082                    {
 3061083                        ApplyTranscodingConditions(playlistItem, condition.Conditions, transcodingVideoCodec, true, true
 1084                        continue;
 1085                    }
 1086                }
 1087            }
 1088
 1089            // Honor requested max channels
 1471090            playlistItem.GlobalMaxAudioChannels = channelsExceedsLimit ? playlistItem.TranscodingMaxAudioChannels : opti
 1091
 1471092            int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(true) ?? 0, playlistItem.TargetAudioCodec, audioStr
 1471093            playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate);
 1094
 1471095            bool? isSecondaryAudio = audioStream is null ? null : item.IsSecondaryAudio(audioStream);
 1471096            int? inputAudioBitrate = audioStream?.BitRate;
 1471097            int? audioChannels = audioStream?.Channels;
 1471098            string? audioProfile = audioStream?.Profile;
 1471099            int? inputAudioSampleRate = audioStream?.SampleRate;
 1471100            int? inputAudioBitDepth = audioStream?.BitDepth;
 1101
 1471102            var appliedAudioConditions = options.Profile.CodecProfiles
 1471103                .Where(i => i.Type == CodecType.VideoAudio &&
 1471104                    i.ContainsAnyCodec(playlistItem.AudioCodecs, container) &&
 1471105                    i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondi
 1471106                // Reverse codec profiles for backward compatibility - first codec profile has higher priority
 1471107                .Reverse();
 1108
 5981109            foreach (var codecProfile in appliedAudioConditions)
 1110            {
 4561111                foreach (var transcodingAudioCodec in playlistItem.AudioCodecs)
 1112                {
 1521113                    if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container))
 1114                    {
 1521115                        ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, t
 1521116                        break;
 1117                    }
 1118                }
 1119            }
 1120
 1471121            var maxBitrateSetting = options.GetMaxBitrate(false);
 1122            // Honor max rate
 1471123            if (maxBitrateSetting.HasValue)
 1124            {
 1471125                var availableBitrateForVideo = maxBitrateSetting.Value;
 1126
 1471127                if (playlistItem.AudioBitrate.HasValue)
 1128                {
 1471129                    availableBitrateForVideo -= playlistItem.AudioBitrate.Value;
 1130                }
 1131
 1132                // Make sure the video bitrate is lower than bitrate settings but at least 64k
 1133                // Don't use Math.Clamp as availableBitrateForVideo can be lower then 64k.
 1471134                var currentValue = playlistItem.VideoBitrate ?? availableBitrateForVideo;
 1471135                playlistItem.VideoBitrate = Math.Max(Math.Min(availableBitrateForVideo, currentValue), 64_000);
 1136            }
 1137
 1471138            _logger.LogDebug(
 1471139                "Transcode Result for Profile: {Profile}, Path: {Path}, PlayMethod: {PlayMethod}, AudioStreamIndex: {Aud
 1471140                options.Profile.Name ?? "Anonymous Profile",
 1471141                item.Path ?? "Unknown path",
 1471142                playlistItem.PlayMethod,
 1471143                audioStream?.Index,
 1471144                playlistItem.SubtitleStreamIndex,
 1471145                playlistItem.TranscodeReasons);
 1471146        }
 1147
 1148        private static int GetDefaultAudioBitrate(string? audioCodec, int? audioChannels)
 1149        {
 601150            if (!string.IsNullOrEmpty(audioCodec))
 1151            {
 1152                // Default to a higher bitrate for stream copy
 601153                if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
 601154                    || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
 601155                    || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
 601156                    || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
 1157                {
 551158                    if ((audioChannels ?? 0) < 2)
 1159                    {
 01160                        return 128000;
 1161                    }
 1162
 551163                    return (audioChannels ?? 0) >= 6 ? 640000 : 384000;
 1164                }
 1165
 51166                if (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase)
 51167                    || string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase))
 1168                {
 01169                    if ((audioChannels ?? 0) < 2)
 1170                    {
 01171                        return 768000;
 1172                    }
 1173
 01174                    return (audioChannels ?? 0) >= 6 ? 3584000 : 1536000;
 1175                }
 1176            }
 1177
 51178            return 192000;
 1179        }
 1180
 1181        private static int GetAudioBitrate(long maxTotalBitrate, IReadOnlyList<string> targetAudioCodecs, MediaStream? a
 1182        {
 1471183            string? targetAudioCodec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0];
 1184
 1471185            int? targetAudioChannels = item.GetTargetAudioChannels(targetAudioCodec);
 1186
 1187            int defaultBitrate;
 1471188            int encoderAudioBitrateLimit = int.MaxValue;
 1189
 1471190            if (audioStream is null)
 1191            {
 11192                defaultBitrate = 192000;
 1193            }
 1194            else
 1195            {
 1461196                if (targetAudioChannels.HasValue
 1461197                    && audioStream.Channels.HasValue
 1461198                    && audioStream.Channels.Value > targetAudioChannels.Value)
 1199                {
 1200                    // Reduce the bitrate if we're down mixing.
 461201                    defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, targetAudioChannels);
 1202                }
 1001203                else if (targetAudioChannels.HasValue
 1001204                         && audioStream.Channels.HasValue
 1001205                         && audioStream.Channels.Value <= targetAudioChannels.Value
 1001206                         && !string.IsNullOrEmpty(audioStream.Codec)
 1001207                         && targetAudioCodecs is not null
 1001208                         && targetAudioCodecs.Count > 0
 1001209                         && !targetAudioCodecs.Any(elem => string.Equals(audioStream.Codec, elem, StringComparison.Ordin
 1210                {
 1211                    // Shift the bitrate if we're transcoding to a different audio codec.
 141212                    defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, audioStream.Channels.Value);
 1213                }
 1214                else
 1215                {
 861216                    defaultBitrate = audioStream.BitRate ?? GetDefaultAudioBitrate(targetAudioCodec, targetAudioChannels
 1217                }
 1218
 1219                // Seeing webm encoding failures when source has 1 audio channel and 22k bitrate.
 1220                // Any attempts to transcode over 64k will fail
 1461221                if (audioStream.Channels == 1
 1461222                    && (audioStream.BitRate ?? 0) < 64000)
 1223                {
 01224                    encoderAudioBitrateLimit = 64000;
 1225                }
 1226            }
 1227
 1471228            if (maxTotalBitrate > 0)
 1229            {
 1471230                defaultBitrate = Math.Min(GetMaxAudioBitrateForTotalBitrate(maxTotalBitrate), defaultBitrate);
 1231            }
 1232
 1471233            return Math.Min(defaultBitrate, encoderAudioBitrateLimit);
 1234        }
 1235
 1236        private static int GetMaxAudioBitrateForTotalBitrate(long totalBitrate)
 1237        {
 1471238            if (totalBitrate <= 640000)
 1239            {
 81240                return 128000;
 1241            }
 1242
 1391243            if (totalBitrate <= 2000000)
 1244            {
 01245                return 384000;
 1246            }
 1247
 1391248            if (totalBitrate <= 3000000)
 1249            {
 01250                return 448000;
 1251            }
 1252
 1391253            if (totalBitrate <= 4000000)
 1254            {
 01255                return 640000;
 1256            }
 1257
 1391258            if (totalBitrate <= 5000000)
 1259            {
 01260                return 768000;
 1261            }
 1262
 1391263            if (totalBitrate <= 10000000)
 1264            {
 81265                return 1536000;
 1266            }
 1267
 1311268            if (totalBitrate <= 15000000)
 1269            {
 01270                return 2304000;
 1271            }
 1272
 1311273            if (totalBitrate <= 20000000)
 1274            {
 141275                return 3584000;
 1276            }
 1277
 1171278            return 7168000;
 1279        }
 1280
 1281        private (DirectPlayProfile? Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeRea
 1282            MediaOptions options,
 1283            MediaSourceInfo mediaSource,
 1284            MediaStream? videoStream,
 1285            MediaStream? audioStream,
 1286            ICollection<MediaStream> candidateAudioStreams,
 1287            MediaStream? subtitleStream,
 1288            bool isEligibleForDirectPlay,
 1289            bool isEligibleForDirectStream)
 1290        {
 2561291            if (options.ForceDirectPlay)
 1292            {
 01293                return (null, PlayMethod.DirectPlay, audioStream?.Index, 0);
 1294            }
 1295
 2561296            if (options.ForceDirectStream)
 1297            {
 01298                return (null, PlayMethod.DirectStream, audioStream?.Index, 0);
 1299            }
 1300
 2561301            DeviceProfile profile = options.Profile;
 2561302            string container = mediaSource.Container;
 1303
 1304            // Check container conditions
 2561305            var containerProfileReasons = GetCompatibilityContainer(options, mediaSource, container, videoStream);
 1306
 1307            // Check video conditions
 2561308            var videoCodecProfileReasons = videoStream is null ? default : GetCompatibilityVideoCodec(options, mediaSour
 1309
 1310            // Check audio candidates profile conditions
 2561311            var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => GetCompatibilityAudioCode
 1312
 2561313            TranscodeReason subtitleProfileReasons = 0;
 2561314            if (subtitleStream is not null)
 1315            {
 2231316                var subtitleProfile = GetSubtitleProfile(mediaSource, subtitleStream, options.Profile.SubtitleProfiles, 
 1317
 2231318                if (subtitleProfile.Method != SubtitleDeliveryMethod.Drop
 2231319                    && subtitleProfile.Method != SubtitleDeliveryMethod.External
 2231320                    && subtitleProfile.Method != SubtitleDeliveryMethod.Embed)
 1321                {
 01322                    _logger.LogDebug("Not eligible for {0} due to unsupported subtitles", PlayMethod.DirectPlay);
 01323                    subtitleProfileReasons |= TranscodeReason.SubtitleCodecNotSupported;
 1324                }
 1325            }
 1326
 2561327            var containerSupported = false;
 2561328            TranscodeReason[] rankings = [TranscodeReason.VideoCodecNotSupported, VideoCodecReasons, TranscodeReason.Aud
 1329
 1330            // Check DirectPlay profiles to see if it can be direct played
 2561331            var analyzedProfiles = profile.DirectPlayProfiles
 2561332                .Where(directPlayProfile => directPlayProfile.Type == DlnaProfileType.Video)
 2561333                .Select((directPlayProfile, order) =>
 2561334                {
 2561335                    TranscodeReason directPlayProfileReasons = 0;
 2561336                    TranscodeReason audioCodecProfileReasons = 0;
 2561337
 2561338                    // Check container type
 2561339                    if (!directPlayProfile.SupportsContainer(container))
 2561340                    {
 2561341                        directPlayProfileReasons |= TranscodeReason.ContainerNotSupported;
 2561342                    }
 2561343                    else
 2561344                    {
 2561345                        containerSupported = true;
 2561346                    }
 2561347
 2561348                    // Check video codec
 2561349                    string? videoCodec = videoStream?.Codec;
 2561350                    if (!directPlayProfile.SupportsVideoCodec(videoCodec))
 2561351                    {
 2561352                        directPlayProfileReasons |= TranscodeReason.VideoCodecNotSupported;
 2561353                    }
 2561354
 2561355                    // Check audio codec
 2561356                    MediaStream? selectedAudioStream = null;
 2561357                    if (candidateAudioStreams.Count != 0)
 2561358                    {
 2561359                        selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.Supp
 2561360                        if (selectedAudioStream is null)
 2561361                        {
 2561362                            directPlayProfileReasons |= TranscodeReason.AudioCodecNotSupported;
 2561363                        }
 2561364                        else
 2561365                        {
 2561366                            audioCodecProfileReasons = audioStreamMatches.GetValueOrDefault(selectedAudioStream);
 2561367                        }
 2561368                    }
 2561369
 2561370                    var failureReasons = directPlayProfileReasons | containerProfileReasons | subtitleProfileReasons;
 2561371
 2561372                    if ((failureReasons & TranscodeReason.VideoCodecNotSupported) == 0)
 2561373                    {
 2561374                        failureReasons |= videoCodecProfileReasons;
 2561375                    }
 2561376
 2561377                    if ((failureReasons & TranscodeReason.AudioCodecNotSupported) == 0)
 2561378                    {
 2561379                        failureReasons |= audioCodecProfileReasons;
 2561380                    }
 2561381
 2561382                    var directStreamFailureReasons = failureReasons & (~DirectStreamReasons);
 2561383
 2561384                    PlayMethod? playMethod = null;
 2561385                    if (failureReasons == 0 && isEligibleForDirectPlay && mediaSource.SupportsDirectPlay)
 2561386                    {
 2561387                        playMethod = PlayMethod.DirectPlay;
 2561388                    }
 2561389                    else if (directStreamFailureReasons == 0 && isEligibleForDirectStream && mediaSource.SupportsDirectS
 2561390                    {
 2561391                        playMethod = PlayMethod.DirectStream;
 2561392                    }
 2561393
 2561394                    var ranked = GetRank(ref failureReasons, rankings);
 2561395
 2561396                    return (Result: (Profile: directPlayProfile, PlayMethod: playMethod, AudioStreamIndex: selectedAudio
 2561397                })
 2561398                .OrderByDescending(analysis => analysis.Result.PlayMethod)
 2561399                .ThenByDescending(analysis => analysis.Rank)
 2561400                .ThenBy(analysis => analysis.Order)
 2561401                .ToArray()
 2561402                .ToLookup(analysis => analysis.Result.PlayMethod is not null);
 1403
 2561404            var profileMatch = analyzedProfiles[true]
 2561405                .Select(analysis => analysis.Result)
 2561406                .FirstOrDefault();
 2561407            if (profileMatch.Profile is not null)
 1408            {
 1251409                return profileMatch;
 1410            }
 1411
 1311412            var failureReasons = analyzedProfiles[false]
 1311413                .Select(analysis => analysis.Result)
 1311414                .Where(result => !containerSupported || !result.TranscodeReason.HasFlag(TranscodeReason.ContainerNotSupp
 1311415                .FirstOrDefault().TranscodeReason;
 1311416            if (failureReasons == 0)
 1417            {
 141418                failureReasons = TranscodeReason.DirectPlayError;
 1419            }
 1420
 1311421            return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons);
 1422        }
 1423
 1424        private TranscodeReason AggregateFailureConditions(MediaSourceInfo mediaSource, DeviceProfile profile, string ty
 1425        {
 16691426            return conditions.Aggregate<ProfileCondition, TranscodeReason>(0, (reasons, i) =>
 16691427            {
 16691428                LogConditionFailure(profile, type, i, mediaSource);
 16691429                var transcodeReasons = GetTranscodeReasonForFailedCondition(i);
 16691430                return reasons | transcodeReasons;
 16691431            });
 1432        }
 1433
 1434        private void LogConditionFailure(DeviceProfile profile, string type, ProfileCondition condition, MediaSourceInfo
 1435        {
 2481436            _logger.LogDebug(
 2481437                "Profile: {0}, DirectPlay=false. Reason={1}.{2} Condition: {3}. ConditionValue: {4}. IsRequired: {5}. Pa
 2481438                type,
 2481439                profile.Name ?? "Unknown Profile",
 2481440                condition.Property,
 2481441                condition.Condition,
 2481442                condition.Value ?? string.Empty,
 2481443                condition.IsRequired,
 2481444                mediaSource.Path ?? "Unknown path");
 2481445        }
 1446
 1447        /// <summary>
 1448        /// Normalizes input container.
 1449        /// </summary>
 1450        /// <param name="mediaSource">The <see cref="MediaSourceInfo"/>.</param>
 1451        /// <param name="subtitleStream">The <see cref="MediaStream"/> of the subtitle stream.</param>
 1452        /// <param name="subtitleProfiles">The list of supported <see cref="SubtitleProfile"/>s.</param>
 1453        /// <param name="playMethod">The <see cref="PlayMethod"/>.</param>
 1454        /// <param name="transcoderSupport">The <see cref="ITranscoderSupport"/>.</param>
 1455        /// <param name="outputContainer">The output container.</param>
 1456        /// <param name="transcodingSubProtocol">The subtitle transcoding protocol.</param>
 1457        /// <returns>The normalized input container.</returns>
 1458        public static SubtitleProfile GetSubtitleProfile(
 1459            MediaSourceInfo mediaSource,
 1460            MediaStream subtitleStream,
 1461            SubtitleProfile[] subtitleProfiles,
 1462            PlayMethod playMethod,
 1463            ITranscoderSupport transcoderSupport,
 1464            string? outputContainer,
 1465            MediaStreamProtocol? transcodingSubProtocol)
 1466        {
 4821467            if (CanConsiderEmbedSubtitle(subtitleStream, playMethod, transcodingSubProtocol, outputContainer))
 1468            {
 1469                // Look for supported embedded subs of the same format
 541470                foreach (var profile in subtitleProfiles)
 1471                {
 151472                    if (!profile.SupportsLanguage(subtitleStream.Language))
 1473                    {
 1474                        continue;
 1475                    }
 1476
 151477                    if (profile.Method != SubtitleDeliveryMethod.Embed)
 1478                    {
 1479                        continue;
 1480                    }
 1481
 61482                    if (!ContainerHelper.ContainsContainer(profile.Container, outputContainer))
 1483                    {
 1484                        continue;
 1485                    }
 1486
 61487                    if (playMethod == PlayMethod.Transcode && !IsSubtitleEmbedSupported(outputContainer))
 1488                    {
 1489                        continue;
 1490                    }
 1491
 61492                    if (subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format) && string.Equals
 1493                    {
 61494                        return profile;
 1495                    }
 1496                }
 1497
 1498                // Look for supported embedded subs of a convertible format
 361499                foreach (var profile in subtitleProfiles)
 1500                {
 91501                    if (!profile.SupportsLanguage(subtitleStream.Language))
 1502                    {
 1503                        continue;
 1504                    }
 1505
 91506                    if (profile.Method != SubtitleDeliveryMethod.Embed)
 1507                    {
 1508                        continue;
 1509                    }
 1510
 01511                    if (!ContainerHelper.ContainsContainer(profile.Container, outputContainer))
 1512                    {
 1513                        continue;
 1514                    }
 1515
 01516                    if (playMethod == PlayMethod.Transcode && !IsSubtitleEmbedSupported(outputContainer))
 1517                    {
 1518                        continue;
 1519                    }
 1520
 01521                    if (subtitleStream.IsTextSubtitleStream && subtitleStream.SupportsSubtitleConversionTo(profile.Forma
 1522                    {
 01523                        return profile;
 1524                    }
 1525                }
 1526            }
 1527
 1528            // Look for an external or hls profile that matches the stream type (text/graphical) and doesn't require con
 4761529            return GetExternalSubtitleProfile(mediaSource, subtitleStream, subtitleProfiles, playMethod, transcoderSuppo
 4761530                GetExternalSubtitleProfile(mediaSource, subtitleStream, subtitleProfiles, playMethod, transcoderSupport,
 4761531                new SubtitleProfile
 4761532                {
 4761533                    Method = SubtitleDeliveryMethod.Encode,
 4761534                    Format = subtitleStream.Codec
 4761535                };
 1536        }
 1537
 1538        private static bool IsSubtitleEmbedSupported(string? transcodingContainer)
 1539        {
 261540            if (!string.IsNullOrEmpty(transcodingContainer))
 1541            {
 241542                if (ContainerHelper.ContainsContainer(transcodingContainer, "ts,mpegts,mp4"))
 1543                {
 71544                    return false;
 1545                }
 1546
 171547                if (ContainerHelper.ContainsContainer(transcodingContainer, "mkv,matroska"))
 1548                {
 101549                    return true;
 1550                }
 1551            }
 1552
 91553            return false;
 1554        }
 1555
 1556        private static bool CanConsiderEmbedSubtitle(MediaStream subtitleStream, PlayMethod playMethod, MediaStreamProto
 1557        {
 4821558            if (subtitleStream.IsExternal)
 1559            {
 4711560                return playMethod == PlayMethod.Transcode
 4711561                    && transcodingSubProtocol != MediaStreamProtocol.hls
 4711562                    && IsSubtitleEmbedSupported(outputContainer);
 1563            }
 1564
 111565            return playMethod != PlayMethod.Transcode
 111566                || transcodingSubProtocol != MediaStreamProtocol.hls;
 1567        }
 1568
 1569        private static SubtitleProfile? GetExternalSubtitleProfile(MediaSourceInfo mediaSource, MediaStream subtitleStre
 1570        {
 51341571            foreach (var profile in subtitleProfiles)
 1572            {
 20371573                if (profile.Method != SubtitleDeliveryMethod.External && profile.Method != SubtitleDeliveryMethod.Hls)
 1574                {
 1575                    continue;
 1576                }
 1577
 14001578                if (profile.Method == SubtitleDeliveryMethod.Hls && playMethod != PlayMethod.Transcode)
 1579                {
 1580                    continue;
 1581                }
 1582
 14001583                if (!profile.SupportsLanguage(subtitleStream.Language))
 1584                {
 1585                    continue;
 1586                }
 1587
 14001588                if (!subtitleStream.IsExternal && playMethod == PlayMethod.Transcode && !transcoderSupport.CanExtractSub
 1589                {
 1590                    continue;
 1591                }
 1592
 1593                // FFmpeg cannot mux VobSub back into an .idx/.sub pair, so extracted VobSub streams are matched against
 13941594                bool isVobSubMksProfile = string.Equals(profile.Format, "mks", StringComparison.OrdinalIgnoreCase)
 13941595                    && subtitleStream.IsVobSubSubtitleStream
 13941596                    && (!subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnor
 1597
 13941598                if ((profile.Method == SubtitleDeliveryMethod.External
 13941599                        && (isVobSubMksProfile || subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profil
 13941600                    (profile.Method == SubtitleDeliveryMethod.Hls && subtitleStream.IsTextSubtitleStream))
 1601                {
 13741602                    bool requiresConversion = !isVobSubMksProfile
 13741603                        && !string.Equals(subtitleStream.Codec, profile.Format, StringComparison.OrdinalIgnoreCase);
 1604
 13741605                    if (!requiresConversion)
 1606                    {
 1861607                        return profile;
 1608                    }
 1609
 11881610                    if (!allowConversion)
 1611                    {
 1612                        continue;
 1613                    }
 1614
 1615                    // TODO: Build this into subtitleStream.SupportsExternalStream
 2861616                    if (mediaSource.IsInfiniteStream)
 1617                    {
 1618                        continue;
 1619                    }
 1620
 2861621                    if (subtitleStream.IsTextSubtitleStream && subtitleStream.SupportsExternalStream && subtitleStream.S
 1622                    {
 2861623                        return profile;
 1624                    }
 1625                }
 1626            }
 1627
 2941628            return null;
 1629        }
 1630
 1631        private bool IsBitrateLimitExceeded(MediaSourceInfo item, long maxBitrate)
 1632        {
 1633            // Don't restrict bitrate if item is remote.
 2801634            if (item.IsRemote)
 1635            {
 01636                return false;
 1637            }
 1638
 1639            // If no maximum bitrate is set, default to no maximum bitrate.
 2801640            long requestedMaxBitrate = maxBitrate > 0 ? maxBitrate : int.MaxValue;
 1641
 1642            // If we don't know the item bitrate, then force a transcode if requested max bitrate is under 40 mbps
 2801643            int itemBitrate = item.Bitrate ?? 40000000;
 1644
 2801645            if (itemBitrate > requestedMaxBitrate)
 1646            {
 241647                _logger.LogDebug(
 241648                    "Bitrate exceeds limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}",
 241649                    itemBitrate,
 241650                    requestedMaxBitrate);
 241651                return true;
 1652            }
 1653
 2561654            return false;
 1655        }
 1656
 1657        private static void ValidateMediaOptions(MediaOptions options, bool isMediaSource)
 1658        {
 2801659            if (options.ItemId.IsEmpty())
 1660            {
 01661                ArgumentException.ThrowIfNullOrEmpty(options.DeviceId);
 1662            }
 1663
 2801664            if (options.Profile is null)
 1665            {
 01666                throw new ArgumentException("Profile is required");
 1667            }
 1668
 2801669            if (options.MediaSources is null)
 1670            {
 01671                throw new ArgumentException("MediaSources is required");
 1672            }
 1673
 2801674            if (isMediaSource)
 1675            {
 2801676                if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
 1677                {
 01678                    throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested");
 1679                }
 1680
 2801681                if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
 1682                {
 01683                    throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested"
 1684                }
 1685            }
 2801686        }
 1687
 1688        private static IEnumerable<ProfileCondition> GetProfileConditionsForVideoAudio(
 1689            IEnumerable<CodecProfile> codecProfiles,
 1690            string container,
 1691            string codec,
 1692            int? audioChannels,
 1693            int? audioBitrate,
 1694            int? audioSampleRate,
 1695            int? audioBitDepth,
 1696            string audioProfile,
 1697            bool? isSecondaryAudio)
 1698        {
 10091699            return codecProfiles
 10091700                .Where(profile => profile.Type == CodecType.VideoAudio &&
 10091701                    profile.ContainsAnyCodec(codec, container) &&
 10091702                    profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(appl
 10091703                .SelectMany(profile => profile.Conditions)
 10091704                .Where(condition => !ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBi
 1705        }
 1706
 1707        private static IEnumerable<ProfileCondition> GetProfileConditionsForAudio(
 1708            IEnumerable<CodecProfile> codecProfiles,
 1709            string container,
 1710            string? codec,
 1711            int? audioChannels,
 1712            int? audioBitrate,
 1713            int? audioSampleRate,
 1714            int? audioBitDepth,
 1715            bool checkConditions)
 1716        {
 01717            var conditions = codecProfiles
 01718                .Where(profile => profile.Type == CodecType.Audio &&
 01719                    profile.ContainsAnyCodec(codec, container) &&
 01720                    profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsAudioConditionSatisfied(applyCond
 01721                .SelectMany(profile => profile.Conditions);
 1722
 01723            if (!checkConditions)
 1724            {
 01725                return conditions;
 1726            }
 1727
 01728            return conditions.Where(condition => !ConditionProcessor.IsAudioConditionSatisfied(condition, audioChannels,
 1729        }
 1730
 1731        private void ApplyTranscodingConditions(StreamInfo item, IEnumerable<ProfileCondition> conditions, string? quali
 1732        {
 35141733            foreach (ProfileCondition condition in conditions)
 1734            {
 12441735                string value = condition.Value;
 1736
 12441737                if (string.IsNullOrEmpty(value))
 1738                {
 1739                    continue;
 1740                }
 1741
 1742                // No way to express this
 12441743                if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 1744                {
 1745                    continue;
 1746                }
 1747
 12441748                switch (condition.Property)
 1749                {
 1750                    case ProfileConditionValue.AudioBitrate:
 1751                        {
 01752                            if (!enableNonQualifiedConditions)
 1753                            {
 1754                                continue;
 1755                            }
 1756
 01757                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 1758                            {
 01759                                if (condition.Condition == ProfileConditionType.Equals)
 1760                                {
 01761                                    item.AudioBitrate = num;
 1762                                }
 01763                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 1764                                {
 01765                                    item.AudioBitrate = Math.Min(num, item.AudioBitrate ?? num);
 1766                                }
 01767                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 1768                                {
 01769                                    item.AudioBitrate = Math.Max(num, item.AudioBitrate ?? num);
 1770                                }
 1771                            }
 1772
 01773                            break;
 1774                        }
 1775
 1776                    case ProfileConditionValue.AudioSampleRate:
 1777                        {
 01778                            if (!enableNonQualifiedConditions)
 1779                            {
 1780                                continue;
 1781                            }
 1782
 01783                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 1784                            {
 01785                                if (condition.Condition == ProfileConditionType.Equals)
 1786                                {
 01787                                    item.AudioSampleRate = num;
 1788                                }
 01789                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 1790                                {
 01791                                    item.AudioSampleRate = Math.Min(num, item.AudioSampleRate ?? num);
 1792                                }
 01793                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 1794                                {
 01795                                    item.AudioSampleRate = Math.Max(num, item.AudioSampleRate ?? num);
 1796                                }
 1797                            }
 1798
 01799                            break;
 1800                        }
 1801
 1802                    case ProfileConditionValue.AudioChannels:
 1803                        {
 281804                            if (string.IsNullOrEmpty(qualifier))
 1805                            {
 01806                                if (!enableNonQualifiedConditions)
 1807                                {
 01808                                    continue;
 1809                                }
 1810                            }
 1811                            else
 1812                            {
 281813                                if (!enableQualifiedConditions)
 1814                                {
 1815                                    continue;
 1816                                }
 1817                            }
 1818
 281819                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 1820                            {
 281821                                if (condition.Condition == ProfileConditionType.Equals)
 1822                                {
 01823                                    item.SetOption(qualifier, "audiochannels", num.ToString(CultureInfo.InvariantCulture
 1824                                }
 281825                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 1826                                {
 281827                                    item.SetOption(qualifier, "audiochannels", Math.Min(num, item.GetTargetAudioChannels
 1828                                }
 01829                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 1830                                {
 01831                                    item.SetOption(qualifier, "audiochannels", Math.Max(num, item.GetTargetAudioChannels
 1832                                }
 1833                            }
 1834
 01835                            break;
 1836                        }
 1837
 1838                    case ProfileConditionValue.IsAvc:
 1839                        {
 01840                            if (!enableNonQualifiedConditions)
 1841                            {
 1842                                continue;
 1843                            }
 1844
 01845                            if (bool.TryParse(value, out var isAvc))
 1846                            {
 01847                                if (isAvc && condition.Condition == ProfileConditionType.Equals)
 1848                                {
 01849                                    item.RequireAvc = true;
 1850                                }
 01851                                else if (!isAvc && condition.Condition == ProfileConditionType.NotEquals)
 1852                                {
 01853                                    item.RequireAvc = true;
 1854                                }
 1855                            }
 1856
 01857                            break;
 1858                        }
 1859
 1860                    case ProfileConditionValue.IsAnamorphic:
 1861                        {
 2011862                            if (!enableNonQualifiedConditions)
 1863                            {
 1864                                continue;
 1865                            }
 1866
 2011867                            if (bool.TryParse(value, out var isAnamorphic))
 1868                            {
 2011869                                if (isAnamorphic && condition.Condition == ProfileConditionType.Equals)
 1870                                {
 01871                                    item.RequireNonAnamorphic = true;
 1872                                }
 2011873                                else if (!isAnamorphic && condition.Condition == ProfileConditionType.NotEquals)
 1874                                {
 01875                                    item.RequireNonAnamorphic = true;
 1876                                }
 1877                            }
 1878
 01879                            break;
 1880                        }
 1881
 1882                    case ProfileConditionValue.IsInterlaced:
 1883                        {
 1091884                            if (string.IsNullOrEmpty(qualifier))
 1885                            {
 01886                                if (!enableNonQualifiedConditions)
 1887                                {
 01888                                    continue;
 1889                                }
 1890                            }
 1891                            else
 1892                            {
 1091893                                if (!enableQualifiedConditions)
 1894                                {
 1895                                    continue;
 1896                                }
 1897                            }
 1898
 1091899                            if (bool.TryParse(value, out var isInterlaced))
 1900                            {
 1091901                                if (!isInterlaced && condition.Condition == ProfileConditionType.Equals)
 1902                                {
 01903                                    item.SetOption(qualifier, "deinterlace", "true");
 1904                                }
 1091905                                else if (isInterlaced && condition.Condition == ProfileConditionType.NotEquals)
 1906                                {
 1091907                                    item.SetOption(qualifier, "deinterlace", "true");
 1908                                }
 1909                            }
 1910
 1091911                            break;
 1912                        }
 1913
 1914                    case ProfileConditionValue.AudioProfile:
 1915                    case ProfileConditionValue.Has64BitOffsets:
 1916                    case ProfileConditionValue.PacketLength:
 1917                    case ProfileConditionValue.NumStreams:
 1918                    case ProfileConditionValue.NumAudioStreams:
 1919                    case ProfileConditionValue.NumVideoStreams:
 1920                    case ProfileConditionValue.IsSecondaryAudio:
 1921                    case ProfileConditionValue.VideoTimestamp:
 1922                        {
 1923                            // Not supported yet
 1924                            break;
 1925                        }
 1926
 1927                    case ProfileConditionValue.RefFrames:
 1928                        {
 21929                            if (string.IsNullOrEmpty(qualifier))
 1930                            {
 01931                                if (!enableNonQualifiedConditions)
 1932                                {
 01933                                    continue;
 1934                                }
 1935                            }
 1936                            else
 1937                            {
 21938                                if (!enableQualifiedConditions)
 1939                                {
 1940                                    continue;
 1941                                }
 1942                            }
 1943
 21944                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 1945                            {
 21946                                if (condition.Condition == ProfileConditionType.Equals)
 1947                                {
 01948                                    item.SetOption(qualifier, "maxrefframes", num.ToString(CultureInfo.InvariantCulture)
 1949                                }
 21950                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 1951                                {
 21952                                    item.SetOption(qualifier, "maxrefframes", Math.Min(num, item.GetTargetRefFrames(qual
 1953                                }
 01954                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 1955                                {
 01956                                    item.SetOption(qualifier, "maxrefframes", Math.Max(num, item.GetTargetRefFrames(qual
 1957                                }
 1958                            }
 1959
 01960                            break;
 1961                        }
 1962
 1963                    case ProfileConditionValue.VideoBitDepth:
 1964                        {
 01965                            if (string.IsNullOrEmpty(qualifier))
 1966                            {
 01967                                if (!enableNonQualifiedConditions)
 1968                                {
 01969                                    continue;
 1970                                }
 1971                            }
 1972                            else
 1973                            {
 01974                                if (!enableQualifiedConditions)
 1975                                {
 1976                                    continue;
 1977                                }
 1978                            }
 1979
 01980                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 1981                            {
 01982                                if (condition.Condition == ProfileConditionType.Equals)
 1983                                {
 01984                                    item.SetOption(qualifier, "videobitdepth", num.ToString(CultureInfo.InvariantCulture
 1985                                }
 01986                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 1987                                {
 01988                                    item.SetOption(qualifier, "videobitdepth", Math.Min(num, item.GetTargetVideoBitDepth
 1989                                }
 01990                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 1991                                {
 01992                                    item.SetOption(qualifier, "videobitdepth", Math.Max(num, item.GetTargetVideoBitDepth
 1993                                }
 1994                            }
 1995
 01996                            break;
 1997                        }
 1998
 1999                    case ProfileConditionValue.VideoProfile:
 2000                        {
 2242001                            if (string.IsNullOrEmpty(qualifier))
 2002                            {
 2003                                continue;
 2004                            }
 2005
 2006                            // Change from split by | to comma
 2007                            // Strip spaces to avoid having to encode
 2242008                            var values = value
 2242009                                .Split('|', StringSplitOptions.RemoveEmptyEntries);
 2010
 2242011                            if (condition.Condition == ProfileConditionType.Equals)
 2012                            {
 02013                                item.SetOption(qualifier, "profile", string.Join(',', values));
 2014                            }
 2242015                            else if (condition.Condition == ProfileConditionType.EqualsAny)
 2016                            {
 2242017                                var currentValue = item.GetOption(qualifier, "profile");
 2242018                                if (!string.IsNullOrEmpty(currentValue) && values.Any(value => value == currentValue))
 2019                                {
 712020                                    item.SetOption(qualifier, "profile", currentValue);
 2021                                }
 2022                                else
 2023                                {
 1532024                                    item.SetOption(qualifier, "profile", string.Join(',', values));
 2025                                }
 2026                            }
 2027
 1532028                            break;
 2029                        }
 2030
 2031                    case ProfileConditionValue.VideoRangeType:
 2032                        {
 2582033                            if (string.IsNullOrEmpty(qualifier))
 2034                            {
 2035                                continue;
 2036                            }
 2037
 2038                            // change from split by | to comma
 2039                            // strip spaces to avoid having to encode
 2582040                            var values = value
 2582041                                .Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
 2042
 2582043                            if (condition.Condition == ProfileConditionType.Equals)
 2044                            {
 02045                                item.SetOption(qualifier, "rangetype", string.Join(',', values));
 2046                            }
 2582047                            else if (condition.Condition == ProfileConditionType.NotEquals)
 2048                            {
 02049                                item.SetOption(qualifier, "rangetype", string.Join(',', Enum.GetNames<VideoRangeType>().
 2050                            }
 2582051                            else if (condition.Condition == ProfileConditionType.EqualsAny)
 2052                            {
 2582053                                var currentValue = item.GetOption(qualifier, "rangetype");
 2582054                                if (!string.IsNullOrEmpty(currentValue) && values.Any(v => string.Equals(v, currentValue
 2055                                {
 02056                                    item.SetOption(qualifier, "rangetype", currentValue);
 2057                                }
 2058                                else
 2059                                {
 2582060                                    item.SetOption(qualifier, "rangetype", string.Join(',', values));
 2061                                }
 2062                            }
 2063
 2582064                            break;
 2065                        }
 2066
 2067                    case ProfileConditionValue.VideoCodecTag:
 2068                        {
 122069                            if (string.IsNullOrEmpty(qualifier))
 2070                            {
 2071                                continue;
 2072                            }
 2073
 2074                            // change from split by | to comma
 2075                            // strip spaces to avoid having to encode
 122076                            var values = value
 122077                                .Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
 2078
 122079                            if (condition.Condition == ProfileConditionType.Equals)
 2080                            {
 02081                                item.SetOption(qualifier, "codectag", string.Join(',', values));
 2082                            }
 122083                            else if (condition.Condition == ProfileConditionType.EqualsAny)
 2084                            {
 122085                                var currentValue = item.GetOption(qualifier, "codectag");
 122086                                if (!string.IsNullOrEmpty(currentValue) && values.Any(v => string.Equals(v, currentValue
 2087                                {
 02088                                    item.SetOption(qualifier, "codectag", currentValue);
 2089                                }
 2090                                else
 2091                                {
 122092                                    item.SetOption(qualifier, "codectag", string.Join(',', values));
 2093                                }
 2094                            }
 2095
 122096                            break;
 2097                        }
 2098
 2099                    case ProfileConditionValue.VideoRotation:
 2100                        {
 02101                            if (string.IsNullOrEmpty(qualifier))
 2102                            {
 2103                                continue;
 2104                            }
 2105
 2106                            // change from split by | to comma
 2107                            // strip spaces to avoid having to encode
 02108                            var values = value
 02109                                .Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
 2110
 02111                            if (condition.Condition == ProfileConditionType.Equals)
 2112                            {
 02113                                item.SetOption(qualifier, "rotation", string.Join(',', values));
 2114                            }
 02115                            else if (condition.Condition == ProfileConditionType.EqualsAny)
 2116                            {
 02117                                var currentValue = item.GetOption(qualifier, "rotation");
 02118                                if (!string.IsNullOrEmpty(currentValue) && values.Any(v => string.Equals(v, currentValue
 2119                                {
 02120                                    item.SetOption(qualifier, "rotation", currentValue);
 2121                                }
 2122                                else
 2123                                {
 02124                                    item.SetOption(qualifier, "rotation", string.Join(',', values));
 2125                                }
 2126                            }
 2127
 02128                            break;
 2129                        }
 2130
 2131                    case ProfileConditionValue.Height:
 2132                        {
 02133                            if (!enableNonQualifiedConditions)
 2134                            {
 2135                                continue;
 2136                            }
 2137
 02138                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 2139                            {
 02140                                if (condition.Condition == ProfileConditionType.Equals)
 2141                                {
 02142                                    item.MaxHeight = num;
 2143                                }
 02144                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 2145                                {
 02146                                    item.MaxHeight = Math.Min(num, item.MaxHeight ?? num);
 2147                                }
 02148                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 2149                                {
 02150                                    item.MaxHeight = Math.Max(num, item.MaxHeight ?? num);
 2151                                }
 2152                            }
 2153
 02154                            break;
 2155                        }
 2156
 2157                    case ProfileConditionValue.VideoBitrate:
 2158                        {
 322159                            if (!enableNonQualifiedConditions)
 2160                            {
 2161                                continue;
 2162                            }
 2163
 322164                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 2165                            {
 322166                                if (condition.Condition == ProfileConditionType.Equals)
 2167                                {
 02168                                    item.VideoBitrate = num;
 2169                                }
 322170                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 2171                                {
 322172                                    item.VideoBitrate = Math.Min(num, item.VideoBitrate ?? num);
 2173                                }
 02174                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 2175                                {
 02176                                    item.VideoBitrate = Math.Max(num, item.VideoBitrate ?? num);
 2177                                }
 2178                            }
 2179
 02180                            break;
 2181                        }
 2182
 2183                    case ProfileConditionValue.VideoFramerate:
 2184                        {
 122185                            if (!enableNonQualifiedConditions)
 2186                            {
 2187                                continue;
 2188                            }
 2189
 122190                            if (float.TryParse(value, CultureInfo.InvariantCulture, out var num))
 2191                            {
 122192                                if (condition.Condition == ProfileConditionType.Equals)
 2193                                {
 02194                                    item.MaxFramerate = num;
 2195                                }
 122196                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 2197                                {
 122198                                    item.MaxFramerate = Math.Min(num, item.MaxFramerate ?? num);
 2199                                }
 02200                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 2201                                {
 02202                                    item.MaxFramerate = Math.Max(num, item.MaxFramerate ?? num);
 2203                                }
 2204                            }
 2205
 02206                            break;
 2207                        }
 2208
 2209                    case ProfileConditionValue.VideoLevel:
 2210                        {
 2122211                            if (string.IsNullOrEmpty(qualifier))
 2212                            {
 2213                                continue;
 2214                            }
 2215
 2122216                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 2217                            {
 2122218                                if (condition.Condition == ProfileConditionType.Equals)
 2219                                {
 02220                                    item.SetOption(qualifier, "level", num.ToString(CultureInfo.InvariantCulture));
 2221                                }
 2122222                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 2223                                {
 2122224                                    item.SetOption(qualifier, "level", Math.Min(num, item.GetTargetVideoLevel(qualifier)
 2225                                }
 02226                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 2227                                {
 02228                                    item.SetOption(qualifier, "level", Math.Max(num, item.GetTargetVideoLevel(qualifier)
 2229                                }
 2230                            }
 2231
 02232                            break;
 2233                        }
 2234
 2235                    case ProfileConditionValue.Width:
 2236                        {
 42237                            if (!enableNonQualifiedConditions)
 2238                            {
 2239                                continue;
 2240                            }
 2241
 42242                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 2243                            {
 42244                                if (condition.Condition == ProfileConditionType.Equals)
 2245                                {
 02246                                    item.MaxWidth = num;
 2247                                }
 42248                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 2249                                {
 42250                                    item.MaxWidth = Math.Min(num, item.MaxWidth ?? num);
 2251                                }
 02252                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 2253                                {
 02254                                    item.MaxWidth = Math.Max(num, item.MaxWidth ?? num);
 2255                                }
 2256                            }
 2257
 2258                            break;
 2259                        }
 2260
 2261                    default:
 2262                        break;
 2263                }
 2264            }
 5132265        }
 2266
 2267        private static bool IsAudioContainerSupported(DirectPlayProfile profile, MediaSourceInfo item)
 2268        {
 2269            // Check container type
 02270            if (!profile.SupportsContainer(item.Container))
 2271            {
 02272                return false;
 2273            }
 2274
 2275            // Never direct play audio in matroska when the device only declare support for webm.
 2276            // The first check is not enough because mkv is assumed can be webm.
 2277            // See https://github.com/jellyfin/jellyfin/issues/13344
 02278            return !ContainerHelper.ContainsContainer("mkv", item.Container)
 02279                   || profile.SupportsContainer("mkv");
 2280        }
 2281
 2282        private static bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audi
 2283        {
 02284            if (!IsAudioContainerSupported(profile, item))
 2285            {
 02286                return false;
 2287            }
 2288
 2289            // Check audio codec
 02290            string? audioCodec = audioStream?.Codec;
 02291            if (!profile.SupportsAudioCodec(audioCodec))
 2292            {
 02293                return false;
 2294            }
 2295
 02296            return true;
 2297        }
 2298
 2299        private static bool IsAudioDirectStreamSupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream au
 2300        {
 2301            // Check container type, this should NOT be supported
 2302            // If the container is supported, the file should be directly played
 02303            if (IsAudioContainerSupported(profile, item))
 2304            {
 02305                return false;
 2306            }
 2307
 2308            // Check audio codec, we cannot use the SupportsAudioCodec here
 2309            // Because that one assumes empty container supports all codec, which is just useless
 02310            string? audioCodec = audioStream?.Codec;
 02311            return string.Equals(profile.AudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase)
 02312                   || string.Equals(profile.Container, audioCodec, StringComparison.OrdinalIgnoreCase);
 2313        }
 2314
 2315        private int GetRank(ref TranscodeReason a, TranscodeReason[] rankings)
 2316        {
 12112317            var index = 1;
 87572318            foreach (var flag in rankings)
 2319            {
 36732320                var reason = a & flag;
 36732321                if (reason != 0)
 2322                {
 10112323                    return index;
 2324                }
 2325
 26622326                index++;
 2327            }
 2328
 2002329            return index;
 2330        }
 2331
 2332        /// <summary>
 2333        /// Check the profile conditions.
 2334        /// </summary>
 2335        /// <param name="conditions">Profile conditions.</param>
 2336        /// <param name="mediaSource">Media source.</param>
 2337        /// <param name="videoStream">Video stream.</param>
 2338        /// <returns>Failed profile conditions.</returns>
 2339        private IEnumerable<ProfileCondition> CheckVideoConditions(ProfileCondition[] conditions, MediaSourceInfo mediaS
 2340        {
 7312341            int? width = videoStream?.Width;
 7312342            int? height = videoStream?.Height;
 7312343            int? bitDepth = videoStream?.BitDepth;
 7312344            int? videoBitrate = videoStream?.BitRate;
 7312345            double? videoLevel = videoStream?.Level;
 7312346            string? videoProfile = videoStream?.Profile;
 7312347            VideoRangeType? videoRangeType = videoStream?.VideoRangeType;
 7312348            float videoFramerate = videoStream is null ? 0 : videoStream.ReferenceFrameRate ?? 0;
 7312349            bool? isAnamorphic = videoStream?.IsAnamorphic;
 7312350            bool? isInterlaced = videoStream?.IsInterlaced;
 7312351            string? videoCodecTag = videoStream?.CodecTag;
 7312352            bool? isAvc = videoStream?.IsAVC;
 7312353            int? videoRotation = videoStream?.Rotation;
 2354
 7312355            TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : mediaSource.Time
 7312356            int? packetLength = videoStream?.PacketLength;
 7312357            int? refFrames = videoStream?.RefFrames;
 2358
 7312359            int numStreams = mediaSource.MediaStreams.Count;
 7312360            int? numAudioStreams = mediaSource.GetStreamCount(MediaStreamType.Audio);
 7312361            int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video);
 2362
 7312363            return conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, widt
 2364        }
 2365
 2366        /// <summary>
 2367        /// Check the compatibility of the container.
 2368        /// </summary>
 2369        /// <param name="options">Media options.</param>
 2370        /// <param name="mediaSource">Media source.</param>
 2371        /// <param name="container">Container.</param>
 2372        /// <param name="videoStream">Video stream.</param>
 2373        /// <returns>Transcode reasons if the container is not fully compatible.</returns>
 2374        private TranscodeReason GetCompatibilityContainer(MediaOptions options, MediaSourceInfo mediaSource, string cont
 2375        {
 2562376            var profile = options.Profile;
 2377
 2562378            var failures = AggregateFailureConditions(
 2562379                mediaSource,
 2562380                profile,
 2562381                "VideoCodecProfile",
 2562382                profile.ContainerProfiles
 2562383                    .Where(containerProfile => containerProfile.Type == DlnaProfileType.Video && containerProfile.Contai
 2562384                    .SelectMany(containerProfile => CheckVideoConditions(containerProfile.Conditions, mediaSource, video
 2385
 2562386            return failures;
 2387        }
 2388
 2389        /// <summary>
 2390        /// Check the compatibility of the video codec.
 2391        /// </summary>
 2392        /// <param name="options">Media options.</param>
 2393        /// <param name="mediaSource">Media source.</param>
 2394        /// <param name="container">Container.</param>
 2395        /// <param name="videoStream">Video stream.</param>
 2396        /// <returns>Transcode reasons if the video stream is not fully compatible.</returns>
 2397        private TranscodeReason GetCompatibilityVideoCodec(MediaOptions options, MediaSourceInfo mediaSource, string con
 2398        {
 4042399            var profile = options.Profile;
 2400
 4042401            string videoCodec = videoStream.Codec;
 2402
 4042403            var failures = AggregateFailureConditions(
 4042404                mediaSource,
 4042405                profile,
 4042406                "VideoCodecProfile",
 4042407                profile.CodecProfiles
 4042408                    .Where(codecProfile => codecProfile.Type == CodecType.Video &&
 4042409                        codecProfile.ContainsAnyCodec(videoCodec, container) &&
 4042410                        !CheckVideoConditions(codecProfile.ApplyConditions, mediaSource, videoStream).Any())
 4042411                    .SelectMany(codecProfile => CheckVideoConditions(codecProfile.Conditions, mediaSource, videoStream))
 2412
 4042413            return failures;
 2414        }
 2415
 2416        /// <summary>
 2417        /// Check the compatibility of the audio codec.
 2418        /// </summary>
 2419        /// <param name="options">Media options.</param>
 2420        /// <param name="mediaSource">Media source.</param>
 2421        /// <param name="container">Container.</param>
 2422        /// <param name="audioStream">Audio stream.</param>
 2423        /// <param name="transcodingAudioCodec">Override audio codec.</param>
 2424        /// <param name="isVideo">The media source is video.</param>
 2425        /// <param name="isSecondaryAudio">The audio stream is secondary.</param>
 2426        /// <returns>Transcode reasons if the audio stream is not fully compatible.</returns>
 2427        private TranscodeReason GetCompatibilityAudioCodec(MediaOptions options, MediaSourceInfo mediaSource, string con
 2428        {
 10092429            var profile = options.Profile;
 2430
 10092431            var audioCodec = transcodingAudioCodec ?? audioStream.Codec;
 10092432            var audioProfile = audioStream.Profile;
 10092433            var audioChannels = audioStream.Channels;
 10092434            var audioBitrate = audioStream.BitRate;
 10092435            var audioSampleRate = audioStream.SampleRate;
 10092436            var audioBitDepth = audioStream.BitDepth;
 2437
 10092438            var audioFailureConditions = isVideo
 10092439                ? GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioCodec, audioChannels, audioBi
 10092440                : GetProfileConditionsForAudio(profile.CodecProfiles, container, audioCodec, audioChannels, audioBitrate
 2441
 10092442            var failures = AggregateFailureConditions(mediaSource, profile, "AudioCodecProfile", audioFailureConditions)
 2443
 10092444            return failures;
 2445        }
 2446
 2447        /// <summary>
 2448        /// Check the compatibility of the audio codec for direct playback.
 2449        /// </summary>
 2450        /// <param name="options">Media options.</param>
 2451        /// <param name="mediaSource">Media source.</param>
 2452        /// <param name="container">Container.</param>
 2453        /// <param name="audioStream">Audio stream.</param>
 2454        /// <param name="isVideo">The media source is video.</param>
 2455        /// <param name="isSecondaryAudio">The audio stream is secondary.</param>
 2456        /// <returns>Transcode reasons if the audio stream is not fully compatible for direct playback.</returns>
 2457        private TranscodeReason GetCompatibilityAudioCodecDirect(MediaOptions options, MediaSourceInfo mediaSource, stri
 2458        {
 2752459            var failures = GetCompatibilityAudioCodec(options, mediaSource, container, audioStream, null, isVideo, isSec
 2460
 2752461            if (audioStream.IsExternal)
 2462            {
 62463                failures |= TranscodeReason.AudioIsExternal;
 2464            }
 2465
 2752466            return failures;
 2467        }
 2468    }
 2469}

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)
CanConsiderEmbedSubtitle(MediaBrowser.Model.Entities.MediaStream,MediaBrowser.Model.Session.PlayMethod,System.Nullable`1<Jellyfin.Data.Enums.MediaStreamProtocol>,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)