diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index e9041186f4..bea7a5a0da 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -195,7 +195,7 @@ namespace Emby.Dlna.Didl { var sources = _mediaSourceManager.GetStaticMediaSources(video, true, _user); - streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions + streamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions { ItemId = video.Id, MediaSources = sources.ToArray(), @@ -537,7 +537,7 @@ namespace Emby.Dlna.Didl { var sources = _mediaSourceManager.GetStaticMediaSources(audio, true, _user); - streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions + streamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions { ItemId = audio.Id, MediaSources = sources.ToArray(), diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index 4cda1d8b7a..7b1f942c5a 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -585,7 +585,7 @@ namespace Emby.Dlna.PlayTo { return new PlaylistItem { - StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions + StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions { ItemId = item.Id, MediaSources = mediaSources, @@ -605,7 +605,7 @@ namespace Emby.Dlna.PlayTo { return new PlaylistItem { - StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions + StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions { ItemId = item.Id, MediaSources = mediaSources, diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index e8ce1ca2a2..e0245fe4da 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -181,7 +181,7 @@ namespace Jellyfin.Api.Helpers { var streamBuilder = new StreamBuilder(_mediaEncoder, _logger); - var options = new VideoOptions + var options = new MediaOptions { MediaSources = new[] { mediaSource }, Context = EncodingContext.Streaming, @@ -244,8 +244,8 @@ namespace Jellyfin.Api.Helpers // Beginning of Playback Determination var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); + ? streamBuilder.GetOptimalAudioStream(options) + : streamBuilder.GetOptimalVideoStream(options); if (streamInfo is not null) { diff --git a/MediaBrowser.Model/Dlna/AudioOptions.cs b/MediaBrowser.Model/Dlna/MediaOptions.cs similarity index 79% rename from MediaBrowser.Model/Dlna/AudioOptions.cs rename to MediaBrowser.Model/Dlna/MediaOptions.cs index df4018fdd5..939caf813e 100644 --- a/MediaBrowser.Model/Dlna/AudioOptions.cs +++ b/MediaBrowser.Model/Dlna/MediaOptions.cs @@ -7,11 +7,11 @@ using MediaBrowser.Model.Dto; namespace MediaBrowser.Model.Dlna { /// - /// Class AudioOptions. + /// Class MediaOptions. /// - public class AudioOptions + public class MediaOptions { - public AudioOptions() + public MediaOptions() { Context = EncodingContext.Streaming; @@ -27,8 +27,16 @@ namespace MediaBrowser.Model.Dlna public bool ForceDirectStream { get; set; } + /// + /// Gets or sets an override for allowing stream copy. + /// public bool AllowAudioStreamCopy { get; set; } + /// + /// Gets or sets an override for allowing stream copy. + /// + public bool AllowVideoStreamCopy { get; set; } + public Guid ItemId { get; set; } public MediaSourceInfo[] MediaSources { get; set; } @@ -65,6 +73,16 @@ namespace MediaBrowser.Model.Dlna /// The audio transcoding bitrate. public int? AudioTranscodingBitrate { get; set; } + /// + /// Gets or sets an override for the audio stream index. + /// + public int? AudioStreamIndex { get; set; } + + /// + /// Gets or sets an override for the subtitle stream index. + /// + public int? SubtitleStreamIndex { get; set; } + /// /// Gets the maximum bitrate. /// diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index af35e98eef..00c3234dde 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1,5 +1,4 @@ #nullable disable -#pragma warning disable CS1591 using System; using System.Collections.Generic; @@ -13,6 +12,9 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Model.Dlna { + /// + /// Class StreamBuilder. + /// public class StreamBuilder { // Aliases @@ -24,35 +26,49 @@ namespace MediaBrowser.Model.Dlna private readonly ILogger _logger; private readonly ITranscoderSupport _transcoderSupport; + /// + /// Initializes a new instance of the class. + /// + /// The object. + /// The object. public StreamBuilder(ITranscoderSupport transcoderSupport, ILogger logger) { _transcoderSupport = transcoderSupport; _logger = logger; } + /// + /// Initializes a new instance of the class. + /// + /// The object. public StreamBuilder(ILogger logger) : this(new FullTranscoderSupport(), logger) { } - public StreamInfo BuildAudioItem(AudioOptions options) + /// + /// Gets the optimal audio stream. + /// + /// The object to get the audio stream from. + /// The of the optimal audio stream. + public StreamInfo GetOptimalAudioStream(MediaOptions options) { - ValidateAudioInput(options); + ValidateMediaOptions(options, false); var mediaSources = new List(); - foreach (MediaSourceInfo i in options.MediaSources) + foreach (var mediaSource in options.MediaSources) { if (string.IsNullOrEmpty(options.MediaSourceId) || - string.Equals(i.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)) + string.Equals(mediaSource.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)) { - mediaSources.Add(i); + mediaSources.Add(mediaSource); } } var streams = new List(); foreach (MediaSourceInfo i in mediaSources) { - StreamInfo streamInfo = BuildAudioItem(i, options); + StreamInfo streamInfo = GetOptimalAudioStream(i, options); if (streamInfo is not null) { streams.Add(streamInfo); @@ -68,9 +84,115 @@ namespace MediaBrowser.Model.Dlna return GetOptimalStream(streams, options.GetMaxBitrate(true) ?? 0); } - public StreamInfo BuildVideoItem(VideoOptions options) + private StreamInfo GetOptimalAudioStream(MediaSourceInfo item, MediaOptions options) { - ValidateInput(options); + var playlistItem = new StreamInfo + { + ItemId = options.ItemId, + MediaType = DlnaProfileType.Audio, + MediaSource = item, + RunTimeTicks = item.RunTimeTicks, + Context = options.Context, + DeviceProfile = options.Profile + }; + + if (options.ForceDirectPlay) + { + playlistItem.PlayMethod = PlayMethod.DirectPlay; + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio); + return playlistItem; + } + + if (options.ForceDirectStream) + { + playlistItem.PlayMethod = PlayMethod.DirectStream; + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio); + return playlistItem; + } + + MediaStream audioStream = item.GetDefaultAudioStream(null); + + var directPlayInfo = GetAudioDirectPlayProfile(item, audioStream, options); + + var directPlayMethod = directPlayInfo.PlayMethod; + var transcodeReasons = directPlayInfo.TranscodeReasons; + + var inputAudioChannels = audioStream?.Channels; + var inputAudioBitrate = audioStream?.BitDepth; + var inputAudioSampleRate = audioStream?.SampleRate; + var inputAudioBitDepth = audioStream?.BitDepth; + + if (directPlayMethod.HasValue) + { + var profile = options.Profile; + var audioFailureConditions = GetProfileConditionsForAudio(profile.CodecProfiles, item.Container, audioStream?.Codec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, true); + var audioFailureReasons = AggregateFailureConditions(item, profile, "AudioCodecProfile", audioFailureConditions); + transcodeReasons |= audioFailureReasons; + + if (audioFailureReasons == 0) + { + playlistItem.PlayMethod = directPlayMethod.Value; + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio, directPlayInfo.Profile); + + return playlistItem; + } + } + + TranscodingProfile transcodingProfile = null; + foreach (TranscodingProfile i in options.Profile.TranscodingProfiles) + { + if (i.Type == playlistItem.MediaType + && i.Context == options.Context + && _transcoderSupport.CanEncodeToAudioCodec(i.AudioCodec ?? i.Container)) + { + transcodingProfile = i; + break; + } + } + + if (transcodingProfile != null) + { + if (!item.SupportsTranscoding) + { + return null; + } + + SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile); + + var audioTranscodingConditions = GetProfileConditionsForAudio(options.Profile.CodecProfiles, transcodingProfile.Container, transcodingProfile.AudioCodec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, false).ToArray(); + ApplyTranscodingConditions(playlistItem, audioTranscodingConditions, null, true, true); + + // Honor requested max channels + playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels; + + var configuredBitrate = options.GetMaxBitrate(true); + + long transcodingBitrate = options.AudioTranscodingBitrate ?? + (options.Context == EncodingContext.Streaming ? options.Profile.MusicStreamingTranscodingBitrate : null) ?? + configuredBitrate ?? + 128000; + + if (configuredBitrate.HasValue) + { + transcodingBitrate = Math.Min(configuredBitrate.Value, transcodingBitrate); + } + + var longBitrate = Math.Min(transcodingBitrate, playlistItem.AudioBitrate ?? transcodingBitrate); + playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate); + } + + playlistItem.TranscodeReasons = transcodeReasons; + return playlistItem; + } + + /// + /// Gets the optimal video stream. + /// + /// The object to get the video stream from. + /// The of the optimal video stream. + public StreamInfo GetOptimalVideoStream(MediaOptions options) + { + ValidateMediaOptions(options, true); var mediaSources = new List(); foreach (MediaSourceInfo i in options.MediaSources) @@ -236,6 +358,14 @@ namespace MediaBrowser.Model.Dlna } } + /// + /// Normalizes input container. + /// + /// The input container. + /// The . + /// The . + /// The object to get the video stream from. + /// The the normalized input container. public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type, DirectPlayProfile playProfile = null) { if (string.IsNullOrEmpty(inputContainer)) @@ -264,108 +394,7 @@ namespace MediaBrowser.Model.Dlna return formats[0]; } - private StreamInfo BuildAudioItem(MediaSourceInfo item, AudioOptions options) - { - StreamInfo playlistItem = new StreamInfo - { - ItemId = options.ItemId, - MediaType = DlnaProfileType.Audio, - MediaSource = item, - RunTimeTicks = item.RunTimeTicks, - Context = options.Context, - DeviceProfile = options.Profile - }; - - if (options.ForceDirectPlay) - { - playlistItem.PlayMethod = PlayMethod.DirectPlay; - playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio); - return playlistItem; - } - - if (options.ForceDirectStream) - { - playlistItem.PlayMethod = PlayMethod.DirectStream; - playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio); - return playlistItem; - } - - var audioStream = item.GetDefaultAudioStream(null); - - var directPlayInfo = GetAudioDirectPlayProfile(item, audioStream, options); - - var directPlayMethod = directPlayInfo.PlayMethod; - var transcodeReasons = directPlayInfo.TranscodeReasons; - - int? inputAudioChannels = audioStream?.Channels; - int? inputAudioBitrate = audioStream?.BitDepth; - int? inputAudioSampleRate = audioStream?.SampleRate; - int? inputAudioBitDepth = audioStream?.BitDepth; - - if (directPlayMethod.HasValue) - { - var profile = options.Profile; - var audioFailureConditions = GetProfileConditionsForAudio(profile.CodecProfiles, item.Container, audioStream?.Codec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, true); - var audioFailureReasons = AggregateFailureConditions(item, profile, "AudioCodecProfile", audioFailureConditions); - transcodeReasons |= audioFailureReasons; - - if (audioFailureReasons == 0) - { - playlistItem.PlayMethod = directPlayMethod.Value; - playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio, directPlayInfo.Profile); - - return playlistItem; - } - } - - TranscodingProfile transcodingProfile = null; - foreach (var i in options.Profile.TranscodingProfiles) - { - if (i.Type == playlistItem.MediaType - && i.Context == options.Context - && _transcoderSupport.CanEncodeToAudioCodec(i.AudioCodec ?? i.Container)) - { - transcodingProfile = i; - break; - } - } - - if (transcodingProfile is not null) - { - if (!item.SupportsTranscoding) - { - return null; - } - - SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile); - - var audioTranscodingConditions = GetProfileConditionsForAudio(options.Profile.CodecProfiles, transcodingProfile.Container, transcodingProfile.AudioCodec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, false).ToArray(); - ApplyTranscodingConditions(playlistItem, audioTranscodingConditions, null, true, true); - - // Honor requested max channels - playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels; - - var configuredBitrate = options.GetMaxBitrate(true); - - long transcodingBitrate = options.AudioTranscodingBitrate ?? - (options.Context == EncodingContext.Streaming ? options.Profile.MusicStreamingTranscodingBitrate : null) ?? - configuredBitrate ?? - 128000; - - if (configuredBitrate.HasValue) - { - transcodingBitrate = Math.Min(configuredBitrate.Value, transcodingBitrate); - } - - var longBitrate = Math.Min(transcodingBitrate, playlistItem.AudioBitrate ?? transcodingBitrate); - playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate); - } - - playlistItem.TranscodeReasons = transcodeReasons; - return playlistItem; - } - - private (DirectPlayProfile Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, AudioOptions options) + private (DirectPlayProfile Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, MediaOptions options) { var directPlayProfile = options.Profile.DirectPlayProfiles .FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream)); @@ -388,7 +417,7 @@ namespace MediaBrowser.Model.Dlna // If device requirements are satisfied then allow both direct stream and direct play if (item.SupportsDirectPlay) { - if (IsItemBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectPlay)) + if (!IsBitrateLimitExceeded(item, options.GetMaxBitrate(true) ?? 0)) { if (options.EnableDirectPlay) { @@ -404,7 +433,7 @@ namespace MediaBrowser.Model.Dlna // While options takes the network and other factors into account. Only applies to direct stream if (item.SupportsDirectStream) { - if (IsItemBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream)) + if (!IsBitrateLimitExceeded(item, options.GetMaxBitrate(true) ?? 0)) { if (options.EnableDirectStream) { @@ -427,7 +456,6 @@ namespace MediaBrowser.Model.Dlna var containerSupported = false; var audioSupported = false; var videoSupported = false; - TranscodeReason reasons = 0; foreach (var profile in directPlayProfiles) { @@ -447,6 +475,7 @@ namespace MediaBrowser.Model.Dlna } } + TranscodeReason reasons = 0; if (!containerSupported) { reasons |= TranscodeReason.ContainerNotSupported; @@ -547,7 +576,7 @@ namespace MediaBrowser.Model.Dlna } } - private static void SetStreamInfoOptionsFromDirectPlayProfile(VideoOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile directPlayProfile) + private static void SetStreamInfoOptionsFromDirectPlayProfile(MediaOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile directPlayProfile) { var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile); var protocol = "http"; @@ -562,7 +591,7 @@ namespace MediaBrowser.Model.Dlna playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec); } - private StreamInfo BuildVideoItem(MediaSourceInfo item, VideoOptions options) + private StreamInfo BuildVideoItem(MediaSourceInfo item, MediaOptions options) { ArgumentNullException.ThrowIfNull(item); @@ -601,11 +630,15 @@ namespace MediaBrowser.Model.Dlna var videoStream = item.VideoStream; - var directPlayBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectPlay); - var directStreamBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectStream); - bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayBitrateEligibility == 0); - bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamBitrateEligibility == 0); - var transcodeReasons = directPlayBitrateEligibility | directStreamBitrateEligibility; + var bitrateLimitExceeded = IsBitrateLimitExceeded(item, options.GetMaxBitrate(false) ?? 0); + var isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || !bitrateLimitExceeded); + var isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || !bitrateLimitExceeded); + TranscodeReason transcodeReasons = 0; + + if (bitrateLimitExceeded) + { + transcodeReasons = TranscodeReason.ContainerBitrateExceedsLimit; + } _logger.LogDebug( "Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}", @@ -702,7 +735,7 @@ namespace MediaBrowser.Model.Dlna } } - _logger.LogInformation( + _logger.LogDebug( "StreamBuilder.BuildVideoItem( Profile={0}, Path={1}, AudioStreamIndex={2}, SubtitleStreamIndex={3} ) => ( PlayMethod={4}, TranscodeReason={5} ) {6}", options.Profile.Name ?? "Anonymous Profile", item.Path ?? "Unknown path", @@ -716,7 +749,7 @@ namespace MediaBrowser.Model.Dlna return playlistItem; } - private TranscodingProfile GetVideoTranscodeProfile(MediaSourceInfo item, VideoOptions options, MediaStream videoStream, MediaStream audioStream, IEnumerable candidateAudioStreams, MediaStream subtitleStream, StreamInfo playlistItem) + private TranscodingProfile GetVideoTranscodeProfile(MediaSourceInfo item, MediaOptions options, MediaStream videoStream, MediaStream audioStream, IEnumerable candidateAudioStreams, MediaStream subtitleStream, StreamInfo playlistItem) { if (!(item.SupportsTranscoding || item.SupportsDirectStream)) { @@ -763,7 +796,7 @@ namespace MediaBrowser.Model.Dlna return transcodingProfiles.FirstOrDefault(); } - private void BuildStreamVideoItem(StreamInfo playlistItem, VideoOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable candidateAudioStreams, string container, string videoCodec, string audioCodec) + private void BuildStreamVideoItem(StreamInfo playlistItem, MediaOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable candidateAudioStreams, string container, string videoCodec, string audioCodec) { // Prefer matching video codecs var videoCodecs = ContainerProfile.SplitValue(videoCodec); @@ -867,7 +900,7 @@ namespace MediaBrowser.Model.Dlna // Honor requested max channels playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels; - int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(false) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem); + int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(true) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem); playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate); bool? isSecondaryAudio = audioStream is null ? null : item.IsSecondaryAudio(audioStream); @@ -882,14 +915,14 @@ namespace MediaBrowser.Model.Dlna i.ContainsAnyCodec(audioCodec, container) && i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio))); isFirstAppliedCodecProfile = true; - foreach (var i in appliedAudioConditions) + foreach (var codecProfile in appliedAudioConditions) { var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec); foreach (var transcodingAudioCodec in transcodingAudioCodecs) { - if (i.ContainsAnyCodec(transcodingAudioCodec, container)) + if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container)) { - ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile); + ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile); isFirstAppliedCodecProfile = false; break; } @@ -1050,7 +1083,7 @@ namespace MediaBrowser.Model.Dlna } private (DirectPlayProfile Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile( - VideoOptions options, + MediaOptions options, MediaSourceInfo mediaSource, MediaStream videoStream, MediaStream audioStream, @@ -1237,7 +1270,7 @@ namespace MediaBrowser.Model.Dlna return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons); } - private TranscodeReason CheckVideoAudioStreamDirectPlay(VideoOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream) + private TranscodeReason CheckVideoAudioStreamDirectPlay(MediaOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream) { var profile = options.Profile; var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, mediaSource.IsSecondaryAudio(audioStream)); @@ -1274,23 +1307,17 @@ namespace MediaBrowser.Model.Dlna mediaSource.Path ?? "Unknown path"); } - private TranscodeReason IsBitrateEligibleForDirectPlayback( - MediaSourceInfo item, - long maxBitrate, - VideoOptions options, - PlayMethod playMethod) - { - bool result = IsItemBitrateEligibleForDirectPlayback(item, maxBitrate, playMethod); - if (!result) - { - return TranscodeReason.ContainerBitrateExceedsLimit; - } - else - { - return 0; - } - } - + /// + /// Normalizes input container. + /// + /// The . + /// The of the subtitle stream. + /// The list of supported s. + /// The . + /// The . + /// The output container. + /// The subtitle transoding protocol. + /// The the normalized input container. public static SubtitleProfile GetSubtitleProfile( MediaSourceInfo mediaSource, MediaStream subtitleStream, @@ -1448,14 +1475,8 @@ namespace MediaBrowser.Model.Dlna return null; } - private bool IsItemBitrateEligibleForDirectPlayback(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod) + private bool IsBitrateLimitExceeded(MediaSourceInfo item, long maxBitrate) { - // Don't restrict by bitrate if coming from an external domain - if (item.IsRemote) - { - return true; - } - long requestedMaxBitrate = maxBitrate > 0 ? maxBitrate : 1000000; // If we don't know the bitrate, then force a transcode if requested max bitrate is under 40 mbps @@ -1464,40 +1485,22 @@ namespace MediaBrowser.Model.Dlna if (itemBitrate > requestedMaxBitrate) { _logger.LogDebug( - "Bitrate exceeds {PlayBackMethod} limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}", - playMethod, + "Bitrate exceeds limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}", itemBitrate, requestedMaxBitrate); - return false; + return true; } - return true; + return false; } - private static void ValidateInput(VideoOptions options) - { - ValidateAudioInput(options); - - if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId)) - { - throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested"); - } - - if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId)) - { - throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested"); - } - } - - private static void ValidateAudioInput(AudioOptions options) + private static void ValidateMediaOptions(MediaOptions options, Boolean IsMediaSource) { if (options.ItemId.Equals(default)) { - throw new ArgumentException("ItemId is required"); + ArgumentException.ThrowIfNullOrEmpty(options.DeviceId); } - ArgumentException.ThrowIfNullOrEmpty(options.DeviceId); - if (options.Profile is null) { throw new ArgumentException("Profile is required"); @@ -1507,6 +1510,19 @@ namespace MediaBrowser.Model.Dlna { throw new ArgumentException("MediaSources is required"); } + + if (IsMediaSource) + { + if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId)) + { + throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested"); + } + + if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId)) + { + throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested"); + } + } } private static IEnumerable GetProfileConditionsForVideoAudio( @@ -1824,8 +1840,8 @@ namespace MediaBrowser.Model.Dlna continue; } - // change from split by | to comma - // strip spaces to avoid having to encode + // Change from split by | to comma + // Strip spaces to avoid having to encode var values = value .Split('|', StringSplitOptions.RemoveEmptyEntries); diff --git a/MediaBrowser.Model/Dlna/VideoOptions.cs b/MediaBrowser.Model/Dlna/VideoOptions.cs deleted file mode 100644 index 0cb80af544..0000000000 --- a/MediaBrowser.Model/Dlna/VideoOptions.cs +++ /dev/null @@ -1,16 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Dlna -{ - /// - /// Class VideoOptions. - /// - public class VideoOptions : AudioOptions - { - public int? AudioStreamIndex { get; set; } - - public int? SubtitleStreamIndex { get; set; } - - public bool AllowVideoStreamCopy { get; set; } - } -} diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 5e11a7232d..60be17a741 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -164,7 +164,7 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "") { - var options = await GetVideoOptions(deviceName, mediaSource); + var options = await GetMediaOptions(deviceName, mediaSource); BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol); } @@ -262,7 +262,7 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] public async Task BuildVideoItemWithFirstExplicitStream(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "") { - var options = await GetVideoOptions(deviceName, mediaSource); + var options = await GetMediaOptions(deviceName, mediaSource); options.AudioStreamIndex = 1; options.SubtitleStreamIndex = options.MediaSources[0].MediaStreams.Count - 1; @@ -298,7 +298,7 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "") { - var options = await GetVideoOptions(deviceName, mediaSource); + var options = await GetMediaOptions(deviceName, mediaSource); var streamCount = options.MediaSources[0].MediaStreams.Count; if (streamCount > 0) { @@ -311,7 +311,7 @@ namespace Jellyfin.Model.Tests Assert.Equal(streamInfo?.SubtitleStreamIndex, options.SubtitleStreamIndex); } - private StreamInfo? BuildVideoItemSimpleTest(VideoOptions options, PlayMethod? playMethod, TranscodeReason why, string transcodeMode, string transcodeProtocol) + private StreamInfo? BuildVideoItemSimpleTest(MediaOptions options, PlayMethod? playMethod, TranscodeReason why, string transcodeMode, string transcodeProtocol) { if (string.IsNullOrEmpty(transcodeProtocol)) { @@ -320,28 +320,28 @@ namespace Jellyfin.Model.Tests var builder = GetStreamBuilder(); - var val = builder.BuildVideoItem(options); - Assert.NotNull(val); + var streamInfo = builder.GetOptimalVideoStream(options); + Assert.NotNull(streamInfo); if (playMethod is not null) { - Assert.Equal(playMethod, val.PlayMethod); + Assert.Equal(playMethod, streamInfo.PlayMethod); } - Assert.Equal(why, val.TranscodeReasons); + Assert.Equal(why, streamInfo.TranscodeReasons); var audioStreamIndexInput = options.AudioStreamIndex; - var targetVideoStream = val.TargetVideoStream; - var targetAudioStream = val.TargetAudioStream; + var targetVideoStream = streamInfo.TargetVideoStream; + var targetAudioStream = streamInfo.TargetAudioStream; - var mediaSource = options.MediaSources.First(source => source.Id == val.MediaSourceId); + var mediaSource = options.MediaSources.First(source => source.Id == streamInfo.MediaSourceId); Assert.NotNull(mediaSource); var videoStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Video); var audioStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio); // TODO: Check AudioStreamIndex vs options.AudioStreamIndex var inputAudioStream = mediaSource.GetDefaultAudioStream(audioStreamIndexInput ?? mediaSource.DefaultAudioStreamIndex); - var uri = ParseUri(val); + var uri = ParseUri(streamInfo); if (playMethod == PlayMethod.DirectPlay) { @@ -351,98 +351,99 @@ namespace Jellyfin.Model.Tests // Assert.Contains(uri.Extension, containers); // Check expected video codec (1) - Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec); - Assert.Single(val.TargetVideoCodec); + Assert.Contains(targetVideoStream.Codec, streamInfo.TargetVideoCodec); + Assert.Single(streamInfo.TargetVideoCodec); // Check expected audio codecs (1) - Assert.Contains(targetAudioStream.Codec, val.TargetAudioCodec); - Assert.Single(val.TargetAudioCodec); + Assert.Contains(targetAudioStream.Codec, streamInfo.TargetAudioCodec); + Assert.Single(streamInfo.TargetAudioCodec); // Assert.Single(val.AudioCodecs); if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal)) { - Assert.Equal(val.Container, uri.Extension); + Assert.Equal(streamInfo.Container, uri.Extension); } } else if (playMethod == PlayMethod.DirectStream || playMethod == PlayMethod.Transcode) { - Assert.NotNull(val.Container); - Assert.NotEmpty(val.VideoCodecs); - Assert.NotEmpty(val.AudioCodecs); + Assert.NotNull(streamInfo.Container); + Assert.NotEmpty(streamInfo.VideoCodecs); + Assert.NotEmpty(streamInfo.AudioCodecs); // Check expected container (todo: this could be a test param) if (transcodeProtocol.Equals("http", StringComparison.Ordinal)) { // Assert.Equal("webm", val.Container); - Assert.Equal(val.Container, uri.Extension); + Assert.Equal(streamInfo.Container, uri.Extension); Assert.Equal("stream", uri.Filename); - Assert.Equal("http", val.SubProtocol); + Assert.Equal("http", streamInfo.SubProtocol); } else if (transcodeProtocol.Equals("HLS.mp4", StringComparison.Ordinal)) { - Assert.Equal("mp4", val.Container); + Assert.Equal("mp4", streamInfo.Container); Assert.Equal("m3u8", uri.Extension); Assert.Equal("master", uri.Filename); - Assert.Equal("hls", val.SubProtocol); + Assert.Equal("hls", streamInfo.SubProtocol); } else { - Assert.Equal("ts", val.Container); + Assert.Equal("ts", streamInfo.Container); Assert.Equal("m3u8", uri.Extension); Assert.Equal("master", uri.Filename); - Assert.Equal("hls", val.SubProtocol); + Assert.Equal("hls", streamInfo.SubProtocol); } // Full transcode if (transcodeMode.Equals("Transcode", StringComparison.Ordinal)) { - if ((val.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError)) == 0) + if ((streamInfo.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError)) == 0) { Assert.All( videoStreams, - stream => Assert.DoesNotContain(stream.Codec, val.VideoCodecs)); + stream => Assert.DoesNotContain(stream.Codec, streamInfo.VideoCodecs)); } - // TODO: Fill out tests here + // TODO: fill out tests here } // DirectStream and Remux else { // Check expected video codec (1) - Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec); - Assert.Single(val.TargetVideoCodec); + Assert.Contains(targetVideoStream.Codec, streamInfo.TargetVideoCodec); + Assert.Single(streamInfo.TargetVideoCodec); if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal)) { // Check expected audio codecs (1) if (!targetAudioStream.IsExternal) { - if (val.TranscodeReasons.HasFlag(TranscodeReason.ContainerNotSupported)) + // Check expected audio codecs (1) + if (streamInfo.TranscodeReasons.HasFlag(TranscodeReason.ContainerNotSupported)) { - Assert.Contains(targetAudioStream.Codec, val.AudioCodecs); + Assert.Contains(targetAudioStream.Codec, streamInfo.AudioCodecs); } else { - Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs); + Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs); } } } else if (transcodeMode.Equals("Remux", StringComparison.Ordinal)) { // Check expected audio codecs (1) - Assert.Contains(targetAudioStream.Codec, val.AudioCodecs); - Assert.Single(val.AudioCodecs); + Assert.Contains(targetAudioStream.Codec, streamInfo.AudioCodecs); + Assert.Single(streamInfo.AudioCodecs); } // Video details var videoStream = targetVideoStream; - Assert.False(val.EstimateContentLength); - Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo); - Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, val.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty()); - Assert.Equal(videoStream.Level, val.TargetVideoLevel); - Assert.Equal(videoStream.BitDepth, val.TargetVideoBitDepth); - Assert.InRange(val.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue); + Assert.False(streamInfo.EstimateContentLength); + Assert.Equal(TranscodeSeekInfo.Auto, streamInfo.TranscodeSeekInfo); + Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, streamInfo.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty()); + Assert.Equal(videoStream.Level, streamInfo.TargetVideoLevel); + Assert.Equal(videoStream.BitDepth, streamInfo.TargetVideoBitDepth); + Assert.InRange(streamInfo.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue); // Audio codec not supported if ((why & TranscodeReason.AudioCodecNotSupported) != 0) @@ -453,7 +454,7 @@ namespace Jellyfin.Model.Tests // TODO:fixme if (!targetAudioStream.IsExternal) { - Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs); + Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs); } } @@ -465,7 +466,7 @@ namespace Jellyfin.Model.Tests { if (!stream.IsExternal) { - Assert.DoesNotContain(stream.Codec, val.AudioCodecs); + Assert.DoesNotContain(stream.Codec, streamInfo.AudioCodecs); } }); } @@ -474,14 +475,14 @@ namespace Jellyfin.Model.Tests } else if (playMethod is null) { - Assert.Null(val.SubProtocol); + Assert.Null(streamInfo.SubProtocol); Assert.Equal("stream", uri.Filename); - Assert.False(val.EstimateContentLength); - Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo); + Assert.False(streamInfo.EstimateContentLength); + Assert.Equal(TranscodeSeekInfo.Auto, streamInfo.TranscodeSeekInfo); } - return val; + return streamInfo; } private static async ValueTask TestData(string name) @@ -507,7 +508,7 @@ namespace Jellyfin.Model.Tests return new StreamBuilder(transcodeSupport.Object, logger); } - private static async ValueTask GetVideoOptions(string deviceProfile, params string[] sources) + private static async ValueTask GetMediaOptions(string deviceProfile, params string[] sources) { var mediaSources = sources.Select(src => TestData(src)) .Select(val => val.Result) @@ -516,7 +517,7 @@ namespace Jellyfin.Model.Tests var dp = await TestData(deviceProfile); - return new VideoOptions() + return new MediaOptions() { ItemId = new Guid("11D229B7-2D48-4B95-9F9B-49F6AB75E613"), MediaSourceId = mediaSourceId,