mirror of https://github.com/jellyfin/jellyfin.git
Cleanup and refactor streambuilder
This commit is contained in:
parent
3cb7fe5012
commit
697efec86e
|
@ -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(),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -7,11 +7,11 @@ 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()
|
public MediaOptions()
|
||||||
{
|
{
|
||||||
Context = EncodingContext.Streaming;
|
Context = EncodingContext.Streaming;
|
||||||
|
|
||||||
|
@ -27,8 +27,16 @@ namespace MediaBrowser.Model.Dlna
|
||||||
|
|
||||||
public bool ForceDirectStream { get; set; }
|
public bool ForceDirectStream { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets an override for allowing stream copy.
|
||||||
|
/// </summary>
|
||||||
public bool AllowAudioStreamCopy { get; set; }
|
public bool AllowAudioStreamCopy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets an override for allowing stream copy.
|
||||||
|
/// </summary>
|
||||||
|
public bool AllowVideoStreamCopy { get; set; }
|
||||||
|
|
||||||
public Guid ItemId { get; set; }
|
public Guid ItemId { get; set; }
|
||||||
|
|
||||||
public MediaSourceInfo[] MediaSources { get; set; }
|
public MediaSourceInfo[] MediaSources { get; set; }
|
||||||
|
@ -65,6 +73,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>
|
|
@ -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,35 +26,49 @@ 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 (MediaSourceInfo i in mediaSources)
|
||||||
{
|
{
|
||||||
StreamInfo streamInfo = BuildAudioItem(i, options);
|
StreamInfo streamInfo = GetOptimalAudioStream(i, options);
|
||||||
if (streamInfo is not null)
|
if (streamInfo is not null)
|
||||||
{
|
{
|
||||||
streams.Add(streamInfo);
|
streams.Add(streamInfo);
|
||||||
|
@ -68,9 +84,115 @@ 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 (TranscodingProfile i in options.Profile.TranscodingProfiles)
|
||||||
|
{
|
||||||
|
if (i.Type == playlistItem.MediaType
|
||||||
|
&& i.Context == options.Context
|
||||||
|
&& _transcoderSupport.CanEncodeToAudioCodec(i.AudioCodec ?? i.Container))
|
||||||
|
{
|
||||||
|
transcodingProfile = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transcodingProfile != null)
|
||||||
|
{
|
||||||
|
if (!item.SupportsTranscoding)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile);
|
||||||
|
|
||||||
|
var audioTranscodingConditions = GetProfileConditionsForAudio(options.Profile.CodecProfiles, transcodingProfile.Container, transcodingProfile.AudioCodec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, false).ToArray();
|
||||||
|
ApplyTranscodingConditions(playlistItem, audioTranscodingConditions, null, true, true);
|
||||||
|
|
||||||
|
// Honor requested max channels
|
||||||
|
playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels;
|
||||||
|
|
||||||
|
var configuredBitrate = options.GetMaxBitrate(true);
|
||||||
|
|
||||||
|
long transcodingBitrate = options.AudioTranscodingBitrate ??
|
||||||
|
(options.Context == EncodingContext.Streaming ? options.Profile.MusicStreamingTranscodingBitrate : null) ??
|
||||||
|
configuredBitrate ??
|
||||||
|
128000;
|
||||||
|
|
||||||
|
if (configuredBitrate.HasValue)
|
||||||
|
{
|
||||||
|
transcodingBitrate = Math.Min(configuredBitrate.Value, transcodingBitrate);
|
||||||
|
}
|
||||||
|
|
||||||
|
var longBitrate = Math.Min(transcodingBitrate, playlistItem.AudioBitrate ?? transcodingBitrate);
|
||||||
|
playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate);
|
||||||
|
}
|
||||||
|
|
||||||
|
playlistItem.TranscodeReasons = transcodeReasons;
|
||||||
|
return playlistItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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 (MediaSourceInfo i in options.MediaSources)
|
||||||
|
@ -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,14 +1475,8 @@ 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 by bitrate if coming from an external domain
|
|
||||||
if (item.IsRemote)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
long requestedMaxBitrate = maxBitrate > 0 ? maxBitrate : 1000000;
|
long requestedMaxBitrate = maxBitrate > 0 ? maxBitrate : 1000000;
|
||||||
|
|
||||||
// If we don't know the bitrate, then force a transcode if requested max bitrate is under 40 mbps
|
// If we don't know the bitrate, then force a transcode if requested max bitrate is under 40 mbps
|
||||||
|
@ -1464,40 +1485,22 @@ namespace MediaBrowser.Model.Dlna
|
||||||
if (itemBitrate > requestedMaxBitrate)
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ValidateInput(VideoOptions options)
|
private static void ValidateMediaOptions(MediaOptions options, Boolean IsMediaSource)
|
||||||
{
|
|
||||||
ValidateAudioInput(options);
|
|
||||||
|
|
||||||
if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ValidateAudioInput(AudioOptions options)
|
|
||||||
{
|
{
|
||||||
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)
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Profile is required");
|
throw new ArgumentException("Profile is required");
|
||||||
|
@ -1507,6 +1510,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(
|
||||||
|
@ -1824,8 +1840,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);
|
||||||
|
|
||||||
|
|
|
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue