diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index b422eb78c6..75df18204d 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); var profile = playbackInfoDto?.DeviceProfile; - _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile); + _logger.LogDebug("GetPostedPlaybackInfo profile: {@Profile}", profile); if (profile == null) { @@ -225,14 +225,6 @@ namespace Jellyfin.Api.Controllers } } - if (info.MediaSources != null) - { - foreach (var mediaSource in info.MediaSources) - { - _mediaInfoHelper.NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video); - } - } - return info; } diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index bc9527a0bc..6fcafd426c 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -16,6 +16,7 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -223,7 +224,7 @@ namespace Jellyfin.Api.Controllers DeInterlace = false, RequireNonAnamorphic = false, EnableMpegtsM2TsMode = false, - TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), + TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(), Context = EncodingContext.Static, StreamOptions = new Dictionary(), EnableAdaptiveBitrateStreaming = true @@ -254,7 +255,7 @@ namespace Jellyfin.Api.Controllers CopyTimestamps = true, StartTimeTicks = startTimeTicks, SubtitleMethod = SubtitleDeliveryMethod.Embed, - TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), + TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(), Context = EncodingContext.Static }; diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 3fa516043a..31b9798365 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -191,7 +191,9 @@ namespace Jellyfin.Api.Helpers DeviceId = auth.DeviceId, ItemId = item.Id, Profile = profile, - MaxAudioChannels = maxAudioChannels + MaxAudioChannels = maxAudioChannels, + AllowAudioStreamCopy = allowAudioStreamCopy, + AllowVideoStreamCopy = allowVideoStreamCopy }; if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase)) @@ -208,7 +210,7 @@ namespace Jellyfin.Api.Helpers mediaSource.SupportsDirectPlay = false; } - if (!enableDirectStream) + if (!enableDirectStream || !allowVideoStreamCopy) { mediaSource.SupportsDirectStream = false; } @@ -235,168 +237,79 @@ namespace Jellyfin.Api.Helpers user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)); } - // Beginning of Playback Determination: Attempt DirectPlay first - if (mediaSource.SupportsDirectPlay) + options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress); + + if (!options.ForceDirectStream) { + // direct-stream http streaming is currently broken + options.EnableDirectStream = false; + } + + // Beginning of Playback Determination + var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) + ? streamBuilder.BuildAudioItem(options) + : streamBuilder.BuildVideoItem(options); + + if (streamInfo != null) + { + streamInfo.PlaySessionId = playSessionId; + streamInfo.StartPositionTicks = startTimeTicks; + + mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay; + // Players do not handle this being set according to PlayMethod + mediaSource.SupportsDirectStream = options.EnableDirectStream ? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream : streamInfo.PlayMethod == PlayMethod.DirectPlay; + mediaSource.SupportsTranscoding = streamInfo.PlayMethod == PlayMethod.DirectStream || mediaSource.TranscodingContainer != null; + + if (item is Audio) + { + if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) + { + mediaSource.SupportsTranscoding = false; + } + } + else if (item is Video) + { + if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) + && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) + && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) + { + mediaSource.SupportsTranscoding = false; + } + } + if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) { mediaSource.SupportsDirectPlay = false; - } - else - { - var supportsDirectStream = mediaSource.SupportsDirectStream; - - // Dummy this up to fool StreamBuilder - mediaSource.SupportsDirectStream = true; - options.MaxBitrate = maxBitrate; - - if (item is Audio) - { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) - { - options.ForceDirectPlay = true; - } - } - else if (item is Video) - { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) - && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) - && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) - { - options.ForceDirectPlay = true; - } - } - - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); - - if (streamInfo == null || !streamInfo.IsDirectStream) - { - mediaSource.SupportsDirectPlay = false; - } - - // Set this back to what it was - mediaSource.SupportsDirectStream = supportsDirectStream; - - if (streamInfo != null) - { - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; - } - } - } - - if (mediaSource.SupportsDirectStream) - { - if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) - { mediaSource.SupportsDirectStream = false; + + mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); + mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; + mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; + mediaSource.TranscodingContainer = streamInfo.Container; + mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; } else { - options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress); - - if (item is Audio) + if (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream) { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) - { - options.ForceDirectStream = true; - } - } - else if (item is Video) - { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) - && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) - && user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) - { - options.ForceDirectStream = true; - } - } - - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); - - if (streamInfo == null || !streamInfo.IsDirectStream) - { - mediaSource.SupportsDirectStream = false; - } - - if (streamInfo != null) - { - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; - } - } - } - - if (mediaSource.SupportsTranscoding) - { - options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress); - - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); - - if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) - { - if (streamInfo != null) - { - streamInfo.PlaySessionId = playSessionId; - streamInfo.StartPositionTicks = startTimeTicks; + streamInfo.PlayMethod = PlayMethod.Transcode; mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); - mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; - mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; - mediaSource.TranscodingContainer = streamInfo.Container; - mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; - // Do this after the above so that StartPositionTicks is set - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; - } - } - else - { - if (streamInfo != null) - { - streamInfo.PlaySessionId = playSessionId; - - if (streamInfo.PlayMethod == PlayMethod.Transcode) + if (!allowVideoStreamCopy) { - streamInfo.StartPositionTicks = startTimeTicks; - mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); - - if (!allowVideoStreamCopy) - { - mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; - } - - if (!allowAudioStreamCopy) - { - mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; - } - - mediaSource.TranscodingContainer = streamInfo.Container; - mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; + mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; } if (!allowAudioStreamCopy) { mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; } - - mediaSource.TranscodingContainer = streamInfo.Container; - mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; - - // Do this after the above so that StartPositionTicks is set - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; } } + + // Do this after the above so that StartPositionTicks is set + SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; } foreach (var attachment in mediaSource.MediaAttachments) diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index c8762b7c54..49a3948688 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -479,7 +479,7 @@ namespace Jellyfin.Api.Helpers IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec), IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec), HardwareAccelerationType = hardwareAccelerationType, - TranscodeReasons = state.TranscodeReasons + TranscodeReason = state.TranscodeReason }); } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 2e7349f002..261ce915f7 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1799,7 +1799,7 @@ namespace MediaBrowser.Controller.MediaEncoding return false; } - return request.EnableAutoStreamCopy; + return true; } public bool CanStreamCopyAudio(EncodingJobInfo state, MediaStream audioStream, IEnumerable supportedAudioCodecs) @@ -1856,17 +1856,11 @@ namespace MediaBrowser.Controller.MediaEncoding } // Video bitrate must fall within requested value - if (request.AudioBitRate.HasValue) + if (request.AudioBitRate.HasValue + && audioStream.BitDepth.HasValue + && audioStream.BitRate.Value > request.AudioBitRate.Value) { - if (!audioStream.BitRate.HasValue || audioStream.BitRate.Value <= 0) - { - return false; - } - - if (audioStream.BitRate.Value > request.AudioBitRate.Value) - { - return false; - } + return false; } return request.EnableAutoStreamCopy; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index c4affa5678..4f67435908 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; using Jellyfin.Data.Entities; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; @@ -23,7 +24,7 @@ namespace MediaBrowser.Controller.MediaEncoding public int? OutputAudioBitrate; public int? OutputAudioChannels; - private TranscodeReason[] _transcodeReasons = null; + private TranscodeReason? _transcodeReasons = null; public EncodingJobInfo(TranscodingJobType jobType) { @@ -34,25 +35,23 @@ namespace MediaBrowser.Controller.MediaEncoding SupportedSubtitleCodecs = Array.Empty(); } - public TranscodeReason[] TranscodeReasons + public TranscodeReason TranscodeReason { get { - if (_transcodeReasons == null) + if (!_transcodeReasons.HasValue) { if (BaseRequest.TranscodeReasons == null) { - return Array.Empty(); + _transcodeReasons = 0; + return 0; } - _transcodeReasons = BaseRequest.TranscodeReasons - .Split(',') - .Where(i => !string.IsNullOrEmpty(i)) - .Select(v => (TranscodeReason)Enum.Parse(typeof(TranscodeReason), v, true)) - .ToArray(); + _ = Enum.TryParse(BaseRequest.TranscodeReasons, out var reason); + _transcodeReasons = reason; } - return _transcodeReasons; + return _transcodeReasons.Value; } } diff --git a/MediaBrowser.Model/Dlna/AudioOptions.cs b/MediaBrowser.Model/Dlna/AudioOptions.cs index 4d4d8d78cb..33755e7462 100644 --- a/MediaBrowser.Model/Dlna/AudioOptions.cs +++ b/MediaBrowser.Model/Dlna/AudioOptions.cs @@ -27,6 +27,8 @@ namespace MediaBrowser.Model.Dlna public bool ForceDirectStream { get; set; } + public bool AllowAudioStreamCopy { get; set; } + public Guid ItemId { get; set; } public MediaSourceInfo[] MediaSources { get; set; } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 0948952259..444b0e4b20 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -15,6 +15,12 @@ namespace MediaBrowser.Model.Dlna { public class StreamBuilder { + // Aliases + internal const TranscodeReason ContainerReasons = TranscodeReason.ContainerNotSupported | TranscodeReason.ContainerBitrateExceedsLimit; + internal const TranscodeReason AudioReasons = TranscodeReason.AudioCodecNotSupported | TranscodeReason.AudioBitrateNotSupported | TranscodeReason.AudioChannelsNotSupported | TranscodeReason.AudioProfileNotSupported | TranscodeReason.AudioSampleRateNotSupported | TranscodeReason.SecondaryAudioNotSupported | TranscodeReason.AudioBitDepthNotSupported | TranscodeReason.AudioIsExternal; + internal const TranscodeReason VideoReasons = TranscodeReason.VideoCodecNotSupported | TranscodeReason.VideoResolutionNotSupported | TranscodeReason.AnamorphicVideoNotSupported | TranscodeReason.InterlacedVideoNotSupported | TranscodeReason.VideoBitDepthNotSupported | TranscodeReason.VideoBitrateNotSupported | TranscodeReason.VideoFramerateNotSupported | TranscodeReason.VideoLevelNotSupported | TranscodeReason.RefFramesNotSupported; + internal const TranscodeReason DirectStreamReasons = AudioReasons | TranscodeReason.ContainerNotSupported; + private readonly ILogger _logger; private readonly ITranscoderSupport _transcoderSupport; @@ -143,7 +149,7 @@ namespace MediaBrowser.Model.Dlna }).ThenBy(streams.IndexOf); } - private static TranscodeReason? GetTranscodeReasonForFailedCondition(ProfileCondition condition) + private static TranscodeReason GetTranscodeReasonForFailedCondition(ProfileCondition condition) { switch (condition.Property) { @@ -161,7 +167,7 @@ namespace MediaBrowser.Model.Dlna case ProfileConditionValue.Has64BitOffsets: // TODO - return null; + return 0; case ProfileConditionValue.Height: return TranscodeReason.VideoResolutionNotSupported; @@ -171,7 +177,7 @@ namespace MediaBrowser.Model.Dlna case ProfileConditionValue.IsAvc: // TODO - return null; + return 0; case ProfileConditionValue.IsInterlaced: return TranscodeReason.InterlacedVideoNotSupported; @@ -181,15 +187,15 @@ namespace MediaBrowser.Model.Dlna case ProfileConditionValue.NumAudioStreams: // TODO - return null; + return 0; case ProfileConditionValue.NumVideoStreams: // TODO - return null; + return 0; case ProfileConditionValue.PacketLength: // TODO - return null; + return 0; case ProfileConditionValue.RefFrames: return TranscodeReason.RefFramesNotSupported; @@ -217,17 +223,17 @@ namespace MediaBrowser.Model.Dlna case ProfileConditionValue.VideoTimestamp: // TODO - return null; + return 0; case ProfileConditionValue.Width: return TranscodeReason.VideoResolutionNotSupported; default: - return null; + return 0; } } - public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type) + public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type, DirectPlayProfile playProfile = null) { if (string.IsNullOrEmpty(inputContainer)) { @@ -236,16 +242,12 @@ namespace MediaBrowser.Model.Dlna var formats = ContainerProfile.SplitValue(inputContainer); - if (formats.Length == 1) - { - return formats[0]; - } - if (profile != null) { + var playProfiles = playProfile == null ? profile.DirectPlayProfiles : new[] { playProfile }; foreach (var format in formats) { - foreach (var directPlayProfile in profile.DirectPlayProfiles) + foreach (var directPlayProfile in playProfiles) { if (directPlayProfile.Type == type && directPlayProfile.SupportsContainer(format)) @@ -287,69 +289,27 @@ namespace MediaBrowser.Model.Dlna var audioStream = item.GetDefaultAudioStream(null); - var directPlayInfo = GetAudioDirectPlayMethods(item, audioStream, options); + var directPlayInfo = GetAudioDirectPlayProfile(item, audioStream, options); - var directPlayMethods = directPlayInfo.PlayMethods; - var transcodeReasons = directPlayInfo.TranscodeReasons.ToList(); + 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 (directPlayMethods.Any()) + if (directPlayMethod.HasValue) { - string audioCodec = audioStream?.Codec; + 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; - // Make sure audio codec profiles are satisfied - var conditions = new List(); - foreach (var i in options.Profile.CodecProfiles) + if (audioFailureReasons == 0) { - if (i.Type == CodecType.Audio && i.ContainsAnyCodec(audioCodec, item.Container)) - { - bool applyConditions = true; - foreach (ProfileCondition applyCondition in i.ApplyConditions) - { - if (!ConditionProcessor.IsAudioConditionSatisfied(applyCondition, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth)) - { - LogConditionFailure(options.Profile, "AudioCodecProfile", applyCondition, item); - applyConditions = false; - break; - } - } - - if (applyConditions) - { - conditions.AddRange(i.Conditions); - } - } - } - - bool all = true; - foreach (ProfileCondition c in conditions) - { - if (!ConditionProcessor.IsAudioConditionSatisfied(c, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth)) - { - LogConditionFailure(options.Profile, "AudioCodecProfile", c, item); - var transcodeReason = GetTranscodeReasonForFailedCondition(c); - if (transcodeReason.HasValue) - { - transcodeReasons.Add(transcodeReason.Value); - } - - all = false; - break; - } - } - - if (all) - { - if (directPlayMethods.Contains(PlayMethod.DirectStream)) - { - playlistItem.PlayMethod = PlayMethod.DirectStream; - } - - playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio); + playlistItem.PlayMethod = directPlayMethod.Value; + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio, directPlayInfo.Profile); return playlistItem; } @@ -374,45 +334,9 @@ namespace MediaBrowser.Model.Dlna return null; } - SetStreamInfoOptionsFromTranscodingProfile(playlistItem, transcodingProfile); - - var audioCodecProfiles = new List(); - foreach (var i in options.Profile.CodecProfiles) - { - if (i.Type == CodecType.Audio && i.ContainsAnyCodec(transcodingProfile.AudioCodec, transcodingProfile.Container)) - { - audioCodecProfiles.Add(i); - } - - if (audioCodecProfiles.Count >= 1) - { - break; - } - } - - var audioTranscodingConditions = new List(); - foreach (var i in audioCodecProfiles) - { - bool applyConditions = true; - foreach (var applyCondition in i.ApplyConditions) - { - if (!ConditionProcessor.IsAudioConditionSatisfied(applyCondition, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth)) - { - LogConditionFailure(options.Profile, "AudioCodecProfile", applyCondition, item); - applyConditions = false; - break; - } - } - - if (applyConditions) - { - foreach (ProfileCondition c in i.Conditions) - { - audioTranscodingConditions.Add(c); - } - } - } + 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 @@ -434,7 +358,7 @@ namespace MediaBrowser.Model.Dlna playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate); } - playlistItem.TranscodeReasons = transcodeReasons.ToArray(); + playlistItem.TranscodeReasons = transcodeReasons; return playlistItem; } @@ -448,9 +372,9 @@ namespace MediaBrowser.Model.Dlna return options.GetMaxBitrate(isAudio); } - private (IEnumerable PlayMethods, IEnumerable TranscodeReasons) GetAudioDirectPlayMethods(MediaSourceInfo item, MediaStream audioStream, AudioOptions options) + private (DirectPlayProfile Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, AudioOptions options) { - DirectPlayProfile directPlayProfile = options.Profile.DirectPlayProfiles + var directPlayProfile = options.Profile.DirectPlayProfiles .FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream)); if (directPlayProfile == null) @@ -461,64 +385,56 @@ namespace MediaBrowser.Model.Dlna item.Path ?? "Unknown path", audioStream.Codec ?? "Unknown codec"); - return (Enumerable.Empty(), GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles)); + return (null, null, GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles)); } var playMethods = new List(); - var transcodeReasons = new List(); - - // While options takes the network and other factors into account. Only applies to direct stream - if (item.SupportsDirectStream) - { - if (IsAudioEligibleForDirectPlay(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream)) - { - if (options.EnableDirectStream) - { - playMethods.Add(PlayMethod.DirectStream); - } - } - else - { - transcodeReasons.Add(TranscodeReason.ContainerBitrateExceedsLimit); - } - } + TranscodeReason transcodeReasons = 0; // The profile describes what the device supports // If device requirements are satisfied then allow both direct stream and direct play if (item.SupportsDirectPlay) { - if (IsAudioEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, PlayMethod.DirectPlay)) + if (IsItemBitrateEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, PlayMethod.DirectPlay)) { if (options.EnableDirectPlay) { - playMethods.Add(PlayMethod.DirectPlay); + return (directPlayProfile, PlayMethod.DirectPlay, 0); } } else { - transcodeReasons.Add(TranscodeReason.ContainerBitrateExceedsLimit); + transcodeReasons |= TranscodeReason.ContainerBitrateExceedsLimit; } } - if (playMethods.Count > 0) + // While options takes the network and other factors into account. Only applies to direct stream + if (item.SupportsDirectStream) { - transcodeReasons.Clear(); - } - else - { - transcodeReasons = transcodeReasons.Distinct().ToList(); + if (IsItemBitrateEligibleForDirectPlay(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream)) + { + if (options.EnableDirectStream) + { + return (directPlayProfile, PlayMethod.DirectStream, transcodeReasons); + } + } + else + { + transcodeReasons |= TranscodeReason.ContainerBitrateExceedsLimit; + } } - return (playMethods, transcodeReasons); + return (directPlayProfile, null, transcodeReasons); } - private static List GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable directPlayProfiles) + private static TranscodeReason GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable directPlayProfiles) { var mediaType = videoStream == null ? DlnaProfileType.Audio : DlnaProfileType.Video; var containerSupported = false; var audioSupported = false; var videoSupported = false; + TranscodeReason reasons = 0; foreach (var profile in directPlayProfiles) { @@ -541,20 +457,20 @@ namespace MediaBrowser.Model.Dlna var list = new List(); if (!containerSupported) { - list.Add(TranscodeReason.ContainerNotSupported); + reasons |= TranscodeReason.ContainerNotSupported; } if (videoStream != null && !videoSupported) { - list.Add(TranscodeReason.VideoCodecNotSupported); + reasons |= TranscodeReason.VideoCodecNotSupported; } if (audioStream != null && !audioSupported) { - list.Add(TranscodeReason.AudioCodecNotSupported); + reasons |= TranscodeReason.AudioCodecNotSupported; } - return list; + return reasons; } private static int? GetDefaultSubtitleStreamIndex(MediaSourceInfo item, SubtitleProfile[] subtitleProfiles) @@ -599,29 +515,28 @@ namespace MediaBrowser.Model.Dlna return item.DefaultSubtitleStreamIndex; } - private static void SetStreamInfoOptionsFromTranscodingProfile(StreamInfo playlistItem, TranscodingProfile transcodingProfile) + private static void SetStreamInfoOptionsFromTranscodingProfile(MediaSourceInfo item, StreamInfo playlistItem, TranscodingProfile transcodingProfile) { - if (string.IsNullOrEmpty(transcodingProfile.AudioCodec)) + var container = transcodingProfile.Container; + var protocol = transcodingProfile.Protocol; + + item.TranscodingContainer = container; + item.TranscodingSubProtocol = protocol; + + if (playlistItem.PlayMethod == PlayMethod.Transcode) { - playlistItem.AudioCodecs = Array.Empty(); - } - else - { - playlistItem.AudioCodecs = transcodingProfile.AudioCodec.Split(','); + playlistItem.Container = container; + playlistItem.SubProtocol = protocol; } - playlistItem.Container = transcodingProfile.Container; - playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength; playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo; + if (!string.IsNullOrEmpty(transcodingProfile.MaxAudioChannels) + && int.TryParse(transcodingProfile.MaxAudioChannels, NumberStyles.Any, CultureInfo.InvariantCulture, out int transcodingMaxAudioChannels)) + { + playlistItem.TranscodingMaxAudioChannels = transcodingMaxAudioChannels; + } - if (string.IsNullOrEmpty(transcodingProfile.VideoCodec)) - { - playlistItem.VideoCodecs = Array.Empty(); - } - else - { - playlistItem.VideoCodecs = transcodingProfile.VideoCodec.Split(','); - } + playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength; playlistItem.CopyTimestamps = transcodingProfile.CopyTimestamps; playlistItem.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest; @@ -638,14 +553,21 @@ namespace MediaBrowser.Model.Dlna { playlistItem.SegmentLength = transcodingProfile.SegmentLength; } + } - playlistItem.SubProtocol = transcodingProfile.Protocol; + private static void SetStreamInfoOptionsFromDirectPlayProfile(VideoOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile directPlayProfile) + { + var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile); + var protocol = "http"; - if (!string.IsNullOrEmpty(transcodingProfile.MaxAudioChannels) - && int.TryParse(transcodingProfile.MaxAudioChannels, NumberStyles.Any, CultureInfo.InvariantCulture, out int transcodingMaxAudioChannels)) - { - playlistItem.TranscodingMaxAudioChannels = transcodingMaxAudioChannels; - } + item.TranscodingContainer = container; + item.TranscodingSubProtocol = protocol; + + playlistItem.Container = container; + playlistItem.SubProtocol = protocol; + + playlistItem.VideoCodecs = new[] { item.VideoStream.Codec }; + playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec); } private StreamInfo BuildVideoItem(MediaSourceInfo item, VideoOptions options) @@ -674,13 +596,30 @@ namespace MediaBrowser.Model.Dlna playlistItem.AudioStreamIndex = audioStream.Index; } + // Collect candidate audio streams + IEnumerable candidateAudioStreams = audioStream == null ? Array.Empty() : new[] { audioStream }; + if (!options.AudioStreamIndex.HasValue || options.AudioStreamIndex < 0) + { + if (audioStream?.IsDefault == true) + { + candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault); + } + else + { + candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.Language == audioStream?.Language); + } + } + + candidateAudioStreams = candidateAudioStreams.ToArray(); + var videoStream = item.VideoStream; // TODO: This doesn't account for situations where the device is able to handle the media's bitrate, but the connection isn't fast enough - var directPlayEligibilityResult = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, subtitleStream, audioStream, options, PlayMethod.DirectPlay); - var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, subtitleStream, audioStream, options, PlayMethod.DirectStream); - bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult.DirectPlay); - bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamEligibilityResult.DirectPlay); + var directPlayEligibilityResult = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, options, PlayMethod.DirectPlay); + var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectStream); + bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult == 0); + bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directPlayEligibilityResult == 0); + var transcodeReasons = directPlayEligibilityResult | directStreamEligibilityResult; _logger.LogDebug( "Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}", @@ -689,191 +628,301 @@ namespace MediaBrowser.Model.Dlna isEligibleForDirectPlay, isEligibleForDirectStream); - var transcodeReasons = new List(); - + DirectPlayProfile directPlayProfile = null; if (isEligibleForDirectPlay || isEligibleForDirectStream) { // See if it can be direct played - var directPlayInfo = GetVideoDirectPlayProfile(options, item, videoStream, audioStream, isEligibleForDirectStream); + var directPlayInfo = GetVideoDirectPlayProfile(options, item, videoStream, audioStream, candidateAudioStreams, subtitleStream, isEligibleForDirectPlay, isEligibleForDirectStream); var directPlay = directPlayInfo.PlayMethod; + transcodeReasons |= directPlayInfo.TranscodeReasons; if (directPlay != null) { + directPlayProfile = directPlayInfo.Profile; playlistItem.PlayMethod = directPlay.Value; - playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video); + playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile); + playlistItem.VideoCodecs = new[] { videoStream.Codec }; + + if (directPlay == PlayMethod.DirectPlay) + { + playlistItem.SubProtocol = "http"; + + var audioStreamIndex = directPlayInfo.AudioStreamIndex ?? audioStream?.Index; + if (audioStreamIndex.HasValue) + { + playlistItem.AudioStreamIndex = audioStreamIndex; + playlistItem.AudioCodecs = new[] { item.GetMediaStream(MediaStreamType.Audio, audioStreamIndex.Value)?.Codec }; + } + } + else if (directPlay == PlayMethod.DirectStream) + { + playlistItem.AudioStreamIndex = audioStream?.Index; + if (audioStream != null) + { + playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec); + } + + SetStreamInfoOptionsFromDirectPlayProfile(options, item, playlistItem, directPlayProfile); + BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, directPlayProfile.Container, directPlayProfile.VideoCodec, directPlayProfile.AudioCodec); + } if (subtitleStream != null) { - var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value, _transcoderSupport, item.Container, null); + var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value, _transcoderSupport, directPlayProfile.Container, null); playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method; playlistItem.SubtitleFormat = subtitleProfile.Format; } - - return playlistItem; } - transcodeReasons.AddRange(directPlayInfo.TranscodeReasons); + _logger.LogInformation( + "DirectPlay Result for Profile: {0}, Path: {1}, PlayMethod: {2}, AudioStreamIndex: {3}, SubtitleStreamIndex: {4}, Reasons: {5}", + options.Profile.Name ?? "Anonymous Profile", + item.Path ?? "Unknown path", + directPlayInfo.PlayMethod, + directPlayInfo.AudioStreamIndex ?? audioStream?.Index, + playlistItem.SubtitleStreamIndex, + directPlayInfo.TranscodeReasons); } - if (directPlayEligibilityResult.Reason.HasValue) + playlistItem.TranscodeReasons = transcodeReasons; + + if (playlistItem.PlayMethod != PlayMethod.DirectStream || !options.EnableDirectStream) { - transcodeReasons.Add(directPlayEligibilityResult.Reason.Value); - } - - if (directStreamEligibilityResult.Reason.HasValue) - { - transcodeReasons.Add(directStreamEligibilityResult.Reason.Value); - } - - // Can't direct play, find the transcoding profile - TranscodingProfile transcodingProfile = null; - foreach (var i in options.Profile.TranscodingProfiles) - { - if (i.Type == playlistItem.MediaType && i.Context == options.Context) + // Can't direct play, find the transcoding profile + // If we do this for direct-stream we will overwrite the info + var transcodingProfile = GetVideoTranscodeProfile(item, options, videoStream, audioStream, candidateAudioStreams, subtitleStream, playlistItem); + if (transcodingProfile != null) { - transcodingProfile = i; - break; - } - } + SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile); - if (transcodingProfile != null) - { - if (!item.SupportsTranscoding) - { - return null; - } + BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, transcodingProfile.Container, transcodingProfile.VideoCodec, transcodingProfile.AudioCodec); - if (subtitleStream != null) - { - var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode, _transcoderSupport, transcodingProfile.Container, transcodingProfile.Protocol); - - playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method; - playlistItem.SubtitleFormat = subtitleProfile.Format; - playlistItem.SubtitleCodecs = new[] { subtitleProfile.Format }; - } - - playlistItem.PlayMethod = PlayMethod.Transcode; - - SetStreamInfoOptionsFromTranscodingProfile(playlistItem, transcodingProfile); - - var isFirstAppliedCodecProfile = true; - foreach (var i in options.Profile.CodecProfiles) - { - if (i.Type == CodecType.Video && i.ContainsAnyCodec(transcodingProfile.VideoCodec, transcodingProfile.Container)) + if (subtitleStream != null) { - bool applyConditions = true; - foreach (ProfileCondition applyCondition in i.ApplyConditions) - { - int? width = videoStream?.Width; - int? height = videoStream?.Height; - int? bitDepth = videoStream?.BitDepth; - int? videoBitrate = videoStream?.BitRate; - double? videoLevel = videoStream?.Level; - string videoProfile = videoStream?.Profile; - float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; - bool? isAnamorphic = videoStream?.IsAnamorphic; - bool? isInterlaced = videoStream?.IsInterlaced; - string videoCodecTag = videoStream?.CodecTag; - bool? isAvc = videoStream?.IsAVC; + var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode, _transcoderSupport, transcodingProfile.Container, transcodingProfile.Protocol); - TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp; - int? packetLength = videoStream?.PacketLength; - int? refFrames = videoStream?.RefFrames; - - int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio); - int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video); - - if (!ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)) - { - // LogConditionFailure(options.Profile, "VideoCodecProfile.ApplyConditions", applyCondition, item); - applyConditions = false; - break; - } - } - - if (applyConditions) - { - var transcodingVideoCodecs = ContainerProfile.SplitValue(transcodingProfile.VideoCodec); - foreach (var transcodingVideoCodec in transcodingVideoCodecs) - { - if (i.ContainsAnyCodec(transcodingVideoCodec, transcodingProfile.Container)) - { - ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, isFirstAppliedCodecProfile); - isFirstAppliedCodecProfile = false; - } - } - } - } - } - - // Honor requested max channels - playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels; - - int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(false) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem); - playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate); - - isFirstAppliedCodecProfile = true; - foreach (var i in options.Profile.CodecProfiles) - { - if (i.Type == CodecType.VideoAudio && i.ContainsAnyCodec(transcodingProfile.AudioCodec, transcodingProfile.Container)) - { - bool applyConditions = true; - foreach (ProfileCondition applyCondition in i.ApplyConditions) - { - bool? isSecondaryAudio = audioStream == null ? null : item.IsSecondaryAudio(audioStream); - int? inputAudioBitrate = audioStream == null ? null : audioStream.BitRate; - int? audioChannels = audioStream == null ? null : audioStream.Channels; - string audioProfile = audioStream == null ? null : audioStream.Profile; - int? inputAudioSampleRate = audioStream == null ? null : audioStream.SampleRate; - int? inputAudioBitDepth = audioStream == null ? null : audioStream.BitDepth; - - if (!ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)) - { - // LogConditionFailure(options.Profile, "VideoCodecProfile.ApplyConditions", applyCondition, item); - applyConditions = false; - break; - } - } - - if (applyConditions) - { - var transcodingAudioCodecs = ContainerProfile.SplitValue(transcodingProfile.AudioCodec); - foreach (var transcodingAudioCodec in transcodingAudioCodecs) - { - if (i.ContainsAnyCodec(transcodingAudioCodec, transcodingProfile.Container)) - { - ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile); - isFirstAppliedCodecProfile = false; - } - } - } - } - } - - var maxBitrateSetting = options.GetMaxBitrate(false); - // Honor max rate - if (maxBitrateSetting.HasValue) - { - var availableBitrateForVideo = maxBitrateSetting.Value; - - if (playlistItem.AudioBitrate.HasValue) - { - availableBitrateForVideo -= playlistItem.AudioBitrate.Value; + playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method; + playlistItem.SubtitleFormat = subtitleProfile.Format; + playlistItem.SubtitleCodecs = new[] { subtitleProfile.Format }; } - // Make sure the video bitrate is lower than bitrate settings but at least 64k - long currentValue = playlistItem.VideoBitrate ?? availableBitrateForVideo; - var longBitrate = Math.Max(Math.Min(availableBitrateForVideo, currentValue), 64000); - playlistItem.VideoBitrate = longBitrate >= int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate); + if (playlistItem.PlayMethod != PlayMethod.DirectPlay) + { + playlistItem.PlayMethod = PlayMethod.Transcode; + } } } - playlistItem.TranscodeReasons = transcodeReasons.ToArray(); + _logger.LogInformation( + "StreamBuilder.BuildVideoItem( Profile={0}, Path={1}, AudioStreamIndex={2}, SubtitleStreamIndex={3} ) => ( PlayMethod={4}, TranscodeReason={5} ) {6}", + options.Profile.Name ?? "Anonymous Profile", + item.Path ?? "Unknown path", + options.AudioStreamIndex, + options.SubtitleStreamIndex, + playlistItem.PlayMethod, + playlistItem.TranscodeReasons, + playlistItem.ToUrl("media:", "")); + item.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile); return playlistItem; } + private TranscodingProfile GetVideoTranscodeProfile(MediaSourceInfo item, VideoOptions options, MediaStream videoStream, MediaStream audioStream, IEnumerable candidateAudioStreams, MediaStream subtitleStream, StreamInfo playlistItem) + { + if (!(item.SupportsTranscoding || item.SupportsDirectStream)) + { + return null; + } + + var transcodingProfiles = options.Profile.TranscodingProfiles + .Where(i => i.Type == playlistItem.MediaType && i.Context == options.Context); + + if (options.AllowVideoStreamCopy) + { + // prefer direct copy profile + float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; + TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp; + int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio); + int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video); + + transcodingProfiles = transcodingProfiles.ToLookup(transcodingProfile => + { + var videoCodecs = ContainerProfile.SplitValue(transcodingProfile.VideoCodec); + + if (ContainerProfile.ContainsContainer(videoCodecs, item.VideoStream.Codec)) + { + var videoCodec = transcodingProfile.VideoCodec; + var container = transcodingProfile.Container; + var appliedVideoConditions = options.Profile.CodecProfiles + .Where(i => i.Type == CodecType.Video && + i.ContainsAnyCodec(videoCodec, container)) + .Select(i => + i.ApplyConditions.Any(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC))); + var conditionsSatisfied = !appliedVideoConditions.Any() || !appliedVideoConditions.Any(satisfied => !satisfied); + return conditionsSatisfied ? 1 : 2; + } + + return 3; + }) + .OrderBy(lookup => lookup.Key) + .SelectMany(lookup => lookup); + } + + return transcodingProfiles.FirstOrDefault(); + } + + private void BuildStreamVideoItem(StreamInfo playlistItem, VideoOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable candidateAudioStreams, string container, string videoCodec, string audioCodec) + { + // prefer matching video codecs + var videoCodecs = ContainerProfile.SplitValue(videoCodec); + var directVideoCodec = ContainerProfile.ContainsContainer(videoCodecs, videoStream.Codec) ? videoStream.Codec : null; + playlistItem.VideoCodecs = directVideoCodec != null ? new[] { directVideoCodec } : videoCodecs; + + // copy video codec options as a starting point, this applies to transcode and direct-stream + playlistItem.MaxFramerate = videoStream.AverageFrameRate; + var qualifier = videoStream.Codec; + if (videoStream.Level.HasValue) + { + playlistItem.SetOption(qualifier, "level", videoStream.Level.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (videoStream.BitDepth.HasValue) + { + playlistItem.SetOption(qualifier, "videobitdepth", videoStream.BitDepth.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (!string.IsNullOrEmpty(videoStream.Profile)) + { + playlistItem.SetOption(qualifier, "profile", videoStream.Profile.ToLowerInvariant()); + } + + if (videoStream.Level != 0) + { + playlistItem.SetOption(qualifier, "level", videoStream.Level.ToString()); + } + + // prefer matching audio codecs, could do beter here + var audioCodecs = ContainerProfile.SplitValue(audioCodec); + var directAudioStream = candidateAudioStreams.FirstOrDefault(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec)); + playlistItem.AudioCodecs = audioCodecs; + if (directAudioStream != null) + { + audioStream = directAudioStream; + playlistItem.AudioStreamIndex = audioStream.Index; + playlistItem.AudioCodecs = new[] { audioStream.Codec }; + + // copy matching audio codec options + playlistItem.AudioSampleRate = audioStream.SampleRate; + playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels.ToString()); + + if (!string.IsNullOrEmpty(audioStream.Profile)) + { + playlistItem.SetOption(audioStream.Codec, "profile", audioStream.Profile.ToLowerInvariant()); + } + + if (audioStream.Level != 0) + { + playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.ToString()); + } + } + + int? width = videoStream?.Width; + int? height = videoStream?.Height; + int? bitDepth = videoStream?.BitDepth; + int? videoBitrate = videoStream?.BitRate; + double? videoLevel = videoStream?.Level; + string videoProfile = videoStream?.Profile; + float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; + bool? isAnamorphic = videoStream?.IsAnamorphic; + bool? isInterlaced = videoStream?.IsInterlaced; + string videoCodecTag = videoStream?.CodecTag; + bool? isAvc = videoStream?.IsAVC; + + TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp; + int? packetLength = videoStream?.PacketLength; + int? refFrames = videoStream?.RefFrames; + + int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio); + int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video); + + var appliedVideoConditions = options.Profile.CodecProfiles + .Where(i => i.Type == CodecType.Video && + i.ContainsAnyCodec(videoCodec, container) && + i.ApplyConditions.Any(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))); + var isFirstAppliedCodecProfile = true; + foreach (var i in appliedVideoConditions) + { + var transcodingVideoCodecs = ContainerProfile.SplitValue(videoCodec); + foreach (var transcodingVideoCodec in transcodingVideoCodecs) + { + if (i.ContainsAnyCodec(transcodingVideoCodec, container)) + { + ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, isFirstAppliedCodecProfile); + isFirstAppliedCodecProfile = false; + continue; + } + } + } + + // Honor requested max channels + playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels; + + int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(false) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem); + playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate); + + bool? isSecondaryAudio = audioStream == null ? null : item.IsSecondaryAudio(audioStream); + int? inputAudioBitrate = audioStream == null ? null : audioStream.BitRate; + int? audioChannels = audioStream == null ? null : audioStream.Channels; + string audioProfile = audioStream == null ? null : audioStream.Profile; + int? inputAudioSampleRate = audioStream == null ? null : audioStream.SampleRate; + int? inputAudioBitDepth = audioStream == null ? null : audioStream.BitDepth; + + var appliedAudioConditions = options.Profile.CodecProfiles + .Where(i => i.Type == CodecType.Video && + i.ContainsAnyCodec(audioCodec, container) && + i.ApplyConditions.Any(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio))); + isFirstAppliedCodecProfile = true; + foreach (var i in appliedAudioConditions) + { + var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec); + foreach (var transcodingAudioCodec in transcodingAudioCodecs) + { + if (i.ContainsAnyCodec(transcodingAudioCodec, container)) + { + ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile); + isFirstAppliedCodecProfile = false; + break; + } + } + } + + var maxBitrateSetting = options.GetMaxBitrate(false); + // Honor max rate + if (maxBitrateSetting.HasValue) + { + var availableBitrateForVideo = maxBitrateSetting.Value; + + if (playlistItem.AudioBitrate.HasValue) + { + availableBitrateForVideo -= playlistItem.AudioBitrate.Value; + } + + // Make sure the video bitrate is lower than bitrate settings but at least 64k + var currentValue = playlistItem.VideoBitrate ?? availableBitrateForVideo; + playlistItem.VideoBitrate = Math.Clamp(currentValue, 64_000, availableBitrateForVideo); + } + + _logger.LogInformation( + "Transcode Result for Profile: {Profile}, Path: {Path}, PlayMethod: {PlayMethod}, AudioStreamIndex: {AudioStreamIndex}, SubtitleStreamIndex: {SubtitleStreamIndex}, Reasons: {TranscodeReason}", + options.Profile?.Name ?? "Anonymous Profile", + item.Path ?? "Unknown path", + playlistItem?.PlayMethod, + audioStream?.Index, + playlistItem?.SubtitleStreamIndex, + playlistItem?.TranscodeReasons); + } + private static int GetDefaultAudioBitrate(string audioCodec, int? audioChannels) { if (!string.IsNullOrEmpty(audioCodec)) @@ -1000,63 +1049,30 @@ namespace MediaBrowser.Model.Dlna return 7168000; } - private (PlayMethod? PlayMethod, List TranscodeReasons) GetVideoDirectPlayProfile( + private (DirectPlayProfile Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile( VideoOptions options, MediaSourceInfo mediaSource, MediaStream videoStream, MediaStream audioStream, + IEnumerable candidateAudioStreams, + MediaStream subtitleStream, + bool isEligibleForDirectPlay, bool isEligibleForDirectStream) { if (options.ForceDirectPlay) { - return (PlayMethod.DirectPlay, new List()); + return (null, PlayMethod.DirectPlay, audioStream?.Index, 0); } if (options.ForceDirectStream) { - return (PlayMethod.DirectStream, new List()); + return (null, PlayMethod.DirectStream, audioStream?.Index, 0); } DeviceProfile profile = options.Profile; string container = mediaSource.Container; - // See if it can be direct played - DirectPlayProfile directPlay = null; - foreach (var p in profile.DirectPlayProfiles) - { - if (p.Type == DlnaProfileType.Video && IsVideoDirectPlaySupported(p, container, videoStream, audioStream)) - { - directPlay = p; - break; - } - } - - if (directPlay == null) - { - _logger.LogDebug( - "Container: {Container}, Video: {Video}, Audio: {Audio} cannot be direct played by profile: {Profile} for path: {Path}", - container, - videoStream?.Codec ?? "no video", - audioStream?.Codec ?? "no audio", - profile.Name ?? "unknown profile", - mediaSource.Path ?? "unknown path"); - - return (null, GetTranscodeReasonsFromDirectPlayProfile(mediaSource, videoStream, audioStream, profile.DirectPlayProfiles)); - } - - var conditions = new List(); - foreach (var p in profile.ContainerProfiles) - { - if (p.Type == DlnaProfileType.Video - && p.ContainsContainer(container)) - { - foreach (var c in p.Conditions) - { - conditions.Add(c); - } - } - } - + // video int? width = videoStream?.Width; int? height = videoStream?.Height; int? bitDepth = videoStream?.BitDepth; @@ -1068,12 +1084,9 @@ namespace MediaBrowser.Model.Dlna bool? isInterlaced = videoStream?.IsInterlaced; string videoCodecTag = videoStream?.CodecTag; bool? isAvc = videoStream?.IsAVC; - - int? audioBitrate = audioStream?.BitRate; - int? audioChannels = audioStream?.Channels; - string audioProfile = audioStream?.Profile; - int? audioSampleRate = audioStream?.SampleRate; - int? audioBitDepth = audioStream?.BitDepth; + // audio + var defaultLanguage = audioStream?.Language ?? string.Empty; + var defaultMarked = audioStream?.IsDefault ?? false; TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : mediaSource.Timestamp; int? packetLength = videoStream?.PacketLength; @@ -1082,118 +1095,165 @@ namespace MediaBrowser.Model.Dlna int? numAudioStreams = mediaSource.GetStreamCount(MediaStreamType.Audio); int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video); + var checkVideoConditions = (ProfileCondition[] conditions) => + conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)); + // Check container conditions - foreach (ProfileCondition i in conditions) - { - if (!ConditionProcessor.IsVideoConditionSatisfied(i, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)) - { - LogConditionFailure(profile, "VideoContainerProfile", i, mediaSource); + var containerProfileReasons = AggregateFailureConditions( + mediaSource, + profile, + "VideoCodecProfile", + profile.ContainerProfiles + .Where(containerProfile => containerProfile.Type == DlnaProfileType.Video && containerProfile.ContainsContainer(container)) + .SelectMany(containerProfile => checkVideoConditions(containerProfile.Conditions))); - var transcodeReason = GetTranscodeReasonForFailedCondition(i); - var transcodeReasons = transcodeReason.HasValue - ? new List { transcodeReason.Value } - : new List(); - - return (null, transcodeReasons); - } - } - - string videoCodec = videoStream?.Codec; - - conditions = new List(); - foreach (var i in profile.CodecProfiles) - { - if (i.Type == CodecType.Video && i.ContainsAnyCodec(videoCodec, container)) - { - bool applyConditions = true; - foreach (ProfileCondition applyCondition in i.ApplyConditions) + // Check video conditions + var videoCodecProfileReasons = AggregateFailureConditions( + mediaSource, + profile, + "VideoCodecProfile", + profile.CodecProfiles + .Where(codecProfile => codecProfile.Type == CodecType.Video && codecProfile.ContainsAnyCodec(videoStream?.Codec, container)) + .SelectMany(codecProfile => { - if (!ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)) + var failedApplyConditions = checkVideoConditions(codecProfile.ApplyConditions); + if (!failedApplyConditions.Any()) { - // LogConditionFailure(profile, "VideoCodecProfile.ApplyConditions", applyCondition, mediaSource); - applyConditions = false; - break; - } - } - - if (applyConditions) - { - foreach (ProfileCondition c in i.Conditions) - { - conditions.Add(c); - } - } - } - } - - foreach (ProfileCondition i in conditions) - { - if (!ConditionProcessor.IsVideoConditionSatisfied(i, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)) - { - LogConditionFailure(profile, "VideoCodecProfile", i, mediaSource); - - var transcodeReason = GetTranscodeReasonForFailedCondition(i); - var transcodeReasons = transcodeReason.HasValue - ? new List { transcodeReason.Value } - : new List(); - - return (null, transcodeReasons); - } - } - - if (audioStream != null) - { - string audioCodec = audioStream.Codec; - conditions = new List(); - bool? isSecondaryAudio = mediaSource.IsSecondaryAudio(audioStream); - - foreach (var i in profile.CodecProfiles) - { - if (i.Type == CodecType.VideoAudio && i.ContainsAnyCodec(audioCodec, container)) - { - bool applyConditions = true; - foreach (ProfileCondition applyCondition in i.ApplyConditions) - { - if (!ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio)) - { - // LogConditionFailure(profile, "VideoAudioCodecProfile.ApplyConditions", applyCondition, mediaSource); - applyConditions = false; - break; - } + return Array.Empty(); } - if (applyConditions) - { - foreach (ProfileCondition c in i.Conditions) - { - conditions.Add(c); - } - } - } - } + var failedConditions = checkVideoConditions(codecProfile.Conditions); + return failedApplyConditions.Concat(failedConditions); + })); - foreach (ProfileCondition i in conditions) - { - if (!ConditionProcessor.IsVideoAudioConditionSatisfied(i, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio)) - { - LogConditionFailure(profile, "VideoAudioCodecProfile", i, mediaSource); + // Check audiocandidates profile conditions + var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => CheckVideoAudioStreamDirectPlay(options, mediaSource, container, audioStream, defaultLanguage, defaultMarked)); - var transcodeReason = GetTranscodeReasonForFailedCondition(i); - var transcodeReasons = transcodeReason.HasValue - ? new List { transcodeReason.Value } - : new List(); - - return (null, transcodeReasons); - } - } - } - - if (isEligibleForDirectStream && mediaSource.SupportsDirectStream) + TranscodeReason subtitleProfileReasons = 0; + if (subtitleStream != null) { - return (PlayMethod.DirectStream, new List()); + var subtitleProfile = GetSubtitleProfile(mediaSource, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.DirectPlay, _transcoderSupport, container, null); + + if (subtitleProfile.Method != SubtitleDeliveryMethod.Drop + && subtitleProfile.Method != SubtitleDeliveryMethod.External + && subtitleProfile.Method != SubtitleDeliveryMethod.Embed) + { + _logger.LogDebug("Not eligible for {0} due to unsupported subtitles", PlayMethod.DirectPlay); + subtitleProfileReasons |= TranscodeReason.SubtitleCodecNotSupported; + } } - return (null, new List { TranscodeReason.ContainerBitrateExceedsLimit }); + var rankings = new[] { VideoReasons, AudioReasons, ContainerReasons }; + var rank = (ref TranscodeReason a) => + { + var index = 1; + foreach (var flag in rankings) + { + var reason = a & flag; + if (reason != 0) + { + a = reason; + return index; + } + + index++; + } + + return index; + }; + + // Check DirectPlay profiles to see if it can be direct played + var analyzedProfiles = profile.DirectPlayProfiles + .Where(directPlayProfile => directPlayProfile.Type == DlnaProfileType.Video) + .Select((directPlayProfile, order) => + { + TranscodeReason directPlayProfileReasons = 0; + TranscodeReason audioCodecProfileReasons = 0; + + // Check container type + if (!directPlayProfile.SupportsContainer(container)) + { + directPlayProfileReasons |= TranscodeReason.ContainerNotSupported; + } + + // Check video codec + string videoCodec = videoStream?.Codec; + if (!directPlayProfile.SupportsVideoCodec(videoCodec)) + { + directPlayProfileReasons |= TranscodeReason.VideoCodecNotSupported; + } + + // Check audio codec + var selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec)); + if (selectedAudioStream == null) + { + directPlayProfileReasons |= TranscodeReason.AudioCodecNotSupported; + } + else + { + audioCodecProfileReasons = audioStreamMatches.GetValueOrDefault(selectedAudioStream); + } + + var failureReasons = directPlayProfileReasons | containerProfileReasons | videoCodecProfileReasons | audioCodecProfileReasons | subtitleProfileReasons; + var directStreamFailureReasons = failureReasons & (~DirectStreamReasons); + + PlayMethod? playMethod = null; + if (failureReasons == 0 && isEligibleForDirectPlay && mediaSource.SupportsDirectPlay) + { + playMethod = PlayMethod.DirectPlay; + } + else if (directStreamFailureReasons == 0 && isEligibleForDirectStream && mediaSource.SupportsDirectStream && directPlayProfile != null) + { + playMethod = PlayMethod.DirectStream; + } + + var ranked = rank(ref failureReasons); + return (Result: (Profile: directPlayProfile, PlayMethod: playMethod, AudioStreamIndex: selectedAudioStream?.Index, TranscodeReason: failureReasons), Order: order, Rank: ranked); + }) + .OrderByDescending(analysis => analysis.Result.PlayMethod) + .ThenBy(analysis => analysis.Order) + .ToArray() + .ToLookup(analysis => analysis.Result.PlayMethod != null); + + var profileMatch = analyzedProfiles[true] + .Select(analysis => analysis.Result) + .FirstOrDefault(); + if (profileMatch.Profile != null) + { + return profileMatch; + } + + var failureReasons = analyzedProfiles[false].OrderBy(a => a.Result.TranscodeReason).ThenBy(analysis => analysis.Order).FirstOrDefault().Result.TranscodeReason; + if (failureReasons == 0) + { + failureReasons = TranscodeReason.DirectPlayError; + } + + return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons); + } + + private TranscodeReason CheckVideoAudioStreamDirectPlay(VideoOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream, string language, bool isDefault) + { + var profile = options.Profile; + var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, !audioStream.IsDefault); + + var audioStreamFailureReasons = AggregateFailureConditions(mediaSource, profile, "VideoAudioCodecProfile", audioFailureConditions); + if (audioStream?.IsExternal == true) + { + audioStreamFailureReasons |= TranscodeReason.AudioIsExternal; + } + + return audioStreamFailureReasons; + } + + private TranscodeReason AggregateFailureConditions(MediaSourceInfo mediaSource, DeviceProfile profile, string type, IEnumerable conditions) + { + return conditions.Aggregate(0, (reasons, i) => + { + LogConditionFailure(profile, type, i, mediaSource); + var transcodeReasons = GetTranscodeReasonForFailedCondition(i); + return reasons | transcodeReasons; + }); } private void LogConditionFailure(DeviceProfile profile, string type, ProfileCondition condition, MediaSourceInfo mediaSource) @@ -1209,39 +1269,21 @@ namespace MediaBrowser.Model.Dlna mediaSource.Path ?? "Unknown path"); } - private (bool DirectPlay, TranscodeReason? Reason) IsEligibleForDirectPlay( + private TranscodeReason IsEligibleForDirectPlay( MediaSourceInfo item, long maxBitrate, - MediaStream subtitleStream, - MediaStream audioStream, VideoOptions options, PlayMethod playMethod) { - if (subtitleStream != null) - { - var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, playMethod, _transcoderSupport, item.Container, null); - - if (subtitleProfile.Method != SubtitleDeliveryMethod.Drop - && subtitleProfile.Method != SubtitleDeliveryMethod.External - && subtitleProfile.Method != SubtitleDeliveryMethod.Embed) - { - _logger.LogDebug("Not eligible for {0} due to unsupported subtitles", playMethod); - return (false, TranscodeReason.SubtitleCodecNotSupported); - } - } - - bool result = IsAudioEligibleForDirectPlay(item, maxBitrate, playMethod); + bool result = IsItemBitrateEligibleForDirectPlay(item, maxBitrate, playMethod); if (!result) { - return (false, TranscodeReason.ContainerBitrateExceedsLimit); + return TranscodeReason.ContainerBitrateExceedsLimit; } - - if (audioStream?.IsExternal == true) + else { - return (false, TranscodeReason.AudioIsExternal); + return 0; } - - return (true, null); } public static SubtitleProfile GetSubtitleProfile( @@ -1401,7 +1443,7 @@ namespace MediaBrowser.Model.Dlna return null; } - private bool IsAudioEligibleForDirectPlay(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod) + private bool IsItemBitrateEligibleForDirectPlay(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod) { // Don't restrict by bitrate if coming from an external domain if (item.IsRemote) @@ -1465,6 +1507,47 @@ namespace MediaBrowser.Model.Dlna } } + private static IEnumerable GetProfileConditionsForVideoAudio( + IEnumerable codecProfiles, + string container, + string codec, + int? audioChannels, + int? audioBitrate, + int? audioSampleRate, + int? audioBitDepth, + string audioProfile, + bool? isSecondaryAudio) + { + return codecProfiles + .Where(profile => profile.Type == CodecType.VideoAudio && profile.ContainsAnyCodec(codec, container) && + profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio))) + .SelectMany(profile => profile.Conditions) + .Where(condition => !ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio)); + } + + private static IEnumerable GetProfileConditionsForAudio( + IEnumerable codecProfiles, + string container, + string codec, + int? audioChannels, + int? audioBitrate, + int? audioSampleRate, + int? audioBitDepth, + bool checkConditions) + { + var conditions = codecProfiles + .Where(profile => profile.Type == CodecType.Audio && profile.ContainsAnyCodec(codec, container) && + profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth))) + .SelectMany(profile => profile.Conditions); + + if (!checkConditions) + { + return conditions; + } + + return conditions.Where(condition => !ConditionProcessor.IsAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth)); + } + private void ApplyTranscodingConditions(StreamInfo item, IEnumerable conditions, string qualifier, bool enableQualifiedConditions, bool enableNonQualifiedConditions) { foreach (ProfileCondition condition in conditions) @@ -1744,10 +1827,22 @@ namespace MediaBrowser.Model.Dlna var values = value .Split('|', StringSplitOptions.RemoveEmptyEntries); - if (condition.Condition == ProfileConditionType.Equals || condition.Condition == ProfileConditionType.EqualsAny) + if (condition.Condition == ProfileConditionType.Equals) { item.SetOption(qualifier, "profile", string.Join(',', values)); } + else if (condition.Condition == ProfileConditionType.EqualsAny) + { + var currentValue = item.GetOption(qualifier, "profile"); + if (!string.IsNullOrEmpty(currentValue) && values.Any(value => value == currentValue)) + { + item.SetOption(qualifier, "profile", currentValue); + } + else + { + item.SetOption(qualifier, "profile", string.Join(',', values)); + } + } break; } @@ -1905,29 +2000,5 @@ namespace MediaBrowser.Model.Dlna return true; } - - private bool IsVideoDirectPlaySupported(DirectPlayProfile profile, string container, MediaStream videoStream, MediaStream audioStream) - { - // Check container type - if (!profile.SupportsContainer(container)) - { - return false; - } - - // Check video codec - string videoCodec = videoStream?.Codec; - if (!profile.SupportsVideoCodec(videoCodec)) - { - return false; - } - - // Check audio codec - if (audioStream != null && !profile.SupportsAudioCodec(audioStream.Codec)) - { - return false; - } - - return true; - } } } diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index a678c54e7f..79dfff5c24 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -23,7 +23,6 @@ namespace MediaBrowser.Model.Dlna AudioCodecs = Array.Empty(); VideoCodecs = Array.Empty(); SubtitleCodecs = Array.Empty(); - TranscodeReasons = Array.Empty(); StreamOptions = new Dictionary(StringComparer.OrdinalIgnoreCase); } @@ -103,7 +102,7 @@ namespace MediaBrowser.Model.Dlna public string PlaySessionId { get; set; } - public TranscodeReason[] TranscodeReasons { get; set; } + public TranscodeReason TranscodeReasons { get; set; } public Dictionary StreamOptions { get; private set; } @@ -799,7 +798,7 @@ namespace MediaBrowser.Model.Dlna if (!item.IsDirectStream) { - list.Add(new NameValuePair("TranscodeReasons", string.Join(',', item.TranscodeReasons.Distinct()))); + list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.ToString())); } return list; diff --git a/MediaBrowser.Model/Dlna/VideoOptions.cs b/MediaBrowser.Model/Dlna/VideoOptions.cs index 4194f17c6e..0cb80af544 100644 --- a/MediaBrowser.Model/Dlna/VideoOptions.cs +++ b/MediaBrowser.Model/Dlna/VideoOptions.cs @@ -10,5 +10,7 @@ namespace MediaBrowser.Model.Dlna public int? AudioStreamIndex { get; set; } public int? SubtitleStreamIndex { get; set; } + + public bool AllowVideoStreamCopy { get; set; } } } diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs index 049e14333f..bb98488480 100644 --- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs +++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs @@ -109,7 +109,7 @@ namespace MediaBrowser.Model.Dto public int? AnalyzeDurationMs { get; set; } [JsonIgnore] - public TranscodeReason[] TranscodeReasons { get; set; } + public TranscodeReason TranscodeReasons { get; set; } public int? DefaultAudioStreamIndex { get; set; } @@ -161,7 +161,7 @@ namespace MediaBrowser.Model.Dto public MediaStream GetDefaultAudioStream(int? defaultIndex) { - if (defaultIndex.HasValue) + if (defaultIndex.HasValue && defaultIndex != -1) { var val = defaultIndex.Value; diff --git a/MediaBrowser.Model/Properties/AssemblyInfo.cs b/MediaBrowser.Model/Properties/AssemblyInfo.cs index e50baf604e..6bf1eb0c03 100644 --- a/MediaBrowser.Model/Properties/AssemblyInfo.cs +++ b/MediaBrowser.Model/Properties/AssemblyInfo.cs @@ -16,6 +16,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] [assembly: InternalsVisibleTo("Jellyfin.Model.Tests")] +[assembly: InternalsVisibleTo("Jellyfin.Dlna.Tests")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from diff --git a/MediaBrowser.Model/Session/TranscodeReason.cs b/MediaBrowser.Model/Session/TranscodeReason.cs index 3c95df66d5..9da9f3323b 100644 --- a/MediaBrowser.Model/Session/TranscodeReason.cs +++ b/MediaBrowser.Model/Session/TranscodeReason.cs @@ -1,32 +1,44 @@ #pragma warning disable CS1591 +using System; + namespace MediaBrowser.Model.Session { + [Flags] public enum TranscodeReason { - ContainerNotSupported = 0, - VideoCodecNotSupported = 1, - AudioCodecNotSupported = 2, - ContainerBitrateExceedsLimit = 3, - AudioBitrateNotSupported = 4, - AudioChannelsNotSupported = 5, - VideoResolutionNotSupported = 6, - UnknownVideoStreamInfo = 7, - UnknownAudioStreamInfo = 8, - AudioProfileNotSupported = 9, - AudioSampleRateNotSupported = 10, - AnamorphicVideoNotSupported = 11, - InterlacedVideoNotSupported = 12, - SecondaryAudioNotSupported = 13, - RefFramesNotSupported = 14, - VideoBitDepthNotSupported = 15, - VideoBitrateNotSupported = 16, - VideoFramerateNotSupported = 17, - VideoLevelNotSupported = 18, - VideoProfileNotSupported = 19, - AudioBitDepthNotSupported = 20, - SubtitleCodecNotSupported = 21, - DirectPlayError = 22, - AudioIsExternal = 23 + // Primary + ContainerNotSupported = 1 << 0, + VideoCodecNotSupported = 1 << 1, + AudioCodecNotSupported = 1 << 2, + SubtitleCodecNotSupported = 1 << 3, + AudioIsExternal = 1 << 4, + SecondaryAudioNotSupported = 1 << 5, + + // Video Constraints + VideoProfileNotSupported = 1 << 6, + VideoLevelNotSupported = 1 << 7, + VideoResolutionNotSupported = 1 << 8, + VideoBitDepthNotSupported = 1 << 9, + VideoFramerateNotSupported = 1 << 10, + RefFramesNotSupported = 1 << 11, + AnamorphicVideoNotSupported = 1 << 12, + InterlacedVideoNotSupported = 1 << 13, + + // Audio Constraints + AudioChannelsNotSupported = 1 << 14, + AudioProfileNotSupported = 1 << 15, + AudioSampleRateNotSupported = 1 << 16, + AudioBitDepthNotSupported = 1 << 17, + + // Bitrate Constraints + ContainerBitrateExceedsLimit = 1 << 18, + VideoBitrateNotSupported = 1 << 19, + AudioBitrateNotSupported = 1 << 20, + + // Errors + UnknownVideoStreamInfo = 1 << 21, + UnknownAudioStreamInfo = 1 << 22, + DirectPlayError = 1 << 23, } } diff --git a/MediaBrowser.Model/Session/TranscodingInfo.cs b/MediaBrowser.Model/Session/TranscodingInfo.cs index 68ab691f88..f876fa9614 100644 --- a/MediaBrowser.Model/Session/TranscodingInfo.cs +++ b/MediaBrowser.Model/Session/TranscodingInfo.cs @@ -1,17 +1,10 @@ #nullable disable #pragma warning disable CS1591 -using System; - namespace MediaBrowser.Model.Session { public class TranscodingInfo { - public TranscodingInfo() - { - TranscodeReasons = Array.Empty(); - } - public string AudioCodec { get; set; } public string VideoCodec { get; set; } @@ -36,6 +29,6 @@ namespace MediaBrowser.Model.Session public HardwareEncodingType? HardwareAccelerationType { get; set; } - public TranscodeReason[] TranscodeReasons { get; set; } + public TranscodeReason TranscodeReason { get; set; } } } diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonFlagEnumConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonFlagEnumConverter.cs new file mode 100644 index 0000000000..4fa91fa5e1 --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonFlagEnumConverter.cs @@ -0,0 +1,36 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters; + +/// +/// Enum flag to json array converter. +/// +/// The type of enum. +public class JsonFlagEnumConverter : JsonConverter + where T : struct, Enum +{ + private static readonly T[] _enumValues = Enum.GetValues(); + + /// + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + /// + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WriteStartArray(); + foreach (var enumValue in _enumValues) + { + if (value.HasFlag(enumValue)) + { + writer.WriteStringValue(enumValue.ToString()); + } + } + + writer.WriteEndArray(); + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonFlagEnumConverterFactory.cs b/src/Jellyfin.Extensions/Json/Converters/JsonFlagEnumConverterFactory.cs new file mode 100644 index 0000000000..b74caf345d --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonFlagEnumConverterFactory.cs @@ -0,0 +1,24 @@ +using System; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters; + +/// +/// Json flag enum converter factory. +/// +public class JsonFlagEnumConverterFactory : JsonConverterFactory +{ + /// + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsEnum && typeToConvert.IsDefined(typeof(FlagsAttribute)); + } + + /// + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + return (JsonConverter?)Activator.CreateInstance(typeof(JsonFlagEnumConverter<>).MakeGenericType(typeToConvert)); + } +} diff --git a/src/Jellyfin.Extensions/Json/JsonDefaults.cs b/src/Jellyfin.Extensions/Json/JsonDefaults.cs index 2cd89dc3bc..97cbee9710 100644 --- a/src/Jellyfin.Extensions/Json/JsonDefaults.cs +++ b/src/Jellyfin.Extensions/Json/JsonDefaults.cs @@ -36,6 +36,7 @@ namespace Jellyfin.Extensions.Json new JsonGuidConverter(), new JsonNullableGuidConverter(), new JsonVersionConverter(), + new JsonFlagEnumConverterFactory(), new JsonStringEnumConverter(), new JsonNullableStructConverterFactory(), new JsonBoolNumberConverter(), diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj index 4918e2e828..8cead4bf25 100644 --- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj +++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj @@ -14,6 +14,12 @@ + + + PreserveNewest + + + diff --git a/tests/Jellyfin.Dlna.Tests/StreamBuilderTests.cs b/tests/Jellyfin.Dlna.Tests/StreamBuilderTests.cs new file mode 100644 index 0000000000..c645ca9a63 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/StreamBuilderTests.cs @@ -0,0 +1,466 @@ +using System; +using System.Collections.Specialized; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Text.Json; +using System.Threading.Tasks; +using Jellyfin.Extensions.Json; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Session; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; + +namespace Jellyfin.MediaBrowser.Model.Tests +{ + public class StreamBuilderTests + { + [Theory] + // Chrome + [InlineData("Chrome", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + // Firefox + [InlineData("Firefox", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + // Safari + [InlineData("SafariNext", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450 + [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450 + [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450 + // AndroidPixel + [InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("AndroidPixel", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("AndroidPixel", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("AndroidPixel", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("AndroidPixel", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("AndroidPixel", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + // Yatse + [InlineData("Yatse", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] + [InlineData("Yatse", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + // RokuSSPlus + [InlineData("RokuSSPlus", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 should be DirectPlay + [InlineData("RokuSSPlus", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("RokuSSPlus", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + // JellyfinMediaPlayer + [InlineData("JellyfinMediaPlayer", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("JellyfinMediaPlayer", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("JellyfinMediaPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("JellyfinMediaPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("JellyfinMediaPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("JellyfinMediaPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + // Chrome-NoHLS + [InlineData("Chrome-NoHLS", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome-NoHLS", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 + [InlineData("Chrome-NoHLS", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome-NoHLS", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")] + [InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")] + [InlineData("Chrome-NoHLS", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome-NoHLS", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome-NoHLS", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + // TranscodeMedia + [InlineData("TranscodeMedia", "mp4-h264-aac-vtt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")] + [InlineData("TranscodeMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")] + [InlineData("TranscodeMedia", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "HLS.mp4")] + [InlineData("TranscodeMedia", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")] + [InlineData("TranscodeMedia", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "HLS.mp4")] + [InlineData("TranscodeMedia", "mkv-av1-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "http")] + [InlineData("TranscodeMedia", "mkv-av1-vorbis-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "http")] + [InlineData("TranscodeMedia", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "http")] + [InlineData("TranscodeMedia", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "DirectStream", "http")] + [InlineData("TranscodeMedia", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.DirectPlayError, "Remux", "http")] + // DirectMedia + [InlineData("DirectMedia", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] + [InlineData("DirectMedia", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] + [InlineData("DirectMedia", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] + [InlineData("DirectMedia", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] + [InlineData("DirectMedia", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] + [InlineData("DirectMedia", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] + [InlineData("DirectMedia", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] + [InlineData("DirectMedia", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] + [InlineData("DirectMedia", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] + // LowBandwidth + [InlineData("LowBandwidth", "mp4-h264-aac-vtt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("LowBandwidth", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("LowBandwidth", "mp4-h264-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("LowBandwidth", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("LowBandwidth", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("LowBandwidth", "mkv-vp9-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("LowBandwidth", "mkv-vp9-ac3-srt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("LowBandwidth", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + // Null + [InlineData("Null", "mp4-h264-aac-vtt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + [InlineData("Null", "mp4-h264-ac3-aac-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + [InlineData("Null", "mp4-h264-ac3-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + [InlineData("Null", "mp4-hevc-aac-srt-15200k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + [InlineData("Null", "mp4-hevc-ac3-aac-srt-15200k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + [InlineData("Null", "mkv-vp9-aac-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + [InlineData("Null", "mkv-vp9-ac3-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + [InlineData("Null", "mkv-vp9-vorbis-vtt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)] + 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); + BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol); + } + + [Theory] + // Chrome + [InlineData("Chrome", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + // Firefox + [InlineData("Firefox", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + // Safari + [InlineData("SafariNext", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450 + [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450 + [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450 + // AndroidPixel + [InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("AndroidPixel", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("AndroidPixel", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("AndroidPixel", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + [InlineData("AndroidPixel", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] + // Yatse + [InlineData("Yatse", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] + [InlineData("Yatse", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + // RokuSSPlus + [InlineData("RokuSSPlus", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 should be DirectPlay + [InlineData("RokuSSPlus", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("RokuSSPlus", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("RokuSSPlus", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + [InlineData("RokuSSPlus", "mp4-hevc-ac3-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 + // JellyfinMediaPlayer + [InlineData("JellyfinMediaPlayer", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("JellyfinMediaPlayer", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("JellyfinMediaPlayer", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // #6450 + [InlineData("JellyfinMediaPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.ContainerBitrateExceedsLimit, "Transcode")] // #6450 + [InlineData("JellyfinMediaPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("JellyfinMediaPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("JellyfinMediaPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // #6450 + 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); + options.AudioStreamIndex = 1; + options.SubtitleStreamIndex = options.MediaSources[0].MediaStreams.Count - 1; + + var streamInfo = BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol); + Assert.Equal(streamInfo?.AudioStreamIndex, options.AudioStreamIndex); + Assert.Equal(streamInfo?.SubtitleStreamIndex, options.SubtitleStreamIndex); + } + + [Theory] + // Chrome + [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 + [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + // Firefox + [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 + [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] + // Yatse + [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + // RokuSSPlus + [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + [InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 + 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 streamCount = options.MediaSources[0].MediaStreams.Count; + options.AudioStreamIndex = streamCount - 2; + options.SubtitleStreamIndex = streamCount - 1; + + var streamInfo = BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol); + Assert.Equal(streamInfo?.AudioStreamIndex, options.AudioStreamIndex); + Assert.Equal(streamInfo?.SubtitleStreamIndex, options.SubtitleStreamIndex); + } + + private StreamInfo? BuildVideoItemSimpleTest(VideoOptions options, PlayMethod? playMethod, TranscodeReason why, string transcodeMode, string transcodeProtocol) + { + if (string.IsNullOrEmpty(transcodeProtocol)) + { + transcodeProtocol = playMethod == PlayMethod.DirectStream ? "http" : "HLS.ts"; + } + + var builder = GetStreamBuilder(); + + var val = builder.BuildVideoItem(options); + Assert.NotNull(val); + + if (playMethod != null) + { + Assert.Equal(playMethod, val.PlayMethod); + } + + Assert.Equal(why, val.TranscodeReasons); + + var audioStreamIndexInput = options.AudioStreamIndex; + var targetVideoStream = val.TargetVideoStream; + var targetAudioStream = val.TargetAudioStream; + + var mediaSource = options.MediaSources.First(source => source.Id == val.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); + + if (playMethod == PlayMethod.DirectPlay) + { + // check expected container + var containers = ContainerProfile.SplitValue(mediaSource.Container); + // TODO: test transcode too + // Assert.Contains(uri.Extension, containers); + + // check expected video codec (1) + Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec); + Assert.Single(val.TargetVideoCodec); + + // check expected audio codecs (1) + Assert.Contains(targetAudioStream.Codec, val.TargetAudioCodec); + Assert.Single(val.TargetAudioCodec); + // Assert.Single(val.AudioCodecs); + + if (transcodeMode == "DirectStream") + { + Assert.Equal(val.Container, uri.Extension); + } + } + else if (playMethod == PlayMethod.DirectStream || playMethod == PlayMethod.Transcode) + { + Assert.NotNull(val.Container); + Assert.NotEmpty(val.VideoCodecs); + Assert.NotEmpty(val.AudioCodecs); + + // check expected container (todo: this could be a test param) + if (transcodeProtocol == "http") + { + // Assert.Equal("webm", val.Container); + Assert.Equal(val.Container, uri.Extension); + Assert.Equal("stream", uri.Filename); + Assert.Equal("http", val.SubProtocol); + } + else if (transcodeProtocol == "HLS.mp4") + { + Assert.Equal("mp4", val.Container); + Assert.Equal("m3u8", uri.Extension); + Assert.Equal("master", uri.Filename); + Assert.Equal("hls", val.SubProtocol); + } + else + { + Assert.Equal("ts", val.Container); + Assert.Equal("m3u8", uri.Extension); + Assert.Equal("master", uri.Filename); + Assert.Equal("hls", val.SubProtocol); + } + + // Full transcode + if (transcodeMode == "Transcode") + { + if ((val.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError)) == 0) + { + Assert.All( + videoStreams, + stream => Assert.DoesNotContain(stream.Codec, val.VideoCodecs)); + } + + // todo: fill out tests here + } + + // DirectStream and Remux + else + { + // check expected video codec (1) + Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec); + Assert.Single(val.TargetVideoCodec); + + if (transcodeMode == "DirectStream") + { + if (!targetAudioStream.IsExternal) + { + // check expected audio codecs (1) + Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs); + } + } + else if (transcodeMode == "Remux") + { + // check expected audio codecs (1) + Assert.Contains(targetAudioStream.Codec, val.AudioCodecs); + Assert.Single(val.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); + + // audio codec not supported + if ((why & TranscodeReason.AudioCodecNotSupported) != 0) + { + // audio stream specified + if (options.AudioStreamIndex >= 0) + { + // TODO:fixme + if (!targetAudioStream.IsExternal) + { + Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs); + } + } + + // audio stream not specified + else + { + // TODO:fixme + Assert.All(audioStreams, stream => + { + if (!stream.IsExternal) + { + Assert.DoesNotContain(stream.Codec, val.AudioCodecs); + } + }); + } + } + } + } + else if (playMethod == null) + { + Assert.Null(val.SubProtocol); + Assert.Equal("stream", uri.Filename); + + Assert.False(val.EstimateContentLength); + Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo); + } + + return val; + } + + private static async ValueTask TestData(string name) + { + var path = Path.Join("Test Data", typeof(T).Name + "-" + name + ".json"); + using (var stream = File.OpenRead(path)) + { + var value = await JsonSerializer.DeserializeAsync(stream, JsonDefaults.Options); + if (value != null) + { + return value; + } + + throw new SerializationException("Invalid test data: " + name); + } + } + + private StreamBuilder GetStreamBuilder() + { + var transcodeSupport = new Mock(); + var logger = new NullLogger(); + + return new StreamBuilder(transcodeSupport.Object, logger); + } + + private static async ValueTask GetVideoOptions(string deviceProfile, params string[] sources) + { + var mediaSources = sources.Select(src => TestData(src)) + .Select(val => val.Result) + .ToArray(); + var mediaSourceId = mediaSources[0]?.Id; + + var dp = await TestData(deviceProfile); + + return new VideoOptions() + { + ItemId = new Guid("11D229B7-2D48-4B95-9F9B-49F6AB75E613"), + MediaSourceId = mediaSourceId, + MediaSources = mediaSources, + DeviceId = "test-deviceId", + Profile = dp, + AllowAudioStreamCopy = true, + AllowVideoStreamCopy = true, + }; + } + + private static (string Path, NameValueCollection Query, string Filename, string Extension) ParseUri(StreamInfo val) + { + var href = val.ToUrl("media:", "ACCESSTOKEN").Split("?", 2); + var path = href[0]; + + var queryString = href.ElementAtOrDefault(1); + var query = string.IsNullOrEmpty(queryString) ? System.Web.HttpUtility.ParseQueryString(queryString ?? string.Empty) : new NameValueCollection(); + + var filename = System.IO.Path.GetFileNameWithoutExtension(path); + var extension = System.IO.Path.GetExtension(path); + if (extension.Length > 0) + { + extension = extension.Substring(1); + } + + return (path, query, filename, extension); + } + } +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-AndroidPixel.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-AndroidPixel.json new file mode 100644 index 0000000000..68ce3ea4ab --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-AndroidPixel.json @@ -0,0 +1,332 @@ +{ + "Name": "Jellyfin Android", + "EnableAlbumArtInDidl": false, + "EnableSingleAlbumArtLimit": false, + "EnableSingleSubtitleLimit": false, + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxAlbumArtWidth": 2147483647, + "MaxAlbumArtHeight": 2147483647, + "MaxStreamingBitrate": 8000000, + "MaxStaticBitrate": 8000000, + "MusicStreamingTranscodingBitrate": 128000, + "TimelineOffsetSeconds": 0, + "RequiresPlainVideoItems": false, + "RequiresPlainFolders": false, + "EnableMSMediaReceiverRegistrar": false, + "IgnoreTranscodeByteRangeRequests": false, + "DirectPlayProfiles": [ + { + "Container": "mp4", + "AudioCodec": "mp3,aac,alac,ac3", + "VideoCodec": "h263,mpeg4,h264,hevc,av1", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp4", + "AudioCodec": "mp3,aac,alac,ac3", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "fmp4", + "AudioCodec": "mp3,aac,ac3,eac3", + "VideoCodec": "h263,mpeg4,h264,hevc,av1", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "fmp4", + "AudioCodec": "mp3,aac,ac3,eac3", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webm", + "AudioCodec": "vorbis,opus", + "VideoCodec": "vp8,vp9,av1", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "webm", + "AudioCodec": "vorbis,opus", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "mkv", + "AudioCodec": "pcm_s8,pcm_s16be,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_alaw,pcm_mulaw,mp3,aac,vorbis,opus,flac,alac,ac3,eac3,dts,mlp,truehd", + "VideoCodec": "h263,mpeg4,h264,hevc,av1,vp8,vp9,av1", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mkv", + "AudioCodec": "pcm_s8,pcm_s16be,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_alaw,pcm_mulaw,mp3,aac,vorbis,opus,flac,alac,ac3,eac3,dts,mlp,truehd", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp3", + "AudioCodec": "mp3", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "ogg", + "AudioCodec": "vorbis,opus,flac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "wav", + "AudioCodec": "pcm_s8,pcm_s16be,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_alaw,pcm_mulaw", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "mpegts", + "AudioCodec": "pcm_s8,pcm_s16be,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_alaw,pcm_mulaw,mp3,aac,ac3,eac3,dts,mlp,truehd", + "VideoCodec": "mpeg4,h264,hevc", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mpegts", + "AudioCodec": "pcm_s8,pcm_s16be,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_alaw,pcm_mulaw,mp3,aac,ac3,eac3,dts,mlp,truehd", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "flv", + "AudioCodec": "mp3,aac", + "VideoCodec": "mpeg4,h264", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "flv", + "AudioCodec": "mp3,aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "aac", + "AudioCodec": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "flac", + "AudioCodec": "flac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "3gp", + "AudioCodec": "3gpp,aac,flac", + "VideoCodec": "h263,mpeg4,h264,hevc", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "3gp", + "AudioCodec": "3gpp,aac,flac", + "Type": "Audio", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Container": "ts", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "mp1,mp2,mp3,aac,ac3,eac3,dts,mlp,truehd", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mkv", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "pcm_s8,pcm_s16be,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_alaw,pcm_mulaw,mp1,mp2,mp3,aac,vorbis,opus,flac,alac,ac3,eac3,dts,mlp,truehd", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "ContainerProfiles": [ + { + "Type": "Video", + "Container": "mp4", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "mp4", + "$type": "ContainerProfile" + }, + { + "Type": "Video", + "Container": "fmp4", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "fmp4", + "$type": "ContainerProfile" + }, + { + "Type": "Video", + "Container": "webm", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "webm", + "$type": "ContainerProfile" + }, + { + "Type": "Video", + "Container": "mkv", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "mkv", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "mp3", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "ogg", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "wav", + "$type": "ContainerProfile" + }, + { + "Type": "Video", + "Container": "mpegts", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "mpegts", + "$type": "ContainerProfile" + }, + { + "Type": "Video", + "Container": "flv", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "flv", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "aac", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "flac", + "$type": "ContainerProfile" + }, + { + "Type": "Video", + "Container": "3gp", + "$type": "ContainerProfile" + }, + { + "Type": "Audio", + "Container": "3gp", + "$type": "ContainerProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "srt", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "subrip", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ttml", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "subrip", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ttml", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "vtt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "webvtt", + "Method": "External", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Chrome-NoHLS.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Chrome-NoHLS.json new file mode 100644 index 0000000000..5d1f5f1620 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Chrome-NoHLS.json @@ -0,0 +1,430 @@ +{ + "EnableAlbumArtInDidl": false, + "EnableSingleAlbumArtLimit": false, + "EnableSingleSubtitleLimit": false, + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxAlbumArtWidth": 0, + "MaxAlbumArtHeight": 0, + "MaxStreamingBitrate": 120000000, + "MaxStaticBitrate": 100000000, + "MusicStreamingTranscodingBitrate": 384000, + "TimelineOffsetSeconds": 0, + "RequiresPlainVideoItems": false, + "RequiresPlainFolders": false, + "EnableMSMediaReceiverRegistrar": false, + "IgnoreTranscodeByteRangeRequests": false, + "DirectPlayProfiles": [ + { + "Container": "webm", + "AudioCodec": "vorbis,opus", + "VideoCodec": "vp8,vp9,av1", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp4,m4v", + "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", + "VideoCodec": "h264,vp8,vp9,av1", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mov", + "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", + "VideoCodec": "h264", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "opus", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webm", + "AudioCodec": "opus", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4a", + "AudioCodec": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4b", + "AudioCodec": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "flac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4a", + "AudioCodec": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4b", + "AudioCodec": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webma", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webm", + "AudioCodec": "webma", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "ogg", + "Type": "Audio", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "opus", + "Type": "Audio", + "AudioCodec": "opus", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "AudioCodec": "wav", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "opus", + "Type": "Audio", + "AudioCodec": "opus", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "AudioCodec": "wav", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp4", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "webm", + "Type": "Video", + "VideoCodec": "vp8,vp9,av1,vpx", + "AudioCodec": "vorbis,opus", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp4", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "CodecProfiles": [ + { + "Type": "VideoAudio", + "Conditions": [ + { + "Condition": "Equals", + "Property": "IsSecondaryAudio", + "Value": "false", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "aac", + "$type": "CodecProfile" + }, + { + "Type": "VideoAudio", + "Conditions": [ + { + "Condition": "Equals", + "Property": "IsSecondaryAudio", + "Value": "false", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "$type": "CodecProfile" + }, + { + "Type": "Video", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "high|main|baseline|constrained baseline|high 10", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "52", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "NotEquals", + "Property": "IsInterlaced", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "h264", + "$type": "CodecProfile" + }, + { + "Type": "Video", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "main", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "120", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "NotEquals", + "Property": "IsInterlaced", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "hevc", + "$type": "CodecProfile" + } + ], + "ResponseProfiles": [ + { + "Container": "m4v", + "Type": "Video", + "MimeType": "video/mp4", + "$type": "ResponseProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "vtt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "External", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Chrome.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Chrome.json new file mode 100644 index 0000000000..81bb97ac82 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Chrome.json @@ -0,0 +1,448 @@ +{ + "EnableAlbumArtInDidl": false, + "EnableSingleAlbumArtLimit": false, + "EnableSingleSubtitleLimit": false, + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxAlbumArtWidth": 0, + "MaxAlbumArtHeight": 0, + "MaxStreamingBitrate": 120000000, + "MaxStaticBitrate": 100000000, + "MusicStreamingTranscodingBitrate": 384000, + "TimelineOffsetSeconds": 0, + "RequiresPlainVideoItems": false, + "RequiresPlainFolders": false, + "EnableMSMediaReceiverRegistrar": false, + "IgnoreTranscodeByteRangeRequests": false, + "DirectPlayProfiles": [ + { + "Container": "webm", + "AudioCodec": "vorbis,opus", + "VideoCodec": "vp8,vp9,av1", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp4,m4v", + "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", + "VideoCodec": "h264,vp8,vp9,av1", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mov", + "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", + "VideoCodec": "h264", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "opus", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webm", + "AudioCodec": "opus", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4a", + "AudioCodec": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4b", + "AudioCodec": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "flac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4a", + "AudioCodec": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4b", + "AudioCodec": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webma", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webm", + "AudioCodec": "webma", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "ogg", + "Type": "Audio", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Container": "ts", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 2, + "SegmentLength": 0, + "BreakOnNonKeyFrames": true, + "$type": "TranscodingProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "opus", + "Type": "Audio", + "AudioCodec": "opus", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "AudioCodec": "wav", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "opus", + "Type": "Audio", + "AudioCodec": "opus", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "AudioCodec": "wav", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "ts", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "aac,mp3", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 2, + "SegmentLength": 0, + "BreakOnNonKeyFrames": true, + "$type": "TranscodingProfile" + }, + { + "Container": "webm", + "Type": "Video", + "VideoCodec": "vp8,vp9,av1,vpx", + "AudioCodec": "vorbis,opus", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp4", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "CodecProfiles": [ + { + "Type": "VideoAudio", + "Conditions": [ + { + "Condition": "Equals", + "Property": "IsSecondaryAudio", + "Value": "false", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "aac", + "$type": "CodecProfile" + }, + { + "Type": "VideoAudio", + "Conditions": [ + { + "Condition": "Equals", + "Property": "IsSecondaryAudio", + "Value": "false", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "$type": "CodecProfile" + }, + { + "Type": "Video", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "high|main|baseline|constrained baseline|high 10", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "52", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "NotEquals", + "Property": "IsInterlaced", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "h264", + "$type": "CodecProfile" + }, + { + "Type": "Video", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "main", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "120", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "NotEquals", + "Property": "IsInterlaced", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "hevc", + "$type": "CodecProfile" + } + ], + "ResponseProfiles": [ + { + "Container": "m4v", + "Type": "Video", + "MimeType": "video/mp4", + "$type": "ResponseProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "vtt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "External", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-DirectMedia.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-DirectMedia.json new file mode 100644 index 0000000000..d1df7341e1 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-DirectMedia.json @@ -0,0 +1,90 @@ +{ + "Name": "Jellyfin Media Player", + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxStreamingBitrate": 20000000, + "MaxStaticBitrate": 20000000, + "MusicStreamingTranscodingBitrate": 1280000, + "TimelineOffsetSeconds": 5, + "DirectPlayProfiles": [ + { + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Type": "Photo", + "$type": "DirectPlayProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "srt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "smi", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "smi", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "pgssub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "dvdsub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "pgs", + "Method": "Embed", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Firefox.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Firefox.json new file mode 100644 index 0000000000..9874793d37 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Firefox.json @@ -0,0 +1,441 @@ +{ + "EnableAlbumArtInDidl": false, + "EnableSingleAlbumArtLimit": false, + "EnableSingleSubtitleLimit": false, + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxAlbumArtWidth": 0, + "MaxAlbumArtHeight": 0, + "MaxStreamingBitrate": 120000000, + "MaxStaticBitrate": 100000000, + "MusicStreamingTranscodingBitrate": 384000, + "TimelineOffsetSeconds": 0, + "RequiresPlainVideoItems": false, + "RequiresPlainFolders": false, + "EnableMSMediaReceiverRegistrar": false, + "IgnoreTranscodeByteRangeRequests": false, + "DirectPlayProfiles": [ + { + "Container": "webm", + "AudioCodec": "vorbis,opus", + "VideoCodec": "vp8,vp9,av1", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp4,m4v", + "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", + "VideoCodec": "h264,vp8,vp9,av1", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "opus", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webm", + "AudioCodec": "opus", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4a", + "AudioCodec": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4b", + "AudioCodec": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "flac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4a", + "AudioCodec": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4b", + "AudioCodec": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webma", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webm", + "AudioCodec": "webma", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "ogg", + "Type": "Audio", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Container": "ts", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 2, + "SegmentLength": 0, + "BreakOnNonKeyFrames": true, + "$type": "TranscodingProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "opus", + "Type": "Audio", + "AudioCodec": "opus", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "AudioCodec": "wav", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "opus", + "Type": "Audio", + "AudioCodec": "opus", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "AudioCodec": "wav", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "ts", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "aac,mp3", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 2, + "SegmentLength": 0, + "BreakOnNonKeyFrames": true, + "$type": "TranscodingProfile" + }, + { + "Container": "webm", + "Type": "Video", + "VideoCodec": "vp8,vp9,av1,vpx", + "AudioCodec": "vorbis,opus", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp4", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "aac,mp3,opus,flac,alac,vorbis", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "CodecProfiles": [ + { + "Type": "VideoAudio", + "Conditions": [ + { + "Condition": "Equals", + "Property": "IsSecondaryAudio", + "Value": "false", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "aac", + "$type": "CodecProfile" + }, + { + "Type": "VideoAudio", + "Conditions": [ + { + "Condition": "Equals", + "Property": "IsSecondaryAudio", + "Value": "false", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "$type": "CodecProfile" + }, + { + "Type": "Video", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "high|main|baseline|constrained baseline", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "52", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "NotEquals", + "Property": "IsInterlaced", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "h264", + "$type": "CodecProfile" + }, + { + "Type": "Video", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "main", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "120", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "NotEquals", + "Property": "IsInterlaced", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "hevc", + "$type": "CodecProfile" + } + ], + "ResponseProfiles": [ + { + "Container": "m4v", + "Type": "Video", + "MimeType": "video/mp4", + "$type": "ResponseProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "vtt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "External", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-JellyfinMediaPlayer.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-JellyfinMediaPlayer.json new file mode 100644 index 0000000000..da9a1a4ada --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-JellyfinMediaPlayer.json @@ -0,0 +1,137 @@ +{ + "Name": "Jellyfin Media Player", + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxStreamingBitrate": 8000000, + "MaxStaticBitrate": 8000000, + "MusicStreamingTranscodingBitrate": 1280000, + "TimelineOffsetSeconds": 5, + "DirectPlayProfiles": [ + { + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Type": "Photo", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Type": "Audio", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "ts", + "Type": "Video", + "VideoCodec": "h264,h265,hevc,mpeg4,mpeg2video", + "AudioCodec": "aac,mp3,ac3,opus,flac,vorbis", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "jpeg", + "Type": "Photo", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "srt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "smi", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "smi", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "pgssub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "dvdsub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "pgs", + "Method": "Embed", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-LowBandwidth.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-LowBandwidth.json new file mode 100644 index 0000000000..82b73fb0f8 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-LowBandwidth.json @@ -0,0 +1,137 @@ +{ + "Name": "Jellyfin Media Player", + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxStreamingBitrate": 120000, + "MaxStaticBitrate": 100000, + "MusicStreamingTranscodingBitrate": 3840, + "TimelineOffsetSeconds": 5, + "DirectPlayProfiles": [ + { + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Type": "Photo", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Type": "Audio", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "ts", + "Type": "Video", + "VideoCodec": "h264,h265,hevc,mpeg4,mpeg2video", + "AudioCodec": "aac,mp3,ac3,opus,flac,vorbis", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "jpeg", + "Type": "Photo", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "srt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "smi", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "smi", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "pgssub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "dvdsub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "pgs", + "Method": "Embed", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Null.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Null.json new file mode 100644 index 0000000000..d463bd896f --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Null.json @@ -0,0 +1,9 @@ +{ + "Name": "Jellyfin Media Player", + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxStreamingBitrate": 120000, + "MaxStaticBitrate": 100000, + "MusicStreamingTranscodingBitrate": 3840, + "TimelineOffsetSeconds": 5, + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-RokuSSPlus.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-RokuSSPlus.json new file mode 100644 index 0000000000..37b923558b --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-RokuSSPlus.json @@ -0,0 +1,211 @@ +{ + "EnableAlbumArtInDidl": false, + "EnableSingleAlbumArtLimit": false, + "EnableSingleSubtitleLimit": false, + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxAlbumArtWidth": 0, + "MaxAlbumArtHeight": 0, + "MaxStreamingBitrate": 120000000, + "MaxStaticBitrate": 100000000, + "MusicStreamingTranscodingBitrate": 192000, + "TimelineOffsetSeconds": 0, + "RequiresPlainVideoItems": false, + "RequiresPlainFolders": false, + "EnableMSMediaReceiverRegistrar": false, + "IgnoreTranscodeByteRangeRequests": false, + "DirectPlayProfiles": [ + { + "Container": "mp4,m4v,mov", + "AudioCodec": "mp3,pcm,lpcm,wav,alac,aac", + "VideoCodec": "h264,h265,hevc,mpeg2video", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mkv,webm", + "AudioCodec": "mp3,pcm,lpcm,wav,flac,alac,aac,opus,vorbis", + "VideoCodec": "h264,vp8,h265,hevc,vp9,mpeg2video", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp3,pcm,lpcm,wav,wma,flac,alac,aac,wmapro", + "Type": "Audio", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": " 2", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "2", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "2", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": " 2", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "ts", + "Type": "Video", + "VideoCodec": "h264,mpeg2video", + "AudioCodec": "aac", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": " 2", + "MinSegments": 1, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp4", + "Type": "Video", + "VideoCodec": "h264,h265,hevc,mpeg2video", + "AudioCodec": "mp3,pcm,lpcm,wav,alac,aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "CodecProfiles": [ + { + "Type": "Video", + "Conditions": [ + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "high|main|baseline|constrained baseline", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "51", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "h264", + "$type": "CodecProfile" + }, + { + "Type": "Video", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "main|main 10", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "NotEquals", + "Property": "IsInterlaced", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "hevc", + "$type": "CodecProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "vtt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ttml", + "Method": "External", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-RokuSSPlusNext.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-RokuSSPlusNext.json new file mode 100644 index 0000000000..542bf6370a --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-RokuSSPlusNext.json @@ -0,0 +1,211 @@ +{ + "EnableAlbumArtInDidl": false, + "EnableSingleAlbumArtLimit": false, + "EnableSingleSubtitleLimit": false, + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxAlbumArtWidth": 0, + "MaxAlbumArtHeight": 0, + "MaxStreamingBitrate": 120000000, + "MaxStaticBitrate": 100000000, + "MusicStreamingTranscodingBitrate": 192000, + "TimelineOffsetSeconds": 0, + "RequiresPlainVideoItems": false, + "RequiresPlainFolders": false, + "EnableMSMediaReceiverRegistrar": false, + "IgnoreTranscodeByteRangeRequests": false, + "DirectPlayProfiles": [ + { + "Container": "mp4,m4v,mov", + "AudioCodec": "mp3,pcm,lpcm,wav,alac,aac", + "VideoCodec": "h264,h265,hevc,mpeg2video", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mkv,webm", + "AudioCodec": "mp3,pcm,lpcm,wav,flac,alac,aac,opus,vorbis", + "VideoCodec": "h264,vp8,h265,hevc,vp9,mpeg2video", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp3,pcm,lpcm,wav,wma,flac,alac,aac,wmapro", + "Type": "Audio", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": " 2", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "2", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "2", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": " 2", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "ts", + "Type": "Video", + "VideoCodec": "h264,h265,hevc,mpeg2video", + "AudioCodec": "aac", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": " 2", + "MinSegments": 1, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp4", + "Type": "Video", + "VideoCodec": "h264,h265,hevc,mpeg2video", + "AudioCodec": "mp3,pcm,lpcm,wav,alac,aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "CodecProfiles": [ + { + "Type": "Video", + "Conditions": [ + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "high|main|baseline|constrained baseline", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "51", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "h264", + "$type": "CodecProfile" + }, + { + "Type": "Video", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "main|main 10", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "NotEquals", + "Property": "IsInterlaced", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "hevc", + "$type": "CodecProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "vtt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ttml", + "Method": "External", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-SafariNext.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-SafariNext.json new file mode 100644 index 0000000000..3b5a0c2549 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-SafariNext.json @@ -0,0 +1,357 @@ +{ + "EnableAlbumArtInDidl": false, + "EnableSingleAlbumArtLimit": false, + "EnableSingleSubtitleLimit": false, + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxAlbumArtWidth": 0, + "MaxAlbumArtHeight": 0, + "MaxStreamingBitrate": 120000000, + "MaxStaticBitrate": 100000000, + "MusicStreamingTranscodingBitrate": 384000, + "TimelineOffsetSeconds": 0, + "RequiresPlainVideoItems": false, + "RequiresPlainFolders": false, + "EnableMSMediaReceiverRegistrar": false, + "IgnoreTranscodeByteRangeRequests": false, + "DirectPlayProfiles": [ + { + "Container": "webm", + "AudioCodec": "vorbis", + "VideoCodec": "vp8,vp9", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp4,m4v", + "AudioCodec": "aac,mp3,ac3,eac3,flac,alac,vorbis", + "VideoCodec": "h264,vp8,vp9", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mov", + "AudioCodec": "aac,mp3,ac3,eac3,flac,alac,vorbis", + "VideoCodec": "h264", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4a", + "AudioCodec": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4b", + "AudioCodec": "aac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "flac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4a", + "AudioCodec": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "m4b", + "AudioCodec": "alac", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webma", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "webm", + "AudioCodec": "webma", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 2, + "SegmentLength": 0, + "BreakOnNonKeyFrames": true, + "$type": "TranscodingProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "AudioCodec": "wav", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "aac", + "Type": "Audio", + "AudioCodec": "aac", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "wav", + "Type": "Audio", + "AudioCodec": "wav", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp4", + "Type": "Video", + "AudioCodec": "aac,ac3,eac3,flac,alac", + "VideoCodec": "hevc,h264", + "Context": "Streaming", + "Protocol": "hls", + "MaxAudioChannels": "2", + "MinSegments": "2", + "BreakOnNonKeyFrames": true + }, + { + "Container": "ts", + "Type": "Video", + "AudioCodec": "aac,mp3,ac3,eac3", + "VideoCodec": "h264", + "Context": "Streaming", + "Protocol": "hls", + "MaxAudioChannels": "2", + "MinSegments": "2", + "BreakOnNonKeyFrames": true + }, + { + "Container": "webm", + "Type": "Video", + "AudioCodec": "vorbis", + "VideoCodec": "vp8,vpx", + "Context": "Streaming", + "Protocol": "http", + "MaxAudioChannels": "2" + }, + { + "Container": "mp4", + "Type": "Video", + "AudioCodec": "aac,mp3,ac3,eac3,flac,alac,vorbis", + "VideoCodec": "h264", + "Context": "Static", + "Protocol": "http" + } + ], + "CodecProfiles": [ + { + "Type": "Video", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "high|main|baseline|constrained baseline", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "52", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "NotEquals", + "Property": "IsInterlaced", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "h264", + "$type": "CodecProfile" + }, + { + "Type": "Video", + "Conditions": [ + { + "Condition": "NotEquals", + "Property": "IsAnamorphic", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "main|main 10", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "183", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "NotEquals", + "Property": "IsInterlaced", + "Value": "true", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "hevc", + "$type": "CodecProfile" + } + ], + "ResponseProfiles": [ + { + "Container": "m4v", + "Type": "Video", + "MimeType": "video/mp4", + "$type": "ResponseProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "vtt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "External", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-TranscodeMedia.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-TranscodeMedia.json new file mode 100644 index 0000000000..9fc1ae6bb2 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-TranscodeMedia.json @@ -0,0 +1,139 @@ +{ + "Name": "Jellyfin Media Player", + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxStreamingBitrate": 20000000, + "MaxStaticBitrate": 20000000, + "MusicStreamingTranscodingBitrate": 1280000, + "TimelineOffsetSeconds": 5, + "TranscodingProfiles": [ + { + "Type": "Audio", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp4", + "Type": "Video", + "AudioCodec": "aac,flac,alac", + "VideoCodec": "hevc,h264", + "Context": "Streaming", + "Protocol": "hls", + "MaxAudioChannels": "2", + "MinSegments": "2", + "BreakOnNonKeyFrames": true, + "$type": "TranscodingProfile" + }, + { + "Container": "ts", + "Type": "Video", + "AudioCodec": "aac,mp3", + "VideoCodec": "h264", + "Context": "Streaming", + "Protocol": "hls", + "MaxAudioChannels": "2", + "MinSegments": "2", + "BreakOnNonKeyFrames": true, + "$type": "TranscodingProfile" + }, + { + "Container": "webm", + "Type": "Video", + "AudioCodec": "vorbis", + "VideoCodec": "vp9,vp8,vpx,av1", + "Context": "Streaming", + "Protocol": "http", + "MaxAudioChannels": "2", + "$type": "TranscodingProfile" + }, + { + "Container": "jpeg", + "Type": "Photo", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "srt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "smi", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "smi", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "pgssub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "dvdsub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "pgs", + "Method": "Embed", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Yatse.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Yatse.json new file mode 100644 index 0000000000..256c8dc2f0 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Yatse.json @@ -0,0 +1,189 @@ +{ + "EnableAlbumArtInDidl": false, + "EnableSingleAlbumArtLimit": false, + "EnableSingleSubtitleLimit": false, + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxAlbumArtWidth": 0, + "MaxAlbumArtHeight": 0, + "MaxStreamingBitrate": 120000000, + "MaxStaticBitrate": 100000000, + "MusicStreamingTranscodingBitrate": 192000, + "TimelineOffsetSeconds": 0, + "RequiresPlainVideoItems": false, + "RequiresPlainFolders": false, + "EnableMSMediaReceiverRegistrar": false, + "IgnoreTranscodeByteRangeRequests": false, + "DirectPlayProfiles": [ + { + "Container": "", + "AudioCodec": "aac", + "VideoCodec": "", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "ts,mp4,mka,m4a,mp3,mp2,wav,flac,ogg", + "AudioCodec": "", + "VideoCodec": "", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "", + "AudioCodec": "", + "VideoCodec": "", + "Type": "Photo", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Container": "ts", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "aac", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": true, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "VideoCodec": "", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "VideoCodec": "", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "CodecProfiles": [ + { + "Type": "VideoAudio", + "Conditions": [ + { + "Condition": "Equals", + "Property": "IsSecondaryAudio", + "Value": "false", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "", + "Container": "", + "$type": "CodecProfile" + } + ], + "ResponseProfiles": [ + { + "Container": "m4v", + "Type": "Video", + "MimeType": "video/mp4", + "$type": "ResponseProfile" + }, + { + "Container": "mov", + "Type": "Video", + "MimeType": "video/webm", + "$type": "ResponseProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "vtt", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "smi", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "subrip", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "dvdsub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "pgs", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "pgssub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "External", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Yatse2.json b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Yatse2.json new file mode 100644 index 0000000000..256c8dc2f0 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/DeviceProfile-Yatse2.json @@ -0,0 +1,189 @@ +{ + "EnableAlbumArtInDidl": false, + "EnableSingleAlbumArtLimit": false, + "EnableSingleSubtitleLimit": false, + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxAlbumArtWidth": 0, + "MaxAlbumArtHeight": 0, + "MaxStreamingBitrate": 120000000, + "MaxStaticBitrate": 100000000, + "MusicStreamingTranscodingBitrate": 192000, + "TimelineOffsetSeconds": 0, + "RequiresPlainVideoItems": false, + "RequiresPlainFolders": false, + "EnableMSMediaReceiverRegistrar": false, + "IgnoreTranscodeByteRangeRequests": false, + "DirectPlayProfiles": [ + { + "Container": "", + "AudioCodec": "aac", + "VideoCodec": "", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "ts,mp4,mka,m4a,mp3,mp2,wav,flac,ogg", + "AudioCodec": "", + "VideoCodec": "", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "", + "AudioCodec": "", + "VideoCodec": "", + "Type": "Photo", + "$type": "DirectPlayProfile" + } + ], + "TranscodingProfiles": [ + { + "Container": "ts", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "aac", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": true, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "VideoCodec": "", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Static", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "VideoCodec": "", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MaxAudioChannels": "6", + "MinSegments": 0, + "SegmentLength": 0, + "BreakOnNonKeyFrames": false, + "$type": "TranscodingProfile" + } + ], + "CodecProfiles": [ + { + "Type": "VideoAudio", + "Conditions": [ + { + "Condition": "Equals", + "Property": "IsSecondaryAudio", + "Value": "false", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "", + "Container": "", + "$type": "CodecProfile" + } + ], + "ResponseProfiles": [ + { + "Container": "m4v", + "Type": "Video", + "MimeType": "video/mp4", + "$type": "ResponseProfile" + }, + { + "Container": "mov", + "Type": "Video", + "MimeType": "video/webm", + "$type": "ResponseProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "vtt", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "smi", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "subrip", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "dvdsub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "pgs", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "pgssub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "External", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-av1-aac-srt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-av1-aac-srt-2600k.json new file mode 100644 index 0000000000..da185aacfb --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-av1-aac-srt-2600k.json @@ -0,0 +1,73 @@ +{ + "Id": "a766d122b58e45d9492d17af66748bf5", + "Path": "/Media/MyVideo-720p.mkv", + "Container": "mkv,webm", + "Size": 835317696, + "Name": "MyVideo-1080p", + "ETag": "579a34c6d5dfb23f61539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "av1", + "Language": "eng", + "ColorTransfer": "bt709", + "ColorPrimaries": "bt709", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "1080p AV1 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "Main", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": -99 + }, + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - AAC - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "Profile": "LC", + "Index": 1, + "Score": 203 + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-av1-vorbis-srt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-av1-vorbis-srt-2600k.json new file mode 100644 index 0000000000..774dba32ae --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-av1-vorbis-srt-2600k.json @@ -0,0 +1,72 @@ +{ + "Id": "a766d122b58e45d9492d17af66748bf5", + "Path": "/Media/MyVideo-720p.mkv", + "Container": "mkv,webm", + "Size": 835317696, + "Name": "MyVideo-1080p", + "ETag": "579a34c6d5dfb23f61539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "av1", + "Language": "eng", + "ColorTransfer": "bt709", + "ColorPrimaries": "bt709", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "1080p AV1 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "Main", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": -99 + }, + { + "Codec": "vorbis", + "CodecTag": "ogg", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - Vorbis - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "Index": 1, + "Score": 203 + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-aac-srt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-aac-srt-2600k.json new file mode 100644 index 0000000000..0a85a13533 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-aac-srt-2600k.json @@ -0,0 +1,73 @@ +{ + "Id": "a766d122b58e45d9492d17af66748bf5", + "Path": "/Media/MyVideo-720p.mkv", + "Container": "mkv,webm", + "Size": 835317696, + "Name": "MyVideo-1080p", + "ETag": "579a34c6d5dfb23f61539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "vp9", + "Language": "eng", + "ColorTransfer": "bt709", + "ColorPrimaries": "bt709", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "1080p VP9 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "Profile 0", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": -99 + }, + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - AAC - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "Profile": "LC", + "Index": 1, + "Score": 203 + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-ac3-srt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-ac3-srt-2600k.json new file mode 100644 index 0000000000..2b932ff52a --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-ac3-srt-2600k.json @@ -0,0 +1,72 @@ +{ + "Id": "a766d122b58e45d9492d17af66748bf5", + "Path": "/Media/MyVideo-720p.mkv", + "Container": "mkv,webm", + "Size": 835317696, + "Name": "MyVideo-1080p", + "ETag": "579a34c6d5dfb23f61539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "vp9", + "Language": "eng", + "ColorTransfer": "bt709", + "ColorPrimaries": "bt709", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "1080p VP9 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "Profile 0", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": -99 + }, + { + "Codec": "ac3", + "CodecTag": "ac-3", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - Dolby Digital - 5.1 - Default", + "ChannelLayout": "5.1", + "BitRate": 384000, + "Channels": 6, + "SampleRate": 48000, + "IsDefault": true, + "Index": 1, + "Score": 202 + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-vorbis-srt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-vorbis-srt-2600k.json new file mode 100644 index 0000000000..56b04b7898 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-vorbis-srt-2600k.json @@ -0,0 +1,73 @@ +{ + "Id": "a766d122b58e45d9492d17af66748bf5", + "Path": "/Media/MyVideo-720p.mkv", + "Container": "mkv,webm", + "Size": 835317696, + "Name": "MyVideo-1080p", + "ETag": "579a34c6d5dfb23f61539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "vp9", + "Language": "eng", + "ColorTransfer": "bt709", + "ColorPrimaries": "bt709", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "1080p VP9 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "Profile 0", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": -99 + }, + { + "Codec": "vorbis", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - Vorbis - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "Profile": "LC", + "Index": 1, + "Score": 203 + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + } + ], + "Bitrate": 2590008, + "RequiredHttpHeaders": {}, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-vorbis-vtt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-vorbis-vtt-2600k.json new file mode 100644 index 0000000000..1ee7eade98 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mkv-vp9-vorbis-vtt-2600k.json @@ -0,0 +1,72 @@ +{ + "Id": "a766d122b58e45d9492d17af66748bf5", + "Path": "/Media/MyVideo-720p.mkv", + "Container": "mkv,webm", + "Size": 835317696, + "Name": "MyVideo-1080p", + "ETag": "579a34c6d5dfb23f61539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "vp9", + "Language": "eng", + "ColorTransfer": "bt709", + "ColorPrimaries": "bt709", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "1080p VP9 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "Profile 0", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": -99 + }, + { + "Codec": "vorbis", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - Vorbis - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "Profile": "LC", + "Index": 1, + "Score": 203 + }, + { + "Codec": "webvtt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-aac-srt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-aac-srt-2600k.json new file mode 100644 index 0000000000..21911843d1 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-aac-srt-2600k.json @@ -0,0 +1,72 @@ +{ + "Id": "a766d122b58e45d9492d17af77748bf5", + "Path": "/Media/MyVideo-720p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 835317696, + "Name": "MyVideo-720p", + "ETag": "579a34c6d5dfb21d81539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "h264", + "CodecTag": "avc1", + "Language": "eng", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "720p H264 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "High", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": 41 + }, + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - AAC - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "Profile": "LC", + "Index": 1, + "Score": 203 + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-aac-vtt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-aac-vtt-2600k.json new file mode 100644 index 0000000000..77954a31a2 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-aac-vtt-2600k.json @@ -0,0 +1,72 @@ +{ + "Id": "a766d122b58e45d9492d17af77748bf5", + "Path": "/Media/MyVideo-720p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 835317696, + "Name": "MyVideo-720p", + "ETag": "579a34c6d5dfb21d81539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "h264", + "CodecTag": "avc1", + "Language": "eng", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "720p H264 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "High", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": 41 + }, + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - AAC - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "Profile": "LC", + "Index": 1, + "Score": 203 + }, + { + "Codec": "webvtt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-srt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-srt-2600k.json new file mode 100644 index 0000000000..70bbb9d0d1 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-srt-2600k.json @@ -0,0 +1,87 @@ +{ + "Id": "a766d122b58e45d9492d17af77748bf5", + "Path": "/Media/MyVideo-720p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 835317696, + "Name": "MyVideo-720p", + "ETag": "579a34c6d5dfb21d81539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "h264", + "CodecTag": "avc1", + "Language": "eng", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "720p H264 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "High", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": 41 + }, + { + "Codec": "ac3", + "CodecTag": "ac-3", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - Dolby Digital - 5.1 - Default", + "ChannelLayout": "5.1", + "BitRate": 384000, + "Channels": 6, + "SampleRate": 48000, + "IsDefault": true, + "Index": 1, + "Score": 202 + }, + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - AAC - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "Profile": "LC", + "Index": 2, + "Score": 203 + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 3, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt" + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 3 +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aacDef-srt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aacDef-srt-2600k.json new file mode 100644 index 0000000000..036e41f077 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aacDef-srt-2600k.json @@ -0,0 +1,87 @@ +{ + "Id": "a766d122b58e45d9492d17af77748bf5", + "Path": "/Media/MyVideo-720p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 835317696, + "Name": "MyVideo-720p", + "ETag": "579a34c6d5dfb21d81539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "h264", + "CodecTag": "avc1", + "Language": "eng", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "720p H264 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "High", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": 41 + }, + { + "Codec": "ac3", + "CodecTag": "ac-3", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - Dolby Digital - 5.1 - Default", + "ChannelLayout": "5.1", + "BitRate": 384000, + "Channels": 6, + "SampleRate": 48000, + "IsDefault": true, + "Index": 1, + "Score": 202 + }, + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - AAC - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "Profile": "LC", + "Index": 2, + "Score": 203 + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 3, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt" + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 2, + "DefaultSubtitleStreamIndex": 3 +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aacExt-srt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aacExt-srt-2600k.json new file mode 100644 index 0000000000..b81c4597f6 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aacExt-srt-2600k.json @@ -0,0 +1,89 @@ +{ + "Id": "a766d122b58e45d9492d17af77748bf5", + "Path": "/Media/MyVideo-720p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 835317696, + "Name": "MyVideo-720p", + "ETag": "579a34c6d5dfb21d81539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "h264", + "CodecTag": "avc1", + "Language": "eng", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "720p H264 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "High", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": 41 + }, + { + "Codec": "ac3", + "CodecTag": "ac-3", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - Dolby Digital - 5.1 - Default", + "ChannelLayout": "5.1", + "BitRate": 384000, + "Channels": 6, + "SampleRate": 48000, + "IsDefault": true, + "Index": 1, + "Score": 202 + }, + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - AAC - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "IsExternal": true, + "Profile": "LC", + "Index": 2, + "Score": 203, + "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt" + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 3, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt" + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 3 +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-srt-2600k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-srt-2600k.json new file mode 100644 index 0000000000..b71fd4a6a2 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-srt-2600k.json @@ -0,0 +1,71 @@ +{ + "Id": "a766d122b58e45d9492d17af77748bf5", + "Path": "/Media/MyVideo-720p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 835317696, + "Name": "MyVideo-720p", + "ETag": "579a34c6d5dfb21d81539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "h264", + "CodecTag": "avc1", + "Language": "eng", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "720p H264 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "High", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": 41 + }, + { + "Codec": "ac3", + "CodecTag": "ac-3", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - Dolby Digital - 5.1 - Default", + "ChannelLayout": "5.1", + "BitRate": 384000, + "Channels": 6, + "SampleRate": 48000, + "IsDefault": true, + "Index": 1, + "Score": 202 + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-aac-srt-15200k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-aac-srt-15200k.json new file mode 100644 index 0000000000..4c6409e7b0 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-aac-srt-15200k.json @@ -0,0 +1,75 @@ +{ + "Id": "f6eab7118618ab26e61e495a1853481a", + "Path": "/Media/MyVideo-WEBDL-2160p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 6521110016, + "Name": "MyVideo WEBDL-2160p", + "ETag": "a2fb84b618ba2467fe377543f879e9bf", + "RunTimeTicks": 34318510080, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "hevc", + "CodecTag": "hev1", + "Language": "eng", + "ColorSpace": "bt2020nc", + "ColorTransfer": "smpte2084", + "ColorPrimaries": "bt2020", + "TimeBase": "1/16000", + "VideoRange": "HDR", + "DisplayTitle": "4K HEVC HDR", + "BitRate": 14715079, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 2160, + "Width": 3840, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "Main 10", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p10le", + "Level": 150 + }, + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - AAC - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "Profile": "LC", + "Index": 1, + "Score": 203 + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt" + } + ], + "Bitrate": 15201382, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aac-srt-15200k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aac-srt-15200k.json new file mode 100644 index 0000000000..385bb72602 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aac-srt-15200k.json @@ -0,0 +1,89 @@ +{ + "Id": "f6eab7118618ab26e61e495a1853481a", + "Path": "/Media/MyVideo-WEBDL-2160p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 6521110016, + "Name": "MyVideo WEBDL-2160p", + "ETag": "a2fb84b618ba2467fe377543f879e9bf", + "RunTimeTicks": 34318510080, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "hevc", + "CodecTag": "hev1", + "Language": "eng", + "ColorSpace": "bt2020nc", + "ColorTransfer": "smpte2084", + "ColorPrimaries": "bt2020", + "TimeBase": "1/16000", + "VideoRange": "HDR", + "DisplayTitle": "4K HEVC HDR", + "BitRate": 14715079, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 2160, + "Width": 3840, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "Main 10", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p10le", + "Level": 150 + }, + { + "Codec": "ac3", + "CodecTag": "ac-3", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - Dolby Digital - 5.1 - Default", + "ChannelLayout": "5.1", + "BitRate": 384000, + "Channels": 6, + "SampleRate": 48000, + "IsDefault": true, + "Index": 1, + "Score": 202 + }, + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - AAC - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "Profile": "LC", + "Index": 2, + "Score": 203 + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 3, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt" + } + ], + "Bitrate": 15201382, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 3 +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aacExt-srt-15200k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aacExt-srt-15200k.json new file mode 100644 index 0000000000..fd1950bde1 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-aacExt-srt-15200k.json @@ -0,0 +1,91 @@ +{ + "Id": "f6eab7118618ab26e61e495a1853481a", + "Path": "/Media/MyVideo-WEBDL-2160p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 6521110016, + "Name": "MyVideo WEBDL-2160p", + "ETag": "a2fb84b618ba2467fe377543f879e9bf", + "RunTimeTicks": 34318510080, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "hevc", + "CodecTag": "hev1", + "Language": "eng", + "ColorSpace": "bt2020nc", + "ColorTransfer": "smpte2084", + "ColorPrimaries": "bt2020", + "TimeBase": "1/16000", + "VideoRange": "HDR", + "DisplayTitle": "4K HEVC HDR", + "BitRate": 14715079, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 2160, + "Width": 3840, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "Main 10", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p10le", + "Level": 150 + }, + { + "Codec": "ac3", + "CodecTag": "ac-3", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - Dolby Digital - 5.1 - Default", + "ChannelLayout": "5.1", + "BitRate": 384000, + "Channels": 6, + "SampleRate": 48000, + "IsDefault": true, + "Index": 1, + "Score": 202 + }, + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - AAC - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "IsExternal": true, + "Profile": "LC", + "Index": 2, + "Score": 203, + "Path": "/Media/MyVideo-WEBDL-2160p.eng.m4a" + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 3, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt" + } + ], + "Bitrate": 15201382, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 3 +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-srt-15200k.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-srt-15200k.json new file mode 100644 index 0000000000..dde7c15ea0 --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-mp4-hevc-ac3-srt-15200k.json @@ -0,0 +1,74 @@ +{ + "Id": "f6eab7118618ab26e61e495a1853481a", + "Path": "/Media/MyVideo-WEBDL-2160p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 6521110016, + "Name": "MyVideo WEBDL-2160p", + "ETag": "a2fb84b618ba2467fe377543f879e9bf", + "RunTimeTicks": 34318510080, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "hevc", + "CodecTag": "hev1", + "Language": "eng", + "ColorSpace": "bt2020nc", + "ColorTransfer": "smpte2084", + "ColorPrimaries": "bt2020", + "TimeBase": "1/16000", + "VideoRange": "HDR", + "DisplayTitle": "4K HEVC HDR", + "BitRate": 14715079, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 2160, + "Width": 3840, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "Main 10", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p10le", + "Level": 150 + }, + { + "Codec": "ac3", + "CodecTag": "ac-3", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - Dolby Digital - 5.1 - Default", + "ChannelLayout": "5.1", + "BitRate": 384000, + "Channels": 6, + "SampleRate": 48000, + "IsDefault": true, + "Index": 1, + "Score": 202 + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt" + } + ], + "Bitrate": 15201382, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-raw.json b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-raw.json new file mode 100644 index 0000000000..9ea55b805d --- /dev/null +++ b/tests/Jellyfin.Dlna.Tests/Test Data/MediaSourceInfo-raw.json @@ -0,0 +1,102 @@ +{ + "Id": "a766d122b58e45d9492d17af77748bf5", + "Path": "/Media/MyVideo-720p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 835317696, + "Name": "MyVideo-720p", + "ETag": "579a34c6d5dfb21d81539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "h264", + "CodecTag": "avc1", + "Language": "eng", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "720p H264 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "High", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": 41 + }, + { + "Codec": "ac3", + "CodecTag": "ac-3", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - Dolby Digital - 5.1 - Default", + "ChannelLayout": "5.1", + "BitRate": 384000, + "Channels": 6, + "SampleRate": 48000, + "IsDefault": true, + "Index": 1, + "Score": 202 + }, + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - AAC - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "Profile": "LC", + "Index": 2, + "Score": 203 + }, + { + "Codec": "mov_text", + "CodecTag": "tx3g", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 3, + "Score": 6421, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "IsDefault": true, + "Type": 2, + "Index": 4, + "Score": 6422, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt" + } + ], + "Bitrate": 2590008, + "RequiredHttpHeaders": {}, + "DefaultSubtitleStreamIndex": 1 +} diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonFlagEnumTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonFlagEnumTests.cs new file mode 100644 index 0000000000..c8652b3233 --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonFlagEnumTests.cs @@ -0,0 +1,28 @@ +using System.Text.Json; +using Jellyfin.Extensions.Json.Converters; +using MediaBrowser.Model.Session; +using Xunit; + +namespace Jellyfin.Extensions.Tests.Json.Converters; + +public class JsonFlagEnumTests +{ + private readonly JsonSerializerOptions _jsonOptions = new() + { + Converters = + { + new JsonFlagEnumConverter() + } + }; + + [Theory] + [InlineData(TranscodeReason.AudioIsExternal | TranscodeReason.ContainerNotSupported, "[\"ContainerNotSupported\",\"AudioIsExternal\"]")] + [InlineData(TranscodeReason.AudioIsExternal | TranscodeReason.ContainerNotSupported | TranscodeReason.VideoBitDepthNotSupported, "[\"ContainerNotSupported\",\"AudioIsExternal\",\"VideoBitDepthNotSupported\"]")] + [InlineData((TranscodeReason)0, "[]")] + public void Serialize_Transcode_Reason(TranscodeReason transcodeReason, string output) + { + var result = JsonSerializer.Serialize(transcodeReason, _jsonOptions); + + Assert.Equal(output, result); + } +}