Merge pull request #7494 from Shadowghost/streambuilder-cleanup

This commit is contained in:
Bond-009 2022-12-29 15:21:28 +01:00 committed by GitHub
commit 817996da4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 311 additions and 260 deletions

View File

@ -195,7 +195,7 @@ namespace Emby.Dlna.Didl
{ {
var sources = _mediaSourceManager.GetStaticMediaSources(video, true, _user); var sources = _mediaSourceManager.GetStaticMediaSources(video, true, _user);
streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions streamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions
{ {
ItemId = video.Id, ItemId = video.Id,
MediaSources = sources.ToArray(), MediaSources = sources.ToArray(),
@ -537,7 +537,7 @@ namespace Emby.Dlna.Didl
{ {
var sources = _mediaSourceManager.GetStaticMediaSources(audio, true, _user); var sources = _mediaSourceManager.GetStaticMediaSources(audio, true, _user);
streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions streamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions
{ {
ItemId = audio.Id, ItemId = audio.Id,
MediaSources = sources.ToArray(), MediaSources = sources.ToArray(),

View File

@ -585,7 +585,7 @@ namespace Emby.Dlna.PlayTo
{ {
return new PlaylistItem return new PlaylistItem
{ {
StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions
{ {
ItemId = item.Id, ItemId = item.Id,
MediaSources = mediaSources, MediaSources = mediaSources,
@ -605,7 +605,7 @@ namespace Emby.Dlna.PlayTo
{ {
return new PlaylistItem return new PlaylistItem
{ {
StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions
{ {
ItemId = item.Id, ItemId = item.Id,
MediaSources = mediaSources, MediaSources = mediaSources,

View File

@ -181,7 +181,7 @@ namespace Jellyfin.Api.Helpers
{ {
var streamBuilder = new StreamBuilder(_mediaEncoder, _logger); var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
var options = new VideoOptions var options = new MediaOptions
{ {
MediaSources = new[] { mediaSource }, MediaSources = new[] { mediaSource },
Context = EncodingContext.Streaming, Context = EncodingContext.Streaming,
@ -244,8 +244,8 @@ namespace Jellyfin.Api.Helpers
// Beginning of Playback Determination // Beginning of Playback Determination
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
? streamBuilder.BuildAudioItem(options) ? streamBuilder.GetOptimalAudioStream(options)
: streamBuilder.BuildVideoItem(options); : streamBuilder.GetOptimalVideoStream(options);
if (streamInfo is not null) if (streamInfo is not null)
{ {

View File

@ -1,5 +1,4 @@
#nullable disable #nullable disable
#pragma warning disable CS1591
using System; using System;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
@ -7,11 +6,14 @@ using MediaBrowser.Model.Dto;
namespace MediaBrowser.Model.Dlna namespace MediaBrowser.Model.Dlna
{ {
/// <summary> /// <summary>
/// Class AudioOptions. /// Class MediaOptions.
/// </summary> /// </summary>
public class AudioOptions public class MediaOptions
{ {
public AudioOptions() /// <summary>
/// Initializes a new instance of the <see cref="MediaOptions"/> class.
/// </summary>
public MediaOptions()
{ {
Context = EncodingContext.Streaming; Context = EncodingContext.Streaming;
@ -19,20 +21,49 @@ namespace MediaBrowser.Model.Dlna
EnableDirectStream = true; EnableDirectStream = true;
} }
/// <summary>
/// Gets or sets a value indicating whether direct playback is allowed.
/// </summary>
public bool EnableDirectPlay { get; set; } public bool EnableDirectPlay { get; set; }
/// <summary>
/// Gets or sets a value indicating whether direct streaming is allowed.
/// </summary>
public bool EnableDirectStream { get; set; } public bool EnableDirectStream { get; set; }
/// <summary>
/// Gets or sets a value indicating whether direct playback is forced.
/// </summary>
public bool ForceDirectPlay { get; set; } public bool ForceDirectPlay { get; set; }
/// <summary>
/// Gets or sets a value indicating whether direct streaming is forced.
/// </summary>
public bool ForceDirectStream { get; set; } public bool ForceDirectStream { get; set; }
/// <summary>
/// Gets or sets a value indicating whether audio stream copy is allowed.
/// </summary>
public bool AllowAudioStreamCopy { get; set; } public bool AllowAudioStreamCopy { get; set; }
/// <summary>
/// Gets or sets a value indicating whether video stream copy is allowed.
/// </summary>
public bool AllowVideoStreamCopy { get; set; }
/// <summary>
/// Gets or sets the item id.
/// </summary>
public Guid ItemId { get; set; } public Guid ItemId { get; set; }
/// <summary>
/// Gets or sets the media sources.
/// </summary>
public MediaSourceInfo[] MediaSources { get; set; } public MediaSourceInfo[] MediaSources { get; set; }
/// <summary>
/// Gets or sets the device profile.
/// </summary>
public DeviceProfile Profile { get; set; } public DeviceProfile Profile { get; set; }
/// <summary> /// <summary>
@ -40,6 +71,9 @@ namespace MediaBrowser.Model.Dlna
/// </summary> /// </summary>
public string MediaSourceId { get; set; } public string MediaSourceId { get; set; }
/// <summary>
/// Gets or sets the device id.
/// </summary>
public string DeviceId { get; set; } public string DeviceId { get; set; }
/// <summary> /// <summary>
@ -49,7 +83,7 @@ namespace MediaBrowser.Model.Dlna
public int? MaxAudioChannels { get; set; } public int? MaxAudioChannels { get; set; }
/// <summary> /// <summary>
/// Gets or sets the application's configured quality setting. /// Gets or sets the application's configured maximum bitrate.
/// </summary> /// </summary>
public int? MaxBitrate { get; set; } public int? MaxBitrate { get; set; }
@ -65,6 +99,16 @@ namespace MediaBrowser.Model.Dlna
/// <value>The audio transcoding bitrate.</value> /// <value>The audio transcoding bitrate.</value>
public int? AudioTranscodingBitrate { get; set; } public int? AudioTranscodingBitrate { get; set; }
/// <summary>
/// Gets or sets an override for the audio stream index.
/// </summary>
public int? AudioStreamIndex { get; set; }
/// <summary>
/// Gets or sets an override for the subtitle stream index.
/// </summary>
public int? SubtitleStreamIndex { get; set; }
/// <summary> /// <summary>
/// Gets the maximum bitrate. /// Gets the maximum bitrate.
/// </summary> /// </summary>

View File

@ -1,5 +1,4 @@
#nullable disable #nullable disable
#pragma warning disable CS1591
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -13,6 +12,9 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.Model.Dlna namespace MediaBrowser.Model.Dlna
{ {
/// <summary>
/// Class StreamBuilder.
/// </summary>
public class StreamBuilder public class StreamBuilder
{ {
// Aliases // Aliases
@ -24,42 +26,56 @@ namespace MediaBrowser.Model.Dlna
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ITranscoderSupport _transcoderSupport; private readonly ITranscoderSupport _transcoderSupport;
/// <summary>
/// Initializes a new instance of the <see cref="StreamBuilder"/> class.
/// </summary>
/// <param name="transcoderSupport">The <see cref="ITranscoderSupport"/> object.</param>
/// <param name="logger">The <see cref="ILogger"/> object.</param>
public StreamBuilder(ITranscoderSupport transcoderSupport, ILogger logger) public StreamBuilder(ITranscoderSupport transcoderSupport, ILogger logger)
{ {
_transcoderSupport = transcoderSupport; _transcoderSupport = transcoderSupport;
_logger = logger; _logger = logger;
} }
/// <summary>
/// Initializes a new instance of the <see cref="StreamBuilder"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/> object.</param>
public StreamBuilder(ILogger<StreamBuilder> logger) public StreamBuilder(ILogger<StreamBuilder> logger)
: this(new FullTranscoderSupport(), logger) : this(new FullTranscoderSupport(), logger)
{ {
} }
public StreamInfo BuildAudioItem(AudioOptions options) /// <summary>
/// Gets the optimal audio stream.
/// </summary>
/// <param name="options">The <see cref="MediaOptions"/> object to get the audio stream from.</param>
/// <returns>The <see cref="StreamInfo"/> of the optimal audio stream.</returns>
public StreamInfo GetOptimalAudioStream(MediaOptions options)
{ {
ValidateAudioInput(options); ValidateMediaOptions(options, false);
var mediaSources = new List<MediaSourceInfo>(); var mediaSources = new List<MediaSourceInfo>();
foreach (MediaSourceInfo i in options.MediaSources) foreach (var mediaSource in options.MediaSources)
{ {
if (string.IsNullOrEmpty(options.MediaSourceId) || if (string.IsNullOrEmpty(options.MediaSourceId) ||
string.Equals(i.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)) string.Equals(mediaSource.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
{ {
mediaSources.Add(i); mediaSources.Add(mediaSource);
} }
} }
var streams = new List<StreamInfo>(); var streams = new List<StreamInfo>();
foreach (MediaSourceInfo i in mediaSources) foreach (var mediaSourceInfo in mediaSources)
{ {
StreamInfo streamInfo = BuildAudioItem(i, options); StreamInfo streamInfo = GetOptimalAudioStream(mediaSourceInfo, options);
if (streamInfo is not null) if (streamInfo is not null)
{ {
streams.Add(streamInfo); streams.Add(streamInfo);
} }
} }
foreach (StreamInfo stream in streams) foreach (var stream in streams)
{ {
stream.DeviceId = options.DeviceId; stream.DeviceId = options.DeviceId;
stream.DeviceProfileId = options.Profile.Id; stream.DeviceProfileId = options.Profile.Id;
@ -68,31 +84,137 @@ namespace MediaBrowser.Model.Dlna
return GetOptimalStream(streams, options.GetMaxBitrate(true) ?? 0); return GetOptimalStream(streams, options.GetMaxBitrate(true) ?? 0);
} }
public StreamInfo BuildVideoItem(VideoOptions options) private StreamInfo GetOptimalAudioStream(MediaSourceInfo item, MediaOptions options)
{ {
ValidateInput(options); var playlistItem = new StreamInfo
{
ItemId = options.ItemId,
MediaType = DlnaProfileType.Audio,
MediaSource = item,
RunTimeTicks = item.RunTimeTicks,
Context = options.Context,
DeviceProfile = options.Profile
};
if (options.ForceDirectPlay)
{
playlistItem.PlayMethod = PlayMethod.DirectPlay;
playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio);
return playlistItem;
}
if (options.ForceDirectStream)
{
playlistItem.PlayMethod = PlayMethod.DirectStream;
playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio);
return playlistItem;
}
MediaStream audioStream = item.GetDefaultAudioStream(null);
var directPlayInfo = GetAudioDirectPlayProfile(item, audioStream, options);
var directPlayMethod = directPlayInfo.PlayMethod;
var transcodeReasons = directPlayInfo.TranscodeReasons;
var inputAudioChannels = audioStream?.Channels;
var inputAudioBitrate = audioStream?.BitDepth;
var inputAudioSampleRate = audioStream?.SampleRate;
var inputAudioBitDepth = audioStream?.BitDepth;
if (directPlayMethod.HasValue)
{
var profile = options.Profile;
var audioFailureConditions = GetProfileConditionsForAudio(profile.CodecProfiles, item.Container, audioStream?.Codec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, true);
var audioFailureReasons = AggregateFailureConditions(item, profile, "AudioCodecProfile", audioFailureConditions);
transcodeReasons |= audioFailureReasons;
if (audioFailureReasons == 0)
{
playlistItem.PlayMethod = directPlayMethod.Value;
playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio, directPlayInfo.Profile);
return playlistItem;
}
}
TranscodingProfile transcodingProfile = null;
foreach (var tcProfile in options.Profile.TranscodingProfiles)
{
if (tcProfile.Type == playlistItem.MediaType
&& tcProfile.Context == options.Context
&& _transcoderSupport.CanEncodeToAudioCodec(transcodingProfile.AudioCodec ?? tcProfile.Container))
{
transcodingProfile = tcProfile;
break;
}
}
if (transcodingProfile != null)
{
if (!item.SupportsTranscoding)
{
return null;
}
SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile);
var audioTranscodingConditions = GetProfileConditionsForAudio(options.Profile.CodecProfiles, transcodingProfile.Container, transcodingProfile.AudioCodec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, false).ToArray();
ApplyTranscodingConditions(playlistItem, audioTranscodingConditions, null, true, true);
// Honor requested max channels
playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels;
var configuredBitrate = options.GetMaxBitrate(true);
long transcodingBitrate = options.AudioTranscodingBitrate
?? (options.Context == EncodingContext.Streaming ? options.Profile.MusicStreamingTranscodingBitrate : null)
?? configuredBitrate
?? 128000;
if (configuredBitrate.HasValue)
{
transcodingBitrate = Math.Min(configuredBitrate.Value, transcodingBitrate);
}
var longBitrate = Math.Min(transcodingBitrate, playlistItem.AudioBitrate ?? transcodingBitrate);
playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate);
}
playlistItem.TranscodeReasons = transcodeReasons;
return playlistItem;
}
/// <summary>
/// Gets the optimal video stream.
/// </summary>
/// <param name="options">The <see cref="MediaOptions"/> object to get the video stream from.</param>
/// <returns>The <see cref="StreamInfo"/> of the optimal video stream.</returns>
public StreamInfo GetOptimalVideoStream(MediaOptions options)
{
ValidateMediaOptions(options, true);
var mediaSources = new List<MediaSourceInfo>(); var mediaSources = new List<MediaSourceInfo>();
foreach (MediaSourceInfo i in options.MediaSources) foreach (var mediaSourceInfo in options.MediaSources)
{ {
if (string.IsNullOrEmpty(options.MediaSourceId) || if (string.IsNullOrEmpty(options.MediaSourceId) ||
string.Equals(i.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase)) string.Equals(mediaSourceInfo.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
{ {
mediaSources.Add(i); mediaSources.Add(mediaSourceInfo);
} }
} }
var streams = new List<StreamInfo>(); var streams = new List<StreamInfo>();
foreach (MediaSourceInfo i in mediaSources) foreach (var mediaSourceInfo in mediaSources)
{ {
var streamInfo = BuildVideoItem(i, options); var streamInfo = BuildVideoItem(mediaSourceInfo, options);
if (streamInfo is not null) if (streamInfo is not null)
{ {
streams.Add(streamInfo); streams.Add(streamInfo);
} }
} }
foreach (StreamInfo stream in streams) foreach (var stream in streams)
{ {
stream.DeviceId = options.DeviceId; stream.DeviceId = options.DeviceId;
stream.DeviceProfileId = options.Profile.Id; stream.DeviceProfileId = options.Profile.Id;
@ -236,6 +358,14 @@ namespace MediaBrowser.Model.Dlna
} }
} }
/// <summary>
/// Normalizes input container.
/// </summary>
/// <param name="inputContainer">The input container.</param>
/// <param name="profile">The <see cref="DeviceProfile"/>.</param>
/// <param name="type">The <see cref="DlnaProfileType"/>.</param>
/// <param name="playProfile">The <see cref="DirectPlayProfile"/> object to get the video stream from.</param>
/// <returns>The the normalized input container.</returns>
public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type, DirectPlayProfile playProfile = null) public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile profile, DlnaProfileType type, DirectPlayProfile playProfile = null)
{ {
if (string.IsNullOrEmpty(inputContainer)) if (string.IsNullOrEmpty(inputContainer))
@ -264,108 +394,7 @@ namespace MediaBrowser.Model.Dlna
return formats[0]; return formats[0];
} }
private StreamInfo BuildAudioItem(MediaSourceInfo item, AudioOptions options) private (DirectPlayProfile Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, MediaOptions options)
{
StreamInfo playlistItem = new StreamInfo
{
ItemId = options.ItemId,
MediaType = DlnaProfileType.Audio,
MediaSource = item,
RunTimeTicks = item.RunTimeTicks,
Context = options.Context,
DeviceProfile = options.Profile
};
if (options.ForceDirectPlay)
{
playlistItem.PlayMethod = PlayMethod.DirectPlay;
playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio);
return playlistItem;
}
if (options.ForceDirectStream)
{
playlistItem.PlayMethod = PlayMethod.DirectStream;
playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio);
return playlistItem;
}
var audioStream = item.GetDefaultAudioStream(null);
var directPlayInfo = GetAudioDirectPlayProfile(item, audioStream, options);
var directPlayMethod = directPlayInfo.PlayMethod;
var transcodeReasons = directPlayInfo.TranscodeReasons;
int? inputAudioChannels = audioStream?.Channels;
int? inputAudioBitrate = audioStream?.BitDepth;
int? inputAudioSampleRate = audioStream?.SampleRate;
int? inputAudioBitDepth = audioStream?.BitDepth;
if (directPlayMethod.HasValue)
{
var profile = options.Profile;
var audioFailureConditions = GetProfileConditionsForAudio(profile.CodecProfiles, item.Container, audioStream?.Codec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, true);
var audioFailureReasons = AggregateFailureConditions(item, profile, "AudioCodecProfile", audioFailureConditions);
transcodeReasons |= audioFailureReasons;
if (audioFailureReasons == 0)
{
playlistItem.PlayMethod = directPlayMethod.Value;
playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Audio, directPlayInfo.Profile);
return playlistItem;
}
}
TranscodingProfile transcodingProfile = null;
foreach (var i in options.Profile.TranscodingProfiles)
{
if (i.Type == playlistItem.MediaType
&& i.Context == options.Context
&& _transcoderSupport.CanEncodeToAudioCodec(i.AudioCodec ?? i.Container))
{
transcodingProfile = i;
break;
}
}
if (transcodingProfile is not null)
{
if (!item.SupportsTranscoding)
{
return null;
}
SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile);
var audioTranscodingConditions = GetProfileConditionsForAudio(options.Profile.CodecProfiles, transcodingProfile.Container, transcodingProfile.AudioCodec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, false).ToArray();
ApplyTranscodingConditions(playlistItem, audioTranscodingConditions, null, true, true);
// Honor requested max channels
playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels;
var configuredBitrate = options.GetMaxBitrate(true);
long transcodingBitrate = options.AudioTranscodingBitrate ??
(options.Context == EncodingContext.Streaming ? options.Profile.MusicStreamingTranscodingBitrate : null) ??
configuredBitrate ??
128000;
if (configuredBitrate.HasValue)
{
transcodingBitrate = Math.Min(configuredBitrate.Value, transcodingBitrate);
}
var longBitrate = Math.Min(transcodingBitrate, playlistItem.AudioBitrate ?? transcodingBitrate);
playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate);
}
playlistItem.TranscodeReasons = transcodeReasons;
return playlistItem;
}
private (DirectPlayProfile Profile, PlayMethod? PlayMethod, TranscodeReason TranscodeReasons) GetAudioDirectPlayProfile(MediaSourceInfo item, MediaStream audioStream, AudioOptions options)
{ {
var directPlayProfile = options.Profile.DirectPlayProfiles var directPlayProfile = options.Profile.DirectPlayProfiles
.FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream)); .FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream));
@ -388,7 +417,7 @@ namespace MediaBrowser.Model.Dlna
// If device requirements are satisfied then allow both direct stream and direct play // If device requirements are satisfied then allow both direct stream and direct play
if (item.SupportsDirectPlay) if (item.SupportsDirectPlay)
{ {
if (IsItemBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectPlay)) if (!IsBitrateLimitExceeded(item, options.GetMaxBitrate(true) ?? 0))
{ {
if (options.EnableDirectPlay) if (options.EnableDirectPlay)
{ {
@ -404,7 +433,7 @@ namespace MediaBrowser.Model.Dlna
// While options takes the network and other factors into account. Only applies to direct stream // While options takes the network and other factors into account. Only applies to direct stream
if (item.SupportsDirectStream) if (item.SupportsDirectStream)
{ {
if (IsItemBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream)) if (!IsBitrateLimitExceeded(item, options.GetMaxBitrate(true) ?? 0))
{ {
if (options.EnableDirectStream) if (options.EnableDirectStream)
{ {
@ -427,7 +456,6 @@ namespace MediaBrowser.Model.Dlna
var containerSupported = false; var containerSupported = false;
var audioSupported = false; var audioSupported = false;
var videoSupported = false; var videoSupported = false;
TranscodeReason reasons = 0;
foreach (var profile in directPlayProfiles) foreach (var profile in directPlayProfiles)
{ {
@ -447,6 +475,7 @@ namespace MediaBrowser.Model.Dlna
} }
} }
TranscodeReason reasons = 0;
if (!containerSupported) if (!containerSupported)
{ {
reasons |= TranscodeReason.ContainerNotSupported; reasons |= TranscodeReason.ContainerNotSupported;
@ -547,7 +576,7 @@ namespace MediaBrowser.Model.Dlna
} }
} }
private static void SetStreamInfoOptionsFromDirectPlayProfile(VideoOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile directPlayProfile) private static void SetStreamInfoOptionsFromDirectPlayProfile(MediaOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile directPlayProfile)
{ {
var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile); var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile);
var protocol = "http"; var protocol = "http";
@ -562,7 +591,7 @@ namespace MediaBrowser.Model.Dlna
playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec); playlistItem.AudioCodecs = ContainerProfile.SplitValue(directPlayProfile.AudioCodec);
} }
private StreamInfo BuildVideoItem(MediaSourceInfo item, VideoOptions options) private StreamInfo BuildVideoItem(MediaSourceInfo item, MediaOptions options)
{ {
ArgumentNullException.ThrowIfNull(item); ArgumentNullException.ThrowIfNull(item);
@ -601,11 +630,15 @@ namespace MediaBrowser.Model.Dlna
var videoStream = item.VideoStream; var videoStream = item.VideoStream;
var directPlayBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectPlay); var bitrateLimitExceeded = IsBitrateLimitExceeded(item, options.GetMaxBitrate(false) ?? 0);
var directStreamBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectStream); var isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || !bitrateLimitExceeded);
bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayBitrateEligibility == 0); var isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || !bitrateLimitExceeded);
bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamBitrateEligibility == 0); TranscodeReason transcodeReasons = 0;
var transcodeReasons = directPlayBitrateEligibility | directStreamBitrateEligibility;
if (bitrateLimitExceeded)
{
transcodeReasons = TranscodeReason.ContainerBitrateExceedsLimit;
}
_logger.LogDebug( _logger.LogDebug(
"Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}", "Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
@ -702,7 +735,7 @@ namespace MediaBrowser.Model.Dlna
} }
} }
_logger.LogInformation( _logger.LogDebug(
"StreamBuilder.BuildVideoItem( Profile={0}, Path={1}, AudioStreamIndex={2}, SubtitleStreamIndex={3} ) => ( PlayMethod={4}, TranscodeReason={5} ) {6}", "StreamBuilder.BuildVideoItem( Profile={0}, Path={1}, AudioStreamIndex={2}, SubtitleStreamIndex={3} ) => ( PlayMethod={4}, TranscodeReason={5} ) {6}",
options.Profile.Name ?? "Anonymous Profile", options.Profile.Name ?? "Anonymous Profile",
item.Path ?? "Unknown path", item.Path ?? "Unknown path",
@ -716,7 +749,7 @@ namespace MediaBrowser.Model.Dlna
return playlistItem; return playlistItem;
} }
private TranscodingProfile GetVideoTranscodeProfile(MediaSourceInfo item, VideoOptions options, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, MediaStream subtitleStream, StreamInfo playlistItem) private TranscodingProfile GetVideoTranscodeProfile(MediaSourceInfo item, MediaOptions options, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, MediaStream subtitleStream, StreamInfo playlistItem)
{ {
if (!(item.SupportsTranscoding || item.SupportsDirectStream)) if (!(item.SupportsTranscoding || item.SupportsDirectStream))
{ {
@ -763,7 +796,7 @@ namespace MediaBrowser.Model.Dlna
return transcodingProfiles.FirstOrDefault(); return transcodingProfiles.FirstOrDefault();
} }
private void BuildStreamVideoItem(StreamInfo playlistItem, VideoOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, string container, string videoCodec, string audioCodec) private void BuildStreamVideoItem(StreamInfo playlistItem, MediaOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, string container, string videoCodec, string audioCodec)
{ {
// Prefer matching video codecs // Prefer matching video codecs
var videoCodecs = ContainerProfile.SplitValue(videoCodec); var videoCodecs = ContainerProfile.SplitValue(videoCodec);
@ -867,7 +900,7 @@ namespace MediaBrowser.Model.Dlna
// Honor requested max channels // Honor requested max channels
playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels; playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels;
int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(false) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem); int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(true) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem);
playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate); playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate);
bool? isSecondaryAudio = audioStream is null ? null : item.IsSecondaryAudio(audioStream); bool? isSecondaryAudio = audioStream is null ? null : item.IsSecondaryAudio(audioStream);
@ -882,14 +915,14 @@ namespace MediaBrowser.Model.Dlna
i.ContainsAnyCodec(audioCodec, container) && i.ContainsAnyCodec(audioCodec, container) &&
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio))); i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)));
isFirstAppliedCodecProfile = true; isFirstAppliedCodecProfile = true;
foreach (var i in appliedAudioConditions) foreach (var codecProfile in appliedAudioConditions)
{ {
var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec); var transcodingAudioCodecs = ContainerProfile.SplitValue(audioCodec);
foreach (var transcodingAudioCodec in transcodingAudioCodecs) foreach (var transcodingAudioCodec in transcodingAudioCodecs)
{ {
if (i.ContainsAnyCodec(transcodingAudioCodec, container)) if (codecProfile.ContainsAnyCodec(transcodingAudioCodec, container))
{ {
ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile); ApplyTranscodingConditions(playlistItem, codecProfile.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile);
isFirstAppliedCodecProfile = false; isFirstAppliedCodecProfile = false;
break; break;
} }
@ -1050,7 +1083,7 @@ namespace MediaBrowser.Model.Dlna
} }
private (DirectPlayProfile Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile( private (DirectPlayProfile Profile, PlayMethod? PlayMethod, int? AudioStreamIndex, TranscodeReason TranscodeReasons) GetVideoDirectPlayProfile(
VideoOptions options, MediaOptions options,
MediaSourceInfo mediaSource, MediaSourceInfo mediaSource,
MediaStream videoStream, MediaStream videoStream,
MediaStream audioStream, MediaStream audioStream,
@ -1237,7 +1270,7 @@ namespace MediaBrowser.Model.Dlna
return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons); return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons);
} }
private TranscodeReason CheckVideoAudioStreamDirectPlay(VideoOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream) private TranscodeReason CheckVideoAudioStreamDirectPlay(MediaOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream)
{ {
var profile = options.Profile; var profile = options.Profile;
var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, mediaSource.IsSecondaryAudio(audioStream)); var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, mediaSource.IsSecondaryAudio(audioStream));
@ -1274,23 +1307,17 @@ namespace MediaBrowser.Model.Dlna
mediaSource.Path ?? "Unknown path"); mediaSource.Path ?? "Unknown path");
} }
private TranscodeReason IsBitrateEligibleForDirectPlayback( /// <summary>
MediaSourceInfo item, /// Normalizes input container.
long maxBitrate, /// </summary>
VideoOptions options, /// <param name="mediaSource">The <see cref="MediaSourceInfo"/>.</param>
PlayMethod playMethod) /// <param name="subtitleStream">The <see cref="MediaStream"/> of the subtitle stream.</param>
{ /// <param name="subtitleProfiles">The list of supported <see cref="SubtitleProfile"/>s.</param>
bool result = IsItemBitrateEligibleForDirectPlayback(item, maxBitrate, playMethod); /// <param name="playMethod">The <see cref="PlayMethod"/>.</param>
if (!result) /// <param name="transcoderSupport">The <see cref="ITranscoderSupport"/>.</param>
{ /// <param name="outputContainer">The output container.</param>
return TranscodeReason.ContainerBitrateExceedsLimit; /// <param name="transcodingSubProtocol">The subtitle transoding protocol.</param>
} /// <returns>The the normalized input container.</returns>
else
{
return 0;
}
}
public static SubtitleProfile GetSubtitleProfile( public static SubtitleProfile GetSubtitleProfile(
MediaSourceInfo mediaSource, MediaSourceInfo mediaSource,
MediaStream subtitleStream, MediaStream subtitleStream,
@ -1448,12 +1475,12 @@ namespace MediaBrowser.Model.Dlna
return null; return null;
} }
private bool IsItemBitrateEligibleForDirectPlayback(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod) private bool IsBitrateLimitExceeded(MediaSourceInfo item, long maxBitrate)
{ {
// Don't restrict bitrate if item is remote. // Don't restrict bitrate if item is remote.
if (item.IsRemote) if (item.IsRemote)
{ {
return true; return false;
} }
// If no maximum bitrate is set, default to no maximum bitrate. // If no maximum bitrate is set, default to no maximum bitrate.
@ -1465,39 +1492,21 @@ namespace MediaBrowser.Model.Dlna
if (itemBitrate > requestedMaxBitrate) if (itemBitrate > requestedMaxBitrate)
{ {
_logger.LogDebug( _logger.LogDebug(
"Bitrate exceeds {PlayBackMethod} limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}", "Bitrate exceeds limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}",
playMethod,
itemBitrate, itemBitrate,
requestedMaxBitrate); requestedMaxBitrate);
return false;
}
return true; return true;
} }
private static void ValidateInput(VideoOptions options) return false;
{
ValidateAudioInput(options);
if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
{
throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested");
} }
if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId)) private static void ValidateMediaOptions(MediaOptions options, bool isMediaSource)
{
throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested");
}
}
private static void ValidateAudioInput(AudioOptions options)
{ {
if (options.ItemId.Equals(default)) if (options.ItemId.Equals(default))
{ {
throw new ArgumentException("ItemId is required");
}
ArgumentException.ThrowIfNullOrEmpty(options.DeviceId); ArgumentException.ThrowIfNullOrEmpty(options.DeviceId);
}
if (options.Profile is null) if (options.Profile is null)
{ {
@ -1508,6 +1517,19 @@ namespace MediaBrowser.Model.Dlna
{ {
throw new ArgumentException("MediaSources is required"); throw new ArgumentException("MediaSources is required");
} }
if (isMediaSource)
{
if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
{
throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested");
}
if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
{
throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested");
}
}
} }
private static IEnumerable<ProfileCondition> GetProfileConditionsForVideoAudio( private static IEnumerable<ProfileCondition> GetProfileConditionsForVideoAudio(
@ -1825,8 +1847,8 @@ namespace MediaBrowser.Model.Dlna
continue; continue;
} }
// change from split by | to comma // Change from split by | to comma
// strip spaces to avoid having to encode // Strip spaces to avoid having to encode
var values = value var values = value
.Split('|', StringSplitOptions.RemoveEmptyEntries); .Split('|', StringSplitOptions.RemoveEmptyEntries);

View File

@ -1,16 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Model.Dlna
{
/// <summary>
/// Class VideoOptions.
/// </summary>
public class VideoOptions : AudioOptions
{
public int? AudioStreamIndex { get; set; }
public int? SubtitleStreamIndex { get; set; }
public bool AllowVideoStreamCopy { get; set; }
}
}

View File

@ -164,7 +164,7 @@ namespace Jellyfin.Model.Tests
[InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)]
public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "") public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "")
{ {
var options = await GetVideoOptions(deviceName, mediaSource); var options = await GetMediaOptions(deviceName, mediaSource);
BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol); BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol);
} }
@ -262,7 +262,7 @@ namespace Jellyfin.Model.Tests
[InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)]
public async Task BuildVideoItemWithFirstExplicitStream(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "") public async Task BuildVideoItemWithFirstExplicitStream(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "")
{ {
var options = await GetVideoOptions(deviceName, mediaSource); var options = await GetMediaOptions(deviceName, mediaSource);
options.AudioStreamIndex = 1; options.AudioStreamIndex = 1;
options.SubtitleStreamIndex = options.MediaSources[0].MediaStreams.Count - 1; options.SubtitleStreamIndex = options.MediaSources[0].MediaStreams.Count - 1;
@ -298,7 +298,7 @@ namespace Jellyfin.Model.Tests
[InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] [InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")]
public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "") public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "")
{ {
var options = await GetVideoOptions(deviceName, mediaSource); var options = await GetMediaOptions(deviceName, mediaSource);
var streamCount = options.MediaSources[0].MediaStreams.Count; var streamCount = options.MediaSources[0].MediaStreams.Count;
if (streamCount > 0) if (streamCount > 0)
{ {
@ -311,7 +311,7 @@ namespace Jellyfin.Model.Tests
Assert.Equal(streamInfo?.SubtitleStreamIndex, options.SubtitleStreamIndex); Assert.Equal(streamInfo?.SubtitleStreamIndex, options.SubtitleStreamIndex);
} }
private StreamInfo? BuildVideoItemSimpleTest(VideoOptions options, PlayMethod? playMethod, TranscodeReason why, string transcodeMode, string transcodeProtocol) private StreamInfo? BuildVideoItemSimpleTest(MediaOptions options, PlayMethod? playMethod, TranscodeReason why, string transcodeMode, string transcodeProtocol)
{ {
if (string.IsNullOrEmpty(transcodeProtocol)) if (string.IsNullOrEmpty(transcodeProtocol))
{ {
@ -320,28 +320,28 @@ namespace Jellyfin.Model.Tests
var builder = GetStreamBuilder(); var builder = GetStreamBuilder();
var val = builder.BuildVideoItem(options); var streamInfo = builder.GetOptimalVideoStream(options);
Assert.NotNull(val); Assert.NotNull(streamInfo);
if (playMethod is not null) if (playMethod is not null)
{ {
Assert.Equal(playMethod, val.PlayMethod); Assert.Equal(playMethod, streamInfo.PlayMethod);
} }
Assert.Equal(why, val.TranscodeReasons); Assert.Equal(why, streamInfo.TranscodeReasons);
var audioStreamIndexInput = options.AudioStreamIndex; var audioStreamIndexInput = options.AudioStreamIndex;
var targetVideoStream = val.TargetVideoStream; var targetVideoStream = streamInfo.TargetVideoStream;
var targetAudioStream = val.TargetAudioStream; var targetAudioStream = streamInfo.TargetAudioStream;
var mediaSource = options.MediaSources.First(source => source.Id == val.MediaSourceId); var mediaSource = options.MediaSources.First(source => source.Id == streamInfo.MediaSourceId);
Assert.NotNull(mediaSource); Assert.NotNull(mediaSource);
var videoStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Video); var videoStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Video);
var audioStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio); var audioStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio);
// TODO: Check AudioStreamIndex vs options.AudioStreamIndex // TODO: Check AudioStreamIndex vs options.AudioStreamIndex
var inputAudioStream = mediaSource.GetDefaultAudioStream(audioStreamIndexInput ?? mediaSource.DefaultAudioStreamIndex); var inputAudioStream = mediaSource.GetDefaultAudioStream(audioStreamIndexInput ?? mediaSource.DefaultAudioStreamIndex);
var uri = ParseUri(val); var uri = ParseUri(streamInfo);
if (playMethod == PlayMethod.DirectPlay) if (playMethod == PlayMethod.DirectPlay)
{ {
@ -351,98 +351,99 @@ namespace Jellyfin.Model.Tests
// Assert.Contains(uri.Extension, containers); // Assert.Contains(uri.Extension, containers);
// Check expected video codec (1) // Check expected video codec (1)
Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec); Assert.Contains(targetVideoStream.Codec, streamInfo.TargetVideoCodec);
Assert.Single(val.TargetVideoCodec); Assert.Single(streamInfo.TargetVideoCodec);
// Check expected audio codecs (1) // Check expected audio codecs (1)
Assert.Contains(targetAudioStream.Codec, val.TargetAudioCodec); Assert.Contains(targetAudioStream.Codec, streamInfo.TargetAudioCodec);
Assert.Single(val.TargetAudioCodec); Assert.Single(streamInfo.TargetAudioCodec);
// Assert.Single(val.AudioCodecs); // Assert.Single(val.AudioCodecs);
if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal)) if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal))
{ {
Assert.Equal(val.Container, uri.Extension); Assert.Equal(streamInfo.Container, uri.Extension);
} }
} }
else if (playMethod == PlayMethod.DirectStream || playMethod == PlayMethod.Transcode) else if (playMethod == PlayMethod.DirectStream || playMethod == PlayMethod.Transcode)
{ {
Assert.NotNull(val.Container); Assert.NotNull(streamInfo.Container);
Assert.NotEmpty(val.VideoCodecs); Assert.NotEmpty(streamInfo.VideoCodecs);
Assert.NotEmpty(val.AudioCodecs); Assert.NotEmpty(streamInfo.AudioCodecs);
// Check expected container (todo: this could be a test param) // Check expected container (todo: this could be a test param)
if (transcodeProtocol.Equals("http", StringComparison.Ordinal)) if (transcodeProtocol.Equals("http", StringComparison.Ordinal))
{ {
// Assert.Equal("webm", val.Container); // Assert.Equal("webm", val.Container);
Assert.Equal(val.Container, uri.Extension); Assert.Equal(streamInfo.Container, uri.Extension);
Assert.Equal("stream", uri.Filename); Assert.Equal("stream", uri.Filename);
Assert.Equal("http", val.SubProtocol); Assert.Equal("http", streamInfo.SubProtocol);
} }
else if (transcodeProtocol.Equals("HLS.mp4", StringComparison.Ordinal)) else if (transcodeProtocol.Equals("HLS.mp4", StringComparison.Ordinal))
{ {
Assert.Equal("mp4", val.Container); Assert.Equal("mp4", streamInfo.Container);
Assert.Equal("m3u8", uri.Extension); Assert.Equal("m3u8", uri.Extension);
Assert.Equal("master", uri.Filename); Assert.Equal("master", uri.Filename);
Assert.Equal("hls", val.SubProtocol); Assert.Equal("hls", streamInfo.SubProtocol);
} }
else else
{ {
Assert.Equal("ts", val.Container); Assert.Equal("ts", streamInfo.Container);
Assert.Equal("m3u8", uri.Extension); Assert.Equal("m3u8", uri.Extension);
Assert.Equal("master", uri.Filename); Assert.Equal("master", uri.Filename);
Assert.Equal("hls", val.SubProtocol); Assert.Equal("hls", streamInfo.SubProtocol);
} }
// Full transcode // Full transcode
if (transcodeMode.Equals("Transcode", StringComparison.Ordinal)) if (transcodeMode.Equals("Transcode", StringComparison.Ordinal))
{ {
if ((val.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError)) == 0) if ((streamInfo.TranscodeReasons & (StreamBuilder.ContainerReasons | TranscodeReason.DirectPlayError)) == 0)
{ {
Assert.All( Assert.All(
videoStreams, videoStreams,
stream => Assert.DoesNotContain(stream.Codec, val.VideoCodecs)); stream => Assert.DoesNotContain(stream.Codec, streamInfo.VideoCodecs));
} }
// TODO: Fill out tests here // TODO: fill out tests here
} }
// DirectStream and Remux // DirectStream and Remux
else else
{ {
// Check expected video codec (1) // Check expected video codec (1)
Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec); Assert.Contains(targetVideoStream.Codec, streamInfo.TargetVideoCodec);
Assert.Single(val.TargetVideoCodec); Assert.Single(streamInfo.TargetVideoCodec);
if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal)) if (transcodeMode.Equals("DirectStream", StringComparison.Ordinal))
{ {
// Check expected audio codecs (1) // Check expected audio codecs (1)
if (!targetAudioStream.IsExternal) if (!targetAudioStream.IsExternal)
{ {
if (val.TranscodeReasons.HasFlag(TranscodeReason.ContainerNotSupported)) // Check expected audio codecs (1)
if (streamInfo.TranscodeReasons.HasFlag(TranscodeReason.ContainerNotSupported))
{ {
Assert.Contains(targetAudioStream.Codec, val.AudioCodecs); Assert.Contains(targetAudioStream.Codec, streamInfo.AudioCodecs);
} }
else else
{ {
Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs); Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs);
} }
} }
} }
else if (transcodeMode.Equals("Remux", StringComparison.Ordinal)) else if (transcodeMode.Equals("Remux", StringComparison.Ordinal))
{ {
// Check expected audio codecs (1) // Check expected audio codecs (1)
Assert.Contains(targetAudioStream.Codec, val.AudioCodecs); Assert.Contains(targetAudioStream.Codec, streamInfo.AudioCodecs);
Assert.Single(val.AudioCodecs); Assert.Single(streamInfo.AudioCodecs);
} }
// Video details // Video details
var videoStream = targetVideoStream; var videoStream = targetVideoStream;
Assert.False(val.EstimateContentLength); Assert.False(streamInfo.EstimateContentLength);
Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo); Assert.Equal(TranscodeSeekInfo.Auto, streamInfo.TranscodeSeekInfo);
Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, val.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty<string>()); Assert.Contains(videoStream.Profile?.ToLowerInvariant() ?? string.Empty, streamInfo.TargetVideoProfile?.Split(",").Select(s => s.ToLowerInvariant()) ?? Array.Empty<string>());
Assert.Equal(videoStream.Level, val.TargetVideoLevel); Assert.Equal(videoStream.Level, streamInfo.TargetVideoLevel);
Assert.Equal(videoStream.BitDepth, val.TargetVideoBitDepth); Assert.Equal(videoStream.BitDepth, streamInfo.TargetVideoBitDepth);
Assert.InRange(val.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue); Assert.InRange(streamInfo.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue);
// Audio codec not supported // Audio codec not supported
if ((why & TranscodeReason.AudioCodecNotSupported) != 0) if ((why & TranscodeReason.AudioCodecNotSupported) != 0)
@ -453,7 +454,7 @@ namespace Jellyfin.Model.Tests
// TODO:fixme // TODO:fixme
if (!targetAudioStream.IsExternal) if (!targetAudioStream.IsExternal)
{ {
Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs); Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs);
} }
} }
@ -465,7 +466,7 @@ namespace Jellyfin.Model.Tests
{ {
if (!stream.IsExternal) if (!stream.IsExternal)
{ {
Assert.DoesNotContain(stream.Codec, val.AudioCodecs); Assert.DoesNotContain(stream.Codec, streamInfo.AudioCodecs);
} }
}); });
} }
@ -474,14 +475,14 @@ namespace Jellyfin.Model.Tests
} }
else if (playMethod is null) else if (playMethod is null)
{ {
Assert.Null(val.SubProtocol); Assert.Null(streamInfo.SubProtocol);
Assert.Equal("stream", uri.Filename); Assert.Equal("stream", uri.Filename);
Assert.False(val.EstimateContentLength); Assert.False(streamInfo.EstimateContentLength);
Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo); Assert.Equal(TranscodeSeekInfo.Auto, streamInfo.TranscodeSeekInfo);
} }
return val; return streamInfo;
} }
private static async ValueTask<T> TestData<T>(string name) private static async ValueTask<T> TestData<T>(string name)
@ -507,7 +508,7 @@ namespace Jellyfin.Model.Tests
return new StreamBuilder(transcodeSupport.Object, logger); return new StreamBuilder(transcodeSupport.Object, logger);
} }
private static async ValueTask<VideoOptions> GetVideoOptions(string deviceProfile, params string[] sources) private static async ValueTask<MediaOptions> GetMediaOptions(string deviceProfile, params string[] sources)
{ {
var mediaSources = sources.Select(src => TestData<MediaSourceInfo>(src)) var mediaSources = sources.Select(src => TestData<MediaSourceInfo>(src))
.Select(val => val.Result) .Select(val => val.Result)
@ -516,7 +517,7 @@ namespace Jellyfin.Model.Tests
var dp = await TestData<DeviceProfile>(deviceProfile); var dp = await TestData<DeviceProfile>(deviceProfile);
return new VideoOptions() return new MediaOptions()
{ {
ItemId = new Guid("11D229B7-2D48-4B95-9F9B-49F6AB75E613"), ItemId = new Guid("11D229B7-2D48-4B95-9F9B-49F6AB75E613"),
MediaSourceId = mediaSourceId, MediaSourceId = mediaSourceId,