diff options
| author | JPVenson <github@jpb.email> | 2024-10-08 09:34:34 +0000 |
|---|---|---|
| committer | JPVenson <github@jpb.email> | 2024-10-08 09:34:34 +0000 |
| commit | d3a3d9fce3b891eb0be274a0cdc45a989e557652 (patch) | |
| tree | bd232ef477c259f1fafa204016f6efd4dcb8691f /MediaBrowser.Model | |
| parent | ee1bdf4e222125ed7382165fd7e09599ca4bd4aa (diff) | |
| parent | aaf20592bb0bbdf4f0f0d99fed091758e68ae850 (diff) | |
Merge remote-tracking branch 'jellyfinorigin/master' into feature/EFUserData
Diffstat (limited to 'MediaBrowser.Model')
37 files changed, 2633 insertions, 1302 deletions
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 4c5213d4e..2720c0bdf 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA1819 // XML serialization handles collections improperly, so we need to use arrays + #nullable disable using MediaBrowser.Model.Entities; @@ -30,9 +32,9 @@ public class EncodingOptions EnableTonemapping = false; EnableVppTonemapping = false; EnableVideoToolboxTonemapping = false; - TonemappingAlgorithm = "bt2390"; - TonemappingMode = "auto"; - TonemappingRange = "auto"; + TonemappingAlgorithm = TonemappingAlgorithm.bt2390; + TonemappingMode = TonemappingMode.auto; + TonemappingRange = TonemappingRange.auto; TonemappingDesat = 0; TonemappingPeak = 100; TonemappingParam = 0; @@ -41,9 +43,11 @@ public class EncodingOptions H264Crf = 23; H265Crf = 28; DeinterlaceDoubleRate = false; - DeinterlaceMethod = "yadif"; + DeinterlaceMethod = DeinterlaceMethod.yadif; EnableDecodingColorDepth10Hevc = true; EnableDecodingColorDepth10Vp9 = true; + EnableDecodingColorDepth10HevcRext = false; + EnableDecodingColorDepth12HevcRext = false; // Enhanced Nvdec or system native decoder is required for DoVi to SDR tone-mapping. EnableEnhancedNvdecDecoder = true; PreferSystemNativeHwDecoder = true; @@ -53,8 +57,8 @@ public class EncodingOptions AllowHevcEncoding = false; AllowAv1Encoding = false; EnableSubtitleExtraction = true; - AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" }; - HardwareDecodingCodecs = new string[] { "h264", "vc1" }; + AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = ["mkv"]; + HardwareDecodingCodecs = ["h264", "vc1"]; } /// <summary> @@ -120,7 +124,7 @@ public class EncodingOptions /// <summary> /// Gets or sets the hardware acceleration type. /// </summary> - public string HardwareAccelerationType { get; set; } + public HardwareAccelerationType HardwareAccelerationType { get; set; } /// <summary> /// Gets or sets the FFmpeg path as set by the user via the UI. @@ -160,17 +164,17 @@ public class EncodingOptions /// <summary> /// Gets or sets the tone-mapping algorithm. /// </summary> - public string TonemappingAlgorithm { get; set; } + public TonemappingAlgorithm TonemappingAlgorithm { get; set; } /// <summary> /// Gets or sets the tone-mapping mode. /// </summary> - public string TonemappingMode { get; set; } + public TonemappingMode TonemappingMode { get; set; } /// <summary> /// Gets or sets the tone-mapping range. /// </summary> - public string TonemappingRange { get; set; } + public TonemappingRange TonemappingRange { get; set; } /// <summary> /// Gets or sets the tone-mapping desaturation. @@ -210,7 +214,7 @@ public class EncodingOptions /// <summary> /// Gets or sets the encoder preset. /// </summary> - public string EncoderPreset { get; set; } + public EncoderPreset? EncoderPreset { get; set; } /// <summary> /// Gets or sets a value indicating whether the framerate is doubled when deinterlacing. @@ -220,7 +224,7 @@ public class EncodingOptions /// <summary> /// Gets or sets the deinterlace method. /// </summary> - public string DeinterlaceMethod { get; set; } + public DeinterlaceMethod DeinterlaceMethod { get; set; } /// <summary> /// Gets or sets a value indicating whether 10bit HEVC decoding is enabled. @@ -233,6 +237,16 @@ public class EncodingOptions public bool EnableDecodingColorDepth10Vp9 { get; set; } /// <summary> + /// Gets or sets a value indicating whether 8/10bit HEVC RExt decoding is enabled. + /// </summary> + public bool EnableDecodingColorDepth10HevcRext { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether 12bit HEVC RExt decoding is enabled. + /// </summary> + public bool EnableDecodingColorDepth12HevcRext { get; set; } + + /// <summary> /// Gets or sets a value indicating whether the enhanced NVDEC is enabled. /// </summary> public bool EnableEnhancedNvdecDecoder { get; set; } diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index b0f5c2a11..6054ba34e 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -2,15 +2,20 @@ using System; using System.ComponentModel; +using System.Linq; namespace MediaBrowser.Model.Configuration { public class LibraryOptions { + private static readonly string[] _defaultTagDelimiters = ["/", "|", ";", "\\"]; + public LibraryOptions() { TypeOptions = Array.Empty<TypeOptions>(); DisabledSubtitleFetchers = Array.Empty<string>(); + DisabledMediaSegmentProviders = Array.Empty<string>(); + MediaSegmentProvideOrder = Array.Empty<string>(); SubtitleFetcherOrder = Array.Empty<string>(); DisabledLocalMetadataReaders = Array.Empty<string>(); DisabledLyricFetchers = Array.Empty<string>(); @@ -24,9 +29,15 @@ namespace MediaBrowser.Model.Configuration EnablePhotos = true; SaveSubtitlesWithMedia = true; SaveLyricsWithMedia = false; + SaveTrickplayWithMedia = false; PathInfos = Array.Empty<MediaPathInfo>(); EnableAutomaticSeriesGrouping = true; SeasonZeroDisplayName = "Specials"; + + PreferNonstandardArtistsTag = false; + UseCustomTagDelimiters = false; + CustomTagDelimiters = _defaultTagDelimiters; + DelimiterWhitelist = Array.Empty<string>(); } public bool Enabled { get; set; } = true; @@ -86,6 +97,10 @@ namespace MediaBrowser.Model.Configuration public string[] SubtitleFetcherOrder { get; set; } + public string[] DisabledMediaSegmentProviders { get; set; } + + public string[] MediaSegmentProvideOrder { get; set; } + public bool SkipSubtitlesIfEmbeddedSubtitlesPresent { get; set; } public bool SkipSubtitlesIfAudioTrackMatches { get; set; } @@ -99,10 +114,23 @@ namespace MediaBrowser.Model.Configuration [DefaultValue(false)] public bool SaveLyricsWithMedia { get; set; } + [DefaultValue(false)] + public bool SaveTrickplayWithMedia { get; set; } + public string[] DisabledLyricFetchers { get; set; } public string[] LyricFetcherOrder { get; set; } + [DefaultValue(false)] + public bool PreferNonstandardArtistsTag { get; set; } + + [DefaultValue(false)] + public bool UseCustomTagDelimiters { get; set; } + + public string[] CustomTagDelimiters { get; set; } + + public string[] DelimiterWhitelist { get; set; } + public bool AutomaticallyAddToCollection { get; set; } public EmbeddedSubtitleOptions AllowEmbeddedSubtitles { get; set; } diff --git a/MediaBrowser.Model/Configuration/MediaPathInfo.cs b/MediaBrowser.Model/Configuration/MediaPathInfo.cs index a7bc43590..25a5d5606 100644 --- a/MediaBrowser.Model/Configuration/MediaPathInfo.cs +++ b/MediaBrowser.Model/Configuration/MediaPathInfo.cs @@ -16,7 +16,5 @@ namespace MediaBrowser.Model.Configuration } public string Path { get; set; } - - public string? NetworkPath { get; set; } } } diff --git a/MediaBrowser.Model/Configuration/MetadataPluginType.cs b/MediaBrowser.Model/Configuration/MetadataPluginType.cs index ef303726d..670d6e383 100644 --- a/MediaBrowser.Model/Configuration/MetadataPluginType.cs +++ b/MediaBrowser.Model/Configuration/MetadataPluginType.cs @@ -14,6 +14,7 @@ namespace MediaBrowser.Model.Configuration MetadataFetcher, MetadataSaver, SubtitleFetcher, - LyricFetcher + LyricFetcher, + MediaSegmentProvider } } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 52f7e53b8..5ad588200 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -96,8 +96,6 @@ public class ServerConfiguration : BaseApplicationConfiguration /// <value>The metadata path.</value> public string MetadataPath { get; set; } = string.Empty; - public string MetadataNetworkPath { get; set; } = string.Empty; - /// <summary> /// Gets or sets the preferred metadata language. /// </summary> diff --git a/MediaBrowser.Model/Devices/DeviceInfo.cs b/MediaBrowser.Model/Devices/DeviceInfo.cs index 4962992a0..115598613 100644 --- a/MediaBrowser.Model/Devices/DeviceInfo.cs +++ b/MediaBrowser.Model/Devices/DeviceInfo.cs @@ -1,69 +1,84 @@ -#nullable disable -#pragma warning disable CS1591 - using System; using MediaBrowser.Model.Session; -namespace MediaBrowser.Model.Devices +namespace MediaBrowser.Model.Devices; + +/// <summary> +/// A class for device Information. +/// </summary> +public class DeviceInfo { - public class DeviceInfo + /// <summary> + /// Initializes a new instance of the <see cref="DeviceInfo"/> class. + /// </summary> + public DeviceInfo() { - public DeviceInfo() - { - Capabilities = new ClientCapabilities(); - } + Capabilities = new ClientCapabilities(); + } - public string Name { get; set; } + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string? Name { get; set; } - public string CustomName { get; set; } + /// <summary> + /// Gets or sets the custom name. + /// </summary> + /// <value>The custom name.</value> + public string? CustomName { get; set; } - /// <summary> - /// Gets or sets the access token. - /// </summary> - public string AccessToken { get; set; } + /// <summary> + /// Gets or sets the access token. + /// </summary> + /// <value>The access token.</value> + public string? AccessToken { get; set; } - /// <summary> - /// Gets or sets the identifier. - /// </summary> - /// <value>The identifier.</value> - public string Id { get; set; } + /// <summary> + /// Gets or sets the identifier. + /// </summary> + /// <value>The identifier.</value> + public string? Id { get; set; } - /// <summary> - /// Gets or sets the last name of the user. - /// </summary> - /// <value>The last name of the user.</value> - public string LastUserName { get; set; } + /// <summary> + /// Gets or sets the last name of the user. + /// </summary> + /// <value>The last name of the user.</value> + public string? LastUserName { get; set; } - /// <summary> - /// Gets or sets the name of the application. - /// </summary> - /// <value>The name of the application.</value> - public string AppName { get; set; } + /// <summary> + /// Gets or sets the name of the application. + /// </summary> + /// <value>The name of the application.</value> + public string? AppName { get; set; } - /// <summary> - /// Gets or sets the application version. - /// </summary> - /// <value>The application version.</value> - public string AppVersion { get; set; } + /// <summary> + /// Gets or sets the application version. + /// </summary> + /// <value>The application version.</value> + public string? AppVersion { get; set; } - /// <summary> - /// Gets or sets the last user identifier. - /// </summary> - /// <value>The last user identifier.</value> - public Guid LastUserId { get; set; } + /// <summary> + /// Gets or sets the last user identifier. + /// </summary> + /// <value>The last user identifier.</value> + public Guid? LastUserId { get; set; } - /// <summary> - /// Gets or sets the date last modified. - /// </summary> - /// <value>The date last modified.</value> - public DateTime DateLastActivity { get; set; } + /// <summary> + /// Gets or sets the date last modified. + /// </summary> + /// <value>The date last modified.</value> + public DateTime? DateLastActivity { get; set; } - /// <summary> - /// Gets or sets the capabilities. - /// </summary> - /// <value>The capabilities.</value> - public ClientCapabilities Capabilities { get; set; } + /// <summary> + /// Gets or sets the capabilities. + /// </summary> + /// <value>The capabilities.</value> + public ClientCapabilities Capabilities { get; set; } - public string IconUrl { get; set; } - } + /// <summary> + /// Gets or sets the icon URL. + /// </summary> + /// <value>The icon URL.</value> + public string? IconUrl { get; set; } } diff --git a/MediaBrowser.Model/Dlna/CodecProfile.cs b/MediaBrowser.Model/Dlna/CodecProfile.cs index 07c1a29a4..da34eddcd 100644 --- a/MediaBrowser.Model/Dlna/CodecProfile.cs +++ b/MediaBrowser.Model/Dlna/CodecProfile.cs @@ -1,74 +1,94 @@ -#nullable disable -#pragma warning disable CS1591 - using System; +using System.Collections.Generic; +using System.Linq; using System.Xml.Serialization; -using Jellyfin.Extensions; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Model.Dlna; -namespace MediaBrowser.Model.Dlna +/// <summary> +/// Defines the <see cref="CodecProfile"/>. +/// </summary> +public class CodecProfile { - public class CodecProfile + /// <summary> + /// Initializes a new instance of the <see cref="CodecProfile"/> class. + /// </summary> + public CodecProfile() { - public CodecProfile() - { - Conditions = Array.Empty<ProfileCondition>(); - ApplyConditions = Array.Empty<ProfileCondition>(); - } - - [XmlAttribute("type")] - public CodecType Type { get; set; } - - public ProfileCondition[] Conditions { get; set; } - - public ProfileCondition[] ApplyConditions { get; set; } - - [XmlAttribute("codec")] - public string Codec { get; set; } + Conditions = []; + ApplyConditions = []; + } - [XmlAttribute("container")] - public string Container { get; set; } + /// <summary> + /// Gets or sets the <see cref="CodecType"/> which this container must meet. + /// </summary> + [XmlAttribute("type")] + public CodecType Type { get; set; } - [XmlAttribute("subcontainer")] - public string SubContainer { get; set; } + /// <summary> + /// Gets or sets the list of <see cref="ProfileCondition"/> which this profile must meet. + /// </summary> + public ProfileCondition[] Conditions { get; set; } - public string[] GetCodecs() - { - return ContainerProfile.SplitValue(Codec); - } + /// <summary> + /// Gets or sets the list of <see cref="ProfileCondition"/> to apply if this profile is met. + /// </summary> + public ProfileCondition[] ApplyConditions { get; set; } - private bool ContainsContainer(string container, bool useSubContainer = false) - { - var containerToCheck = useSubContainer && string.Equals(Container, "hls", StringComparison.OrdinalIgnoreCase) ? SubContainer : Container; - return ContainerProfile.ContainsContainer(containerToCheck, container); - } + /// <summary> + /// Gets or sets the codec(s) that this profile applies to. + /// </summary> + [XmlAttribute("codec")] + public string? Codec { get; set; } - public bool ContainsAnyCodec(string codec, string container, bool useSubContainer = false) - { - return ContainsAnyCodec(ContainerProfile.SplitValue(codec), container, useSubContainer); - } + /// <summary> + /// Gets or sets the container(s) which this profile will be applied to. + /// </summary> + [XmlAttribute("container")] + public string? Container { get; set; } - public bool ContainsAnyCodec(string[] codec, string container, bool useSubContainer = false) - { - if (!ContainsContainer(container, useSubContainer)) - { - return false; - } + /// <summary> + /// Gets or sets the sub-container(s) which this profile will be applied to. + /// </summary> + [XmlAttribute("subcontainer")] + public string? SubContainer { get; set; } - var codecs = GetCodecs(); - if (codecs.Length == 0) - { - return true; - } + /// <summary> + /// Checks to see whether the codecs and containers contain the given parameters. + /// </summary> + /// <param name="codecs">The codecs to match.</param> + /// <param name="container">The container to match.</param> + /// <param name="useSubContainer">Consider sub-containers.</param> + /// <returns>True if both conditions are met.</returns> + public bool ContainsAnyCodec(IReadOnlyList<string> codecs, string? container, bool useSubContainer = false) + { + var containerToCheck = useSubContainer && string.Equals(Container, "hls", StringComparison.OrdinalIgnoreCase) ? SubContainer : Container; + return ContainerHelper.ContainsContainer(containerToCheck, container) && codecs.Any(c => ContainerHelper.ContainsContainer(Codec, false, c)); + } - foreach (var val in codec) - { - if (codecs.Contains(val, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } + /// <summary> + /// Checks to see whether the codecs and containers contain the given parameters. + /// </summary> + /// <param name="codec">The codec to match.</param> + /// <param name="container">The container to match.</param> + /// <param name="useSubContainer">Consider sub-containers.</param> + /// <returns>True if both conditions are met.</returns> + public bool ContainsAnyCodec(string? codec, string? container, bool useSubContainer = false) + { + return ContainsAnyCodec(codec.AsSpan(), container, useSubContainer); + } - return false; - } + /// <summary> + /// Checks to see whether the codecs and containers contain the given parameters. + /// </summary> + /// <param name="codec">The codec to match.</param> + /// <param name="container">The container to match.</param> + /// <param name="useSubContainer">Consider sub-containers.</param> + /// <returns>True if both conditions are met.</returns> + public bool ContainsAnyCodec(ReadOnlySpan<char> codec, string? container, bool useSubContainer = false) + { + var containerToCheck = useSubContainer && string.Equals(Container, "hls", StringComparison.OrdinalIgnoreCase) ? SubContainer : Container; + return ContainerHelper.ContainsContainer(containerToCheck, container) && ContainerHelper.ContainsContainer(Codec, false, codec); } } diff --git a/MediaBrowser.Model/Dlna/ContainerProfile.cs b/MediaBrowser.Model/Dlna/ContainerProfile.cs index 978004268..a42179907 100644 --- a/MediaBrowser.Model/Dlna/ContainerProfile.cs +++ b/MediaBrowser.Model/Dlna/ContainerProfile.cs @@ -1,74 +1,49 @@ -#pragma warning disable CS1591 +#pragma warning disable CA1819 // Properties should not return arrays using System; +using System.Collections.Generic; using System.Xml.Serialization; -using Jellyfin.Extensions; +using MediaBrowser.Model.Extensions; -namespace MediaBrowser.Model.Dlna +namespace MediaBrowser.Model.Dlna; + +/// <summary> +/// Defines the <see cref="ContainerProfile"/>. +/// </summary> +public class ContainerProfile { - public class ContainerProfile + /// <summary> + /// Gets or sets the <see cref="DlnaProfileType"/> which this container must meet. + /// </summary> + [XmlAttribute("type")] + public DlnaProfileType Type { get; set; } + + /// <summary> + /// Gets or sets the list of <see cref="ProfileCondition"/> which this container will be applied to. + /// </summary> + public ProfileCondition[] Conditions { get; set; } = []; + + /// <summary> + /// Gets or sets the container(s) which this container must meet. + /// </summary> + [XmlAttribute("container")] + public string? Container { get; set; } + + /// <summary> + /// Gets or sets the sub container(s) which this container must meet. + /// </summary> + [XmlAttribute("subcontainer")] + public string? SubContainer { get; set; } + + /// <summary> + /// Returns true if an item in <paramref name="container"/> appears in the <see cref="Container"/> property. + /// </summary> + /// <param name="container">The item to match.</param> + /// <param name="useSubContainer">Consider subcontainers.</param> + /// <returns>The result of the operation.</returns> + public bool ContainsContainer(ReadOnlySpan<char> container, bool useSubContainer = false) { - [XmlAttribute("type")] - public DlnaProfileType Type { get; set; } - - public ProfileCondition[] Conditions { get; set; } = Array.Empty<ProfileCondition>(); - - [XmlAttribute("container")] - public string Container { get; set; } = string.Empty; - - public static string[] SplitValue(string? value) - { - if (string.IsNullOrEmpty(value)) - { - return Array.Empty<string>(); - } - - return value.Split(',', StringSplitOptions.RemoveEmptyEntries); - } - - public bool ContainsContainer(string? container) - { - var containers = SplitValue(Container); - - return ContainsContainer(containers, container); - } - - public static bool ContainsContainer(string? profileContainers, string? inputContainer) - { - var isNegativeList = false; - if (profileContainers is not null && profileContainers.StartsWith('-')) - { - isNegativeList = true; - profileContainers = profileContainers.Substring(1); - } - - return ContainsContainer(SplitValue(profileContainers), isNegativeList, inputContainer); - } - - public static bool ContainsContainer(string[]? profileContainers, string? inputContainer) - { - return ContainsContainer(profileContainers, false, inputContainer); - } - - public static bool ContainsContainer(string[]? profileContainers, bool isNegativeList, string? inputContainer) - { - if (profileContainers is null || profileContainers.Length == 0) - { - // Empty profiles always support all containers/codecs - return true; - } - - var allInputContainers = SplitValue(inputContainer); - - foreach (var container in allInputContainers) - { - if (profileContainers.Contains(container, StringComparison.OrdinalIgnoreCase)) - { - return !isNegativeList; - } - } - - return isNegativeList; - } + var containerToCheck = useSubContainer && string.Equals(Container, "hls", StringComparison.OrdinalIgnoreCase) ? SubContainer : Container; + return ContainerHelper.ContainsContainer(containerToCheck, container); } } diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs index 2addebbfc..995b7633a 100644 --- a/MediaBrowser.Model/Dlna/DeviceProfile.cs +++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs @@ -1,74 +1,71 @@ #pragma warning disable CA1819 // Properties should not return arrays using System; -using System.Xml.Serialization; -namespace MediaBrowser.Model.Dlna +namespace MediaBrowser.Model.Dlna; + +/// <summary> +/// A <see cref="DeviceProfile" /> represents a set of metadata which determines which content a certain device is able to play. +/// <br/> +/// Specifically, it defines the supported <see cref="ContainerProfiles">containers</see> and +/// <see cref="CodecProfiles">codecs</see> (video and/or audio, including codec profiles and levels) +/// the device is able to direct play (without transcoding or remuxing), +/// as well as which <see cref="TranscodingProfiles">containers/codecs to transcode to</see> in case it isn't. +/// </summary> +public class DeviceProfile { /// <summary> - /// A <see cref="DeviceProfile" /> represents a set of metadata which determines which content a certain device is able to play. - /// <br/> - /// Specifically, it defines the supported <see cref="ContainerProfiles">containers</see> and - /// <see cref="CodecProfiles">codecs</see> (video and/or audio, including codec profiles and levels) - /// the device is able to direct play (without transcoding or remuxing), - /// as well as which <see cref="TranscodingProfiles">containers/codecs to transcode to</see> in case it isn't. + /// Gets or sets the name of this device profile. User profiles must have a unique name. /// </summary> - public class DeviceProfile - { - /// <summary> - /// Gets or sets the name of this device profile. - /// </summary> - public string? Name { get; set; } + public string? Name { get; set; } - /// <summary> - /// Gets or sets the Id. - /// </summary> - [XmlIgnore] - public string? Id { get; set; } + /// <summary> + /// Gets or sets the unique internal identifier. + /// </summary> + public Guid? Id { get; set; } - /// <summary> - /// Gets or sets the maximum allowed bitrate for all streamed content. - /// </summary> - public int? MaxStreamingBitrate { get; set; } = 8000000; + /// <summary> + /// Gets or sets the maximum allowed bitrate for all streamed content. + /// </summary> + public int? MaxStreamingBitrate { get; set; } = 8000000; - /// <summary> - /// Gets or sets the maximum allowed bitrate for statically streamed content (= direct played files). - /// </summary> - public int? MaxStaticBitrate { get; set; } = 8000000; + /// <summary> + /// Gets or sets the maximum allowed bitrate for statically streamed content (= direct played files). + /// </summary> + public int? MaxStaticBitrate { get; set; } = 8000000; - /// <summary> - /// Gets or sets the maximum allowed bitrate for transcoded music streams. - /// </summary> - public int? MusicStreamingTranscodingBitrate { get; set; } = 128000; + /// <summary> + /// Gets or sets the maximum allowed bitrate for transcoded music streams. + /// </summary> + public int? MusicStreamingTranscodingBitrate { get; set; } = 128000; - /// <summary> - /// Gets or sets the maximum allowed bitrate for statically streamed (= direct played) music files. - /// </summary> - public int? MaxStaticMusicBitrate { get; set; } = 8000000; + /// <summary> + /// Gets or sets the maximum allowed bitrate for statically streamed (= direct played) music files. + /// </summary> + public int? MaxStaticMusicBitrate { get; set; } = 8000000; - /// <summary> - /// Gets or sets the direct play profiles. - /// </summary> - public DirectPlayProfile[] DirectPlayProfiles { get; set; } = Array.Empty<DirectPlayProfile>(); + /// <summary> + /// Gets or sets the direct play profiles. + /// </summary> + public DirectPlayProfile[] DirectPlayProfiles { get; set; } = []; - /// <summary> - /// Gets or sets the transcoding profiles. - /// </summary> - public TranscodingProfile[] TranscodingProfiles { get; set; } = Array.Empty<TranscodingProfile>(); + /// <summary> + /// Gets or sets the transcoding profiles. + /// </summary> + public TranscodingProfile[] TranscodingProfiles { get; set; } = []; - /// <summary> - /// Gets or sets the container profiles. - /// </summary> - public ContainerProfile[] ContainerProfiles { get; set; } = Array.Empty<ContainerProfile>(); + /// <summary> + /// Gets or sets the container profiles. Failing to meet these optional conditions causes transcoding to occur. + /// </summary> + public ContainerProfile[] ContainerProfiles { get; set; } = []; - /// <summary> - /// Gets or sets the codec profiles. - /// </summary> - public CodecProfile[] CodecProfiles { get; set; } = Array.Empty<CodecProfile>(); + /// <summary> + /// Gets or sets the codec profiles. + /// </summary> + public CodecProfile[] CodecProfiles { get; set; } = []; - /// <summary> - /// Gets or sets the subtitle profiles. - /// </summary> - public SubtitleProfile[] SubtitleProfiles { get; set; } = Array.Empty<SubtitleProfile>(); - } + /// <summary> + /// Gets or sets the subtitle profiles. + /// </summary> + public SubtitleProfile[] SubtitleProfiles { get; set; } = []; } diff --git a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs index f68235d86..438df3441 100644 --- a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs +++ b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs @@ -1,36 +1,65 @@ -#pragma warning disable CS1591 - using System.Xml.Serialization; +using MediaBrowser.Model.Extensions; + +namespace MediaBrowser.Model.Dlna; -namespace MediaBrowser.Model.Dlna +/// <summary> +/// Defines the <see cref="DirectPlayProfile"/>. +/// </summary> +public class DirectPlayProfile { - public class DirectPlayProfile - { - [XmlAttribute("container")] - public string? Container { get; set; } + /// <summary> + /// Gets or sets the container. + /// </summary> + [XmlAttribute("container")] + public string Container { get; set; } = string.Empty; - [XmlAttribute("audioCodec")] - public string? AudioCodec { get; set; } + /// <summary> + /// Gets or sets the audio codec. + /// </summary> + [XmlAttribute("audioCodec")] + public string? AudioCodec { get; set; } - [XmlAttribute("videoCodec")] - public string? VideoCodec { get; set; } + /// <summary> + /// Gets or sets the video codec. + /// </summary> + [XmlAttribute("videoCodec")] + public string? VideoCodec { get; set; } - [XmlAttribute("type")] - public DlnaProfileType Type { get; set; } + /// <summary> + /// Gets or sets the Dlna profile type. + /// </summary> + [XmlAttribute("type")] + public DlnaProfileType Type { get; set; } - public bool SupportsContainer(string? container) - { - return ContainerProfile.ContainsContainer(Container, container); - } + /// <summary> + /// Returns whether the <see cref="Container"/> supports the <paramref name="container"/>. + /// </summary> + /// <param name="container">The container to match against.</param> + /// <returns>True if supported.</returns> + public bool SupportsContainer(string? container) + { + return ContainerHelper.ContainsContainer(Container, container); + } - public bool SupportsVideoCodec(string? codec) - { - return Type == DlnaProfileType.Video && ContainerProfile.ContainsContainer(VideoCodec, codec); - } + /// <summary> + /// Returns whether the <see cref="VideoCodec"/> supports the <paramref name="codec"/>. + /// </summary> + /// <param name="codec">The codec to match against.</param> + /// <returns>True if supported.</returns> + public bool SupportsVideoCodec(string? codec) + { + return Type == DlnaProfileType.Video && ContainerHelper.ContainsContainer(VideoCodec, codec); + } - public bool SupportsAudioCodec(string? codec) - { - return (Type == DlnaProfileType.Audio || Type == DlnaProfileType.Video) && ContainerProfile.ContainsContainer(AudioCodec, codec); - } + /// <summary> + /// Returns whether the <see cref="AudioCodec"/> supports the <paramref name="codec"/>. + /// </summary> + /// <param name="codec">The codec to match against.</param> + /// <returns>True if supported.</returns> + public bool SupportsAudioCodec(string? codec) + { + // Video profiles can have audio codec restrictions too, therefore incude Video as valid type. + return (Type == DlnaProfileType.Audio || Type == DlnaProfileType.Video) && ContainerHelper.ContainsContainer(AudioCodec, codec); } } diff --git a/MediaBrowser.Model/Dlna/MediaOptions.cs b/MediaBrowser.Model/Dlna/MediaOptions.cs index eca971e95..6b26ca94b 100644 --- a/MediaBrowser.Model/Dlna/MediaOptions.cs +++ b/MediaBrowser.Model/Dlna/MediaOptions.cs @@ -50,6 +50,11 @@ namespace MediaBrowser.Model.Dlna public bool AllowVideoStreamCopy { get; set; } /// <summary> + /// Gets or sets a value indicating whether always burn in subtitles when transcoding. + /// </summary> + public bool AlwaysBurnInSubtitleWhenTranscoding { get; set; } + + /// <summary> /// Gets or sets the item id. /// </summary> public Guid ItemId { get; set; } diff --git a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs index 5d7daa81a..1a636b240 100644 --- a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs +++ b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs @@ -2,28 +2,33 @@ #pragma warning disable CS1591 using System; +using System.Linq; namespace MediaBrowser.Model.Dlna { public static class ResolutionNormalizer { - private static readonly ResolutionConfiguration[] Configurations = - new[] - { - new ResolutionConfiguration(426, 320000), - new ResolutionConfiguration(640, 400000), - new ResolutionConfiguration(720, 950000), - new ResolutionConfiguration(1280, 2500000), - new ResolutionConfiguration(1920, 4000000), - new ResolutionConfiguration(2560, 20000000), - new ResolutionConfiguration(3840, 35000000) - }; + // Please note: all bitrate here are in the scale of SDR h264 bitrate at 30fps + private static readonly ResolutionConfiguration[] _configurations = + [ + new ResolutionConfiguration(416, 365000), + new ResolutionConfiguration(640, 730000), + new ResolutionConfiguration(768, 1100000), + new ResolutionConfiguration(960, 3000000), + new ResolutionConfiguration(1280, 6000000), + new ResolutionConfiguration(1920, 13500000), + new ResolutionConfiguration(2560, 28000000), + new ResolutionConfiguration(3840, 50000000) + ]; public static ResolutionOptions Normalize( int? inputBitrate, int outputBitrate, + int h264EquivalentOutputBitrate, int? maxWidth, - int? maxHeight) + int? maxHeight, + float? targetFps, + bool isHdr = false) // We are not doing HDR transcoding for now, leave for future use { // If the bitrate isn't changing, then don't downscale the resolution if (inputBitrate.HasValue && outputBitrate >= inputBitrate.Value) @@ -38,16 +43,26 @@ namespace MediaBrowser.Model.Dlna } } - var resolutionConfig = GetResolutionConfiguration(outputBitrate); - if (resolutionConfig is not null) + var referenceBitrate = h264EquivalentOutputBitrate * (30.0f / (targetFps ?? 30.0f)); + + if (isHdr) { - var originvalValue = maxWidth; + referenceBitrate *= 0.8f; + } - maxWidth = Math.Min(resolutionConfig.MaxWidth, maxWidth ?? resolutionConfig.MaxWidth); - if (!originvalValue.HasValue || originvalValue.Value != maxWidth.Value) - { - maxHeight = null; - } + var resolutionConfig = GetResolutionConfiguration(Convert.ToInt32(referenceBitrate)); + + if (resolutionConfig is null) + { + return new ResolutionOptions { MaxWidth = maxWidth, MaxHeight = maxHeight }; + } + + var originWidthValue = maxWidth; + + maxWidth = Math.Min(resolutionConfig.MaxWidth, maxWidth ?? resolutionConfig.MaxWidth); + if (!originWidthValue.HasValue || originWidthValue.Value != maxWidth.Value) + { + maxHeight = null; } return new ResolutionOptions @@ -59,19 +74,7 @@ namespace MediaBrowser.Model.Dlna private static ResolutionConfiguration GetResolutionConfiguration(int outputBitrate) { - ResolutionConfiguration previousOption = null; - - foreach (var config in Configurations) - { - if (outputBitrate <= config.MaxBitrate) - { - return previousOption ?? config; - } - - previousOption = config; - } - - return null; + return _configurations.FirstOrDefault(config => outputBitrate <= config.MaxBitrate); } } } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 7f387bfaa..a25ddc367 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -6,6 +6,7 @@ using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; @@ -19,15 +20,17 @@ namespace MediaBrowser.Model.Dlna { // 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; + internal const TranscodeReason AudioCodecReasons = TranscodeReason.AudioBitrateNotSupported | TranscodeReason.AudioChannelsNotSupported | TranscodeReason.AudioProfileNotSupported | TranscodeReason.AudioSampleRateNotSupported | TranscodeReason.SecondaryAudioNotSupported | TranscodeReason.AudioBitDepthNotSupported | TranscodeReason.AudioIsExternal; + internal const TranscodeReason AudioReasons = TranscodeReason.AudioCodecNotSupported | AudioCodecReasons; + internal const TranscodeReason VideoCodecReasons = TranscodeReason.VideoResolutionNotSupported | TranscodeReason.AnamorphicVideoNotSupported | TranscodeReason.InterlacedVideoNotSupported | TranscodeReason.VideoBitDepthNotSupported | TranscodeReason.VideoBitrateNotSupported | TranscodeReason.VideoFramerateNotSupported | TranscodeReason.VideoLevelNotSupported | TranscodeReason.RefFramesNotSupported | TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.VideoProfileNotSupported; + internal const TranscodeReason VideoReasons = TranscodeReason.VideoCodecNotSupported | VideoCodecReasons; + internal const TranscodeReason DirectStreamReasons = AudioReasons | TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecTagNotSupported; private readonly ILogger _logger; private readonly ITranscoderSupport _transcoderSupport; - private static readonly string[] _supportedHlsVideoCodecs = new string[] { "h264", "hevc", "vp9", "av1" }; - private static readonly string[] _supportedHlsAudioCodecsTs = new string[] { "aac", "ac3", "eac3", "mp3" }; - private static readonly string[] _supportedHlsAudioCodecsMp4 = new string[] { "aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd" }; + private static readonly string[] _supportedHlsVideoCodecs = ["h264", "hevc", "vp9", "av1"]; + private static readonly string[] _supportedHlsAudioCodecsTs = ["aac", "ac3", "eac3", "mp3"]; + private static readonly string[] _supportedHlsAudioCodecsMp4 = ["aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd"]; /// <summary> /// Initializes a new instance of the <see cref="StreamBuilder"/> class. @@ -49,7 +52,7 @@ namespace MediaBrowser.Model.Dlna { ValidateMediaOptions(options, false); - var streams = new List<StreamInfo>(); + List<StreamInfo> streams = []; foreach (var mediaSource in options.MediaSources) { if (!(string.IsNullOrEmpty(options.MediaSourceId) @@ -62,7 +65,7 @@ namespace MediaBrowser.Model.Dlna if (streamInfo is not null) { streamInfo.DeviceId = options.DeviceId; - streamInfo.DeviceProfileId = options.Profile.Id; + streamInfo.DeviceProfileId = options.Profile.Id?.ToString("N", CultureInfo.InvariantCulture); streams.Add(streamInfo); } } @@ -127,7 +130,7 @@ namespace MediaBrowser.Model.Dlna if (directPlayMethod is PlayMethod.DirectStream) { var remuxContainer = item.TranscodingContainer ?? "ts"; - var supportedHlsContainers = new[] { "ts", "mp4" }; + string[] supportedHlsContainers = ["ts", "mp4"]; // If the container specified for the profile is an HLS supported container, use that container instead, overriding the preference // The client should be responsible to ensure this container is compatible remuxContainer = Array.Exists(supportedHlsContainers, element => string.Equals(element, directPlayInfo.Profile?.Container, StringComparison.OrdinalIgnoreCase)) ? directPlayInfo.Profile?.Container : remuxContainer; @@ -224,7 +227,7 @@ namespace MediaBrowser.Model.Dlna ? options.MediaSources : options.MediaSources.Where(x => string.Equals(x.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)); - var streams = new List<StreamInfo>(); + List<StreamInfo> streams = []; foreach (var mediaSourceInfo in mediaSources) { var streamInfo = BuildVideoItem(mediaSourceInfo, options); @@ -237,7 +240,7 @@ namespace MediaBrowser.Model.Dlna foreach (var stream in streams) { stream.DeviceId = options.DeviceId; - stream.DeviceProfileId = options.Profile.Id; + stream.DeviceProfileId = options.Profile.Id?.ToString("N", CultureInfo.InvariantCulture); } return GetOptimalStream(streams, options.GetMaxBitrate(false) ?? 0); @@ -352,7 +355,7 @@ namespace MediaBrowser.Model.Dlna return TranscodeReason.VideoBitrateNotSupported; case ProfileConditionValue.VideoCodecTag: - return TranscodeReason.VideoCodecNotSupported; + return TranscodeReason.VideoCodecTagNotSupported; case ProfileConditionValue.VideoFramerate: return TranscodeReason.VideoFramerateNotSupported; @@ -388,30 +391,31 @@ namespace MediaBrowser.Model.Dlna /// <returns>The normalized input container.</returns> public static string? NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile? profile, DlnaProfileType type, DirectPlayProfile? playProfile = null) { - if (string.IsNullOrEmpty(inputContainer)) + // If the source is Live TV the inputContainer will be null until the mediasource is probed on first access + if (profile is null || string.IsNullOrEmpty(inputContainer) || !inputContainer.Contains(',', StringComparison.OrdinalIgnoreCase)) { - return null; + return inputContainer; } - var formats = ContainerProfile.SplitValue(inputContainer); - - if (profile is not null) + var formats = ContainerHelper.Split(inputContainer); + var playProfiles = playProfile is null ? profile.DirectPlayProfiles : [playProfile]; + foreach (var format in formats) { - var playProfiles = playProfile is null ? profile.DirectPlayProfiles : new[] { playProfile }; - foreach (var format in formats) + foreach (var directPlayProfile in playProfiles) { - foreach (var directPlayProfile in playProfiles) + if (directPlayProfile.Type != type) { - if (directPlayProfile.Type == type - && directPlayProfile.SupportsContainer(format)) - { - return format; - } + continue; + } + + if (directPlayProfile.SupportsContainer(format)) + { + return format; } } } - return formats[0]; + return inputContainer; } private (DirectPlayProfile? Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, MediaOptions options) @@ -531,7 +535,6 @@ namespace MediaBrowser.Model.Dlna private static int? GetDefaultSubtitleStreamIndex(MediaSourceInfo item, SubtitleProfile[] subtitleProfiles) { int highestScore = -1; - foreach (var stream in item.MediaStreams) { if (stream.Type == MediaStreamType.Subtitle @@ -542,7 +545,7 @@ namespace MediaBrowser.Model.Dlna } } - var topStreams = new List<MediaStream>(); + List<MediaStream> topStreams = []; foreach (var stream in item.MediaStreams) { if (stream.Type == MediaStreamType.Subtitle && stream.Score.HasValue && stream.Score.Value == highestScore) @@ -621,8 +624,8 @@ namespace MediaBrowser.Model.Dlna playlistItem.Container = container; playlistItem.SubProtocol = protocol; - playlistItem.VideoCodecs = new[] { item.VideoStream.Codec }; - playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile?.AudioCodec); + playlistItem.VideoCodecs = [item.VideoStream.Codec]; + playlistItem.AudioCodecs = ContainerHelper.Split(directPlayProfile?.AudioCodec); } private StreamInfo BuildVideoItem(MediaSourceInfo item, MediaOptions options) @@ -637,7 +640,8 @@ namespace MediaBrowser.Model.Dlna RunTimeTicks = item.RunTimeTicks, Context = options.Context, DeviceProfile = options.Profile, - SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile.SubtitleProfiles) + SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile.SubtitleProfiles), + AlwaysBurnInSubtitleWhenTranscoding = options.AlwaysBurnInSubtitleWhenTranscoding }; var subtitleStream = playlistItem.SubtitleStreamIndex.HasValue ? item.GetMediaStream(MediaStreamType.Subtitle, playlistItem.SubtitleStreamIndex.Value) : null; @@ -649,7 +653,7 @@ namespace MediaBrowser.Model.Dlna } // Collect candidate audio streams - ICollection<MediaStream> candidateAudioStreams = audioStream is null ? Array.Empty<MediaStream>() : new[] { audioStream }; + ICollection<MediaStream> candidateAudioStreams = audioStream is null ? [] : [audioStream]; if (!options.AudioStreamIndex.HasValue || options.AudioStreamIndex < 0) { if (audioStream?.IsDefault == true) @@ -700,7 +704,8 @@ namespace MediaBrowser.Model.Dlna directPlayProfile = directPlayInfo.Profile; playlistItem.PlayMethod = directPlay.Value; playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile); - playlistItem.VideoCodecs = new[] { videoStream.Codec }; + var videoCodec = videoStream?.Codec; + playlistItem.VideoCodecs = videoCodec is null ? [] : [videoCodec]; if (directPlay == PlayMethod.DirectPlay) { @@ -711,7 +716,7 @@ namespace MediaBrowser.Model.Dlna { playlistItem.AudioStreamIndex = audioStreamIndex; var audioCodec = item.GetMediaStream(MediaStreamType.Audio, audioStreamIndex.Value)?.Codec; - playlistItem.AudioCodecs = audioCodec is null ? Array.Empty<string>() : new[] { audioCodec }; + playlistItem.AudioCodecs = audioCodec is null ? [] : [audioCodec]; } } else if (directPlay == PlayMethod.DirectStream) @@ -719,7 +724,7 @@ namespace MediaBrowser.Model.Dlna playlistItem.AudioStreamIndex = audioStream?.Index; if (audioStream is not null) { - playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile?.AudioCodec); + playlistItem.AudioCodecs = ContainerHelper.Split(directPlayProfile?.AudioCodec); } SetStreamInfoOptionsFromDirectPlayProfile(options, item, playlistItem, directPlayProfile); @@ -751,8 +756,9 @@ namespace MediaBrowser.Model.Dlna { // 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 is not null) + var (transcodingProfile, playMethod) = GetVideoTranscodeProfile(item, options, videoStream, audioStream, playlistItem); + + if (transcodingProfile is not null && playMethod.HasValue) { SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile); @@ -763,10 +769,9 @@ namespace MediaBrowser.Model.Dlna if (subtitleStream is not 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.SubtitleCodecs = [subtitleProfile.Format]; } if ((playlistItem.TranscodeReasons & (VideoReasons | TranscodeReason.ContainerBitrateExceedsLimit)) != 0) @@ -790,58 +795,94 @@ namespace MediaBrowser.Model.Dlna return playlistItem; } - private TranscodingProfile? GetVideoTranscodeProfile( + private (TranscodingProfile? Profile, PlayMethod? PlayMethod) GetVideoTranscodeProfile( MediaSourceInfo item, MediaOptions options, MediaStream? videoStream, MediaStream? audioStream, - IEnumerable<MediaStream> candidateAudioStreams, - MediaStream? subtitleStream, StreamInfo playlistItem) { if (!(item.SupportsTranscoding || item.SupportsDirectStream)) { - return null; + return (null, null); } var transcodingProfiles = options.Profile.TranscodingProfiles .Where(i => i.Type == playlistItem.MediaType && i.Context == options.Context); - if (options.AllowVideoStreamCopy) + if (item.UseMostCompatibleTranscodingProfile) { - // prefer direct copy profile - float videoFramerate = videoStream?.AverageFrameRate ?? videoStream?.RealFrameRate ?? 0; - TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp; - int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio); - int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video); + transcodingProfiles = transcodingProfiles.Where(i => string.Equals(i.Container, "ts", StringComparison.OrdinalIgnoreCase)); + } + + var videoCodec = videoStream?.Codec; + float videoFramerate = videoStream?.ReferenceFrameRate ?? 0; + TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp; + int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio); + int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video); + + var audioCodec = audioStream?.Codec; + var audioProfile = audioStream?.Profile; + var audioChannels = audioStream?.Channels; + var audioBitrate = audioStream?.BitRate; + var audioSampleRate = audioStream?.SampleRate; + var audioBitDepth = audioStream?.BitDepth; - transcodingProfiles = transcodingProfiles.ToLookup(transcodingProfile => + var analyzedProfiles = transcodingProfiles + .Select(transcodingProfile => { - var videoCodecs = ContainerProfile.SplitValue(transcodingProfile.VideoCodec); + var rank = (Video: 3, Audio: 3); + + var container = transcodingProfile.Container; + + if (options.AllowVideoStreamCopy) + { + if (ContainerHelper.ContainsContainer(transcodingProfile.VideoCodec, videoCodec)) + { + var appliedVideoConditions = options.Profile.CodecProfiles + .Where(i => i.Type == CodecType.Video && + i.ContainsAnyCodec(videoCodec, container) && + i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC))) + .Select(i => + i.Conditions.All(condition => ConditionProcessor.IsVideoConditionSatisfied(condition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC))); + + // An empty appliedVideoConditions means that the codec has no conditions for the current video stream + var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied); + rank.Video = conditionsSatisfied ? 1 : 2; + } + } - if (ContainerProfile.ContainsContainer(videoCodecs, item.VideoStream?.Codec)) + if (options.AllowAudioStreamCopy) { - var videoCodec = videoStream?.Codec; - var container = transcodingProfile.Container; - var appliedVideoConditions = options.Profile.CodecProfiles - .Where(i => i.Type == CodecType.Video && - i.ContainsAnyCodec(videoCodec, container) && - i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC))) - .Select(i => - i.Conditions.All(condition => ConditionProcessor.IsVideoConditionSatisfied(condition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC))); - - // An empty appliedVideoConditions means that the codec has no conditions for the current video stream - var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied); - return conditionsSatisfied ? 1 : 2; + if (ContainerHelper.ContainsContainer(transcodingProfile.AudioCodec, audioCodec)) + { + var appliedVideoConditions = options.Profile.CodecProfiles + .Where(i => i.Type == CodecType.VideoAudio && + i.ContainsAnyCodec(audioCodec, container) && + i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false))) + .Select(i => + i.Conditions.All(condition => ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false))); + + // An empty appliedVideoConditions means that the codec has no conditions for the current audio stream + var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied); + rank.Audio = conditionsSatisfied ? 1 : 2; + } + } + + PlayMethod playMethod = PlayMethod.Transcode; + + if (rank.Video == 1) + { + playMethod = PlayMethod.DirectStream; } - return 3; + return (Profile: transcodingProfile, PlayMethod: playMethod, Rank: rank); }) - .OrderBy(lookup => lookup.Key) - .SelectMany(lookup => lookup); - } + .OrderBy(analysis => analysis.Rank); - return transcodingProfiles.FirstOrDefault(); + var profileMatch = analyzedProfiles.FirstOrDefault(); + + return (profileMatch.Profile, profileMatch.PlayMethod); } private void BuildStreamVideoItem( @@ -856,26 +897,24 @@ namespace MediaBrowser.Model.Dlna string? audioCodec) { // Prefer matching video codecs - var videoCodecs = ContainerProfile.SplitValue(videoCodec); + var videoCodecs = ContainerHelper.Split(videoCodec).ToList(); - // Enforce HLS video codec restrictions - if (playlistItem.SubProtocol == MediaStreamProtocol.hls) + if (videoCodecs.Count == 0 && videoStream is not null) { - videoCodecs = videoCodecs.Where(codec => _supportedHlsVideoCodecs.Contains(codec)).ToArray(); + // Add the original codec if no codec is specified + videoCodecs.Add(videoStream.Codec); } - var directVideoCodec = ContainerProfile.ContainsContainer(videoCodecs, videoStream?.Codec) ? videoStream?.Codec : null; - if (directVideoCodec is not null) + // Enforce HLS video codec restrictions + if (playlistItem.SubProtocol == MediaStreamProtocol.hls) { - // merge directVideoCodec to videoCodecs - Array.Resize(ref videoCodecs, videoCodecs.Length + 1); - videoCodecs[^1] = directVideoCodec; + videoCodecs = videoCodecs.Where(codec => _supportedHlsVideoCodecs.Contains(codec)).ToList(); } playlistItem.VideoCodecs = videoCodecs; // Copy video codec options as a starting point, this applies to transcode and direct-stream - playlistItem.MaxFramerate = videoStream?.AverageFrameRate; + playlistItem.MaxFramerate = videoStream?.ReferenceFrameRate; var qualifier = videoStream?.Codec; if (videoStream?.Level is not null) { @@ -893,22 +932,28 @@ namespace MediaBrowser.Model.Dlna } // Prefer matching audio codecs, could do better here - var audioCodecs = ContainerProfile.SplitValue(audioCodec); + var audioCodecs = ContainerHelper.Split(audioCodec).ToList(); + + if (audioCodecs.Count == 0 && audioStream is not null) + { + // Add the original codec if no codec is specified + audioCodecs.Add(audioStream.Codec); + } // Enforce HLS audio codec restrictions if (playlistItem.SubProtocol == MediaStreamProtocol.hls) { if (string.Equals(playlistItem.Container, "mp4", StringComparison.OrdinalIgnoreCase)) { - audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsMp4.Contains(codec)).ToArray(); + audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsMp4.Contains(codec)).ToList(); } else { - audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsTs.Contains(codec)).ToArray(); + audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsTs.Contains(codec)).ToList(); } } - var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec)).FirstOrDefault(); + var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerHelper.ContainsContainer(audioCodecs, false, stream.Codec)).FirstOrDefault(); var directAudioStream = audioStreamWithSupportedCodec?.Channels is not null && audioStreamWithSupportedCodec.Channels.Value <= (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue) ? audioStreamWithSupportedCodec : null; @@ -925,7 +970,8 @@ namespace MediaBrowser.Model.Dlna { audioStream = directAudioStream; playlistItem.AudioStreamIndex = audioStream.Index; - playlistItem.AudioCodecs = new[] { audioStream.Codec }; + audioCodecs = [audioStream.Codec]; + playlistItem.AudioCodecs = audioCodecs; // Copy matching audio codec options playlistItem.AudioSampleRate = audioStream.SampleRate; @@ -949,7 +995,7 @@ namespace MediaBrowser.Model.Dlna double? videoLevel = videoStream?.Level; string? videoProfile = videoStream?.Profile; VideoRangeType? videoRangeType = videoStream?.VideoRangeType; - float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; + float videoFramerate = videoStream is null ? 0 : videoStream.ReferenceFrameRate ?? 0; bool? isAnamorphic = videoStream?.IsAnamorphic; bool? isInterlaced = videoStream?.IsInterlaced; string? videoCodecTag = videoStream?.CodecTag; @@ -966,19 +1012,17 @@ namespace MediaBrowser.Model.Dlna var appliedVideoConditions = options.Profile.CodecProfiles .Where(i => i.Type == CodecType.Video && - i.ContainsAnyCodec(videoStream?.Codec, container, useSubContainer) && + i.ContainsAnyCodec(playlistItem.VideoCodecs, container, useSubContainer) && i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))) // Reverse codec profiles for backward compatibility - first codec profile has higher priority .Reverse(); - - foreach (var i in appliedVideoConditions) + foreach (var condition in appliedVideoConditions) { - var transcodingVideoCodecs = ContainerProfile.SplitValue(videoCodec); - foreach (var transcodingVideoCodec in transcodingVideoCodecs) + foreach (var transcodingVideoCodec in playlistItem.VideoCodecs) { - if (i.ContainsAnyCodec(transcodingVideoCodec, container, useSubContainer)) + if (condition.ContainsAnyCodec(transcodingVideoCodec, container, useSubContainer)) { - ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, true); + ApplyTranscodingConditions(playlistItem, condition.Conditions, transcodingVideoCodec, true, true); continue; } } @@ -999,15 +1043,14 @@ namespace MediaBrowser.Model.Dlna var appliedAudioConditions = options.Profile.CodecProfiles .Where(i => i.Type == CodecType.VideoAudio && - i.ContainsAnyCodec(audioStream?.Codec, container) && + i.ContainsAnyCodec(playlistItem.AudioCodecs, container) && i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio))) // Reverse codec profiles for backward compatibility - first codec profile has higher priority .Reverse(); foreach (var codecProfile in appliedAudioConditions) { - var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec); - foreach (var transcodingAudioCodec in transcodingAudioCodecs) + foreach (var transcodingAudioCodec in playlistItem.AudioCodecs) { if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container)) { @@ -1077,9 +1120,9 @@ namespace MediaBrowser.Model.Dlna return 192000; } - private static int GetAudioBitrate(long maxTotalBitrate, string[] targetAudioCodecs, MediaStream? audioStream, StreamInfo item) + private static int GetAudioBitrate(long maxTotalBitrate, IReadOnlyList<string> targetAudioCodecs, MediaStream? audioStream, StreamInfo item) { - string? targetAudioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0]; + string? targetAudioCodec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0]; int? targetAudioChannels = item.GetTargetAudioChannels(targetAudioCodec); @@ -1096,7 +1139,7 @@ namespace MediaBrowser.Model.Dlna && audioStream.Channels.HasValue && audioStream.Channels.Value > targetAudioChannels.Value) { - // Reduce the bitrate if we're downmixing. + // Reduce the bitrate if we're down mixing. defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, targetAudioChannels); } else if (targetAudioChannels.HasValue @@ -1104,8 +1147,8 @@ namespace MediaBrowser.Model.Dlna && audioStream.Channels.Value <= targetAudioChannels.Value && !string.IsNullOrEmpty(audioStream.Codec) && targetAudioCodecs is not null - && targetAudioCodecs.Length > 0 - && !Array.Exists(targetAudioCodecs, elem => string.Equals(audioStream.Codec, elem, StringComparison.OrdinalIgnoreCase))) + && targetAudioCodecs.Count > 0 + && !targetAudioCodecs.Any(elem => string.Equals(audioStream.Codec, elem, StringComparison.OrdinalIgnoreCase))) { // Shift the bitrate if we're transcoding to a different audio codec. defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, audioStream.Channels.Value); @@ -1208,7 +1251,7 @@ namespace MediaBrowser.Model.Dlna double? videoLevel = videoStream?.Level; string? videoProfile = videoStream?.Profile; VideoRangeType? videoRangeType = videoStream?.VideoRangeType; - float videoFramerate = videoStream is null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0; + float videoFramerate = videoStream is null ? 0 : videoStream.ReferenceFrameRate ?? 0; bool? isAnamorphic = videoStream?.IsAnamorphic; bool? isInterlaced = videoStream?.IsInterlaced; string? videoCodecTag = videoStream?.CodecTag; @@ -1244,7 +1287,7 @@ namespace MediaBrowser.Model.Dlna !checkVideoConditions(codecProfile.ApplyConditions).Any()) .SelectMany(codecProfile => checkVideoConditions(codecProfile.Conditions))); - // Check audiocandidates profile conditions + // Check audio candidates profile conditions var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => CheckVideoAudioStreamDirectPlay(options, mediaSource, container, audioStream)); TranscodeReason subtitleProfileReasons = 0; @@ -1261,25 +1304,8 @@ namespace MediaBrowser.Model.Dlna } } - 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) - { - return index; - } - - index++; - } - - return index; - }; - var containerSupported = false; + TranscodeReason[] rankings = [TranscodeReason.VideoCodecNotSupported, VideoCodecReasons, TranscodeReason.AudioCodecNotSupported, AudioCodecReasons, ContainerReasons]; // Check DirectPlay profiles to see if it can be direct played var analyzedProfiles = profile.DirectPlayProfiles @@ -1345,7 +1371,8 @@ namespace MediaBrowser.Model.Dlna playMethod = PlayMethod.DirectStream; } - var ranked = rank(ref failureReasons); + var ranked = GetRank(ref failureReasons, rankings); + return (Result: (Profile: directPlayProfile, PlayMethod: playMethod, AudioStreamIndex: selectedAudioStream?.Index, TranscodeReason: failureReasons), Order: order, Rank: ranked); }) .OrderByDescending(analysis => analysis.Result.PlayMethod) @@ -1364,7 +1391,7 @@ namespace MediaBrowser.Model.Dlna var failureReasons = analyzedProfiles[false] .Select(analysis => analysis.Result) - .Where(result => !containerSupported || (result.TranscodeReason & TranscodeReason.ContainerNotSupported) == 0) + .Where(result => !containerSupported || !result.TranscodeReason.HasFlag(TranscodeReason.ContainerNotSupported)) .FirstOrDefault().TranscodeReason; if (failureReasons == 0) { @@ -1420,7 +1447,7 @@ namespace MediaBrowser.Model.Dlna /// <param name="playMethod">The <see cref="PlayMethod"/>.</param> /// <param name="transcoderSupport">The <see cref="ITranscoderSupport"/>.</param> /// <param name="outputContainer">The output container.</param> - /// <param name="transcodingSubProtocol">The subtitle transoding protocol.</param> + /// <param name="transcodingSubProtocol">The subtitle transcoding protocol.</param> /// <returns>The normalized input container.</returns> public static SubtitleProfile GetSubtitleProfile( MediaSourceInfo mediaSource, @@ -1446,7 +1473,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (!ContainerProfile.ContainsContainer(profile.Container, outputContainer)) + if (!ContainerHelper.ContainsContainer(profile.Container, outputContainer)) { continue; } @@ -1475,7 +1502,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (!ContainerProfile.ContainsContainer(profile.Container, outputContainer)) + if (!ContainerHelper.ContainsContainer(profile.Container, outputContainer)) { continue; } @@ -1506,17 +1533,12 @@ namespace MediaBrowser.Model.Dlna { if (!string.IsNullOrEmpty(transcodingContainer)) { - string[] normalizedContainers = ContainerProfile.SplitValue(transcodingContainer); - - if (ContainerProfile.ContainsContainer(normalizedContainers, "ts") - || ContainerProfile.ContainsContainer(normalizedContainers, "mpegts") - || ContainerProfile.ContainsContainer(normalizedContainers, "mp4")) + if (ContainerHelper.ContainsContainer(transcodingContainer, "ts,mpegts,mp4")) { return false; } - if (ContainerProfile.ContainsContainer(normalizedContainers, "mkv") - || ContainerProfile.ContainsContainer(normalizedContainers, "matroska")) + if (ContainerHelper.ContainsContainer(transcodingContainer, "mkv,matroska")) { return true; } @@ -2219,5 +2241,22 @@ namespace MediaBrowser.Model.Dlna return false; } + + private int GetRank(ref TranscodeReason a, TranscodeReason[] rankings) + { + var index = 1; + foreach (var flag in rankings) + { + var reason = a & flag; + if (reason != 0) + { + return index; + } + + index++; + } + + return index; + } } } diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index c8a341d41..1ae4e1962 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -1,9 +1,6 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; using Jellyfin.Data.Enums; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; @@ -11,1007 +8,1308 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Session; -namespace MediaBrowser.Model.Dlna +namespace MediaBrowser.Model.Dlna; + +/// <summary> +/// Class holding information on a stream. +/// </summary> +public class StreamInfo { /// <summary> - /// Class StreamInfo. + /// Initializes a new instance of the <see cref="StreamInfo"/> class. /// </summary> - public class StreamInfo + public StreamInfo() { - public StreamInfo() - { - AudioCodecs = Array.Empty<string>(); - VideoCodecs = Array.Empty<string>(); - SubtitleCodecs = Array.Empty<string>(); - StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } + AudioCodecs = []; + VideoCodecs = []; + SubtitleCodecs = []; + StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } - public Guid ItemId { get; set; } + /// <summary> + /// Gets or sets the item id. + /// </summary> + /// <value>The item id.</value> + public Guid ItemId { get; set; } - public PlayMethod PlayMethod { get; set; } + /// <summary> + /// Gets or sets the play method. + /// </summary> + /// <value>The play method.</value> + public PlayMethod PlayMethod { get; set; } - public EncodingContext Context { get; set; } + /// <summary> + /// Gets or sets the encoding context. + /// </summary> + /// <value>The encoding context.</value> + public EncodingContext Context { get; set; } - public DlnaProfileType MediaType { get; set; } + /// <summary> + /// Gets or sets the media type. + /// </summary> + /// <value>The media type.</value> + public DlnaProfileType MediaType { get; set; } - public string? Container { get; set; } + /// <summary> + /// Gets or sets the container. + /// </summary> + /// <value>The container.</value> + public string? Container { get; set; } - public MediaStreamProtocol SubProtocol { get; set; } + /// <summary> + /// Gets or sets the sub protocol. + /// </summary> + /// <value>The sub protocol.</value> + public MediaStreamProtocol SubProtocol { get; set; } - public long StartPositionTicks { get; set; } + /// <summary> + /// Gets or sets the start position ticks. + /// </summary> + /// <value>The start position ticks.</value> + public long StartPositionTicks { get; set; } - public int? SegmentLength { get; set; } + /// <summary> + /// Gets or sets the segment length. + /// </summary> + /// <value>The segment length.</value> + public int? SegmentLength { get; set; } - public int? MinSegments { get; set; } + /// <summary> + /// Gets or sets the minimum segments count. + /// </summary> + /// <value>The minimum segments count.</value> + public int? MinSegments { get; set; } - public bool BreakOnNonKeyFrames { get; set; } + /// <summary> + /// Gets or sets a value indicating whether the stream can be broken on non-keyframes. + /// </summary> + public bool BreakOnNonKeyFrames { get; set; } - public bool RequireAvc { get; set; } + /// <summary> + /// Gets or sets a value indicating whether the stream requires AVC. + /// </summary> + public bool RequireAvc { get; set; } - public bool RequireNonAnamorphic { get; set; } + /// <summary> + /// Gets or sets a value indicating whether the stream requires AVC. + /// </summary> + public bool RequireNonAnamorphic { get; set; } - public bool CopyTimestamps { get; set; } + /// <summary> + /// Gets or sets a value indicating whether timestamps should be copied. + /// </summary> + public bool CopyTimestamps { get; set; } - public bool EnableMpegtsM2TsMode { get; set; } + /// <summary> + /// Gets or sets a value indicating whether timestamps should be copied. + /// </summary> + public bool EnableMpegtsM2TsMode { get; set; } - public bool EnableSubtitlesInManifest { get; set; } + /// <summary> + /// Gets or sets a value indicating whether the subtitle manifest is enabled. + /// </summary> + public bool EnableSubtitlesInManifest { get; set; } - public string[] AudioCodecs { get; set; } + /// <summary> + /// Gets or sets the audio codecs. + /// </summary> + /// <value>The audio codecs.</value> + public IReadOnlyList<string> AudioCodecs { get; set; } - public string[] VideoCodecs { get; set; } + /// <summary> + /// Gets or sets the video codecs. + /// </summary> + /// <value>The video codecs.</value> + public IReadOnlyList<string> VideoCodecs { get; set; } - public int? AudioStreamIndex { get; set; } + /// <summary> + /// Gets or sets the audio stream index. + /// </summary> + /// <value>The audio stream index.</value> + public int? AudioStreamIndex { get; set; } - public int? SubtitleStreamIndex { get; set; } + /// <summary> + /// Gets or sets the video stream index. + /// </summary> + /// <value>The subtitle stream index.</value> + public int? SubtitleStreamIndex { get; set; } - public int? TranscodingMaxAudioChannels { get; set; } + /// <summary> + /// Gets or sets the maximum transcoding audio channels. + /// </summary> + /// <value>The maximum transcoding audio channels.</value> + public int? TranscodingMaxAudioChannels { get; set; } + + /// <summary> + /// Gets or sets the global maximum audio channels. + /// </summary> + /// <value>The global maximum audio channels.</value> + public int? GlobalMaxAudioChannels { get; set; } - public int? GlobalMaxAudioChannels { get; set; } + /// <summary> + /// Gets or sets the audio bitrate. + /// </summary> + /// <value>The audio bitrate.</value> + public int? AudioBitrate { get; set; } - public int? AudioBitrate { get; set; } + /// <summary> + /// Gets or sets the audio sample rate. + /// </summary> + /// <value>The audio sample rate.</value> + public int? AudioSampleRate { get; set; } - public int? AudioSampleRate { get; set; } + /// <summary> + /// Gets or sets the video bitrate. + /// </summary> + /// <value>The video bitrate.</value> + public int? VideoBitrate { get; set; } - public int? VideoBitrate { get; set; } + /// <summary> + /// Gets or sets the maximum output width. + /// </summary> + /// <value>The output width.</value> + public int? MaxWidth { get; set; } - public int? MaxWidth { get; set; } + /// <summary> + /// Gets or sets the maximum output height. + /// </summary> + /// <value>The maximum output height.</value> + public int? MaxHeight { get; set; } - public int? MaxHeight { get; set; } + /// <summary> + /// Gets or sets the maximum framerate. + /// </summary> + /// <value>The maximum framerate.</value> + public float? MaxFramerate { get; set; } - public float? MaxFramerate { get; set; } + /// <summary> + /// Gets or sets the device profile. + /// </summary> + /// <value>The device profile.</value> + public required DeviceProfile DeviceProfile { get; set; } - public required DeviceProfile DeviceProfile { get; set; } + /// <summary> + /// Gets or sets the device profile id. + /// </summary> + /// <value>The device profile id.</value> + public string? DeviceProfileId { get; set; } - public string? DeviceProfileId { get; set; } + /// <summary> + /// Gets or sets the device id. + /// </summary> + /// <value>The device id.</value> + public string? DeviceId { get; set; } - public string? DeviceId { get; set; } + /// <summary> + /// Gets or sets the runtime ticks. + /// </summary> + /// <value>The runtime ticks.</value> + public long? RunTimeTicks { get; set; } - public long? RunTimeTicks { get; set; } + /// <summary> + /// Gets or sets the transcode seek info. + /// </summary> + /// <value>The transcode seek info.</value> + public TranscodeSeekInfo TranscodeSeekInfo { get; set; } - public TranscodeSeekInfo TranscodeSeekInfo { get; set; } + /// <summary> + /// Gets or sets a value indicating whether content length should be estimated. + /// </summary> + public bool EstimateContentLength { get; set; } - public bool EstimateContentLength { get; set; } + /// <summary> + /// Gets or sets the media source info. + /// </summary> + /// <value>The media source info.</value> + public MediaSourceInfo? MediaSource { get; set; } - public MediaSourceInfo? MediaSource { get; set; } + /// <summary> + /// Gets or sets the subtitle codecs. + /// </summary> + /// <value>The subtitle codecs.</value> + public IReadOnlyList<string> SubtitleCodecs { get; set; } - public string[] SubtitleCodecs { get; set; } + /// <summary> + /// Gets or sets the subtitle delivery method. + /// </summary> + /// <value>The subtitle delivery method.</value> + public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; } - public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; } + /// <summary> + /// Gets or sets the subtitle format. + /// </summary> + /// <value>The subtitle format.</value> + public string? SubtitleFormat { get; set; } - public string? SubtitleFormat { get; set; } + /// <summary> + /// Gets or sets the play session id. + /// </summary> + /// <value>The play session id.</value> + public string? PlaySessionId { get; set; } - public string? PlaySessionId { get; set; } + /// <summary> + /// Gets or sets the transcode reasons. + /// </summary> + /// <value>The transcode reasons.</value> + public TranscodeReason TranscodeReasons { get; set; } - public TranscodeReason TranscodeReasons { get; set; } + /// <summary> + /// Gets the stream options. + /// </summary> + /// <value>The stream options.</value> + public Dictionary<string, string> StreamOptions { get; private set; } - public Dictionary<string, string> StreamOptions { get; private set; } + /// <summary> + /// Gets the media source id. + /// </summary> + /// <value>The media source id.</value> + public string? MediaSourceId => MediaSource?.Id; - public string? MediaSourceId => MediaSource?.Id; + /// <summary> + /// Gets or sets a value indicating whether audio VBR encoding is enabled. + /// </summary> + public bool EnableAudioVbrEncoding { get; set; } - public bool EnableAudioVbrEncoding { get; set; } + /// <summary> + /// Gets or sets a value indicating whether always burn in subtitles when transcoding. + /// </summary> + public bool AlwaysBurnInSubtitleWhenTranscoding { get; set; } - public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay) - && PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay; + /// <summary> + /// Gets a value indicating whether the stream is direct. + /// </summary> + public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay) + && PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay; - /// <summary> - /// Gets the audio stream that will be used. - /// </summary> - public MediaStream? TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex); + /// <summary> + /// Gets the audio stream that will be used in the output stream. + /// </summary> + /// <value>The audio stream.</value> + public MediaStream? TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex); - /// <summary> - /// Gets the video stream that will be used. - /// </summary> - public MediaStream? TargetVideoStream => MediaSource?.VideoStream; + /// <summary> + /// Gets the video stream that will be used in the output stream. + /// </summary> + /// <value>The video stream.</value> + public MediaStream? TargetVideoStream => MediaSource?.VideoStream; - /// <summary> - /// Gets the audio sample rate that will be in the output stream. - /// </summary> - public int? TargetAudioSampleRate + /// <summary> + /// Gets the audio sample rate that will be in the output stream. + /// </summary> + /// <value>The target audio sample rate.</value> + public int? TargetAudioSampleRate + { + get { - get - { - var stream = TargetAudioStream; - return AudioSampleRate.HasValue && !IsDirectStream - ? AudioSampleRate - : stream?.SampleRate; - } + var stream = TargetAudioStream; + return AudioSampleRate.HasValue && !IsDirectStream + ? AudioSampleRate + : stream?.SampleRate; } + } - /// <summary> - /// Gets the audio sample rate that will be in the output stream. - /// </summary> - public int? TargetAudioBitDepth + /// <summary> + /// Gets the audio bit depth that will be in the output stream. + /// </summary> + /// <value>The target bit depth.</value> + public int? TargetAudioBitDepth + { + get { - get + if (IsDirectStream) { - if (IsDirectStream) - { - return TargetAudioStream?.BitDepth; - } - - var targetAudioCodecs = TargetAudioCodec; - var audioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0]; - if (!string.IsNullOrEmpty(audioCodec)) - { - return GetTargetAudioBitDepth(audioCodec); - } - return TargetAudioStream?.BitDepth; } - } - /// <summary> - /// Gets the audio sample rate that will be in the output stream. - /// </summary> - public int? TargetVideoBitDepth - { - get + var targetAudioCodecs = TargetAudioCodec; + var audioCodec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0]; + if (!string.IsNullOrEmpty(audioCodec)) { - if (IsDirectStream) - { - return TargetVideoStream?.BitDepth; - } - - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec)) - { - return GetTargetVideoBitDepth(videoCodec); - } - - return TargetVideoStream?.BitDepth; + return GetTargetAudioBitDepth(audioCodec); } + + return TargetAudioStream?.BitDepth; } + } - /// <summary> - /// Gets the target reference frames. - /// </summary> - /// <value>The target reference frames.</value> - public int? TargetRefFrames + /// <summary> + /// Gets the video bit depth that will be in the output stream. + /// </summary> + /// <value>The target video bit depth.</value> + public int? TargetVideoBitDepth + { + get { - get + if (IsDirectStream) { - if (IsDirectStream) - { - return TargetVideoStream?.RefFrames; - } - - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec)) - { - return GetTargetRefFrames(videoCodec); - } + return TargetVideoStream?.BitDepth; + } - return TargetVideoStream?.RefFrames; + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) + { + return GetTargetVideoBitDepth(videoCodec); } + + return TargetVideoStream?.BitDepth; } + } - /// <summary> - /// Gets the audio sample rate that will be in the output stream. - /// </summary> - public float? TargetFramerate + /// <summary> + /// Gets the target reference frames that will be in the output stream. + /// </summary> + /// <value>The target reference frames.</value> + public int? TargetRefFrames + { + get { - get + if (IsDirectStream) { - var stream = TargetVideoStream; - return MaxFramerate.HasValue && !IsDirectStream - ? MaxFramerate - : stream is null ? null : stream.AverageFrameRate ?? stream.RealFrameRate; + return TargetVideoStream?.RefFrames; } - } - /// <summary> - /// Gets the audio sample rate that will be in the output stream. - /// </summary> - public double? TargetVideoLevel - { - get + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) { - if (IsDirectStream) - { - return TargetVideoStream?.Level; - } + return GetTargetRefFrames(videoCodec); + } - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec)) - { - return GetTargetVideoLevel(videoCodec); - } + return TargetVideoStream?.RefFrames; + } + } - return TargetVideoStream?.Level; - } + /// <summary> + /// Gets the target framerate that will be in the output stream. + /// </summary> + /// <value>The target framerate.</value> + public float? TargetFramerate + { + get + { + var stream = TargetVideoStream; + return MaxFramerate.HasValue && !IsDirectStream + ? MaxFramerate + : stream?.ReferenceFrameRate; } + } - /// <summary> - /// Gets the audio sample rate that will be in the output stream. - /// </summary> - public int? TargetPacketLength + /// <summary> + /// Gets the target video level that will be in the output stream. + /// </summary> + /// <value>The target video level.</value> + public double? TargetVideoLevel + { + get { - get + if (IsDirectStream) { - var stream = TargetVideoStream; - return !IsDirectStream - ? null - : stream?.PacketLength; + return TargetVideoStream?.Level; } - } - /// <summary> - /// Gets the audio sample rate that will be in the output stream. - /// </summary> - public string? TargetVideoProfile - { - get + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) { - if (IsDirectStream) - { - return TargetVideoStream?.Profile; - } + return GetTargetVideoLevel(videoCodec); + } - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec)) - { - return GetOption(videoCodec, "profile"); - } + return TargetVideoStream?.Level; + } + } - return TargetVideoStream?.Profile; - } + /// <summary> + /// Gets the target packet length that will be in the output stream. + /// </summary> + /// <value>The target packet length.</value> + public int? TargetPacketLength + { + get + { + var stream = TargetVideoStream; + return !IsDirectStream + ? null + : stream?.PacketLength; } + } - /// <summary> - /// Gets the target video range type that will be in the output stream. - /// </summary> - public VideoRangeType TargetVideoRangeType + /// <summary> + /// Gets the target video profile that will be in the output stream. + /// </summary> + /// <value>The target video profile.</value> + public string? TargetVideoProfile + { + get { - get + if (IsDirectStream) { - if (IsDirectStream) - { - return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown; - } - - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec) - && Enum.TryParse(GetOption(videoCodec, "rangetype"), true, out VideoRangeType videoRangeType)) - { - return videoRangeType; - } + return TargetVideoStream?.Profile; + } - return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown; + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) + { + return GetOption(videoCodec, "profile"); } + + return TargetVideoStream?.Profile; } + } - /// <summary> - /// Gets the target video codec tag. - /// </summary> - /// <value>The target video codec tag.</value> - public string? TargetVideoCodecTag + /// <summary> + /// Gets the target video range type that will be in the output stream. + /// </summary> + /// <value>The video range type.</value> + public VideoRangeType TargetVideoRangeType + { + get { - get + if (IsDirectStream) { - var stream = TargetVideoStream; - return !IsDirectStream - ? null - : stream?.CodecTag; + return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown; } - } - /// <summary> - /// Gets the audio bitrate that will be in the output stream. - /// </summary> - public int? TargetAudioBitrate - { - get + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec) + && Enum.TryParse(GetOption(videoCodec, "rangetype"), true, out VideoRangeType videoRangeType)) { - var stream = TargetAudioStream; - return AudioBitrate.HasValue && !IsDirectStream - ? AudioBitrate - : stream?.BitRate; + return videoRangeType; } + + return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown; } + } - /// <summary> - /// Gets the audio channels that will be in the output stream. - /// </summary> - public int? TargetAudioChannels + /// <summary> + /// Gets the target video codec tag. + /// </summary> + /// <value>The video codec tag.</value> + public string? TargetVideoCodecTag + { + get { - get - { - if (IsDirectStream) - { - return TargetAudioStream?.Channels; - } + var stream = TargetVideoStream; + return !IsDirectStream + ? null + : stream?.CodecTag; + } + } - var targetAudioCodecs = TargetAudioCodec; - var codec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0]; - if (!string.IsNullOrEmpty(codec)) - { - return GetTargetRefFrames(codec); - } + /// <summary> + /// Gets the audio bitrate that will be in the output stream. + /// </summary> + /// <value>The audio bitrate.</value> + public int? TargetAudioBitrate + { + get + { + var stream = TargetAudioStream; + return AudioBitrate.HasValue && !IsDirectStream + ? AudioBitrate + : stream?.BitRate; + } + } + /// <summary> + /// Gets the amount of audio channels that will be in the output stream. + /// </summary> + /// <value>The target audio channels.</value> + public int? TargetAudioChannels + { + get + { + if (IsDirectStream) + { return TargetAudioStream?.Channels; } + + var targetAudioCodecs = TargetAudioCodec; + var codec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0]; + if (!string.IsNullOrEmpty(codec)) + { + return GetTargetRefFrames(codec); + } + + return TargetAudioStream?.Channels; } + } - /// <summary> - /// Gets the audio codec that will be in the output stream. - /// </summary> - public string[] TargetAudioCodec + /// <summary> + /// Gets the audio codec that will be in the output stream. + /// </summary> + /// <value>The audio codec.</value> + public IReadOnlyList<string> TargetAudioCodec + { + get { - get - { - var stream = TargetAudioStream; + var stream = TargetAudioStream; - string? inputCodec = stream?.Codec; + string? inputCodec = stream?.Codec; - if (IsDirectStream) - { - return string.IsNullOrEmpty(inputCodec) ? Array.Empty<string>() : new[] { inputCodec }; - } + if (IsDirectStream) + { + return string.IsNullOrEmpty(inputCodec) ? [] : [inputCodec]; + } - foreach (string codec in AudioCodecs) + foreach (string codec in AudioCodecs) + { + if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) - { - return string.IsNullOrEmpty(codec) ? Array.Empty<string>() : new[] { codec }; - } + return string.IsNullOrEmpty(codec) ? [] : [codec]; } - - return AudioCodecs; } + + return AudioCodecs; } + } - public string[] TargetVideoCodec + /// <summary> + /// Gets the video codec that will be in the output stream. + /// </summary> + /// <value>The target video codec.</value> + public IReadOnlyList<string> TargetVideoCodec + { + get { - get - { - var stream = TargetVideoStream; + var stream = TargetVideoStream; - string? inputCodec = stream?.Codec; + string? inputCodec = stream?.Codec; - if (IsDirectStream) - { - return string.IsNullOrEmpty(inputCodec) ? Array.Empty<string>() : new[] { inputCodec }; - } + if (IsDirectStream) + { + return string.IsNullOrEmpty(inputCodec) ? [] : [inputCodec]; + } - foreach (string codec in VideoCodecs) + foreach (string codec in VideoCodecs) + { + if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) - { - return string.IsNullOrEmpty(codec) ? Array.Empty<string>() : new[] { codec }; - } + return string.IsNullOrEmpty(codec) ? [] : [codec]; } - - return VideoCodecs; } + + return VideoCodecs; } + } - /// <summary> - /// Gets the audio channels that will be in the output stream. - /// </summary> - public long? TargetSize + /// <summary> + /// Gets the target size of the output stream. + /// </summary> + /// <value>The target size.</value> + public long? TargetSize + { + get { - get + if (IsDirectStream) { - if (IsDirectStream) - { - return MediaSource?.Size; - } - - if (RunTimeTicks.HasValue) - { - int? totalBitrate = TargetTotalBitrate; + return MediaSource?.Size; + } - double totalSeconds = RunTimeTicks.Value; - // Convert to ms - totalSeconds /= 10000; - // Convert to seconds - totalSeconds /= 1000; + if (RunTimeTicks.HasValue) + { + int? totalBitrate = TargetTotalBitrate; - return totalBitrate.HasValue ? - Convert.ToInt64(totalBitrate.Value * totalSeconds) : - null; - } + double totalSeconds = RunTimeTicks.Value; + // Convert to ms + totalSeconds /= 10000; + // Convert to seconds + totalSeconds /= 1000; - return null; + return totalBitrate.HasValue ? + Convert.ToInt64(totalBitrate.Value * totalSeconds) : + null; } + + return null; } + } - public int? TargetVideoBitrate + /// <summary> + /// Gets the target video bitrate of the output stream. + /// </summary> + /// <value>The video bitrate.</value> + public int? TargetVideoBitrate + { + get { - get - { - var stream = TargetVideoStream; + var stream = TargetVideoStream; - return VideoBitrate.HasValue && !IsDirectStream - ? VideoBitrate - : stream?.BitRate; - } + return VideoBitrate.HasValue && !IsDirectStream + ? VideoBitrate + : stream?.BitRate; } + } - public TransportStreamTimestamp TargetTimestamp + /// <summary> + /// Gets the target timestamp of the output stream. + /// </summary> + /// <value>The target timestamp.</value> + public TransportStreamTimestamp TargetTimestamp + { + get { - get - { - var defaultValue = string.Equals(Container, "m2ts", StringComparison.OrdinalIgnoreCase) - ? TransportStreamTimestamp.Valid - : TransportStreamTimestamp.None; + var defaultValue = string.Equals(Container, "m2ts", StringComparison.OrdinalIgnoreCase) + ? TransportStreamTimestamp.Valid + : TransportStreamTimestamp.None; - return !IsDirectStream - ? defaultValue - : MediaSource is null ? defaultValue : MediaSource.Timestamp ?? TransportStreamTimestamp.None; - } + return !IsDirectStream + ? defaultValue + : MediaSource is null ? defaultValue : MediaSource.Timestamp ?? TransportStreamTimestamp.None; } + } - public int? TargetTotalBitrate => (TargetAudioBitrate ?? 0) + (TargetVideoBitrate ?? 0); + /// <summary> + /// Gets the target total bitrate of the output stream. + /// </summary> + /// <value>The target total bitrate.</value> + public int? TargetTotalBitrate => (TargetAudioBitrate ?? 0) + (TargetVideoBitrate ?? 0); - public bool? IsTargetAnamorphic + /// <summary> + /// Gets a value indicating whether the output stream is anamorphic. + /// </summary> + public bool? IsTargetAnamorphic + { + get { - get + if (IsDirectStream) { - if (IsDirectStream) - { - return TargetVideoStream?.IsAnamorphic; - } - - return false; + return TargetVideoStream?.IsAnamorphic; } + + return false; } + } - public bool? IsTargetInterlaced + /// <summary> + /// Gets a value indicating whether the output stream is interlaced. + /// </summary> + public bool? IsTargetInterlaced + { + get { - get + if (IsDirectStream) { - if (IsDirectStream) - { - return TargetVideoStream?.IsInterlaced; - } - - var targetVideoCodecs = TargetVideoCodec; - var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0]; - if (!string.IsNullOrEmpty(videoCodec)) - { - if (string.Equals(GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - } - return TargetVideoStream?.IsInterlaced; } - } - public bool? IsTargetAVC - { - get + var targetVideoCodecs = TargetVideoCodec; + var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0]; + if (!string.IsNullOrEmpty(videoCodec)) { - if (IsDirectStream) + if (string.Equals(GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase)) { - return TargetVideoStream?.IsAVC; + return false; } - - return true; } + + return TargetVideoStream?.IsInterlaced; } + } - public int? TargetWidth + /// <summary> + /// Gets a value indicating whether the output stream is AVC. + /// </summary> + public bool? IsTargetAVC + { + get { - get + if (IsDirectStream) { - var videoStream = TargetVideoStream; - - if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue) - { - ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value); - - size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0); - - return size.Width; - } - - return MaxWidth; + return TargetVideoStream?.IsAVC; } + + return true; } + } - public int? TargetHeight + /// <summary> + /// Gets the target width of the output stream. + /// </summary> + /// <value>The target width.</value> + public int? TargetWidth + { + get { - get - { - var videoStream = TargetVideoStream; - - if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue) - { - ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value); + var videoStream = TargetVideoStream; - size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0); + if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue) + { + ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value); - return size.Height; - } + size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0); - return MaxHeight; + return size.Width; } + + return MaxWidth; } + } - public int? TargetVideoStreamCount + /// <summary> + /// Gets the target height of the output stream. + /// </summary> + /// <value>The target height.</value> + public int? TargetHeight + { + get { - get + var videoStream = TargetVideoStream; + + if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue) { - if (IsDirectStream) - { - return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue); - } + ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value); - return GetMediaStreamCount(MediaStreamType.Video, 1); + size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0); + + return size.Height; } + + return MaxHeight; } + } - public int? TargetAudioStreamCount + /// <summary> + /// Gets the target video stream count of the output stream. + /// </summary> + /// <value>The target video stream count.</value> + public int? TargetVideoStreamCount + { + get { - get + if (IsDirectStream) { - if (IsDirectStream) - { - return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue); - } - - return GetMediaStreamCount(MediaStreamType.Audio, 1); + return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue); } + + return GetMediaStreamCount(MediaStreamType.Video, 1); } + } - public void SetOption(string? qualifier, string name, string value) + /// <summary> + /// Gets the target audio stream count of the output stream. + /// </summary> + /// <value>The target audio stream count.</value> + public int? TargetAudioStreamCount + { + get { - if (string.IsNullOrEmpty(qualifier)) - { - SetOption(name, value); - } - else + if (IsDirectStream) { - SetOption(qualifier + "-" + name, value); + return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue); } + + return GetMediaStreamCount(MediaStreamType.Audio, 1); } + } - public void SetOption(string name, string value) + /// <summary> + /// Sets a stream option. + /// </summary> + /// <param name="qualifier">The qualifier.</param> + /// <param name="name">The name.</param> + /// <param name="value">The value.</param> + public void SetOption(string? qualifier, string name, string value) + { + if (string.IsNullOrEmpty(qualifier)) { - StreamOptions[name] = value; + SetOption(name, value); } - - public string? GetOption(string? qualifier, string name) + else { - var value = GetOption(qualifier + "-" + name); + SetOption(qualifier + "-" + name, value); + } + } - if (string.IsNullOrEmpty(value)) - { - value = GetOption(name); - } + /// <summary> + /// Sets a stream option. + /// </summary> + /// <param name="name">The name.</param> + /// <param name="value">The value.</param> + public void SetOption(string name, string value) + { + StreamOptions[name] = value; + } - return value; - } + /// <summary> + /// Gets a stream option. + /// </summary> + /// <param name="qualifier">The qualifier.</param> + /// <param name="name">The name.</param> + /// <returns>The value.</returns> + public string? GetOption(string? qualifier, string name) + { + var value = GetOption(qualifier + "-" + name); - public string? GetOption(string name) + if (string.IsNullOrEmpty(value)) { - if (StreamOptions.TryGetValue(name, out var value)) - { - return value; - } - - return null; + value = GetOption(name); } - public string ToUrl(string baseUrl, string? accessToken) + return value; + } + + /// <summary> + /// Gets a stream option. + /// </summary> + /// <param name="name">The name.</param> + /// <returns>The value.</returns> + public string? GetOption(string name) + { + if (StreamOptions.TryGetValue(name, out var value)) { - ArgumentException.ThrowIfNullOrEmpty(baseUrl); + return value; + } - var list = new List<string>(); - foreach (NameValuePair pair in BuildParams(this, accessToken)) - { - if (string.IsNullOrEmpty(pair.Value)) - { - continue; - } + return null; + } - // Try to keep the url clean by omitting defaults - if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase) - && string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase)) - { - continue; - } + /// <summary> + /// Returns this output stream URL for this class. + /// </summary> + /// <param name="baseUrl">The base Url.</param> + /// <param name="accessToken">The access Token.</param> + /// <returns>A querystring representation of this object.</returns> + public string ToUrl(string baseUrl, string? accessToken) + { + ArgumentException.ThrowIfNullOrEmpty(baseUrl); - if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase) - && string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase)) - { - continue; - } + List<string> list = []; + foreach (NameValuePair pair in BuildParams(this, accessToken)) + { + if (string.IsNullOrEmpty(pair.Value)) + { + continue; + } - if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) - && string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase)) - { - continue; - } + // Try to keep the url clean by omitting defaults + if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase) + && string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase)) + { + continue; + } - var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal); + if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase) + && string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase)) + { + continue; + } - list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue)); + if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) + && string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase)) + { + continue; } - string queryString = string.Join('&', list); + var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal); - return GetUrl(baseUrl, queryString); + list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue)); } - private string GetUrl(string baseUrl, string queryString) - { - ArgumentException.ThrowIfNullOrEmpty(baseUrl); + string queryString = string.Join('&', list); - string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container; + return GetUrl(baseUrl, queryString); + } - baseUrl = baseUrl.TrimEnd('/'); + private string GetUrl(string baseUrl, string queryString) + { + ArgumentException.ThrowIfNullOrEmpty(baseUrl); - if (MediaType == DlnaProfileType.Audio) - { - if (SubProtocol == MediaStreamProtocol.hls) - { - return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); - } + string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container; - return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); - } + baseUrl = baseUrl.TrimEnd('/'); + if (MediaType == DlnaProfileType.Audio) + { if (SubProtocol == MediaStreamProtocol.hls) { - return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); + return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); } - return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); + return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); } - private static IEnumerable<NameValuePair> BuildParams(StreamInfo item, string? accessToken) + if (SubProtocol == MediaStreamProtocol.hls) { - var list = new List<NameValuePair>(); + return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); + } - string audioCodecs = item.AudioCodecs.Length == 0 ? - string.Empty : - string.Join(',', item.AudioCodecs); + return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); + } - string videoCodecs = item.VideoCodecs.Length == 0 ? - string.Empty : - string.Join(',', item.VideoCodecs); + private static List<NameValuePair> BuildParams(StreamInfo item, string? accessToken) + { + List<NameValuePair> list = []; + + string audioCodecs = item.AudioCodecs.Count == 0 ? + string.Empty : + string.Join(',', item.AudioCodecs); + + string videoCodecs = item.VideoCodecs.Count == 0 ? + string.Empty : + string.Join(',', item.VideoCodecs); + + list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty)); + list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty)); + list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty)); + list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + list.Add(new NameValuePair("VideoCodec", videoCodecs)); + list.Add(new NameValuePair("AudioCodec", audioCodecs)); + list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && (item.AlwaysBurnInSubtitleWhenTranscoding || item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External) ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + + list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + + long startPositionTicks = item.StartPositionTicks; + + if (item.SubProtocol == MediaStreamProtocol.hls) + { + list.Add(new NameValuePair("StartTimeTicks", string.Empty)); + } + else + { + list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture))); + } - list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty)); - list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty)); - list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty)); - list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - list.Add(new NameValuePair("VideoCodec", videoCodecs)); - list.Add(new NameValuePair("AudioCodec", audioCodecs)); - list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty)); + list.Add(new NameValuePair("api_key", accessToken ?? string.Empty)); - list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + string? liveStreamId = item.MediaSource?.LiveStreamId; + list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty)); - long startPositionTicks = item.StartPositionTicks; + list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty)); - if (item.SubProtocol == MediaStreamProtocol.hls) - { - list.Add(new NameValuePair("StartTimeTicks", string.Empty)); - } - else + if (!item.IsDirectStream) + { + if (item.RequireNonAnamorphic) { - list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture))); + list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); } - list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty)); - list.Add(new NameValuePair("api_key", accessToken ?? string.Empty)); - - string? liveStreamId = item.MediaSource?.LiveStreamId; - list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty)); + list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty)); - - if (!item.IsDirectStream) + if (item.EnableSubtitlesInManifest) { - if (item.RequireNonAnamorphic) - { - list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - - if (item.EnableSubtitlesInManifest) - { - list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - if (item.EnableMpegtsM2TsMode) - { - list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - if (item.EstimateContentLength) - { - list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } + list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + } - if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto) - { - list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant())); - } + if (item.EnableMpegtsM2TsMode) + { + list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + } - if (item.CopyTimestamps) - { - list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } + if (item.EstimateContentLength) + { + list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + } - list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto) + { + list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant())); + } - list.Add(new NameValuePair("EnableAudioVbrEncoding", item.EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + if (item.CopyTimestamps) + { + list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); } - list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty)); + list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - string subtitleCodecs = item.SubtitleCodecs.Length == 0 ? - string.Empty : - string.Join(",", item.SubtitleCodecs); + list.Add(new NameValuePair("EnableAudioVbrEncoding", item.EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + } - list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty)); + list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty)); - if (item.SubProtocol == MediaStreamProtocol.hls) - { - list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty)); + string subtitleCodecs = item.SubtitleCodecs.Count == 0 ? + string.Empty : + string.Join(",", item.SubtitleCodecs); - if (item.SegmentLength.HasValue) - { - list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture))); - } + list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty)); - if (item.MinSegments.HasValue) - { - list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture))); - } + if (item.SubProtocol == MediaStreamProtocol.hls) + { + list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty)); - list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture))); + if (item.SegmentLength.HasValue) + { + list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture))); } - foreach (var pair in item.StreamOptions) + if (item.MinSegments.HasValue) { - if (string.IsNullOrEmpty(pair.Value)) - { - continue; - } - - // strip spaces to avoid having to encode h264 profile names - list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal))); + list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture))); } - if (!item.IsDirectStream) + list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture))); + } + + foreach (var pair in item.StreamOptions) + { + if (string.IsNullOrEmpty(pair.Value)) { - list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.ToString())); + continue; } - return list; + // strip spaces to avoid having to encode h264 profile names + list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal))); } - public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string? accessToken) + if (!item.IsDirectStream) { - return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken); + list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.ToString())); } - public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string? accessToken) + return list; + } + + /// <summary> + /// Gets the subtitle profiles. + /// </summary> + /// <param name="transcoderSupport">The transcoder support.</param> + /// <param name="includeSelectedTrackOnly">If only the selected track should be included.</param> + /// <param name="baseUrl">The base URL.</param> + /// <param name="accessToken">The access token.</param> + /// <returns>The <see cref="SubtitleStreamInfo"/> of the profiles.</returns> + public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string? accessToken) + { + return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken); + } + + /// <summary> + /// Gets the subtitle profiles. + /// </summary> + /// <param name="transcoderSupport">The transcoder support.</param> + /// <param name="includeSelectedTrackOnly">If only the selected track should be included.</param> + /// <param name="enableAllProfiles">If all profiles are enabled.</param> + /// <param name="baseUrl">The base URL.</param> + /// <param name="accessToken">The access token.</param> + /// <returns>The <see cref="SubtitleStreamInfo"/> of the profiles.</returns> + public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string? accessToken) + { + if (MediaSource is null) { - if (MediaSource is null) - { - return Enumerable.Empty<SubtitleStreamInfo>(); - } + return []; + } - var list = new List<SubtitleStreamInfo>(); + List<SubtitleStreamInfo> list = []; - // HLS will preserve timestamps so we can just grab the full subtitle stream - long startPositionTicks = SubProtocol == MediaStreamProtocol.hls - ? 0 - : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0); + // HLS will preserve timestamps so we can just grab the full subtitle stream + long startPositionTicks = SubProtocol == MediaStreamProtocol.hls + ? 0 + : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0); - // First add the selected track - if (SubtitleStreamIndex.HasValue) + // First add the selected track + if (SubtitleStreamIndex.HasValue) + { + foreach (var stream in MediaSource.MediaStreams) { - foreach (var stream in MediaSource.MediaStreams) + if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value) { - if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value) - { - AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks); - } + AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks); } } + } - if (!includeSelectedTrackOnly) + if (!includeSelectedTrackOnly) + { + foreach (var stream in MediaSource.MediaStreams) { - foreach (var stream in MediaSource.MediaStreams) + if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value)) { - if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value)) - { - AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks); - } + AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks); } } - - return list; } - private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string? accessToken, long startPositionTicks) + return list; + } + + private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string? accessToken, long startPositionTicks) + { + if (enableAllProfiles) { - if (enableAllProfiles) + foreach (var profile in DeviceProfile.SubtitleProfiles) { - foreach (var profile in DeviceProfile.SubtitleProfiles) - { - var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport); - if (info is not null) - { - list.Add(info); - } - } - } - else - { - var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport); + var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport); if (info is not null) { list.Add(info); } } } - - private SubtitleStreamInfo? GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string? accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport) + else { - if (MediaSource is null) + var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport); + if (info is not null) { - return null; + list.Add(info); } + } + } - var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol); - var info = new SubtitleStreamInfo - { - IsForced = stream.IsForced, - Language = stream.Language, - Name = stream.Language ?? "Unknown", - Format = subtitleProfile.Format, - Index = stream.Index, - DeliveryMethod = subtitleProfile.Method, - DisplayTitle = stream.DisplayTitle - }; + private SubtitleStreamInfo? GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string? accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport) + { + if (MediaSource is null) + { + return null; + } - if (info.DeliveryMethod == SubtitleDeliveryMethod.External) + var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol); + var info = new SubtitleStreamInfo + { + IsForced = stream.IsForced, + Language = stream.Language, + Name = stream.Language ?? "Unknown", + Format = subtitleProfile.Format, + Index = stream.Index, + DeliveryMethod = subtitleProfile.Method, + DisplayTitle = stream.DisplayTitle + }; + + if (info.DeliveryMethod == SubtitleDeliveryMethod.External) + { + if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal) { - if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal) + info.Url = string.Format( + CultureInfo.InvariantCulture, + "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}", + baseUrl, + ItemId, + MediaSourceId, + stream.Index.ToString(CultureInfo.InvariantCulture), + startPositionTicks.ToString(CultureInfo.InvariantCulture), + subtitleProfile.Format); + + if (!string.IsNullOrEmpty(accessToken)) { - info.Url = string.Format( - CultureInfo.InvariantCulture, - "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}", - baseUrl, - ItemId, - MediaSourceId, - stream.Index.ToString(CultureInfo.InvariantCulture), - startPositionTicks.ToString(CultureInfo.InvariantCulture), - subtitleProfile.Format); - - if (!string.IsNullOrEmpty(accessToken)) - { - info.Url += "?api_key=" + accessToken; - } - - info.IsExternalUrl = false; + info.Url += "?api_key=" + accessToken; } - else - { - info.Url = stream.Path; - info.IsExternalUrl = true; - } - } - - return info; - } - - public int? GetTargetVideoBitDepth(string? codec) - { - var value = GetOption(codec, "videobitdepth"); - if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) + info.IsExternalUrl = false; + } + else { - return result; + info.Url = stream.Path; + info.IsExternalUrl = true; } - - return null; } - public int? GetTargetAudioBitDepth(string? codec) - { - var value = GetOption(codec, "audiobitdepth"); + return info; + } - if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) - { - return result; - } + /// <summary> + /// Gets the target video bit depth. + /// </summary> + /// <param name="codec">The codec.</param> + /// <returns>The target video bit depth.</returns> + public int? GetTargetVideoBitDepth(string? codec) + { + var value = GetOption(codec, "videobitdepth"); - return null; + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) + { + return result; } - public double? GetTargetVideoLevel(string? codec) - { - var value = GetOption(codec, "level"); + return null; + } - if (double.TryParse(value, CultureInfo.InvariantCulture, out var result)) - { - return result; - } + /// <summary> + /// Gets the target audio bit depth. + /// </summary> + /// <param name="codec">The codec.</param> + /// <returns>The target audio bit depth.</returns> + public int? GetTargetAudioBitDepth(string? codec) + { + var value = GetOption(codec, "audiobitdepth"); - return null; + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) + { + return result; } - public int? GetTargetRefFrames(string? codec) - { - var value = GetOption(codec, "maxrefframes"); + return null; + } - if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) - { - return result; - } + /// <summary> + /// Gets the target video level. + /// </summary> + /// <param name="codec">The codec.</param> + /// <returns>The target video level.</returns> + public double? GetTargetVideoLevel(string? codec) + { + var value = GetOption(codec, "level"); - return null; + if (double.TryParse(value, CultureInfo.InvariantCulture, out var result)) + { + return result; } - public int? GetTargetAudioChannels(string? codec) + return null; + } + + /// <summary> + /// Gets the target reference frames. + /// </summary> + /// <param name="codec">The codec.</param> + /// <returns>The target reference frames.</returns> + public int? GetTargetRefFrames(string? codec) + { + var value = GetOption(codec, "maxrefframes"); + + if (int.TryParse(value, CultureInfo.InvariantCulture, out var result)) { - var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels; + return result; + } - var value = GetOption(codec, "audiochannels"); - if (string.IsNullOrEmpty(value)) - { - return defaultValue; - } + return null; + } - if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) - { - return Math.Min(result, defaultValue ?? result); - } + /// <summary> + /// Gets the target audio channels. + /// </summary> + /// <param name="codec">The codec.</param> + /// <returns>The target audio channels.</returns> + public int? GetTargetAudioChannels(string? codec) + { + var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels; + var value = GetOption(codec, "audiochannels"); + if (string.IsNullOrEmpty(value)) + { return defaultValue; } - private int? GetMediaStreamCount(MediaStreamType type, int limit) + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { - var count = MediaSource?.GetStreamCount(type); + return Math.Min(result, defaultValue ?? result); + } - if (count.HasValue) - { - count = Math.Min(count.Value, limit); - } + return defaultValue; + } + + /// <summary> + /// Gets the media stream count. + /// </summary> + /// <param name="type">The type.</param> + /// <param name="limit">The limit.</param> + /// <returns>The media stream count.</returns> + private int? GetMediaStreamCount(MediaStreamType type, int limit) + { + var count = MediaSource?.GetStreamCount(type); - return count; + if (count.HasValue) + { + count = Math.Min(count.Value, limit); } + + return count; } } diff --git a/MediaBrowser.Model/Dlna/SubtitleProfile.cs b/MediaBrowser.Model/Dlna/SubtitleProfile.cs index 9ebde25ff..1879f2dd2 100644 --- a/MediaBrowser.Model/Dlna/SubtitleProfile.cs +++ b/MediaBrowser.Model/Dlna/SubtitleProfile.cs @@ -1,48 +1,62 @@ #nullable disable -#pragma warning disable CS1591 -using System; using System.Xml.Serialization; -using Jellyfin.Extensions; +using MediaBrowser.Model.Extensions; -namespace MediaBrowser.Model.Dlna +namespace MediaBrowser.Model.Dlna; + +/// <summary> +/// A class for subtitle profile information. +/// </summary> +public class SubtitleProfile { - public class SubtitleProfile + /// <summary> + /// Gets or sets the format. + /// </summary> + [XmlAttribute("format")] + public string Format { get; set; } + + /// <summary> + /// Gets or sets the delivery method. + /// </summary> + [XmlAttribute("method")] + public SubtitleDeliveryMethod Method { get; set; } + + /// <summary> + /// Gets or sets the DIDL mode. + /// </summary> + [XmlAttribute("didlMode")] + public string DidlMode { get; set; } + + /// <summary> + /// Gets or sets the language. + /// </summary> + [XmlAttribute("language")] + public string Language { get; set; } + + /// <summary> + /// Gets or sets the container. + /// </summary> + [XmlAttribute("container")] + public string Container { get; set; } + + /// <summary> + /// Checks if a language is supported. + /// </summary> + /// <param name="subLanguage">The language to check for support.</param> + /// <returns><c>true</c> if supported.</returns> + public bool SupportsLanguage(string subLanguage) { - [XmlAttribute("format")] - public string Format { get; set; } - - [XmlAttribute("method")] - public SubtitleDeliveryMethod Method { get; set; } - - [XmlAttribute("didlMode")] - public string DidlMode { get; set; } - - [XmlAttribute("language")] - public string Language { get; set; } - - [XmlAttribute("container")] - public string Container { get; set; } - - public string[] GetLanguages() + if (string.IsNullOrEmpty(Language)) { - return ContainerProfile.SplitValue(Language); + return true; } - public bool SupportsLanguage(string subLanguage) + if (string.IsNullOrEmpty(subLanguage)) { - if (string.IsNullOrEmpty(Language)) - { - return true; - } - - if (string.IsNullOrEmpty(subLanguage)) - { - subLanguage = "und"; - } - - var languages = GetLanguages(); - return languages.Length == 0 || languages.Contains(subLanguage, StringComparison.OrdinalIgnoreCase); + subLanguage = "und"; } + + return ContainerHelper.ContainsContainer(Language, subLanguage); } } diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs index a556799de..5a9fa22ae 100644 --- a/MediaBrowser.Model/Dlna/TranscodingProfile.cs +++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs @@ -1,82 +1,130 @@ -#pragma warning disable CS1591 - -using System; using System.ComponentModel; using System.Xml.Serialization; using Jellyfin.Data.Enums; -namespace MediaBrowser.Model.Dlna +namespace MediaBrowser.Model.Dlna; + +/// <summary> +/// A class for transcoding profile information. +/// </summary> +public class TranscodingProfile { - public class TranscodingProfile + /// <summary> + /// Initializes a new instance of the <see cref="TranscodingProfile" /> class. + /// </summary> + public TranscodingProfile() { - public TranscodingProfile() - { - Conditions = Array.Empty<ProfileCondition>(); - } - - [XmlAttribute("container")] - public string Container { get; set; } = string.Empty; - - [XmlAttribute("type")] - public DlnaProfileType Type { get; set; } - - [XmlAttribute("videoCodec")] - public string VideoCodec { get; set; } = string.Empty; - - [XmlAttribute("audioCodec")] - public string AudioCodec { get; set; } = string.Empty; - - [XmlAttribute("protocol")] - public MediaStreamProtocol Protocol { get; set; } = MediaStreamProtocol.http; - - [DefaultValue(false)] - [XmlAttribute("estimateContentLength")] - public bool EstimateContentLength { get; set; } - - [DefaultValue(false)] - [XmlAttribute("enableMpegtsM2TsMode")] - public bool EnableMpegtsM2TsMode { get; set; } - - [DefaultValue(TranscodeSeekInfo.Auto)] - [XmlAttribute("transcodeSeekInfo")] - public TranscodeSeekInfo TranscodeSeekInfo { get; set; } - - [DefaultValue(false)] - [XmlAttribute("copyTimestamps")] - public bool CopyTimestamps { get; set; } - - [DefaultValue(EncodingContext.Streaming)] - [XmlAttribute("context")] - public EncodingContext Context { get; set; } - - [DefaultValue(false)] - [XmlAttribute("enableSubtitlesInManifest")] - public bool EnableSubtitlesInManifest { get; set; } - - [XmlAttribute("maxAudioChannels")] - public string? MaxAudioChannels { get; set; } - - [DefaultValue(0)] - [XmlAttribute("minSegments")] - public int MinSegments { get; set; } - - [DefaultValue(0)] - [XmlAttribute("segmentLength")] - public int SegmentLength { get; set; } - - [DefaultValue(false)] - [XmlAttribute("breakOnNonKeyFrames")] - public bool BreakOnNonKeyFrames { get; set; } - - public ProfileCondition[] Conditions { get; set; } - - [DefaultValue(true)] - [XmlAttribute("enableAudioVbrEncoding")] - public bool EnableAudioVbrEncoding { get; set; } = true; - - public string[] GetAudioCodecs() - { - return ContainerProfile.SplitValue(AudioCodec); - } + Conditions = []; } + + /// <summary> + /// Gets or sets the container. + /// </summary> + [XmlAttribute("container")] + public string Container { get; set; } = string.Empty; + + /// <summary> + /// Gets or sets the DLNA profile type. + /// </summary> + [XmlAttribute("type")] + public DlnaProfileType Type { get; set; } + + /// <summary> + /// Gets or sets the video codec. + /// </summary> + [XmlAttribute("videoCodec")] + public string VideoCodec { get; set; } = string.Empty; + + /// <summary> + /// Gets or sets the audio codec. + /// </summary> + [XmlAttribute("audioCodec")] + public string AudioCodec { get; set; } = string.Empty; + + /// <summary> + /// Gets or sets the protocol. + /// </summary> + [XmlAttribute("protocol")] + public MediaStreamProtocol Protocol { get; set; } = MediaStreamProtocol.http; + + /// <summary> + /// Gets or sets a value indicating whether the content length should be estimated. + /// </summary> + [DefaultValue(false)] + [XmlAttribute("estimateContentLength")] + public bool EstimateContentLength { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether M2TS mode is enabled. + /// </summary> + [DefaultValue(false)] + [XmlAttribute("enableMpegtsM2TsMode")] + public bool EnableMpegtsM2TsMode { get; set; } + + /// <summary> + /// Gets or sets the transcoding seek info mode. + /// </summary> + [DefaultValue(TranscodeSeekInfo.Auto)] + [XmlAttribute("transcodeSeekInfo")] + public TranscodeSeekInfo TranscodeSeekInfo { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether timestamps should be copied. + /// </summary> + [DefaultValue(false)] + [XmlAttribute("copyTimestamps")] + public bool CopyTimestamps { get; set; } + + /// <summary> + /// Gets or sets the encoding context. + /// </summary> + [DefaultValue(EncodingContext.Streaming)] + [XmlAttribute("context")] + public EncodingContext Context { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether subtitles are allowed in the manifest. + /// </summary> + [DefaultValue(false)] + [XmlAttribute("enableSubtitlesInManifest")] + public bool EnableSubtitlesInManifest { get; set; } + + /// <summary> + /// Gets or sets the maximum audio channels. + /// </summary> + [XmlAttribute("maxAudioChannels")] + public string? MaxAudioChannels { get; set; } + + /// <summary> + /// Gets or sets the minimum amount of segments. + /// </summary> + [DefaultValue(0)] + [XmlAttribute("minSegments")] + public int MinSegments { get; set; } + + /// <summary> + /// Gets or sets the segment length. + /// </summary> + [DefaultValue(0)] + [XmlAttribute("segmentLength")] + public int SegmentLength { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether breaking the video stream on non-keyframes is supported. + /// </summary> + [DefaultValue(false)] + [XmlAttribute("breakOnNonKeyFrames")] + public bool BreakOnNonKeyFrames { get; set; } + + /// <summary> + /// Gets or sets the profile conditions. + /// </summary> + public ProfileCondition[] Conditions { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether variable bitrate encoding is supported. + /// </summary> + [DefaultValue(true)] + [XmlAttribute("enableAudioVbrEncoding")] + public bool EnableAudioVbrEncoding { get; set; } = true; } diff --git a/MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs b/MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs new file mode 100644 index 000000000..5963ed270 --- /dev/null +++ b/MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Jellyfin.Data.Enums; +using Jellyfin.Extensions.Json.Converters; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Model.Dto; + +/// <summary> +/// Client capabilities dto. +/// </summary> +public class ClientCapabilitiesDto +{ + /// <summary> + /// Gets or sets the list of playable media types. + /// </summary> + [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + public IReadOnlyList<MediaType> PlayableMediaTypes { get; set; } = []; + + /// <summary> + /// Gets or sets the list of supported commands. + /// </summary> + [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + public IReadOnlyList<GeneralCommandType> SupportedCommands { get; set; } = []; + + /// <summary> + /// Gets or sets a value indicating whether session supports media control. + /// </summary> + public bool SupportsMediaControl { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether session supports a persistent identifier. + /// </summary> + public bool SupportsPersistentIdentifier { get; set; } + + /// <summary> + /// Gets or sets the device profile. + /// </summary> + public DeviceProfile? DeviceProfile { get; set; } + + /// <summary> + /// Gets or sets the app store url. + /// </summary> + public string? AppStoreUrl { get; set; } + + /// <summary> + /// Gets or sets the icon url. + /// </summary> + public string? IconUrl { get; set; } + + /// <summary> + /// Convert the dto to the full <see cref="ClientCapabilities"/> model. + /// </summary> + /// <returns>The converted <see cref="ClientCapabilities"/> model.</returns> + public ClientCapabilities ToClientCapabilities() + { + return new ClientCapabilities + { + PlayableMediaTypes = PlayableMediaTypes, + SupportedCommands = SupportedCommands, + SupportsMediaControl = SupportsMediaControl, + SupportsPersistentIdentifier = SupportsPersistentIdentifier, + DeviceProfile = DeviceProfile, + AppStoreUrl = AppStoreUrl, + IconUrl = IconUrl + }; + } +} diff --git a/MediaBrowser.Model/Dto/DeviceInfoDto.cs b/MediaBrowser.Model/Dto/DeviceInfoDto.cs new file mode 100644 index 000000000..ac7a731a9 --- /dev/null +++ b/MediaBrowser.Model/Dto/DeviceInfoDto.cs @@ -0,0 +1,83 @@ +using System; + +namespace MediaBrowser.Model.Dto; + +/// <summary> +/// A DTO representing device information. +/// </summary> +public class DeviceInfoDto +{ + /// <summary> + /// Initializes a new instance of the <see cref="DeviceInfoDto"/> class. + /// </summary> + public DeviceInfoDto() + { + Capabilities = new ClientCapabilitiesDto(); + } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string? Name { get; set; } + + /// <summary> + /// Gets or sets the custom name. + /// </summary> + /// <value>The custom name.</value> + public string? CustomName { get; set; } + + /// <summary> + /// Gets or sets the access token. + /// </summary> + /// <value>The access token.</value> + public string? AccessToken { get; set; } + + /// <summary> + /// Gets or sets the identifier. + /// </summary> + /// <value>The identifier.</value> + public string? Id { get; set; } + + /// <summary> + /// Gets or sets the last name of the user. + /// </summary> + /// <value>The last name of the user.</value> + public string? LastUserName { get; set; } + + /// <summary> + /// Gets or sets the name of the application. + /// </summary> + /// <value>The name of the application.</value> + public string? AppName { get; set; } + + /// <summary> + /// Gets or sets the application version. + /// </summary> + /// <value>The application version.</value> + public string? AppVersion { get; set; } + + /// <summary> + /// Gets or sets the last user identifier. + /// </summary> + /// <value>The last user identifier.</value> + public Guid? LastUserId { get; set; } + + /// <summary> + /// Gets or sets the date last modified. + /// </summary> + /// <value>The date last modified.</value> + public DateTime? DateLastActivity { get; set; } + + /// <summary> + /// Gets or sets the capabilities. + /// </summary> + /// <value>The capabilities.</value> + public ClientCapabilitiesDto Capabilities { get; set; } + + /// <summary> + /// Gets or sets the icon URL. + /// </summary> + /// <value>The icon URL.</value> + public string? IconUrl { get; set; } +} diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs index 1c6037325..eff2e09da 100644 --- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs +++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; using MediaBrowser.Model.Dlna; @@ -24,6 +25,7 @@ namespace MediaBrowser.Model.Dto SupportsDirectStream = true; SupportsDirectPlay = true; SupportsProbing = true; + UseMostCompatibleTranscodingProfile = false; } public MediaProtocol Protocol { get; set; } @@ -70,6 +72,9 @@ namespace MediaBrowser.Model.Dto public bool IsInfiniteStream { get; set; } + [DefaultValue(false)] + public bool UseMostCompatibleTranscodingProfile { get; set; } + public bool RequiresOpening { get; set; } public string OpenToken { get; set; } @@ -98,6 +103,8 @@ namespace MediaBrowser.Model.Dto public int? Bitrate { get; set; } + public int? FallbackMaxStreamingBitrate { get; set; } + public TransportStreamTimestamp? Timestamp { get; set; } public Dictionary<string, string> RequiredHttpHeaders { get; set; } diff --git a/MediaBrowser.Model/Dto/PlaylistDto.cs b/MediaBrowser.Model/Dto/PlaylistDto.cs new file mode 100644 index 000000000..d4de75a78 --- /dev/null +++ b/MediaBrowser.Model/Dto/PlaylistDto.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Model.Dto; + +/// <summary> +/// DTO for playlists. +/// </summary> +public class PlaylistDto +{ + /// <summary> + /// Gets or sets a value indicating whether the playlist is publicly readable. + /// </summary> + public bool OpenAccess { get; set; } + + /// <summary> + /// Gets or sets the share permissions. + /// </summary> + public required IReadOnlyList<PlaylistUserPermissions> Shares { get; set; } + + /// <summary> + /// Gets or sets the item ids. + /// </summary> + public required IReadOnlyList<Guid> ItemIds { get; set; } +} diff --git a/MediaBrowser.Model/Dto/SessionInfoDto.cs b/MediaBrowser.Model/Dto/SessionInfoDto.cs new file mode 100644 index 000000000..2496c933a --- /dev/null +++ b/MediaBrowser.Model/Dto/SessionInfoDto.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using Jellyfin.Data.Enums; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Model.Dto; + +/// <summary> +/// Session info DTO. +/// </summary> +public class SessionInfoDto +{ + /// <summary> + /// Gets or sets the play state. + /// </summary> + /// <value>The play state.</value> + public PlayerStateInfo? PlayState { get; set; } + + /// <summary> + /// Gets or sets the additional users. + /// </summary> + /// <value>The additional users.</value> + public IReadOnlyList<SessionUserInfo>? AdditionalUsers { get; set; } + + /// <summary> + /// Gets or sets the client capabilities. + /// </summary> + /// <value>The client capabilities.</value> + public ClientCapabilitiesDto? Capabilities { get; set; } + + /// <summary> + /// Gets or sets the remote end point. + /// </summary> + /// <value>The remote end point.</value> + public string? RemoteEndPoint { get; set; } + + /// <summary> + /// Gets or sets the playable media types. + /// </summary> + /// <value>The playable media types.</value> + public IReadOnlyList<MediaType> PlayableMediaTypes { get; set; } = []; + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + public string? Id { get; set; } + + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets the username. + /// </summary> + /// <value>The username.</value> + public string? UserName { get; set; } + + /// <summary> + /// Gets or sets the type of the client. + /// </summary> + /// <value>The type of the client.</value> + public string? Client { get; set; } + + /// <summary> + /// Gets or sets the last activity date. + /// </summary> + /// <value>The last activity date.</value> + public DateTime LastActivityDate { get; set; } + + /// <summary> + /// Gets or sets the last playback check in. + /// </summary> + /// <value>The last playback check in.</value> + public DateTime LastPlaybackCheckIn { get; set; } + + /// <summary> + /// Gets or sets the last paused date. + /// </summary> + /// <value>The last paused date.</value> + public DateTime? LastPausedDate { get; set; } + + /// <summary> + /// Gets or sets the name of the device. + /// </summary> + /// <value>The name of the device.</value> + public string? DeviceName { get; set; } + + /// <summary> + /// Gets or sets the type of the device. + /// </summary> + /// <value>The type of the device.</value> + public string? DeviceType { get; set; } + + /// <summary> + /// Gets or sets the now playing item. + /// </summary> + /// <value>The now playing item.</value> + public BaseItemDto? NowPlayingItem { get; set; } + + /// <summary> + /// Gets or sets the now viewing item. + /// </summary> + /// <value>The now viewing item.</value> + public BaseItemDto? NowViewingItem { get; set; } + + /// <summary> + /// Gets or sets the device id. + /// </summary> + /// <value>The device id.</value> + public string? DeviceId { get; set; } + + /// <summary> + /// Gets or sets the application version. + /// </summary> + /// <value>The application version.</value> + public string? ApplicationVersion { get; set; } + + /// <summary> + /// Gets or sets the transcoding info. + /// </summary> + /// <value>The transcoding info.</value> + public TranscodingInfo? TranscodingInfo { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this session is active. + /// </summary> + /// <value><c>true</c> if this session is active; otherwise, <c>false</c>.</value> + public bool IsActive { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the session supports media control. + /// </summary> + /// <value><c>true</c> if this session supports media control; otherwise, <c>false</c>.</value> + public bool SupportsMediaControl { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the session supports remote control. + /// </summary> + /// <value><c>true</c> if this session supports remote control; otherwise, <c>false</c>.</value> + public bool SupportsRemoteControl { get; set; } + + /// <summary> + /// Gets or sets the now playing queue. + /// </summary> + /// <value>The now playing queue.</value> + public IReadOnlyList<QueueItem>? NowPlayingQueue { get; set; } + + /// <summary> + /// Gets or sets the now playing queue full items. + /// </summary> + /// <value>The now playing queue full items.</value> + public IReadOnlyList<BaseItemDto>? NowPlayingQueueFullItems { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the session has a custom device name. + /// </summary> + /// <value><c>true</c> if this session has a custom device name; otherwise, <c>false</c>.</value> + public bool HasCustomDeviceName { get; set; } + + /// <summary> + /// Gets or sets the playlist item id. + /// </summary> + /// <value>The splaylist item id.</value> + public string? PlaylistItemId { get; set; } + + /// <summary> + /// Gets or sets the server id. + /// </summary> + /// <value>The server id.</value> + public string? ServerId { get; set; } + + /// <summary> + /// Gets or sets the user primary image tag. + /// </summary> + /// <value>The user primary image tag.</value> + public string? UserPrimaryImageTag { get; set; } + + /// <summary> + /// Gets or sets the supported commands. + /// </summary> + /// <value>The supported commands.</value> + public IReadOnlyList<GeneralCommandType> SupportedCommands { get; set; } = []; +} diff --git a/MediaBrowser.Model/Entities/DeinterlaceMethod.cs b/MediaBrowser.Model/Entities/DeinterlaceMethod.cs new file mode 100644 index 000000000..d05aac433 --- /dev/null +++ b/MediaBrowser.Model/Entities/DeinterlaceMethod.cs @@ -0,0 +1,19 @@ +#pragma warning disable SA1300 // Lowercase required for backwards compat. + +namespace MediaBrowser.Model.Entities; + +/// <summary> +/// Enum containing deinterlace methods. +/// </summary> +public enum DeinterlaceMethod +{ + /// <summary> + /// YADIF. + /// </summary> + yadif = 0, + + /// <summary> + /// BWDIF. + /// </summary> + bwdif = 1 +} diff --git a/MediaBrowser.Model/Entities/EncoderPreset.cs b/MediaBrowser.Model/Entities/EncoderPreset.cs new file mode 100644 index 000000000..74c071433 --- /dev/null +++ b/MediaBrowser.Model/Entities/EncoderPreset.cs @@ -0,0 +1,64 @@ +#pragma warning disable SA1300 // Lowercase required for backwards compat. + +namespace MediaBrowser.Model.Entities; + +/// <summary> +/// Enum containing encoder presets. +/// </summary> +public enum EncoderPreset +{ + /// <summary> + /// Auto preset. + /// </summary> + auto = 0, + + /// <summary> + /// Placebo preset. + /// </summary> + placebo = 1, + + /// <summary> + /// Veryslow preset. + /// </summary> + veryslow = 2, + + /// <summary> + /// Slower preset. + /// </summary> + slower = 3, + + /// <summary> + /// Slow preset. + /// </summary> + slow = 4, + + /// <summary> + /// Medium preset. + /// </summary> + medium = 5, + + /// <summary> + /// Fast preset. + /// </summary> + fast = 6, + + /// <summary> + /// Faster preset. + /// </summary> + faster = 7, + + /// <summary> + /// Veryfast preset. + /// </summary> + veryfast = 8, + + /// <summary> + /// Superfast preset. + /// </summary> + superfast = 9, + + /// <summary> + /// Ultrafast preset. + /// </summary> + ultrafast = 10 +} diff --git a/MediaBrowser.Model/Entities/HardwareAccelerationType.cs b/MediaBrowser.Model/Entities/HardwareAccelerationType.cs new file mode 100644 index 000000000..198a2e00f --- /dev/null +++ b/MediaBrowser.Model/Entities/HardwareAccelerationType.cs @@ -0,0 +1,49 @@ +#pragma warning disable SA1300 // Lowercase required for backwards compat. + +namespace MediaBrowser.Model.Entities; + +/// <summary> +/// Enum containing hardware acceleration types. +/// </summary> +public enum HardwareAccelerationType +{ + /// <summary> + /// Software accelleration. + /// </summary> + none = 0, + + /// <summary> + /// AMD AMF. + /// </summary> + amf = 1, + + /// <summary> + /// Intel Quick Sync Video. + /// </summary> + qsv = 2, + + /// <summary> + /// NVIDIA NVENC. + /// </summary> + nvenc = 3, + + /// <summary> + /// Video4Linux2 V4L2M2M. + /// </summary> + v4l2m2m = 4, + + /// <summary> + /// Video Acceleration API (VAAPI). + /// </summary> + vaapi = 5, + + /// <summary> + /// Video ToolBox. + /// </summary> + videotoolbox = 6, + + /// <summary> + /// Rockchip Media Process Platform (RKMPP). + /// </summary> + rkmpp = 7 +} diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index a0e8c39be..85c1f797b 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -200,7 +200,8 @@ namespace MediaBrowser.Model.Entities || dvProfile == 5 || dvProfile == 7 || dvProfile == 8 - || dvProfile == 9)) + || dvProfile == 9 + || dvProfile == 10)) { var title = "Dolby Vision Profile " + dvProfile; @@ -526,6 +527,23 @@ namespace MediaBrowser.Model.Entities public float? RealFrameRate { get; set; } /// <summary> + /// Gets the framerate used as reference. + /// Prefer AverageFrameRate, if that is null or an unrealistic value + /// then fallback to RealFrameRate. + /// </summary> + /// <value>The reference frame rate.</value> + public float? ReferenceFrameRate + { + get + { + // In some cases AverageFrameRate for videos will be read as 1000fps even if it is not. + // This is probably due to a library compatability issue. + // See https://github.com/jellyfin/jellyfin/pull/12603#discussion_r1748044018 for more info. + return AverageFrameRate < 1000 ? AverageFrameRate : RealFrameRate; + } + } + + /// <summary> /// Gets or sets the profile. /// </summary> /// <value>The profile.</value> @@ -760,7 +778,7 @@ namespace MediaBrowser.Model.Entities var blPresentFlag = BlPresentFlag == 1; var dvBlCompatId = DvBlSignalCompatibilityId; - var isDoViProfile = dvProfile == 5 || dvProfile == 7 || dvProfile == 8; + var isDoViProfile = dvProfile == 5 || dvProfile == 7 || dvProfile == 8 || dvProfile == 10; var isDoViFlag = rpuPresentFlag && blPresentFlag && (dvBlCompatId == 0 || dvBlCompatId == 1 || dvBlCompatId == 4 || dvBlCompatId == 2 || dvBlCompatId == 6); if ((isDoViProfile && isDoViFlag) @@ -783,6 +801,17 @@ namespace MediaBrowser.Model.Entities _ => (VideoRange.SDR, VideoRangeType.SDR) }, 7 => (VideoRange.HDR, VideoRangeType.HDR10), + 10 => dvBlCompatId switch + { + 0 => (VideoRange.HDR, VideoRangeType.DOVI), + 1 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10), + 2 => (VideoRange.SDR, VideoRangeType.DOVIWithSDR), + 4 => (VideoRange.HDR, VideoRangeType.DOVIWithHLG), + // While not in Dolby Spec, Profile 8 CCid 6 media are possible to create, and since CCid 6 stems from Bluray (Profile 7 originally) an HDR10 base layer is guaranteed to exist. + 6 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10), + // There is no other case to handle here as per Dolby Spec. Default case included for completeness and linting purposes + _ => (VideoRange.SDR, VideoRangeType.SDR) + }, _ => (VideoRange.SDR, VideoRangeType.SDR) }; } diff --git a/MediaBrowser.Model/Entities/TonemappingAlgorithm.cs b/MediaBrowser.Model/Entities/TonemappingAlgorithm.cs new file mode 100644 index 000000000..488006e0b --- /dev/null +++ b/MediaBrowser.Model/Entities/TonemappingAlgorithm.cs @@ -0,0 +1,49 @@ +#pragma warning disable SA1300 // Lowercase required for backwards compat. + +namespace MediaBrowser.Model.Entities; + +/// <summary> +/// Enum containing tonemapping algorithms. +/// </summary> +public enum TonemappingAlgorithm +{ + /// <summary> + /// None. + /// </summary> + none = 0, + + /// <summary> + /// Clip. + /// </summary> + clip = 1, + + /// <summary> + /// Linear. + /// </summary> + linear = 2, + + /// <summary> + /// Gamma. + /// </summary> + gamma = 3, + + /// <summary> + /// Reinhard. + /// </summary> + reinhard = 4, + + /// <summary> + /// Hable. + /// </summary> + hable = 5, + + /// <summary> + /// Mobius. + /// </summary> + mobius = 6, + + /// <summary> + /// BT2390. + /// </summary> + bt2390 = 7 +} diff --git a/MediaBrowser.Model/Entities/TonemappingMode.cs b/MediaBrowser.Model/Entities/TonemappingMode.cs new file mode 100644 index 000000000..e10a0b4ad --- /dev/null +++ b/MediaBrowser.Model/Entities/TonemappingMode.cs @@ -0,0 +1,34 @@ +#pragma warning disable SA1300 // Lowercase required for backwards compat. + +namespace MediaBrowser.Model.Entities; + +/// <summary> +/// Enum containing tonemapping modes. +/// </summary> +public enum TonemappingMode +{ + /// <summary> + /// Auto. + /// </summary> + auto = 0, + + /// <summary> + /// Max. + /// </summary> + max = 1, + + /// <summary> + /// RGB. + /// </summary> + rgb = 2, + + /// <summary> + /// Lum. + /// </summary> + lum = 3, + + /// <summary> + /// ITP. + /// </summary> + itp = 4 +} diff --git a/MediaBrowser.Model/Entities/TonemappingRange.cs b/MediaBrowser.Model/Entities/TonemappingRange.cs new file mode 100644 index 000000000..b1446b81c --- /dev/null +++ b/MediaBrowser.Model/Entities/TonemappingRange.cs @@ -0,0 +1,24 @@ +#pragma warning disable SA1300 // Lowercase required for backwards compat. + +namespace MediaBrowser.Model.Entities; + +/// <summary> +/// Enum containing tonemapping ranges. +/// </summary> +public enum TonemappingRange +{ + /// <summary> + /// Auto. + /// </summary> + auto = 0, + + /// <summary> + /// TV. + /// </summary> + tv = 1, + + /// <summary> + /// PC. + /// </summary> + pc = 2 +} diff --git a/MediaBrowser.Model/Extensions/ContainerHelper.cs b/MediaBrowser.Model/Extensions/ContainerHelper.cs new file mode 100644 index 000000000..c86328ba6 --- /dev/null +++ b/MediaBrowser.Model/Extensions/ContainerHelper.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using Jellyfin.Extensions; + +namespace MediaBrowser.Model.Extensions; + +/// <summary> +/// Defines the <see cref="ContainerHelper"/> class. +/// </summary> +public static class ContainerHelper +{ + /// <summary> + /// Compares two containers, returning true if an item in <paramref name="inputContainer"/> exists + /// in <paramref name="profileContainers"/>. + /// </summary> + /// <param name="profileContainers">The comma-delimited string being searched. + /// If the parameter begins with the <c>-</c> character, the operation is reversed.</param> + /// <param name="inputContainer">The comma-delimited string being matched.</param> + /// <returns>The result of the operation.</returns> + public static bool ContainsContainer(string? profileContainers, string? inputContainer) + { + var isNegativeList = false; + if (profileContainers != null && profileContainers.StartsWith('-')) + { + isNegativeList = true; + profileContainers = profileContainers[1..]; + } + + return ContainsContainer(profileContainers, isNegativeList, inputContainer); + } + + /// <summary> + /// Compares two containers, returning true if an item in <paramref name="inputContainer"/> exists + /// in <paramref name="profileContainers"/>. + /// </summary> + /// <param name="profileContainers">The comma-delimited string being searched. + /// If the parameter begins with the <c>-</c> character, the operation is reversed.</param> + /// <param name="inputContainer">The comma-delimited string being matched.</param> + /// <returns>The result of the operation.</returns> + public static bool ContainsContainer(string? profileContainers, ReadOnlySpan<char> inputContainer) + { + var isNegativeList = false; + if (profileContainers != null && profileContainers.StartsWith('-')) + { + isNegativeList = true; + profileContainers = profileContainers[1..]; + } + + return ContainsContainer(profileContainers, isNegativeList, inputContainer); + } + + /// <summary> + /// Compares two containers, returning <paramref name="isNegativeList"/> if an item in <paramref name="inputContainer"/> + /// does not exist in <paramref name="profileContainers"/>. + /// </summary> + /// <param name="profileContainers">The comma-delimited string being searched.</param> + /// <param name="isNegativeList">The boolean result to return if a match is not found.</param> + /// <param name="inputContainer">The comma-delimited string being matched.</param> + /// <returns>The result of the operation.</returns> + public static bool ContainsContainer(string? profileContainers, bool isNegativeList, string? inputContainer) + { + if (string.IsNullOrEmpty(inputContainer)) + { + return isNegativeList; + } + + return ContainsContainer(profileContainers, isNegativeList, inputContainer.AsSpan()); + } + + /// <summary> + /// Compares two containers, returning <paramref name="isNegativeList"/> if an item in <paramref name="inputContainer"/> + /// does not exist in <paramref name="profileContainers"/>. + /// </summary> + /// <param name="profileContainers">The comma-delimited string being searched.</param> + /// <param name="isNegativeList">The boolean result to return if a match is not found.</param> + /// <param name="inputContainer">The comma-delimited string being matched.</param> + /// <returns>The result of the operation.</returns> + public static bool ContainsContainer(string? profileContainers, bool isNegativeList, ReadOnlySpan<char> inputContainer) + { + if (string.IsNullOrEmpty(profileContainers)) + { + // Empty profiles always support all containers/codecs. + return true; + } + + var allInputContainers = inputContainer.Split(','); + var allProfileContainers = profileContainers.SpanSplit(','); + foreach (var container in allInputContainers) + { + if (!container.IsEmpty) + { + foreach (var profile in allProfileContainers) + { + if (!profile.IsEmpty && container.Equals(profile, StringComparison.OrdinalIgnoreCase)) + { + return !isNegativeList; + } + } + } + } + + return isNegativeList; + } + + /// <summary> + /// Compares two containers, returning <paramref name="isNegativeList"/> if an item in <paramref name="inputContainer"/> + /// does not exist in <paramref name="profileContainers"/>. + /// </summary> + /// <param name="profileContainers">The profile containers being matched searched.</param> + /// <param name="isNegativeList">The boolean result to return if a match is not found.</param> + /// <param name="inputContainer">The comma-delimited string being matched.</param> + /// <returns>The result of the operation.</returns> + public static bool ContainsContainer(IReadOnlyList<string>? profileContainers, bool isNegativeList, string inputContainer) + { + if (profileContainers is null) + { + // Empty profiles always support all containers/codecs. + return true; + } + + var allInputContainers = Split(inputContainer); + foreach (var container in allInputContainers) + { + foreach (var profile in profileContainers) + { + if (string.Equals(profile, container, StringComparison.OrdinalIgnoreCase)) + { + return !isNegativeList; + } + } + } + + return isNegativeList; + } + + /// <summary> + /// Splits and input string. + /// </summary> + /// <param name="input">The input string.</param> + /// <returns>The result of the operation.</returns> + public static string[] Split(string? input) + { + return input?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? []; + } +} diff --git a/MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs b/MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs new file mode 100644 index 000000000..4a814f22a --- /dev/null +++ b/MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs @@ -0,0 +1,32 @@ +using System; +using System.Linq; +using MediaBrowser.Model.Configuration; + +namespace MediaBrowser.Model.Extensions; + +/// <summary> +/// Extensions for <see cref="LibraryOptions"/>. +/// </summary> +public static class LibraryOptionsExtension +{ + /// <summary> + /// Get the custom tag delimiters. + /// </summary> + /// <param name="options">This LibraryOptions.</param> + /// <returns>CustomTagDelimiters in char[].</returns> + public static char[] GetCustomTagDelimiters(this LibraryOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + return options.CustomTagDelimiters.Select<string, char?>(x => + { + var isChar = char.TryParse(x, out var c); + if (isChar) + { + return c; + } + + return null; + }).Where(x => x is not null).Select(x => x!.Value).ToArray(); + } +} diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index ec381d423..2085328dd 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -34,6 +34,13 @@ namespace MediaBrowser.Model.IO string MakeAbsolutePath(string folderPath, string filePath); /// <summary> + /// Moves a directory to a new location. + /// </summary> + /// <param name="source">Source directory.</param> + /// <param name="destination">Destination directory.</param> + void MoveDirectory(string source, string destination); + + /// <summary> /// Returns a <see cref="FileSystemMetadata" /> object for the specified file or directory path. /// </summary> /// <param name="path">A path to a file or directory.</param> diff --git a/MediaBrowser.Model/LiveTv/TunerHostInfo.cs b/MediaBrowser.Model/LiveTv/TunerHostInfo.cs index a832169c2..a355387b1 100644 --- a/MediaBrowser.Model/LiveTv/TunerHostInfo.cs +++ b/MediaBrowser.Model/LiveTv/TunerHostInfo.cs @@ -9,6 +9,9 @@ namespace MediaBrowser.Model.LiveTv { AllowHWTranscoding = true; IgnoreDts = true; + AllowStreamSharing = true; + AllowFmp4TranscodingContainer = false; + FallbackMaxStreamingBitrate = 30000000; } public string Id { get; set; } @@ -25,6 +28,12 @@ namespace MediaBrowser.Model.LiveTv public bool AllowHWTranscoding { get; set; } + public bool AllowFmp4TranscodingContainer { get; set; } + + public bool AllowStreamSharing { get; set; } + + public int FallbackMaxStreamingBitrate { get; set; } + public bool EnableStreamLooping { get; set; } public string Source { get; set; } diff --git a/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs b/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs index 24eab1a74..92f467eb0 100644 --- a/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs +++ b/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs @@ -13,6 +13,7 @@ namespace MediaBrowser.Model.MediaInfo { EnableDirectPlay = true; EnableDirectStream = true; + AlwaysBurnInSubtitleWhenTranscoding = false; DirectPlayProtocols = new MediaProtocol[] { MediaProtocol.Http }; } @@ -40,6 +41,8 @@ namespace MediaBrowser.Model.MediaInfo public bool EnableDirectStream { get; set; } + public bool AlwaysBurnInSubtitleWhenTranscoding { get; set; } + public IReadOnlyList<MediaProtocol> DirectPlayProtocols { get; set; } } } diff --git a/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs b/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs new file mode 100644 index 000000000..8c1f44de8 --- /dev/null +++ b/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs @@ -0,0 +1,14 @@ +using System; + +namespace MediaBrowser.Model; + +/// <summary> +/// Model containing the arguments for enumerating the requested media item. +/// </summary> +public record MediaSegmentGenerationRequest +{ + /// <summary> + /// Gets the Id to the BaseItem the segments should be extracted from. + /// </summary> + public Guid ItemId { get; init; } +} diff --git a/MediaBrowser.Model/Session/HardwareEncodingType.cs b/MediaBrowser.Model/Session/HardwareEncodingType.cs deleted file mode 100644 index cf424fef5..000000000 --- a/MediaBrowser.Model/Session/HardwareEncodingType.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace MediaBrowser.Model.Session -{ - /// <summary> - /// Enum HardwareEncodingType. - /// </summary> - public enum HardwareEncodingType - { - /// <summary> - /// AMD AMF. - /// </summary> - AMF = 0, - - /// <summary> - /// Intel Quick Sync Video. - /// </summary> - QSV = 1, - - /// <summary> - /// NVIDIA NVENC. - /// </summary> - NVENC = 2, - - /// <summary> - /// Video4Linux2 V4L2. - /// </summary> - V4L2M2M = 3, - - /// <summary> - /// Video Acceleration API (VAAPI). - /// </summary> - VAAPI = 4, - - /// <summary> - /// Video ToolBox. - /// </summary> - VideoToolBox = 5, - - /// <summary> - /// Rockchip Media Process Platform (RKMPP). - /// </summary> - RKMPP = 6 - } -} diff --git a/MediaBrowser.Model/Session/TranscodeReason.cs b/MediaBrowser.Model/Session/TranscodeReason.cs index bbdf4536b..39c5ac8fa 100644 --- a/MediaBrowser.Model/Session/TranscodeReason.cs +++ b/MediaBrowser.Model/Session/TranscodeReason.cs @@ -18,6 +18,7 @@ namespace MediaBrowser.Model.Session // Video Constraints VideoProfileNotSupported = 1 << 6, VideoRangeTypeNotSupported = 1 << 24, + VideoCodecTagNotSupported = 1 << 25, VideoLevelNotSupported = 1 << 7, VideoResolutionNotSupported = 1 << 8, VideoBitDepthNotSupported = 1 << 9, diff --git a/MediaBrowser.Model/Session/TranscodingInfo.cs b/MediaBrowser.Model/Session/TranscodingInfo.cs index 000cbd4c5..ae25267ac 100644 --- a/MediaBrowser.Model/Session/TranscodingInfo.cs +++ b/MediaBrowser.Model/Session/TranscodingInfo.cs @@ -1,34 +1,76 @@ #nullable disable -#pragma warning disable CS1591 -namespace MediaBrowser.Model.Session +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Model.Session; + +/// <summary> +/// Class holding information on a runnning transcode. +/// </summary> +public class TranscodingInfo { - public class TranscodingInfo - { - public string AudioCodec { get; set; } + /// <summary> + /// Gets or sets the thread count used for encoding. + /// </summary> + public string AudioCodec { get; set; } - public string VideoCodec { get; set; } + /// <summary> + /// Gets or sets the thread count used for encoding. + /// </summary> + public string VideoCodec { get; set; } - public string Container { get; set; } + /// <summary> + /// Gets or sets the thread count used for encoding. + /// </summary> + public string Container { get; set; } - public bool IsVideoDirect { get; set; } + /// <summary> + /// Gets or sets a value indicating whether the video is passed through. + /// </summary> + public bool IsVideoDirect { get; set; } - public bool IsAudioDirect { get; set; } + /// <summary> + /// Gets or sets a value indicating whether the audio is passed through. + /// </summary> + public bool IsAudioDirect { get; set; } - public int? Bitrate { get; set; } + /// <summary> + /// Gets or sets the bitrate. + /// </summary> + public int? Bitrate { get; set; } - public float? Framerate { get; set; } + /// <summary> + /// Gets or sets the framerate. + /// </summary> + public float? Framerate { get; set; } - public double? CompletionPercentage { get; set; } + /// <summary> + /// Gets or sets the completion percentage. + /// </summary> + public double? CompletionPercentage { get; set; } - public int? Width { get; set; } + /// <summary> + /// Gets or sets the video width. + /// </summary> + public int? Width { get; set; } - public int? Height { get; set; } + /// <summary> + /// Gets or sets the video height. + /// </summary> + public int? Height { get; set; } - public int? AudioChannels { get; set; } + /// <summary> + /// Gets or sets the audio channels. + /// </summary> + public int? AudioChannels { get; set; } - public HardwareEncodingType? HardwareAccelerationType { get; set; } + /// <summary> + /// Gets or sets the hardware acceleration type. + /// </summary> + public HardwareAccelerationType? HardwareAccelerationType { get; set; } - public TranscodeReason TranscodeReasons { get; set; } - } + /// <summary> + /// Gets or sets the transcode reasons. + /// </summary> + public TranscodeReason TranscodeReasons { get; set; } } |
