< 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: 769
Uncovered lines: 326
Coverable lines: 1095
Total lines: 2483
Line coverage: 70.2%
Branch coverage
60%
Covered branches: 608
Total branches: 998
Branch coverage: 60.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/6/2026 - 12:15:23 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: 24697/23/2026 - 12:16:48 AM Line coverage: 70.1% (765/1091) Branch coverage: 60.7% (604/994) Total lines: 24798/3/2026 - 12:16:46 AM Line coverage: 70.2% (769/1095) Branch coverage: 60.9% (608/998) Total lines: 2483 5/6/2026 - 12:15:23 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: 24697/23/2026 - 12:16:48 AM Line coverage: 70.1% (765/1091) Branch coverage: 60.7% (604/994) Total lines: 24798/3/2026 - 12:16:46 AM Line coverage: 70.2% (769/1095) Branch coverage: 60.9% (608/998) Total lines: 2483

Coverage delta

Coverage delta 2 -2

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(...)93.33%313089.47%
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(...)93.75%4848100%
IsVobSubMksDeliveryProfile(...)100%44100%
IsVobSubMksProfile(...)87.5%88100%
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        {
 28242            _transcoderSupport = transcoderSupport;
 28243            _logger = logger;
 28244        }
 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        {
 282232            ValidateMediaOptions(options, true);
 233
 282234            var mediaSources = string.IsNullOrEmpty(options.MediaSourceId)
 282235                ? options.MediaSources
 282236                : options.MediaSources.Where(x => string.Equals(x.Id, options.MediaSourceId, StringComparison.OrdinalIgn
 237
 282238            List<StreamInfo> streams = [];
 1128239            foreach (var mediaSourceInfo in mediaSources)
 240            {
 282241                var streamInfo = BuildVideoItem(mediaSourceInfo, options);
 282242                if (streamInfo is not null)
 243                {
 282244                    streams.Add(streamInfo);
 245                }
 246            }
 247
 1128248            foreach (var stream in streams)
 249            {
 282250                stream.DeviceId = options.DeviceId;
 282251                stream.DeviceProfileId = options.Profile.Id?.ToString("N", CultureInfo.InvariantCulture);
 252            }
 253
 282254            return GetOptimalStream(streams, options.GetMaxBitrate(false) ?? 0);
 255        }
 256
 257        private static StreamInfo? GetOptimalStream(List<StreamInfo> streams, long maxBitrate)
 282258            => SortMediaSources(streams, maxBitrate).FirstOrDefault();
 259
 260        private static IOrderedEnumerable<StreamInfo> SortMediaSources(List<StreamInfo> streams, long maxBitrate)
 261        {
 282262            return streams.OrderBy(i =>
 282263            {
 282264                // Nothing beats direct playing a file
 282265                if (i.PlayMethod == PlayMethod.DirectPlay && i.MediaSource?.Protocol == MediaProtocol.File)
 282266                {
 282267                    return 0;
 282268                }
 282269
 282270                return 1;
 282271            }).ThenBy(i =>
 282272            {
 282273                switch (i.PlayMethod)
 282274                {
 282275                    // Let's assume direct streaming a file is just as desirable as direct playing a remote url
 282276                    case PlayMethod.DirectStream:
 282277                    case PlayMethod.DirectPlay:
 282278                        return 0;
 282279                    default:
 282280                        return 1;
 282281                }
 282282            }).ThenBy(i =>
 282283            {
 282284                switch (i.MediaSource?.Protocol)
 282285                {
 282286                    case MediaProtocol.File:
 282287                        return 0;
 282288                    default:
 282289                        return 1;
 282290                }
 282291            }).ThenBy(i =>
 282292            {
 282293                if (maxBitrate > 0)
 282294                {
 282295                    if (i.MediaSource?.Bitrate is not null)
 282296                    {
 282297                        return Math.Abs(i.MediaSource.Bitrate.Value - maxBitrate);
 282298                    }
 282299                }
 282300
 282301                return 0;
 282302            }).ThenBy(streams.IndexOf);
 303        }
 304
 305        private static TranscodeReason GetTranscodeReasonForFailedCondition(ProfileCondition condition)
 306        {
 252307            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:
 39339                    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
 418409            if (profile is null || string.IsNullOrEmpty(inputContainer) || !inputContainer.Contains(',', StringCompariso
 410            {
 105411                return inputContainer;
 412            }
 413
 313414            var formats = ContainerHelper.Split(inputContainer);
 313415            var playProfiles = playProfile is null ? profile.DirectPlayProfiles : [playProfile];
 1376416            foreach (var format in formats)
 417            {
 2616418                foreach (var directPlayProfile in playProfiles)
 419                {
 933420                    if (directPlayProfile.Type != type)
 421                    {
 422                        continue;
 423                    }
 424
 705425                    if (directPlayProfile.SupportsContainer(format))
 426                    {
 296427                        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                            && (IsVobSubMksProfile(profile, stream)
 549580                                || (!IsVobSubMksDeliveryProfile(profile) && string.Equals(profile.Format, stream.Codec, 
 581                        {
 0582                            return stream.Index;
 583                        }
 584                    }
 585                }
 586            }
 587
 588            // If no optimization panned out, just use the original default
 177589            return item.DefaultSubtitleStreamIndex;
 0590        }
 591
 592        private static void SetStreamInfoOptionsFromTranscodingProfile(MediaSourceInfo item, StreamInfo playlistItem, Tr
 593        {
 149594            var container = transcodingProfile.Container;
 149595            var protocol = transcodingProfile.Protocol;
 596
 149597            item.TranscodingContainer = container;
 149598            item.TranscodingSubProtocol = protocol;
 599
 149600            if (playlistItem.PlayMethod == PlayMethod.Transcode)
 601            {
 149602                playlistItem.Container = container;
 149603                playlistItem.SubProtocol = protocol;
 604            }
 605
 149606            playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
 149607            if (int.TryParse(transcodingProfile.MaxAudioChannels, CultureInfo.InvariantCulture, out int transcodingMaxAu
 608            {
 135609                playlistItem.TranscodingMaxAudioChannels = transcodingMaxAudioChannels;
 610            }
 611
 149612            playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength;
 613
 149614            playlistItem.CopyTimestamps = transcodingProfile.CopyTimestamps;
 149615            playlistItem.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
 149616            playlistItem.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
 617
 149618            playlistItem.EnableAudioVbrEncoding = transcodingProfile.EnableAudioVbrEncoding;
 619
 149620            if (transcodingProfile.MinSegments > 0)
 621            {
 104622                playlistItem.MinSegments = transcodingProfile.MinSegments;
 623            }
 624
 149625            if (transcodingProfile.SegmentLength > 0)
 626            {
 0627                playlistItem.SegmentLength = transcodingProfile.SegmentLength;
 628            }
 149629        }
 630
 631        private static void SetStreamInfoOptionsFromDirectPlayProfile(MediaOptions options, MediaSourceInfo item, Stream
 632        {
 0633            var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileTy
 0634            var protocol = MediaStreamProtocol.http;
 635
 0636            item.TranscodingContainer = container;
 0637            item.TranscodingSubProtocol = protocol;
 638
 0639            playlistItem.Container = container;
 0640            playlistItem.SubProtocol = protocol;
 641
 0642            playlistItem.VideoCodecs = [item.VideoStream.Codec];
 0643            playlistItem.AudioCodecs = ContainerHelper.Split(directPlayProfile?.AudioCodec);
 0644        }
 645
 646        private StreamInfo BuildVideoItem(MediaSourceInfo item, MediaOptions options)
 647        {
 282648            ArgumentNullException.ThrowIfNull(item);
 649
 282650            StreamInfo playlistItem = new StreamInfo
 282651            {
 282652                ItemId = options.ItemId,
 282653                MediaType = DlnaProfileType.Video,
 282654                MediaSource = item,
 282655                RunTimeTicks = item.RunTimeTicks,
 282656                Context = options.Context,
 282657                DeviceProfile = options.Profile,
 282658                SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile
 282659                AlwaysBurnInSubtitleWhenTranscoding = options.AlwaysBurnInSubtitleWhenTranscoding
 282660            };
 661
 282662            var subtitleStream = playlistItem.SubtitleStreamIndex.HasValue ? item.GetMediaStream(MediaStreamType.Subtitl
 663
 282664            var audioStream = item.GetDefaultAudioStream(options.AudioStreamIndex ?? item.DefaultAudioStreamIndex);
 282665            if (audioStream is not null)
 666            {
 281667                playlistItem.AudioStreamIndex = audioStream.Index;
 668            }
 669
 670            // Collect candidate audio streams
 282671            ICollection<MediaStream> candidateAudioStreams = audioStream is null ? [] : [audioStream];
 672            // When the index is explicitly required by client or the default is specified by user, don't do any stream 
 282673            if (!item.DefaultAudioIndexSource.HasFlag(AudioIndexSource.User) && (options.AudioStreamIndex is null or < 0
 674            {
 675                // When user has no preferences allow stream selection on all streams.
 177676                if (item.DefaultAudioIndexSource == AudioIndexSource.None && audioStream is not null)
 677                {
 176678                    candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio).ToAr
 176679                    if (audioStream.IsDefault)
 680                    {
 681                        // If default is picked, only allow selection within default streams.
 176682                        candidateAudioStreams = candidateAudioStreams.Where(stream => stream.IsDefault).ToArray();
 683                    }
 684                }
 685
 177686                if (item.DefaultAudioIndexSource.HasFlag(AudioIndexSource.Language))
 687                {
 688                    // If user has language preference, only allow stream selection within the same language.
 0689                    candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && st
 0690                    if (item.DefaultAudioIndexSource.HasFlag(AudioIndexSource.Default))
 691                    {
 0692                        var defaultStreamsInPreferredLanguage = candidateAudioStreams.Where(stream => stream.IsDefault).
 693
 694                        // If the user also prefers default streams, try limit selection within default tracks in the sa
 695                        // If there is no default stream in the preferred language, allow selection on all default strea
 0696                        candidateAudioStreams = defaultStreamsInPreferredLanguage.Length > 0
 0697                            ? defaultStreamsInPreferredLanguage
 0698                            : item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault
 699                    }
 700                }
 177701                else if (item.DefaultAudioIndexSource.HasFlag(AudioIndexSource.Default))
 702                {
 703                    // If user prefers default streams, only allow stream selection on default streams.
 0704                    candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && st
 705                }
 706            }
 707
 282708            var videoStream = item.VideoStream;
 709
 282710            var bitrateLimitExceeded = IsBitrateLimitExceeded(item, options.GetMaxBitrate(false) ?? 0);
 282711            var isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || !bitrateLimitExceeded)
 282712            var isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || !bitrateLimitExc
 282713            TranscodeReason transcodeReasons = 0;
 714
 715            // Force transcode or remux for BD/DVD folders
 282716            if (item.VideoType == VideoType.Dvd || item.VideoType == VideoType.BluRay)
 717            {
 0718                isEligibleForDirectPlay = false;
 719            }
 720
 282721            if (bitrateLimitExceeded)
 722            {
 24723                transcodeReasons = TranscodeReason.ContainerBitrateExceedsLimit;
 724            }
 725
 282726            _logger.LogDebug(
 282727                "Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
 282728                options.Profile.Name ?? "Unknown Profile",
 282729                item.Path ?? "Unknown path",
 282730                isEligibleForDirectPlay,
 282731                isEligibleForDirectStream);
 732
 282733            DirectPlayProfile? directPlayProfile = null;
 282734            if (isEligibleForDirectPlay || isEligibleForDirectStream)
 735            {
 736                // See if it can be direct played
 258737                var directPlayInfo = GetVideoDirectPlayProfile(options, item, videoStream, audioStream, candidateAudioSt
 258738                var directPlay = directPlayInfo.PlayMethod;
 258739                transcodeReasons |= directPlayInfo.TranscodeReasons;
 740
 258741                if (directPlay.HasValue)
 742                {
 125743                    directPlayProfile = directPlayInfo.Profile;
 125744                    playlistItem.PlayMethod = directPlay.Value;
 125745                    playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profi
 125746                    var videoCodec = videoStream?.Codec;
 125747                    playlistItem.VideoCodecs = videoCodec is null ? [] : [videoCodec];
 748
 125749                    if (directPlay == PlayMethod.DirectPlay)
 750                    {
 125751                        playlistItem.SubProtocol = MediaStreamProtocol.http;
 752
 125753                        var audioStreamIndex = directPlayInfo.AudioStreamIndex ?? audioStream?.Index;
 125754                        if (audioStreamIndex.HasValue)
 755                        {
 125756                            playlistItem.AudioStreamIndex = audioStreamIndex;
 125757                            var audioCodec = item.GetMediaStream(MediaStreamType.Audio, audioStreamIndex.Value)?.Codec;
 125758                            playlistItem.AudioCodecs = audioCodec is null ? [] : [audioCodec];
 759                        }
 760                    }
 0761                    else if (directPlay == PlayMethod.DirectStream)
 762                    {
 0763                        playlistItem.AudioStreamIndex = audioStream?.Index;
 0764                        if (audioStream is not null)
 765                        {
 0766                            playlistItem.AudioCodecs = ContainerHelper.Split(directPlayProfile?.AudioCodec);
 767                        }
 768
 0769                        SetStreamInfoOptionsFromDirectPlayProfile(options, item, playlistItem, directPlayProfile);
 0770                        BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStream
 771                    }
 772
 125773                    if (subtitleStream is not null)
 774                    {
 116775                        var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles,
 776
 116777                        playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
 116778                        playlistItem.SubtitleFormat = subtitleProfile.Format;
 779                    }
 780                }
 781
 258782                _logger.LogDebug(
 258783                    "DirectPlay Result for Profile: {0}, Path: {1}, PlayMethod: {2}, AudioStreamIndex: {3}, SubtitleStre
 258784                    options.Profile.Name ?? "Anonymous Profile",
 258785                    item.Path ?? "Unknown path",
 258786                    directPlayInfo.PlayMethod,
 258787                    directPlayInfo.AudioStreamIndex ?? audioStream?.Index,
 258788                    playlistItem.SubtitleStreamIndex,
 258789                    directPlayInfo.TranscodeReasons);
 790            }
 791
 282792            playlistItem.TranscodeReasons = transcodeReasons;
 793
 282794            if (playlistItem.PlayMethod != PlayMethod.DirectStream && playlistItem.PlayMethod != PlayMethod.DirectPlay)
 795            {
 796                // Can't direct play, find the transcoding profile
 797                // If we do this for direct-stream we will overwrite the info
 157798                var (transcodingProfile, playMethod) = GetVideoTranscodeProfile(item, options, videoStream, audioStream,
 799
 157800                if (transcodingProfile is not null && playMethod.HasValue)
 801                {
 149802                    SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile);
 803
 149804                    BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, t
 805
 149806                    playlistItem.PlayMethod = PlayMethod.Transcode;
 807
 149808                    if (subtitleStream is not null)
 809                    {
 125810                        var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles,
 125811                        playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
 125812                        playlistItem.SubtitleFormat = subtitleProfile.Format;
 125813                        playlistItem.SubtitleCodecs = [subtitleProfile.Format];
 814                    }
 815
 149816                    if ((playlistItem.TranscodeReasons & (VideoReasons | TranscodeReason.ContainerBitrateExceedsLimit)) 
 817                    {
 55818                        ApplyTranscodingConditions(playlistItem, transcodingProfile.Conditions, null, true, true);
 819                    }
 820                }
 821            }
 822
 282823            _logger.LogDebug(
 282824                "StreamBuilder.BuildVideoItem( Profile={0}, Path={1}, AudioStreamIndex={2}, SubtitleStreamIndex={3} ) =>
 282825                options.Profile.Name ?? "Anonymous Profile",
 282826                item.Path ?? "Unknown path",
 282827                options.AudioStreamIndex,
 282828                options.SubtitleStreamIndex,
 282829                playlistItem.PlayMethod,
 282830                playlistItem.TranscodeReasons,
 282831                playlistItem.ToUrl("media:", "<token>", null));
 832
 282833            item.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileT
 282834            return playlistItem;
 835        }
 836
 837        private (TranscodingProfile? Profile, PlayMethod? PlayMethod) GetVideoTranscodeProfile(
 838            MediaSourceInfo item,
 839            MediaOptions options,
 840            MediaStream? videoStream,
 841            MediaStream? audioStream,
 842            StreamInfo playlistItem)
 843        {
 157844            var mediaSource = playlistItem.MediaSource;
 845
 157846            ArgumentNullException.ThrowIfNull(mediaSource);
 847
 157848            if (!(item.SupportsTranscoding || item.SupportsDirectStream))
 849            {
 0850                return (null, null);
 851            }
 852
 157853            var transcodingProfiles = options.Profile.TranscodingProfiles
 157854                .Where(i => i.Type == playlistItem.MediaType && i.Context == options.Context);
 855
 157856            if (item.UseMostCompatibleTranscodingProfile)
 857            {
 0858                transcodingProfiles = transcodingProfiles.Where(i => string.Equals(i.Container, "ts", StringComparison.O
 859            }
 860
 157861            var videoCodec = videoStream?.Codec;
 157862            var audioCodec = audioStream?.Codec;
 863
 157864            var analyzedProfiles = transcodingProfiles
 157865                .Select(transcodingProfile =>
 157866                {
 157867                    var rank = (Video: 3, Audio: 3);
 157868
 157869                    var container = transcodingProfile.Container;
 157870
 157871                    if (videoStream is not null
 157872                        && options.AllowVideoStreamCopy
 157873                        && ContainerHelper.ContainsContainer(transcodingProfile.VideoCodec, videoCodec))
 157874                    {
 157875                        var failures = GetCompatibilityVideoCodec(options, mediaSource, container, videoStream);
 157876                        rank.Video = failures == 0 ? 1 : 2;
 157877                    }
 157878
 157879                    if (audioStream is not null
 157880                        && options.AllowAudioStreamCopy)
 157881                    {
 157882                        // For Audio stream, we prefer the audio codec that can be directly copied, then the codec that 
 157883                        // the transcoding conditions, then the one does not satisfy the transcoding conditions.
 157884                        // For example: A client can support both aac and flac, but flac only supports 2 channels while 
 157885                        // When the source audio is 6 channel flac, we should transcode to 6 channel aac, instead of dow
 157886                        var transcodingAudioCodecs = ContainerHelper.Split(transcodingProfile.AudioCodec);
 157887
 157888                        foreach (var transcodingAudioCodec in transcodingAudioCodecs)
 157889                        {
 157890                            var failures = GetCompatibilityAudioCodec(options, mediaSource, container, audioStream, tran
 157891
 157892                            var rankAudio = 3;
 157893
 157894                            if (failures == 0)
 157895                            {
 157896                                rankAudio = string.Equals(transcodingAudioCodec, audioCodec, StringComparison.OrdinalIgn
 157897                            }
 157898
 157899                            rank.Audio = Math.Min(rank.Audio, rankAudio);
 157900
 157901                            if (rank.Audio == 1)
 157902                            {
 157903                                break;
 157904                            }
 157905                        }
 157906                    }
 157907
 157908                    PlayMethod playMethod = PlayMethod.Transcode;
 157909
 157910                    if (rank.Video == 1)
 157911                    {
 157912                        playMethod = PlayMethod.DirectStream;
 157913                    }
 157914
 157915                    return (Profile: transcodingProfile, PlayMethod: playMethod, Rank: rank);
 157916                })
 157917                .OrderBy(analysis => analysis.Rank);
 918
 157919            var profileMatch = analyzedProfiles.FirstOrDefault();
 920
 157921            return (profileMatch.Profile, profileMatch.PlayMethod);
 922        }
 923
 924        private void BuildStreamVideoItem(
 925            StreamInfo playlistItem,
 926            MediaOptions options,
 927            MediaSourceInfo item,
 928            MediaStream? videoStream,
 929            MediaStream? audioStream,
 930            IEnumerable<MediaStream> candidateAudioStreams,
 931            string? container,
 932            string? videoCodec,
 933            string? audioCodec)
 934        {
 935            // Prefer matching video codecs
 149936            var videoCodecs = ContainerHelper.Split(videoCodec).ToList();
 937
 149938            if (videoCodecs.Count == 0 && videoStream is not null)
 939            {
 940                // Add the original codec if no codec is specified
 0941                videoCodecs.Add(videoStream.Codec);
 942            }
 943
 944            // Enforce HLS video codec restrictions
 149945            if (playlistItem.SubProtocol == MediaStreamProtocol.hls)
 946            {
 135947                videoCodecs = videoCodecs.Where(codec => _supportedHlsVideoCodecs.Contains(codec)).ToList();
 948            }
 949
 149950            playlistItem.VideoCodecs = videoCodecs;
 149951            if (videoStream is not null && !ContainerHelper.ContainsContainer(videoCodecs, false, videoStream.Codec))
 952            {
 31953                playlistItem.TranscodeReasons |= TranscodeReason.VideoCodecNotSupported;
 954            }
 955
 956            // Copy video codec options as a starting point, this applies to transcode and direct-stream
 149957            playlistItem.MaxFramerate = videoStream?.ReferenceFrameRate;
 149958            var qualifier = videoStream?.Codec;
 149959            if (videoStream?.Level is not null)
 960            {
 148961                playlistItem.SetOption(qualifier, "level", videoStream.Level.Value.ToString(CultureInfo.InvariantCulture
 962            }
 963
 149964            if (videoStream?.BitDepth is not null)
 965            {
 148966                playlistItem.SetOption(qualifier, "videobitdepth", videoStream.BitDepth.Value.ToString(CultureInfo.Invar
 967            }
 968
 149969            if (!string.IsNullOrEmpty(videoStream?.Profile))
 970            {
 148971                playlistItem.SetOption(qualifier, "profile", videoStream.Profile.ToLowerInvariant());
 972            }
 973
 974            // Prefer matching audio codecs, could do better here
 149975            var audioCodecs = ContainerHelper.Split(audioCodec).ToList();
 976
 149977            if (audioCodecs.Count == 0 && audioStream is not null)
 978            {
 979                // Add the original codec if no codec is specified
 0980                audioCodecs.Add(audioStream.Codec);
 981            }
 982
 983            // Enforce HLS audio codec restrictions
 149984            if (playlistItem.SubProtocol == MediaStreamProtocol.hls)
 985            {
 135986                if (string.Equals(playlistItem.Container, "mp4", StringComparison.OrdinalIgnoreCase))
 987                {
 74988                    audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsMp4.Contains(codec)).ToList();
 989                }
 990                else
 991                {
 61992                    audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsTs.Contains(codec)).ToList();
 993                }
 994            }
 995
 149996            var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerHelper.ContainsContainer(
 997
 149998            var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && audioStreamWithSupportedCodec.Channe
 999
 1491000            var directAudioFailures = audioStreamWithSupportedCodec is null ? default : GetCompatibilityAudioCodec(optio
 1001
 1491002            playlistItem.TranscodeReasons |= directAudioFailures;
 1491003            if (audioStream is not null && audioStreamWithSupportedCodec is null)
 1004            {
 651005                playlistItem.TranscodeReasons |= TranscodeReason.AudioCodecNotSupported;
 1006            }
 1007
 1491008            var directAudioStreamSatisfied = audioStreamWithSupportedCodec is not null && !channelsExceedsLimit
 1491009                && directAudioFailures == 0;
 1010
 1491011            directAudioStreamSatisfied = directAudioStreamSatisfied && !playlistItem.TranscodeReasons.HasFlag(TranscodeR
 1012
 1491013            var directAudioStream = directAudioStreamSatisfied ? audioStreamWithSupportedCodec : null;
 1014
 1491015            if (channelsExceedsLimit && playlistItem.TargetAudioStream is not null)
 1016            {
 101017                playlistItem.TranscodeReasons |= TranscodeReason.AudioChannelsNotSupported;
 101018                playlistItem.TargetAudioStream.Channels = playlistItem.TranscodingMaxAudioChannels;
 1019            }
 1020
 1491021            playlistItem.AudioCodecs = audioCodecs;
 1491022            if (directAudioStream is not null)
 1023            {
 581024                audioStream = directAudioStream;
 581025                playlistItem.AudioStreamIndex = audioStream.Index;
 581026                audioCodecs = [audioStream.Codec];
 581027                playlistItem.AudioCodecs = audioCodecs;
 1028
 1029                // Copy matching audio codec options
 581030                playlistItem.AudioSampleRate = audioStream.SampleRate;
 581031                playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels?.ToString(CultureInfo.InvariantC
 1032
 581033                if (!string.IsNullOrEmpty(audioStream.Profile))
 1034                {
 561035                    playlistItem.SetOption(audioStream.Codec, "profile", audioStream.Profile.ToLowerInvariant());
 1036                }
 1037
 581038                if (audioStream.Level.HasValue && audioStream.Level.Value != 0)
 1039                {
 01040                    playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.Value.ToString(CultureInfo.Inva
 1041                }
 1042            }
 1043
 1491044            int? width = videoStream?.Width;
 1491045            int? height = videoStream?.Height;
 1491046            int? bitDepth = videoStream?.BitDepth;
 1491047            int? videoBitrate = videoStream?.BitRate;
 1491048            double? videoLevel = videoStream?.Level;
 1491049            string? videoProfile = videoStream?.Profile;
 1491050            VideoRangeType? videoRangeType = videoStream?.VideoRangeType;
 1491051            float videoFramerate = videoStream is null ? 0 : videoStream.ReferenceFrameRate ?? 0;
 1491052            bool? isAnamorphic = videoStream?.IsAnamorphic;
 1491053            bool? isInterlaced = videoStream?.IsInterlaced;
 1491054            string? videoCodecTag = videoStream?.CodecTag;
 1491055            bool? isAvc = videoStream?.IsAVC;
 1491056            int? videoRotation = videoStream?.Rotation;
 1057
 1491058            TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp;
 1491059            int? packetLength = videoStream?.PacketLength;
 1491060            int? refFrames = videoStream?.RefFrames;
 1061
 1491062            int numStreams = item.MediaStreams.Count;
 1491063            int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
 1491064            int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
 1065
 1491066            var useSubContainer = playlistItem.SubProtocol == MediaStreamProtocol.hls;
 1067
 1491068            var appliedVideoConditions = options.Profile.CodecProfiles
 1491069                .Where(i => i.Type == CodecType.Video &&
 1491070                    i.ContainsAnyCodec(playlistItem.VideoCodecs, container, useSubContainer) &&
 1491071                    i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition,
 1491072                // Reverse codec profiles for backward compatibility - first codec profile has higher priority
 1491073                .Reverse();
 9101074            foreach (var condition in appliedVideoConditions)
 1075            {
 24961076                foreach (var transcodingVideoCodec in playlistItem.VideoCodecs)
 1077                {
 9421078                    if (condition.ContainsAnyCodec(transcodingVideoCodec, container, useSubContainer))
 1079                    {
 3141080                        ApplyTranscodingConditions(playlistItem, condition.Conditions, transcodingVideoCodec, true, true
 1081                        continue;
 1082                    }
 1083                }
 1084            }
 1085
 1086            // Honor requested max channels
 1491087            playlistItem.GlobalMaxAudioChannels = channelsExceedsLimit ? playlistItem.TranscodingMaxAudioChannels : opti
 1088
 1491089            int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(true) ?? 0, playlistItem.TargetAudioCodec, audioStr
 1491090            playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate);
 1091
 1491092            bool? isSecondaryAudio = audioStream is null ? null : item.IsSecondaryAudio(audioStream);
 1491093            int? inputAudioBitrate = audioStream?.BitRate;
 1491094            int? audioChannels = audioStream?.Channels;
 1491095            string? audioProfile = audioStream?.Profile;
 1491096            int? inputAudioSampleRate = audioStream?.SampleRate;
 1491097            int? inputAudioBitDepth = audioStream?.BitDepth;
 1098
 1491099            var appliedAudioConditions = options.Profile.CodecProfiles
 1491100                .Where(i => i.Type == CodecType.VideoAudio &&
 1491101                    i.ContainsAnyCodec(playlistItem.AudioCodecs, container) &&
 1491102                    i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondi
 1491103                // Reverse codec profiles for backward compatibility - first codec profile has higher priority
 1491104                .Reverse();
 1105
 6101106            foreach (var codecProfile in appliedAudioConditions)
 1107            {
 4681108                foreach (var transcodingAudioCodec in playlistItem.AudioCodecs)
 1109                {
 1561110                    if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container))
 1111                    {
 1561112                        ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, t
 1561113                        break;
 1114                    }
 1115                }
 1116            }
 1117
 1491118            var maxBitrateSetting = options.GetMaxBitrate(false);
 1119            // Honor max rate
 1491120            if (maxBitrateSetting.HasValue)
 1121            {
 1491122                var availableBitrateForVideo = maxBitrateSetting.Value;
 1123
 1491124                if (playlistItem.AudioBitrate.HasValue)
 1125                {
 1491126                    availableBitrateForVideo -= playlistItem.AudioBitrate.Value;
 1127                }
 1128
 1129                // Make sure the video bitrate is lower than bitrate settings but at least 64k
 1130                // Don't use Math.Clamp as availableBitrateForVideo can be lower then 64k.
 1491131                var currentValue = playlistItem.VideoBitrate ?? availableBitrateForVideo;
 1491132                playlistItem.VideoBitrate = Math.Max(Math.Min(availableBitrateForVideo, currentValue), 64_000);
 1133            }
 1134
 1491135            _logger.LogDebug(
 1491136                "Transcode Result for Profile: {Profile}, Path: {Path}, PlayMethod: {PlayMethod}, AudioStreamIndex: {Aud
 1491137                options.Profile.Name ?? "Anonymous Profile",
 1491138                item.Path ?? "Unknown path",
 1491139                playlistItem.PlayMethod,
 1491140                audioStream?.Index,
 1491141                playlistItem.SubtitleStreamIndex,
 1491142                playlistItem.TranscodeReasons);
 1491143        }
 1144
 1145        private static int GetDefaultAudioBitrate(string? audioCodec, int? audioChannels)
 1146        {
 601147            if (!string.IsNullOrEmpty(audioCodec))
 1148            {
 1149                // Default to a higher bitrate for stream copy
 601150                if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
 601151                    || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
 601152                    || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
 601153                    || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
 1154                {
 551155                    if ((audioChannels ?? 0) < 2)
 1156                    {
 01157                        return 128000;
 1158                    }
 1159
 551160                    return (audioChannels ?? 0) >= 6 ? 640000 : 384000;
 1161                }
 1162
 51163                if (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase)
 51164                    || string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase))
 1165                {
 01166                    if ((audioChannels ?? 0) < 2)
 1167                    {
 01168                        return 768000;
 1169                    }
 1170
 01171                    return (audioChannels ?? 0) >= 6 ? 3584000 : 1536000;
 1172                }
 1173            }
 1174
 51175            return 192000;
 1176        }
 1177
 1178        private static int GetAudioBitrate(long maxTotalBitrate, IReadOnlyList<string> targetAudioCodecs, MediaStream? a
 1179        {
 1491180            string? targetAudioCodec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0];
 1181
 1491182            int? targetAudioChannels = item.GetTargetAudioChannels(targetAudioCodec);
 1183
 1184            int defaultBitrate;
 1491185            int encoderAudioBitrateLimit = int.MaxValue;
 1186
 1491187            if (audioStream is null)
 1188            {
 11189                defaultBitrate = 192000;
 1190            }
 1191            else
 1192            {
 1481193                if (targetAudioChannels.HasValue
 1481194                    && audioStream.Channels.HasValue
 1481195                    && audioStream.Channels.Value > targetAudioChannels.Value)
 1196                {
 1197                    // Reduce the bitrate if we're down mixing.
 461198                    defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, targetAudioChannels);
 1199                }
 1021200                else if (targetAudioChannels.HasValue
 1021201                         && audioStream.Channels.HasValue
 1021202                         && audioStream.Channels.Value <= targetAudioChannels.Value
 1021203                         && !string.IsNullOrEmpty(audioStream.Codec)
 1021204                         && targetAudioCodecs is not null
 1021205                         && targetAudioCodecs.Count > 0
 1021206                         && !targetAudioCodecs.Any(elem => string.Equals(audioStream.Codec, elem, StringComparison.Ordin
 1207                {
 1208                    // Shift the bitrate if we're transcoding to a different audio codec.
 141209                    defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, audioStream.Channels.Value);
 1210                }
 1211                else
 1212                {
 881213                    defaultBitrate = audioStream.BitRate ?? GetDefaultAudioBitrate(targetAudioCodec, targetAudioChannels
 1214                }
 1215
 1216                // Seeing webm encoding failures when source has 1 audio channel and 22k bitrate.
 1217                // Any attempts to transcode over 64k will fail
 1481218                if (audioStream.Channels == 1
 1481219                    && (audioStream.BitRate ?? 0) < 64000)
 1220                {
 01221                    encoderAudioBitrateLimit = 64000;
 1222                }
 1223            }
 1224
 1491225            if (maxTotalBitrate > 0)
 1226            {
 1491227                defaultBitrate = Math.Min(GetMaxAudioBitrateForTotalBitrate(maxTotalBitrate), defaultBitrate);
 1228            }
 1229
 1491230            return Math.Min(defaultBitrate, encoderAudioBitrateLimit);
 1231        }
 1232
 1233        private static int GetMaxAudioBitrateForTotalBitrate(long totalBitrate)
 1234        {
 1491235            if (totalBitrate <= 640000)
 1236            {
 81237                return 128000;
 1238            }
 1239
 1411240            if (totalBitrate <= 2000000)
 1241            {
 01242                return 384000;
 1243            }
 1244
 1411245            if (totalBitrate <= 3000000)
 1246            {
 01247                return 448000;
 1248            }
 1249
 1411250            if (totalBitrate <= 4000000)
 1251            {
 01252                return 640000;
 1253            }
 1254
 1411255            if (totalBitrate <= 5000000)
 1256            {
 01257                return 768000;
 1258            }
 1259
 1411260            if (totalBitrate <= 10000000)
 1261            {
 81262                return 1536000;
 1263            }
 1264
 1331265            if (totalBitrate <= 15000000)
 1266            {
 01267                return 2304000;
 1268            }
 1269
 1331270            if (totalBitrate <= 20000000)
 1271            {
 141272                return 3584000;
 1273            }
 1274
 1191275            return 7168000;
 1276        }
 1277
 1278        private (DirectPlayProfile? Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeRea
 1279            MediaOptions options,
 1280            MediaSourceInfo mediaSource,
 1281            MediaStream? videoStream,
 1282            MediaStream? audioStream,
 1283            ICollection<MediaStream> candidateAudioStreams,
 1284            MediaStream? subtitleStream,
 1285            bool isEligibleForDirectPlay,
 1286            bool isEligibleForDirectStream)
 1287        {
 2581288            if (options.ForceDirectPlay)
 1289            {
 01290                return (null, PlayMethod.DirectPlay, audioStream?.Index, 0);
 1291            }
 1292
 2581293            if (options.ForceDirectStream)
 1294            {
 01295                return (null, PlayMethod.DirectStream, audioStream?.Index, 0);
 1296            }
 1297
 2581298            DeviceProfile profile = options.Profile;
 2581299            string container = mediaSource.Container;
 1300
 1301            // Check container conditions
 2581302            var containerProfileReasons = GetCompatibilityContainer(options, mediaSource, container, videoStream);
 1303
 1304            // Check video conditions
 2581305            var videoCodecProfileReasons = videoStream is null ? default : GetCompatibilityVideoCodec(options, mediaSour
 1306
 1307            // Check audio candidates profile conditions
 2581308            var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => GetCompatibilityAudioCode
 1309
 2581310            TranscodeReason subtitleProfileReasons = 0;
 2581311            if (subtitleStream is not null)
 1312            {
 2251313                var subtitleProfile = GetSubtitleProfile(mediaSource, subtitleStream, options.Profile.SubtitleProfiles, 
 1314
 2251315                if (subtitleProfile.Method != SubtitleDeliveryMethod.Drop
 2251316                    && subtitleProfile.Method != SubtitleDeliveryMethod.External
 2251317                    && subtitleProfile.Method != SubtitleDeliveryMethod.Embed)
 1318                {
 01319                    _logger.LogDebug("Not eligible for {0} due to unsupported subtitles", PlayMethod.DirectPlay);
 01320                    subtitleProfileReasons |= TranscodeReason.SubtitleCodecNotSupported;
 1321                }
 1322            }
 1323
 2581324            var containerSupported = false;
 2581325            TranscodeReason[] rankings = [TranscodeReason.VideoCodecNotSupported, VideoCodecReasons, TranscodeReason.Aud
 1326
 1327            // Check DirectPlay profiles to see if it can be direct played
 2581328            var analyzedProfiles = profile.DirectPlayProfiles
 2581329                .Where(directPlayProfile => directPlayProfile.Type == DlnaProfileType.Video)
 2581330                .Select((directPlayProfile, order) =>
 2581331                {
 2581332                    TranscodeReason directPlayProfileReasons = 0;
 2581333                    TranscodeReason audioCodecProfileReasons = 0;
 2581334
 2581335                    // Check container type
 2581336                    if (!directPlayProfile.SupportsContainer(container))
 2581337                    {
 2581338                        directPlayProfileReasons |= TranscodeReason.ContainerNotSupported;
 2581339                    }
 2581340                    else
 2581341                    {
 2581342                        containerSupported = true;
 2581343                    }
 2581344
 2581345                    // Check video codec
 2581346                    string? videoCodec = videoStream?.Codec;
 2581347                    if (!directPlayProfile.SupportsVideoCodec(videoCodec))
 2581348                    {
 2581349                        directPlayProfileReasons |= TranscodeReason.VideoCodecNotSupported;
 2581350                    }
 2581351
 2581352                    // Check audio codec
 2581353                    MediaStream? selectedAudioStream = null;
 2581354                    if (candidateAudioStreams.Count != 0)
 2581355                    {
 2581356                        selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.Supp
 2581357                        if (selectedAudioStream is null)
 2581358                        {
 2581359                            directPlayProfileReasons |= TranscodeReason.AudioCodecNotSupported;
 2581360                        }
 2581361                        else
 2581362                        {
 2581363                            audioCodecProfileReasons = audioStreamMatches.GetValueOrDefault(selectedAudioStream);
 2581364                        }
 2581365                    }
 2581366
 2581367                    var failureReasons = directPlayProfileReasons | containerProfileReasons | subtitleProfileReasons;
 2581368
 2581369                    if ((failureReasons & TranscodeReason.VideoCodecNotSupported) == 0)
 2581370                    {
 2581371                        failureReasons |= videoCodecProfileReasons;
 2581372                    }
 2581373
 2581374                    if ((failureReasons & TranscodeReason.AudioCodecNotSupported) == 0)
 2581375                    {
 2581376                        failureReasons |= audioCodecProfileReasons;
 2581377                    }
 2581378
 2581379                    var directStreamFailureReasons = failureReasons & (~DirectStreamReasons);
 2581380
 2581381                    PlayMethod? playMethod = null;
 2581382                    if (failureReasons == 0 && isEligibleForDirectPlay && mediaSource.SupportsDirectPlay)
 2581383                    {
 2581384                        playMethod = PlayMethod.DirectPlay;
 2581385                    }
 2581386                    else if (directStreamFailureReasons == 0 && isEligibleForDirectStream && mediaSource.SupportsDirectS
 2581387                    {
 2581388                        playMethod = PlayMethod.DirectStream;
 2581389                    }
 2581390
 2581391                    var ranked = GetRank(ref failureReasons, rankings);
 2581392
 2581393                    return (Result: (Profile: directPlayProfile, PlayMethod: playMethod, AudioStreamIndex: selectedAudio
 2581394                })
 2581395                .OrderByDescending(analysis => analysis.Result.PlayMethod)
 2581396                .ThenByDescending(analysis => analysis.Rank)
 2581397                .ThenBy(analysis => analysis.Order)
 2581398                .ToArray()
 2581399                .ToLookup(analysis => analysis.Result.PlayMethod is not null);
 1400
 2581401            var profileMatch = analyzedProfiles[true]
 2581402                .Select(analysis => analysis.Result)
 2581403                .FirstOrDefault();
 2581404            if (profileMatch.Profile is not null)
 1405            {
 1251406                return profileMatch;
 1407            }
 1408
 1331409            var failureReasons = analyzedProfiles[false]
 1331410                .Select(analysis => analysis.Result)
 1331411                .Where(result => !containerSupported || !result.TranscodeReason.HasFlag(TranscodeReason.ContainerNotSupp
 1331412                .FirstOrDefault().TranscodeReason;
 1331413            if (failureReasons == 0)
 1414            {
 141415                failureReasons = TranscodeReason.DirectPlayError;
 1416            }
 1417
 1331418            return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons);
 1419        }
 1420
 1421        private TranscodeReason AggregateFailureConditions(MediaSourceInfo mediaSource, DeviceProfile profile, string ty
 1422        {
 16851423            return conditions.Aggregate<ProfileCondition, TranscodeReason>(0, (reasons, i) =>
 16851424            {
 16851425                LogConditionFailure(profile, type, i, mediaSource);
 16851426                var transcodeReasons = GetTranscodeReasonForFailedCondition(i);
 16851427                return reasons | transcodeReasons;
 16851428            });
 1429        }
 1430
 1431        private void LogConditionFailure(DeviceProfile profile, string type, ProfileCondition condition, MediaSourceInfo
 1432        {
 2521433            _logger.LogDebug(
 2521434                "Profile: {0}, DirectPlay=false. Reason={1}.{2} Condition: {3}. ConditionValue: {4}. IsRequired: {5}. Pa
 2521435                type,
 2521436                profile.Name ?? "Unknown Profile",
 2521437                condition.Property,
 2521438                condition.Condition,
 2521439                condition.Value ?? string.Empty,
 2521440                condition.IsRequired,
 2521441                mediaSource.Path ?? "Unknown path");
 2521442        }
 1443
 1444        /// <summary>
 1445        /// Normalizes input container.
 1446        /// </summary>
 1447        /// <param name="mediaSource">The <see cref="MediaSourceInfo"/>.</param>
 1448        /// <param name="subtitleStream">The <see cref="MediaStream"/> of the subtitle stream.</param>
 1449        /// <param name="subtitleProfiles">The list of supported <see cref="SubtitleProfile"/>s.</param>
 1450        /// <param name="playMethod">The <see cref="PlayMethod"/>.</param>
 1451        /// <param name="transcoderSupport">The <see cref="ITranscoderSupport"/>.</param>
 1452        /// <param name="outputContainer">The output container.</param>
 1453        /// <param name="transcodingSubProtocol">The subtitle transcoding protocol.</param>
 1454        /// <returns>The normalized input container.</returns>
 1455        public static SubtitleProfile GetSubtitleProfile(
 1456            MediaSourceInfo mediaSource,
 1457            MediaStream subtitleStream,
 1458            SubtitleProfile[] subtitleProfiles,
 1459            PlayMethod playMethod,
 1460            ITranscoderSupport transcoderSupport,
 1461            string? outputContainer,
 1462            MediaStreamProtocol? transcodingSubProtocol)
 1463        {
 4911464            if (CanConsiderEmbedSubtitle(subtitleStream, playMethod, transcodingSubProtocol, outputContainer))
 1465            {
 1466                // Look for supported embedded subs of the same format
 701467                foreach (var profile in subtitleProfiles)
 1468                {
 191469                    if (!profile.SupportsLanguage(subtitleStream.Language))
 1470                    {
 1471                        continue;
 1472                    }
 1473
 191474                    if (profile.Method != SubtitleDeliveryMethod.Embed)
 1475                    {
 1476                        continue;
 1477                    }
 1478
 61479                    if (!ContainerHelper.ContainsContainer(profile.Container, outputContainer))
 1480                    {
 1481                        continue;
 1482                    }
 1483
 61484                    if (playMethod == PlayMethod.Transcode && !IsSubtitleEmbedSupported(outputContainer))
 1485                    {
 1486                        continue;
 1487                    }
 1488
 61489                    if (subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format) && string.Equals
 1490                    {
 61491                        return profile;
 1492                    }
 1493                }
 1494
 1495                // Look for supported embedded subs of a convertible format
 521496                foreach (var profile in subtitleProfiles)
 1497                {
 131498                    if (!profile.SupportsLanguage(subtitleStream.Language))
 1499                    {
 1500                        continue;
 1501                    }
 1502
 131503                    if (profile.Method != SubtitleDeliveryMethod.Embed)
 1504                    {
 1505                        continue;
 1506                    }
 1507
 01508                    if (!ContainerHelper.ContainsContainer(profile.Container, outputContainer))
 1509                    {
 1510                        continue;
 1511                    }
 1512
 01513                    if (playMethod == PlayMethod.Transcode && !IsSubtitleEmbedSupported(outputContainer))
 1514                    {
 1515                        continue;
 1516                    }
 1517
 01518                    if (subtitleStream.IsTextSubtitleStream && subtitleStream.SupportsSubtitleConversionTo(profile.Forma
 1519                    {
 01520                        return profile;
 1521                    }
 1522                }
 1523            }
 1524
 1525            // Look for an external or hls profile that matches the stream type (text/graphical) and doesn't require con
 4851526            return GetExternalSubtitleProfile(mediaSource, subtitleStream, subtitleProfiles, playMethod, transcoderSuppo
 4851527                GetExternalSubtitleProfile(mediaSource, subtitleStream, subtitleProfiles, playMethod, transcoderSupport,
 4851528                new SubtitleProfile
 4851529                {
 4851530                    Method = SubtitleDeliveryMethod.Encode,
 4851531                    Format = subtitleStream.Codec
 4851532                };
 1533        }
 1534
 1535        private static bool IsSubtitleEmbedSupported(string? transcodingContainer)
 1536        {
 291537            if (!string.IsNullOrEmpty(transcodingContainer))
 1538            {
 241539                if (ContainerHelper.ContainsContainer(transcodingContainer, "ts,mpegts,mp4"))
 1540                {
 71541                    return false;
 1542                }
 1543
 171544                if (ContainerHelper.ContainsContainer(transcodingContainer, "mkv,matroska"))
 1545                {
 101546                    return true;
 1547                }
 1548            }
 1549
 121550            return false;
 1551        }
 1552
 1553        private static bool CanConsiderEmbedSubtitle(MediaStream subtitleStream, PlayMethod playMethod, MediaStreamProto
 1554        {
 4911555            if (subtitleStream.IsExternal)
 1556            {
 4741557                return playMethod == PlayMethod.Transcode
 4741558                    && transcodingSubProtocol != MediaStreamProtocol.hls
 4741559                    && IsSubtitleEmbedSupported(outputContainer);
 1560            }
 1561
 171562            return playMethod != PlayMethod.Transcode
 171563                || transcodingSubProtocol != MediaStreamProtocol.hls;
 1564        }
 1565
 1566        private static SubtitleProfile? GetExternalSubtitleProfile(MediaSourceInfo mediaSource, MediaStream subtitleStre
 1567        {
 51661568            foreach (var profile in subtitleProfiles)
 1569            {
 20471570                if (profile.Method != SubtitleDeliveryMethod.External && profile.Method != SubtitleDeliveryMethod.Hls)
 1571                {
 1572                    continue;
 1573                }
 1574
 14101575                if (profile.Method == SubtitleDeliveryMethod.Hls && playMethod != PlayMethod.Transcode)
 1576                {
 1577                    continue;
 1578                }
 1579
 14101580                if (!profile.SupportsLanguage(subtitleStream.Language))
 1581                {
 1582                    continue;
 1583                }
 1584
 14101585                if (!subtitleStream.IsExternal
 14101586                    && playMethod == PlayMethod.Transcode
 14101587                    && !transcoderSupport.CanExtractSubtitles(subtitleStream.Codec)
 14101588                    && !subtitleStream.IsPgsSubtitleStream
 14101589                    && !subtitleStream.IsVobSubSubtitleStream)
 1590                {
 1591                    continue;
 1592                }
 1593
 14081594                bool isVobSubMksProfile = IsVobSubMksProfile(profile, subtitleStream);
 1595
 14081596                if ((profile.Method == SubtitleDeliveryMethod.External
 14081597                        && (isVobSubMksProfile
 14081598                            || (!IsVobSubMksDeliveryProfile(profile) && subtitleStream.IsTextSubtitleStream == MediaStre
 14081599                    (profile.Method == SubtitleDeliveryMethod.Hls && subtitleStream.IsTextSubtitleStream))
 1600                {
 13821601                    bool requiresConversion = !isVobSubMksProfile
 13821602                        && !string.Equals(subtitleStream.Codec, profile.Format, StringComparison.OrdinalIgnoreCase);
 1603
 13821604                    if (!requiresConversion)
 1605                    {
 1941606                        return profile;
 1607                    }
 1608
 11881609                    if (!allowConversion)
 1610                    {
 1611                        continue;
 1612                    }
 1613
 1614                    // TODO: Build this into subtitleStream.SupportsExternalStream
 2861615                    if (mediaSource.IsInfiniteStream)
 1616                    {
 1617                        continue;
 1618                    }
 1619
 2861620                    if (subtitleStream.IsTextSubtitleStream && subtitleStream.SupportsExternalStream && subtitleStream.S
 1621                    {
 2861622                        return profile;
 1623                    }
 1624                }
 1625            }
 1626
 2961627            return null;
 1628        }
 1629
 1630        private static bool IsVobSubMksDeliveryProfile(SubtitleProfile profile)
 1631        {
 39091632            return MediaStream.IsVobSubFormat(profile.Format)
 39091633                && !string.IsNullOrWhiteSpace(profile.Container)
 39091634                && ContainerHelper.ContainsContainer(profile.Container, "mks");
 1635        }
 1636
 1637        private static bool IsVobSubMksProfile(SubtitleProfile profile, MediaStream subtitleStream)
 1638        {
 1639            // FFmpeg cannot mux VobSub back into an .idx/.sub pair, so extracted VobSub streams are exposed as .mks.
 19571640            return IsVobSubMksDeliveryProfile(profile)
 19571641                && subtitleStream.IsVobSubSubtitleStream
 19571642                && (!subtitleStream.IsExternal || subtitleStream.Path?.EndsWith(".mks", StringComparison.OrdinalIgnoreCa
 1643        }
 1644
 1645        private bool IsBitrateLimitExceeded(MediaSourceInfo item, long maxBitrate)
 1646        {
 1647            // Don't restrict bitrate if item is remote.
 2821648            if (item.IsRemote)
 1649            {
 01650                return false;
 1651            }
 1652
 1653            // If no maximum bitrate is set, default to no maximum bitrate.
 2821654            long requestedMaxBitrate = maxBitrate > 0 ? maxBitrate : int.MaxValue;
 1655
 1656            // If we don't know the item bitrate, then force a transcode if requested max bitrate is under 40 mbps
 2821657            int itemBitrate = item.Bitrate ?? 40000000;
 1658
 2821659            if (itemBitrate > requestedMaxBitrate)
 1660            {
 241661                _logger.LogDebug(
 241662                    "Bitrate exceeds limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}",
 241663                    itemBitrate,
 241664                    requestedMaxBitrate);
 241665                return true;
 1666            }
 1667
 2581668            return false;
 1669        }
 1670
 1671        private static void ValidateMediaOptions(MediaOptions options, bool isMediaSource)
 1672        {
 2821673            if (options.ItemId.IsEmpty())
 1674            {
 01675                ArgumentException.ThrowIfNullOrEmpty(options.DeviceId);
 1676            }
 1677
 2821678            if (options.Profile is null)
 1679            {
 01680                throw new ArgumentException("Profile is required");
 1681            }
 1682
 2821683            if (options.MediaSources is null)
 1684            {
 01685                throw new ArgumentException("MediaSources is required");
 1686            }
 1687
 2821688            if (isMediaSource)
 1689            {
 2821690                if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
 1691                {
 01692                    throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested");
 1693                }
 1694
 2821695                if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
 1696                {
 01697                    throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested"
 1698                }
 1699            }
 2821700        }
 1701
 1702        private static IEnumerable<ProfileCondition> GetProfileConditionsForVideoAudio(
 1703            IEnumerable<CodecProfile> codecProfiles,
 1704            string container,
 1705            string codec,
 1706            int? audioChannels,
 1707            int? audioBitrate,
 1708            int? audioSampleRate,
 1709            int? audioBitDepth,
 1710            string audioProfile,
 1711            bool? isSecondaryAudio)
 1712        {
 10171713            return codecProfiles
 10171714                .Where(profile => profile.Type == CodecType.VideoAudio &&
 10171715                    profile.ContainsAnyCodec(codec, container) &&
 10171716                    profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(appl
 10171717                .SelectMany(profile => profile.Conditions)
 10171718                .Where(condition => !ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBi
 1719        }
 1720
 1721        private static IEnumerable<ProfileCondition> GetProfileConditionsForAudio(
 1722            IEnumerable<CodecProfile> codecProfiles,
 1723            string container,
 1724            string? codec,
 1725            int? audioChannels,
 1726            int? audioBitrate,
 1727            int? audioSampleRate,
 1728            int? audioBitDepth,
 1729            bool checkConditions)
 1730        {
 01731            var conditions = codecProfiles
 01732                .Where(profile => profile.Type == CodecType.Audio &&
 01733                    profile.ContainsAnyCodec(codec, container) &&
 01734                    profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsAudioConditionSatisfied(applyCond
 01735                .SelectMany(profile => profile.Conditions);
 1736
 01737            if (!checkConditions)
 1738            {
 01739                return conditions;
 1740            }
 1741
 01742            return conditions.Where(condition => !ConditionProcessor.IsAudioConditionSatisfied(condition, audioChannels,
 1743        }
 1744
 1745        private void ApplyTranscodingConditions(StreamInfo item, IEnumerable<ProfileCondition> conditions, string? quali
 1746        {
 36061747            foreach (ProfileCondition condition in conditions)
 1748            {
 12781749                string value = condition.Value;
 1750
 12781751                if (string.IsNullOrEmpty(value))
 1752                {
 1753                    continue;
 1754                }
 1755
 1756                // No way to express this
 12781757                if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 1758                {
 1759                    continue;
 1760                }
 1761
 12781762                switch (condition.Property)
 1763                {
 1764                    case ProfileConditionValue.AudioBitrate:
 1765                        {
 01766                            if (!enableNonQualifiedConditions)
 1767                            {
 1768                                continue;
 1769                            }
 1770
 01771                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 1772                            {
 01773                                if (condition.Condition == ProfileConditionType.Equals)
 1774                                {
 01775                                    item.AudioBitrate = num;
 1776                                }
 01777                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 1778                                {
 01779                                    item.AudioBitrate = Math.Min(num, item.AudioBitrate ?? num);
 1780                                }
 01781                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 1782                                {
 01783                                    item.AudioBitrate = Math.Max(num, item.AudioBitrate ?? num);
 1784                                }
 1785                            }
 1786
 01787                            break;
 1788                        }
 1789
 1790                    case ProfileConditionValue.AudioSampleRate:
 1791                        {
 01792                            if (!enableNonQualifiedConditions)
 1793                            {
 1794                                continue;
 1795                            }
 1796
 01797                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 1798                            {
 01799                                if (condition.Condition == ProfileConditionType.Equals)
 1800                                {
 01801                                    item.AudioSampleRate = num;
 1802                                }
 01803                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 1804                                {
 01805                                    item.AudioSampleRate = Math.Min(num, item.AudioSampleRate ?? num);
 1806                                }
 01807                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 1808                                {
 01809                                    item.AudioSampleRate = Math.Max(num, item.AudioSampleRate ?? num);
 1810                                }
 1811                            }
 1812
 01813                            break;
 1814                        }
 1815
 1816                    case ProfileConditionValue.AudioChannels:
 1817                        {
 281818                            if (string.IsNullOrEmpty(qualifier))
 1819                            {
 01820                                if (!enableNonQualifiedConditions)
 1821                                {
 01822                                    continue;
 1823                                }
 1824                            }
 1825                            else
 1826                            {
 281827                                if (!enableQualifiedConditions)
 1828                                {
 1829                                    continue;
 1830                                }
 1831                            }
 1832
 281833                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 1834                            {
 281835                                if (condition.Condition == ProfileConditionType.Equals)
 1836                                {
 01837                                    item.SetOption(qualifier, "audiochannels", num.ToString(CultureInfo.InvariantCulture
 1838                                }
 281839                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 1840                                {
 281841                                    item.SetOption(qualifier, "audiochannels", Math.Min(num, item.GetTargetAudioChannels
 1842                                }
 01843                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 1844                                {
 01845                                    item.SetOption(qualifier, "audiochannels", Math.Max(num, item.GetTargetAudioChannels
 1846                                }
 1847                            }
 1848
 01849                            break;
 1850                        }
 1851
 1852                    case ProfileConditionValue.IsAvc:
 1853                        {
 01854                            if (!enableNonQualifiedConditions)
 1855                            {
 1856                                continue;
 1857                            }
 1858
 01859                            if (bool.TryParse(value, out var isAvc))
 1860                            {
 01861                                if (isAvc && condition.Condition == ProfileConditionType.Equals)
 1862                                {
 01863                                    item.RequireAvc = true;
 1864                                }
 01865                                else if (!isAvc && condition.Condition == ProfileConditionType.NotEquals)
 1866                                {
 01867                                    item.RequireAvc = true;
 1868                                }
 1869                            }
 1870
 01871                            break;
 1872                        }
 1873
 1874                    case ProfileConditionValue.IsAnamorphic:
 1875                        {
 2071876                            if (!enableNonQualifiedConditions)
 1877                            {
 1878                                continue;
 1879                            }
 1880
 2071881                            if (bool.TryParse(value, out var isAnamorphic))
 1882                            {
 2071883                                if (isAnamorphic && condition.Condition == ProfileConditionType.Equals)
 1884                                {
 01885                                    item.RequireNonAnamorphic = true;
 1886                                }
 2071887                                else if (!isAnamorphic && condition.Condition == ProfileConditionType.NotEquals)
 1888                                {
 01889                                    item.RequireNonAnamorphic = true;
 1890                                }
 1891                            }
 1892
 01893                            break;
 1894                        }
 1895
 1896                    case ProfileConditionValue.IsInterlaced:
 1897                        {
 1131898                            if (string.IsNullOrEmpty(qualifier))
 1899                            {
 01900                                if (!enableNonQualifiedConditions)
 1901                                {
 01902                                    continue;
 1903                                }
 1904                            }
 1905                            else
 1906                            {
 1131907                                if (!enableQualifiedConditions)
 1908                                {
 1909                                    continue;
 1910                                }
 1911                            }
 1912
 1131913                            if (bool.TryParse(value, out var isInterlaced))
 1914                            {
 1131915                                if (!isInterlaced && condition.Condition == ProfileConditionType.Equals)
 1916                                {
 01917                                    item.SetOption(qualifier, "deinterlace", "true");
 1918                                }
 1131919                                else if (isInterlaced && condition.Condition == ProfileConditionType.NotEquals)
 1920                                {
 1131921                                    item.SetOption(qualifier, "deinterlace", "true");
 1922                                }
 1923                            }
 1924
 1131925                            break;
 1926                        }
 1927
 1928                    case ProfileConditionValue.AudioProfile:
 1929                    case ProfileConditionValue.Has64BitOffsets:
 1930                    case ProfileConditionValue.PacketLength:
 1931                    case ProfileConditionValue.NumStreams:
 1932                    case ProfileConditionValue.NumAudioStreams:
 1933                    case ProfileConditionValue.NumVideoStreams:
 1934                    case ProfileConditionValue.IsSecondaryAudio:
 1935                    case ProfileConditionValue.VideoTimestamp:
 1936                        {
 1937                            // Not supported yet
 1938                            break;
 1939                        }
 1940
 1941                    case ProfileConditionValue.RefFrames:
 1942                        {
 21943                            if (string.IsNullOrEmpty(qualifier))
 1944                            {
 01945                                if (!enableNonQualifiedConditions)
 1946                                {
 01947                                    continue;
 1948                                }
 1949                            }
 1950                            else
 1951                            {
 21952                                if (!enableQualifiedConditions)
 1953                                {
 1954                                    continue;
 1955                                }
 1956                            }
 1957
 21958                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 1959                            {
 21960                                if (condition.Condition == ProfileConditionType.Equals)
 1961                                {
 01962                                    item.SetOption(qualifier, "maxrefframes", num.ToString(CultureInfo.InvariantCulture)
 1963                                }
 21964                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 1965                                {
 21966                                    item.SetOption(qualifier, "maxrefframes", Math.Min(num, item.GetTargetRefFrames(qual
 1967                                }
 01968                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 1969                                {
 01970                                    item.SetOption(qualifier, "maxrefframes", Math.Max(num, item.GetTargetRefFrames(qual
 1971                                }
 1972                            }
 1973
 01974                            break;
 1975                        }
 1976
 1977                    case ProfileConditionValue.VideoBitDepth:
 1978                        {
 01979                            if (string.IsNullOrEmpty(qualifier))
 1980                            {
 01981                                if (!enableNonQualifiedConditions)
 1982                                {
 01983                                    continue;
 1984                                }
 1985                            }
 1986                            else
 1987                            {
 01988                                if (!enableQualifiedConditions)
 1989                                {
 1990                                    continue;
 1991                                }
 1992                            }
 1993
 01994                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 1995                            {
 01996                                if (condition.Condition == ProfileConditionType.Equals)
 1997                                {
 01998                                    item.SetOption(qualifier, "videobitdepth", num.ToString(CultureInfo.InvariantCulture
 1999                                }
 02000                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 2001                                {
 02002                                    item.SetOption(qualifier, "videobitdepth", Math.Min(num, item.GetTargetVideoBitDepth
 2003                                }
 02004                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 2005                                {
 02006                                    item.SetOption(qualifier, "videobitdepth", Math.Max(num, item.GetTargetVideoBitDepth
 2007                                }
 2008                            }
 2009
 02010                            break;
 2011                        }
 2012
 2013                    case ProfileConditionValue.VideoProfile:
 2014                        {
 2302015                            if (string.IsNullOrEmpty(qualifier))
 2016                            {
 2017                                continue;
 2018                            }
 2019
 2020                            // Change from split by | to comma
 2021                            // Strip spaces to avoid having to encode
 2302022                            var values = value
 2302023                                .Split('|', StringSplitOptions.RemoveEmptyEntries);
 2024
 2302025                            if (condition.Condition == ProfileConditionType.Equals)
 2026                            {
 02027                                item.SetOption(qualifier, "profile", string.Join(',', values));
 2028                            }
 2302029                            else if (condition.Condition == ProfileConditionType.EqualsAny)
 2030                            {
 2302031                                var currentValue = item.GetOption(qualifier, "profile");
 2302032                                if (!string.IsNullOrEmpty(currentValue) && values.Any(value => value == currentValue))
 2033                                {
 732034                                    item.SetOption(qualifier, "profile", currentValue);
 2035                                }
 2036                                else
 2037                                {
 1572038                                    item.SetOption(qualifier, "profile", string.Join(',', values));
 2039                                }
 2040                            }
 2041
 1572042                            break;
 2043                        }
 2044
 2045                    case ProfileConditionValue.VideoRangeType:
 2046                        {
 2662047                            if (string.IsNullOrEmpty(qualifier))
 2048                            {
 2049                                continue;
 2050                            }
 2051
 2052                            // change from split by | to comma
 2053                            // strip spaces to avoid having to encode
 2662054                            var values = value
 2662055                                .Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
 2056
 2662057                            if (condition.Condition == ProfileConditionType.Equals)
 2058                            {
 02059                                item.SetOption(qualifier, "rangetype", string.Join(',', values));
 2060                            }
 2662061                            else if (condition.Condition == ProfileConditionType.NotEquals)
 2062                            {
 02063                                item.SetOption(qualifier, "rangetype", string.Join(',', Enum.GetNames<VideoRangeType>().
 2064                            }
 2662065                            else if (condition.Condition == ProfileConditionType.EqualsAny)
 2066                            {
 2662067                                var currentValue = item.GetOption(qualifier, "rangetype");
 2662068                                if (!string.IsNullOrEmpty(currentValue) && values.Any(v => string.Equals(v, currentValue
 2069                                {
 02070                                    item.SetOption(qualifier, "rangetype", currentValue);
 2071                                }
 2072                                else
 2073                                {
 2662074                                    item.SetOption(qualifier, "rangetype", string.Join(',', values));
 2075                                }
 2076                            }
 2077
 2662078                            break;
 2079                        }
 2080
 2081                    case ProfileConditionValue.VideoCodecTag:
 2082                        {
 122083                            if (string.IsNullOrEmpty(qualifier))
 2084                            {
 2085                                continue;
 2086                            }
 2087
 2088                            // change from split by | to comma
 2089                            // strip spaces to avoid having to encode
 122090                            var values = value
 122091                                .Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
 2092
 122093                            if (condition.Condition == ProfileConditionType.Equals)
 2094                            {
 02095                                item.SetOption(qualifier, "codectag", string.Join(',', values));
 2096                            }
 122097                            else if (condition.Condition == ProfileConditionType.EqualsAny)
 2098                            {
 122099                                var currentValue = item.GetOption(qualifier, "codectag");
 122100                                if (!string.IsNullOrEmpty(currentValue) && values.Any(v => string.Equals(v, currentValue
 2101                                {
 02102                                    item.SetOption(qualifier, "codectag", currentValue);
 2103                                }
 2104                                else
 2105                                {
 122106                                    item.SetOption(qualifier, "codectag", string.Join(',', values));
 2107                                }
 2108                            }
 2109
 122110                            break;
 2111                        }
 2112
 2113                    case ProfileConditionValue.VideoRotation:
 2114                        {
 02115                            if (string.IsNullOrEmpty(qualifier))
 2116                            {
 2117                                continue;
 2118                            }
 2119
 2120                            // change from split by | to comma
 2121                            // strip spaces to avoid having to encode
 02122                            var values = value
 02123                                .Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
 2124
 02125                            if (condition.Condition == ProfileConditionType.Equals)
 2126                            {
 02127                                item.SetOption(qualifier, "rotation", string.Join(',', values));
 2128                            }
 02129                            else if (condition.Condition == ProfileConditionType.EqualsAny)
 2130                            {
 02131                                var currentValue = item.GetOption(qualifier, "rotation");
 02132                                if (!string.IsNullOrEmpty(currentValue) && values.Any(v => string.Equals(v, currentValue
 2133                                {
 02134                                    item.SetOption(qualifier, "rotation", currentValue);
 2135                                }
 2136                                else
 2137                                {
 02138                                    item.SetOption(qualifier, "rotation", string.Join(',', values));
 2139                                }
 2140                            }
 2141
 02142                            break;
 2143                        }
 2144
 2145                    case ProfileConditionValue.Height:
 2146                        {
 02147                            if (!enableNonQualifiedConditions)
 2148                            {
 2149                                continue;
 2150                            }
 2151
 02152                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 2153                            {
 02154                                if (condition.Condition == ProfileConditionType.Equals)
 2155                                {
 02156                                    item.MaxHeight = num;
 2157                                }
 02158                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 2159                                {
 02160                                    item.MaxHeight = Math.Min(num, item.MaxHeight ?? num);
 2161                                }
 02162                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 2163                                {
 02164                                    item.MaxHeight = Math.Max(num, item.MaxHeight ?? num);
 2165                                }
 2166                            }
 2167
 02168                            break;
 2169                        }
 2170
 2171                    case ProfileConditionValue.VideoBitrate:
 2172                        {
 322173                            if (!enableNonQualifiedConditions)
 2174                            {
 2175                                continue;
 2176                            }
 2177
 322178                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 2179                            {
 322180                                if (condition.Condition == ProfileConditionType.Equals)
 2181                                {
 02182                                    item.VideoBitrate = num;
 2183                                }
 322184                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 2185                                {
 322186                                    item.VideoBitrate = Math.Min(num, item.VideoBitrate ?? num);
 2187                                }
 02188                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 2189                                {
 02190                                    item.VideoBitrate = Math.Max(num, item.VideoBitrate ?? num);
 2191                                }
 2192                            }
 2193
 02194                            break;
 2195                        }
 2196
 2197                    case ProfileConditionValue.VideoFramerate:
 2198                        {
 122199                            if (!enableNonQualifiedConditions)
 2200                            {
 2201                                continue;
 2202                            }
 2203
 122204                            if (float.TryParse(value, CultureInfo.InvariantCulture, out var num))
 2205                            {
 122206                                if (condition.Condition == ProfileConditionType.Equals)
 2207                                {
 02208                                    item.MaxFramerate = num;
 2209                                }
 122210                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 2211                                {
 122212                                    item.MaxFramerate = Math.Min(num, item.MaxFramerate ?? num);
 2213                                }
 02214                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 2215                                {
 02216                                    item.MaxFramerate = Math.Max(num, item.MaxFramerate ?? num);
 2217                                }
 2218                            }
 2219
 02220                            break;
 2221                        }
 2222
 2223                    case ProfileConditionValue.VideoLevel:
 2224                        {
 2182225                            if (string.IsNullOrEmpty(qualifier))
 2226                            {
 2227                                continue;
 2228                            }
 2229
 2182230                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 2231                            {
 2182232                                if (condition.Condition == ProfileConditionType.Equals)
 2233                                {
 02234                                    item.SetOption(qualifier, "level", num.ToString(CultureInfo.InvariantCulture));
 2235                                }
 2182236                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 2237                                {
 2182238                                    item.SetOption(qualifier, "level", Math.Min(num, item.GetTargetVideoLevel(qualifier)
 2239                                }
 02240                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 2241                                {
 02242                                    item.SetOption(qualifier, "level", Math.Max(num, item.GetTargetVideoLevel(qualifier)
 2243                                }
 2244                            }
 2245
 02246                            break;
 2247                        }
 2248
 2249                    case ProfileConditionValue.Width:
 2250                        {
 42251                            if (!enableNonQualifiedConditions)
 2252                            {
 2253                                continue;
 2254                            }
 2255
 42256                            if (int.TryParse(value, CultureInfo.InvariantCulture, out var num))
 2257                            {
 42258                                if (condition.Condition == ProfileConditionType.Equals)
 2259                                {
 02260                                    item.MaxWidth = num;
 2261                                }
 42262                                else if (condition.Condition == ProfileConditionType.LessThanEqual)
 2263                                {
 42264                                    item.MaxWidth = Math.Min(num, item.MaxWidth ?? num);
 2265                                }
 02266                                else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
 2267                                {
 02268                                    item.MaxWidth = Math.Max(num, item.MaxWidth ?? num);
 2269                                }
 2270                            }
 2271
 2272                            break;
 2273                        }
 2274
 2275                    default:
 2276                        break;
 2277                }
 2278            }
 5252279        }
 2280
 2281        private static bool IsAudioContainerSupported(DirectPlayProfile profile, MediaSourceInfo item)
 2282        {
 2283            // Check container type
 02284            if (!profile.SupportsContainer(item.Container))
 2285            {
 02286                return false;
 2287            }
 2288
 2289            // Never direct play audio in matroska when the device only declare support for webm.
 2290            // The first check is not enough because mkv is assumed can be webm.
 2291            // See https://github.com/jellyfin/jellyfin/issues/13344
 02292            return !ContainerHelper.ContainsContainer("mkv", item.Container)
 02293                   || profile.SupportsContainer("mkv");
 2294        }
 2295
 2296        private static bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audi
 2297        {
 02298            if (!IsAudioContainerSupported(profile, item))
 2299            {
 02300                return false;
 2301            }
 2302
 2303            // Check audio codec
 02304            string? audioCodec = audioStream?.Codec;
 02305            if (!profile.SupportsAudioCodec(audioCodec))
 2306            {
 02307                return false;
 2308            }
 2309
 02310            return true;
 2311        }
 2312
 2313        private static bool IsAudioDirectStreamSupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream au
 2314        {
 2315            // Check container type, this should NOT be supported
 2316            // If the container is supported, the file should be directly played
 02317            if (IsAudioContainerSupported(profile, item))
 2318            {
 02319                return false;
 2320            }
 2321
 2322            // Check audio codec, we cannot use the SupportsAudioCodec here
 2323            // Because that one assumes empty container supports all codec, which is just useless
 02324            string? audioCodec = audioStream?.Codec;
 02325            return string.Equals(profile.AudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase)
 02326                   || string.Equals(profile.Container, audioCodec, StringComparison.OrdinalIgnoreCase);
 2327        }
 2328
 2329        private int GetRank(ref TranscodeReason a, TranscodeReason[] rankings)
 2330        {
 12212331            var index = 1;
 88352332            foreach (var flag in rankings)
 2333            {
 37072334                var reason = a & flag;
 37072335                if (reason != 0)
 2336                {
 10212337                    return index;
 2338                }
 2339
 26862340                index++;
 2341            }
 2342
 2002343            return index;
 2344        }
 2345
 2346        /// <summary>
 2347        /// Check the profile conditions.
 2348        /// </summary>
 2349        /// <param name="conditions">Profile conditions.</param>
 2350        /// <param name="mediaSource">Media source.</param>
 2351        /// <param name="videoStream">Video stream.</param>
 2352        /// <returns>Failed profile conditions.</returns>
 2353        private IEnumerable<ProfileCondition> CheckVideoConditions(ProfileCondition[] conditions, MediaSourceInfo mediaS
 2354        {
 7432355            int? width = videoStream?.Width;
 7432356            int? height = videoStream?.Height;
 7432357            int? bitDepth = videoStream?.BitDepth;
 7432358            int? videoBitrate = videoStream?.BitRate;
 7432359            double? videoLevel = videoStream?.Level;
 7432360            string? videoProfile = videoStream?.Profile;
 7432361            VideoRangeType? videoRangeType = videoStream?.VideoRangeType;
 7432362            float videoFramerate = videoStream is null ? 0 : videoStream.ReferenceFrameRate ?? 0;
 7432363            bool? isAnamorphic = videoStream?.IsAnamorphic;
 7432364            bool? isInterlaced = videoStream?.IsInterlaced;
 7432365            string? videoCodecTag = videoStream?.CodecTag;
 7432366            bool? isAvc = videoStream?.IsAVC;
 7432367            int? videoRotation = videoStream?.Rotation;
 2368
 7432369            TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : mediaSource.Time
 7432370            int? packetLength = videoStream?.PacketLength;
 7432371            int? refFrames = videoStream?.RefFrames;
 2372
 7432373            int numStreams = mediaSource.MediaStreams.Count;
 7432374            int? numAudioStreams = mediaSource.GetStreamCount(MediaStreamType.Audio);
 7432375            int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video);
 2376
 7432377            return conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, widt
 2378        }
 2379
 2380        /// <summary>
 2381        /// Check the compatibility of the container.
 2382        /// </summary>
 2383        /// <param name="options">Media options.</param>
 2384        /// <param name="mediaSource">Media source.</param>
 2385        /// <param name="container">Container.</param>
 2386        /// <param name="videoStream">Video stream.</param>
 2387        /// <returns>Transcode reasons if the container is not fully compatible.</returns>
 2388        private TranscodeReason GetCompatibilityContainer(MediaOptions options, MediaSourceInfo mediaSource, string cont
 2389        {
 2582390            var profile = options.Profile;
 2391
 2582392            var failures = AggregateFailureConditions(
 2582393                mediaSource,
 2582394                profile,
 2582395                "VideoCodecProfile",
 2582396                profile.ContainerProfiles
 2582397                    .Where(containerProfile => containerProfile.Type == DlnaProfileType.Video && containerProfile.Contai
 2582398                    .SelectMany(containerProfile => CheckVideoConditions(containerProfile.Conditions, mediaSource, video
 2399
 2582400            return failures;
 2401        }
 2402
 2403        /// <summary>
 2404        /// Check the compatibility of the video codec.
 2405        /// </summary>
 2406        /// <param name="options">Media options.</param>
 2407        /// <param name="mediaSource">Media source.</param>
 2408        /// <param name="container">Container.</param>
 2409        /// <param name="videoStream">Video stream.</param>
 2410        /// <returns>Transcode reasons if the video stream is not fully compatible.</returns>
 2411        private TranscodeReason GetCompatibilityVideoCodec(MediaOptions options, MediaSourceInfo mediaSource, string con
 2412        {
 4102413            var profile = options.Profile;
 2414
 4102415            string videoCodec = videoStream.Codec;
 2416
 4102417            var failures = AggregateFailureConditions(
 4102418                mediaSource,
 4102419                profile,
 4102420                "VideoCodecProfile",
 4102421                profile.CodecProfiles
 4102422                    .Where(codecProfile => codecProfile.Type == CodecType.Video &&
 4102423                        codecProfile.ContainsAnyCodec(videoCodec, container) &&
 4102424                        !CheckVideoConditions(codecProfile.ApplyConditions, mediaSource, videoStream).Any())
 4102425                    .SelectMany(codecProfile => CheckVideoConditions(codecProfile.Conditions, mediaSource, videoStream))
 2426
 4102427            return failures;
 2428        }
 2429
 2430        /// <summary>
 2431        /// Check the compatibility of the audio codec.
 2432        /// </summary>
 2433        /// <param name="options">Media options.</param>
 2434        /// <param name="mediaSource">Media source.</param>
 2435        /// <param name="container">Container.</param>
 2436        /// <param name="audioStream">Audio stream.</param>
 2437        /// <param name="transcodingAudioCodec">Override audio codec.</param>
 2438        /// <param name="isVideo">The media source is video.</param>
 2439        /// <param name="isSecondaryAudio">The audio stream is secondary.</param>
 2440        /// <returns>Transcode reasons if the audio stream is not fully compatible.</returns>
 2441        private TranscodeReason GetCompatibilityAudioCodec(MediaOptions options, MediaSourceInfo mediaSource, string con
 2442        {
 10172443            var profile = options.Profile;
 2444
 10172445            var audioCodec = transcodingAudioCodec ?? audioStream.Codec;
 10172446            var audioProfile = audioStream.Profile;
 10172447            var audioChannels = audioStream.Channels;
 10172448            var audioBitrate = audioStream.BitRate;
 10172449            var audioSampleRate = audioStream.SampleRate;
 10172450            var audioBitDepth = audioStream.BitDepth;
 2451
 10172452            var audioFailureConditions = isVideo
 10172453                ? GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioCodec, audioChannels, audioBi
 10172454                : GetProfileConditionsForAudio(profile.CodecProfiles, container, audioCodec, audioChannels, audioBitrate
 2455
 10172456            var failures = AggregateFailureConditions(mediaSource, profile, "AudioCodecProfile", audioFailureConditions)
 2457
 10172458            return failures;
 2459        }
 2460
 2461        /// <summary>
 2462        /// Check the compatibility of the audio codec for direct playback.
 2463        /// </summary>
 2464        /// <param name="options">Media options.</param>
 2465        /// <param name="mediaSource">Media source.</param>
 2466        /// <param name="container">Container.</param>
 2467        /// <param name="audioStream">Audio stream.</param>
 2468        /// <param name="isVideo">The media source is video.</param>
 2469        /// <param name="isSecondaryAudio">The audio stream is secondary.</param>
 2470        /// <returns>Transcode reasons if the audio stream is not fully compatible for direct playback.</returns>
 2471        private TranscodeReason GetCompatibilityAudioCodecDirect(MediaOptions options, MediaSourceInfo mediaSource, stri
 2472        {
 2772473            var failures = GetCompatibilityAudioCodec(options, mediaSource, container, audioStream, null, isVideo, isSec
 2474
 2772475            if (audioStream.IsExternal)
 2476            {
 62477                failures |= TranscodeReason.AudioIsExternal;
 2478            }
 2479
 2772480            return failures;
 2481        }
 2482    }
 2483}

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)
IsVobSubMksDeliveryProfile(MediaBrowser.Model.Dlna.SubtitleProfile)
IsVobSubMksProfile(MediaBrowser.Model.Dlna.SubtitleProfile,MediaBrowser.Model.Entities.MediaStream)
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)