2019-01-13 21:02:23 +01:00
using System ;
2019-01-13 20:26:15 +01:00
using System.Collections.Generic ;
using System.Globalization ;
using System.Linq ;
2023-06-15 13:28:01 +02:00
using Jellyfin.Data.Enums ;
2024-01-17 08:51:39 -07:00
using Jellyfin.Extensions ;
2019-01-13 20:26:15 +01:00
using MediaBrowser.Model.Dto ;
2018-12-27 18:27:57 -05:00
using MediaBrowser.Model.Entities ;
2024-09-17 20:29:43 +02:00
using MediaBrowser.Model.Extensions ;
2018-12-27 18:27:57 -05:00
using MediaBrowser.Model.MediaInfo ;
using MediaBrowser.Model.Session ;
2019-01-13 20:26:15 +01:00
using Microsoft.Extensions.Logging ;
2018-12-27 18:27:57 -05:00
namespace MediaBrowser.Model.Dlna
{
2022-03-26 12:11:00 +01:00
/// <summary>
/// Class StreamBuilder.
/// </summary>
2018-12-27 18:27:57 -05:00
public class StreamBuilder
{
2022-01-23 23:49:14 +00:00
// Aliases
internal const TranscodeReason ContainerReasons = TranscodeReason . ContainerNotSupported | TranscodeReason . ContainerBitrateExceedsLimit ;
2024-09-14 01:26:51 +08:00
internal const TranscodeReason AudioCodecReasons = TranscodeReason . AudioBitrateNotSupported | TranscodeReason . AudioChannelsNotSupported | TranscodeReason . AudioProfileNotSupported | TranscodeReason . AudioSampleRateNotSupported | TranscodeReason . SecondaryAudioNotSupported | TranscodeReason . AudioBitDepthNotSupported | TranscodeReason . AudioIsExternal ;
internal const TranscodeReason AudioReasons = TranscodeReason . AudioCodecNotSupported | AudioCodecReasons ;
internal const TranscodeReason VideoCodecReasons = TranscodeReason . VideoResolutionNotSupported | TranscodeReason . AnamorphicVideoNotSupported | TranscodeReason . InterlacedVideoNotSupported | TranscodeReason . VideoBitDepthNotSupported | TranscodeReason . VideoBitrateNotSupported | TranscodeReason . VideoFramerateNotSupported | TranscodeReason . VideoLevelNotSupported | TranscodeReason . RefFramesNotSupported | TranscodeReason . VideoRangeTypeNotSupported | TranscodeReason . VideoProfileNotSupported ;
internal const TranscodeReason VideoReasons = TranscodeReason . VideoCodecNotSupported | VideoCodecReasons ;
2024-09-12 23:53:21 +08:00
internal const TranscodeReason DirectStreamReasons = AudioReasons | TranscodeReason . ContainerNotSupported | TranscodeReason . VideoCodecTagNotSupported ;
2022-01-23 23:49:14 +00:00
2018-12-27 18:27:57 -05:00
private readonly ILogger _logger ;
private readonly ITranscoderSupport _transcoderSupport ;
2024-09-17 20:29:43 +02:00
private static readonly string [ ] _supportedHlsVideoCodecs = [ "h264" , "hevc" , "vp9" , "av1" ] ;
private static readonly string [ ] _supportedHlsAudioCodecsTs = [ "aac" , "ac3" , "eac3" , "mp3" ] ;
2025-02-03 16:59:04 -05:00
private static readonly string [ ] _supportedHlsAudioCodecsMp4 = [ "aac" , "ac3" , "eac3" , "mp3" , "alac" , "flac" , "opus" , "dts" , "truehd" ] ;
2018-12-27 18:27:57 -05:00
2022-03-26 12:11:00 +01:00
/// <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>
2018-12-27 18:27:57 -05:00
public StreamBuilder ( ITranscoderSupport transcoderSupport , ILogger logger )
{
_transcoderSupport = transcoderSupport ;
_logger = logger ;
}
2022-03-26 12:11:00 +01:00
/// <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>
2023-03-07 21:51:48 +01:00
public StreamInfo ? GetOptimalAudioStream ( MediaOptions options )
2018-12-27 18:27:57 -05:00
{
2022-03-26 12:11:00 +01:00
ValidateMediaOptions ( options , false ) ;
2018-12-27 18:27:57 -05:00
2024-09-17 20:29:43 +02:00
List < StreamInfo > streams = [ ] ;
2022-03-26 12:11:00 +01:00
foreach ( var mediaSource in options . MediaSources )
2018-12-27 18:27:57 -05:00
{
2023-03-01 18:57:23 +01:00
if ( ! ( string . IsNullOrEmpty ( options . MediaSourceId )
| | string . Equals ( mediaSource . Id , options . MediaSourceId , StringComparison . OrdinalIgnoreCase ) ) )
2018-12-27 18:27:57 -05:00
{
2023-03-01 18:57:23 +01:00
continue ;
2018-12-27 18:27:57 -05:00
}
2023-03-01 18:57:23 +01:00
StreamInfo ? streamInfo = GetOptimalAudioStream ( mediaSource , options ) ;
2022-12-05 15:01:13 +01:00
if ( streamInfo is not null )
2018-12-27 18:27:57 -05:00
{
2023-03-01 18:57:23 +01:00
streamInfo . DeviceId = options . DeviceId ;
2024-09-22 16:58:23 +02:00
streamInfo . DeviceProfileId = options . Profile . Id ? . ToString ( "N" , CultureInfo . InvariantCulture ) ;
2018-12-27 18:27:57 -05:00
streams . Add ( streamInfo ) ;
}
}
return GetOptimalStream ( streams , options . GetMaxBitrate ( true ) ? ? 0 ) ;
}
2023-03-07 21:51:48 +01:00
private StreamInfo ? GetOptimalAudioStream ( MediaSourceInfo item , MediaOptions options )
2018-12-27 18:27:57 -05:00
{
2022-03-26 12:11:00 +01:00
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 ) ;
2025-03-27 05:24:16 +03:00
ArgumentNullException . ThrowIfNull ( audioStream ) ;
2022-03-26 12:11:00 +01:00
var directPlayInfo = GetAudioDirectPlayProfile ( item , audioStream , options ) ;
var directPlayMethod = directPlayInfo . PlayMethod ;
var transcodeReasons = directPlayInfo . TranscodeReasons ;
2024-04-22 06:19:17 +08:00
if ( directPlayMethod is PlayMethod . DirectPlay )
2022-03-26 12:11:00 +01:00
{
2025-03-27 05:24:16 +03:00
var audioFailureReasons = GetCompatibilityAudioCodec ( options , item , item . Container , audioStream , null , false , false ) ;
2022-03-26 12:11:00 +01:00
transcodeReasons | = audioFailureReasons ;
if ( audioFailureReasons = = 0 )
{
playlistItem . PlayMethod = directPlayMethod . Value ;
playlistItem . Container = NormalizeMediaSourceFormatIntoSingleContainer ( item . Container , options . Profile , DlnaProfileType . Audio , directPlayInfo . Profile ) ;
return playlistItem ;
}
}
2024-04-22 06:19:17 +08:00
if ( directPlayMethod is PlayMethod . DirectStream )
{
var remuxContainer = item . TranscodingContainer ? ? "ts" ;
2024-09-17 20:29:43 +02:00
string [ ] supportedHlsContainers = [ "ts" , "mp4" ] ;
2024-04-22 22:31:41 +08:00
// If the container specified for the profile is an HLS supported container, use that container instead, overriding the preference
// The client should be responsible to ensure this container is compatible
2024-07-18 17:50:19 +08:00
remuxContainer = Array . Exists ( supportedHlsContainers , element = > string . Equals ( element , directPlayInfo . Profile ? . Container , StringComparison . OrdinalIgnoreCase ) ) ? directPlayInfo . Profile ? . Container : remuxContainer ;
2024-04-22 06:19:17 +08:00
bool codeIsSupported ;
if ( item . TranscodingSubProtocol = = MediaStreamProtocol . hls )
{
// Enforce HLS audio codec restrictions
if ( string . Equals ( remuxContainer , "mp4" , StringComparison . OrdinalIgnoreCase ) )
{
codeIsSupported = _supportedHlsAudioCodecsMp4 . Contains ( directPlayInfo . Profile ? . AudioCodec ? ? directPlayInfo . Profile ? . Container ) ;
}
else
{
codeIsSupported = _supportedHlsAudioCodecsTs . Contains ( directPlayInfo . Profile ? . AudioCodec ? ? directPlayInfo . Profile ? . Container ) ;
}
}
else
{
// Let's assume the client has given a correct container for http
codeIsSupported = true ;
}
if ( codeIsSupported )
{
playlistItem . PlayMethod = directPlayMethod . Value ;
playlistItem . Container = remuxContainer ;
playlistItem . TranscodeReasons = transcodeReasons ;
playlistItem . SubProtocol = item . TranscodingSubProtocol ;
2024-04-22 22:31:41 +08:00
item . TranscodingContainer = remuxContainer ;
2024-04-22 06:19:17 +08:00
return playlistItem ;
}
transcodeReasons | = TranscodeReason . AudioCodecNotSupported ;
playlistItem . TranscodeReasons = transcodeReasons ;
}
2023-03-07 21:51:48 +01:00
TranscodingProfile ? transcodingProfile = null ;
2022-12-04 12:41:46 +01:00
foreach ( var tcProfile in options . Profile . TranscodingProfiles )
2022-03-26 12:11:00 +01:00
{
2022-12-04 12:41:46 +01:00
if ( tcProfile . Type = = playlistItem . MediaType
& & tcProfile . Context = = options . Context
2023-02-05 19:27:50 +00:00
& & _transcoderSupport . CanEncodeToAudioCodec ( tcProfile . AudioCodec ? ? tcProfile . Container ) )
2022-03-26 12:11:00 +01:00
{
2022-12-04 12:41:46 +01:00
transcodingProfile = tcProfile ;
2022-03-26 12:11:00 +01:00
break ;
}
}
2023-08-22 21:14:54 +02:00
if ( transcodingProfile is not null )
2022-03-26 12:11:00 +01:00
{
if ( ! item . SupportsTranscoding )
{
return null ;
}
SetStreamInfoOptionsFromTranscodingProfile ( item , playlistItem , transcodingProfile ) ;
2025-03-27 05:24:16 +03:00
var inputAudioChannels = audioStream . Channels ;
var inputAudioBitrate = audioStream . BitRate ;
var inputAudioSampleRate = audioStream . SampleRate ;
var inputAudioBitDepth = audioStream . BitDepth ;
2022-03-26 12:11:00 +01:00
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 ) ;
2022-12-04 12:41:46 +01:00
long transcodingBitrate = options . AudioTranscodingBitrate
? ? ( options . Context = = EncodingContext . Streaming ? options . Profile . MusicStreamingTranscodingBitrate : null )
? ? configuredBitrate
? ? 128000 ;
2022-03-26 12:11:00 +01:00
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 ) ;
2024-11-03 10:55:51 -05:00
// Pure audio transcoding does not support comma separated list of transcoding codec at the moment.
// So just use the AudioCodec as is would be safe enough as the _transcoderSupport.CanEncodeToAudioCodec
// would fail so this profile will not even be picked up.
if ( playlistItem . AudioCodecs . Count = = 0 & & ! string . IsNullOrWhiteSpace ( transcodingProfile . AudioCodec ) )
{
playlistItem . AudioCodecs = [ transcodingProfile . AudioCodec ] ;
}
2022-03-26 12:11:00 +01:00
}
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>
2023-03-07 21:51:48 +01:00
public StreamInfo ? GetOptimalVideoStream ( MediaOptions options )
2018-12-27 18:27:57 -05:00
{
2022-03-26 12:11:00 +01:00
ValidateMediaOptions ( options , true ) ;
2018-12-27 18:27:57 -05:00
2023-07-29 21:35:38 +02:00
var mediaSources = string . IsNullOrEmpty ( options . MediaSourceId )
? options . MediaSources
: options . MediaSources . Where ( x = > string . Equals ( x . Id , options . MediaSourceId , StringComparison . OrdinalIgnoreCase ) ) ;
2018-12-27 18:27:57 -05:00
2024-09-17 20:29:43 +02:00
List < StreamInfo > streams = [ ] ;
2022-12-04 12:41:46 +01:00
foreach ( var mediaSourceInfo in mediaSources )
2018-12-27 18:27:57 -05:00
{
2022-12-04 12:41:46 +01:00
var streamInfo = BuildVideoItem ( mediaSourceInfo , options ) ;
2022-12-05 15:01:13 +01:00
if ( streamInfo is not null )
2018-12-27 18:27:57 -05:00
{
streams . Add ( streamInfo ) ;
}
}
2022-12-04 12:41:46 +01:00
foreach ( var stream in streams )
2018-12-27 18:27:57 -05:00
{
stream . DeviceId = options . DeviceId ;
2024-09-22 16:58:23 +02:00
stream . DeviceProfileId = options . Profile . Id ? . ToString ( "N" , CultureInfo . InvariantCulture ) ;
2018-12-27 18:27:57 -05:00
}
return GetOptimalStream ( streams , options . GetMaxBitrate ( false ) ? ? 0 ) ;
}
2023-03-07 21:51:48 +01:00
private static StreamInfo ? GetOptimalStream ( List < StreamInfo > streams , long maxBitrate )
2019-01-20 11:34:33 +01:00
= > SortMediaSources ( streams , maxBitrate ) . FirstOrDefault ( ) ;
2018-12-27 18:27:57 -05:00
2019-01-20 11:34:33 +01:00
private static IOrderedEnumerable < StreamInfo > SortMediaSources ( List < StreamInfo > streams , long maxBitrate )
2018-12-27 18:27:57 -05:00
{
return streams . OrderBy ( i = >
{
// Nothing beats direct playing a file
2023-07-29 21:35:38 +02:00
if ( i . PlayMethod = = PlayMethod . DirectPlay & & i . MediaSource ? . Protocol = = MediaProtocol . File )
2018-12-27 18:27:57 -05:00
{
return 0 ;
}
return 1 ;
} ) . ThenBy ( i = >
{
switch ( i . PlayMethod )
{
// Let's assume direct streaming a file is just as desirable as direct playing a remote url
case PlayMethod . DirectStream :
case PlayMethod . DirectPlay :
return 0 ;
default :
return 1 ;
}
} ) . ThenBy ( i = >
{
2023-07-29 21:35:38 +02:00
switch ( i . MediaSource ? . Protocol )
2018-12-27 18:27:57 -05:00
{
case MediaProtocol . File :
return 0 ;
default :
return 1 ;
}
} ) . ThenBy ( i = >
{
if ( maxBitrate > 0 )
{
2023-07-29 21:35:38 +02:00
if ( i . MediaSource ? . Bitrate is not null )
2018-12-27 18:27:57 -05:00
{
return Math . Abs ( i . MediaSource . Bitrate . Value - maxBitrate ) ;
}
}
return 0 ;
2019-01-20 11:34:33 +01:00
} ) . ThenBy ( streams . IndexOf ) ;
2018-12-27 18:27:57 -05:00
}
2022-01-23 16:37:52 +00:00
private static TranscodeReason GetTranscodeReasonForFailedCondition ( ProfileCondition condition )
2018-12-27 18:27:57 -05:00
{
switch ( condition . Property )
{
2019-01-20 11:34:33 +01:00
case ProfileConditionValue . AudioBitrate :
2018-12-27 18:27:57 -05:00
return TranscodeReason . AudioBitrateNotSupported ;
case ProfileConditionValue . AudioChannels :
return TranscodeReason . AudioChannelsNotSupported ;
case ProfileConditionValue . AudioProfile :
return TranscodeReason . AudioProfileNotSupported ;
case ProfileConditionValue . AudioSampleRate :
return TranscodeReason . AudioSampleRateNotSupported ;
case ProfileConditionValue . Has64BitOffsets :
// TODO
2022-03-05 13:58:21 -07:00
return 0 ;
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . Height :
return TranscodeReason . VideoResolutionNotSupported ;
case ProfileConditionValue . IsAnamorphic :
return TranscodeReason . AnamorphicVideoNotSupported ;
case ProfileConditionValue . IsAvc :
// TODO
2022-03-05 13:58:21 -07:00
return 0 ;
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . IsInterlaced :
return TranscodeReason . InterlacedVideoNotSupported ;
case ProfileConditionValue . IsSecondaryAudio :
return TranscodeReason . SecondaryAudioNotSupported ;
2025-03-28 15:51:22 +03:00
case ProfileConditionValue . NumStreams :
return TranscodeReason . StreamCountExceedsLimit ;
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . NumAudioStreams :
// TODO
2022-03-05 13:58:21 -07:00
return 0 ;
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . NumVideoStreams :
// TODO
2022-03-05 13:58:21 -07:00
return 0 ;
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . PacketLength :
// TODO
2022-03-05 13:58:21 -07:00
return 0 ;
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . RefFrames :
return TranscodeReason . RefFramesNotSupported ;
case ProfileConditionValue . VideoBitDepth :
return TranscodeReason . VideoBitDepthNotSupported ;
case ProfileConditionValue . AudioBitDepth :
return TranscodeReason . AudioBitDepthNotSupported ;
case ProfileConditionValue . VideoBitrate :
return TranscodeReason . VideoBitrateNotSupported ;
case ProfileConditionValue . VideoCodecTag :
2024-09-12 23:53:21 +08:00
return TranscodeReason . VideoCodecTagNotSupported ;
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . VideoFramerate :
return TranscodeReason . VideoFramerateNotSupported ;
case ProfileConditionValue . VideoLevel :
return TranscodeReason . VideoLevelNotSupported ;
case ProfileConditionValue . VideoProfile :
return TranscodeReason . VideoProfileNotSupported ;
2022-06-17 10:01:06 -06:00
case ProfileConditionValue . VideoRangeType :
return TranscodeReason . VideoRangeTypeNotSupported ;
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . VideoTimestamp :
// TODO
2022-03-05 13:58:21 -07:00
return 0 ;
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . Width :
return TranscodeReason . VideoResolutionNotSupported ;
default :
2022-03-05 13:58:21 -07:00
return 0 ;
2018-12-27 18:27:57 -05:00
}
}
2022-03-26 12:11:00 +01:00
/// <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>
2024-03-15 17:08:03 +08:00
/// <returns>The normalized input container.</returns>
2024-10-04 07:51:08 -05:00
public static string? NormalizeMediaSourceFormatIntoSingleContainer ( string inputContainer , DeviceProfile ? profile , DlnaProfileType type , DirectPlayProfile ? playProfile = null )
2018-12-27 18:27:57 -05:00
{
2024-10-04 07:51:08 -05:00
// If the source is Live TV the inputContainer will be null until the mediasource is probed on first access
if ( profile is null | | string . IsNullOrEmpty ( inputContainer ) | | ! inputContainer . Contains ( ',' , StringComparison . OrdinalIgnoreCase ) )
2018-12-27 18:27:57 -05:00
{
2024-09-17 20:29:43 +02:00
return inputContainer ;
2018-12-27 18:27:57 -05:00
}
2024-09-17 20:29:43 +02:00
var formats = ContainerHelper . Split ( inputContainer ) ;
var playProfiles = playProfile is null ? profile . DirectPlayProfiles : [ playProfile ] ;
foreach ( var format in formats )
2018-12-27 18:27:57 -05:00
{
2024-09-17 20:29:43 +02:00
foreach ( var directPlayProfile in playProfiles )
2018-12-27 18:27:57 -05:00
{
2024-09-17 20:29:43 +02:00
if ( directPlayProfile . Type ! = type )
2018-12-27 18:27:57 -05:00
{
2024-09-17 20:29:43 +02:00
continue ;
}
2024-09-17 23:34:12 +02:00
if ( directPlayProfile . SupportsContainer ( format ) )
2024-09-17 20:29:43 +02:00
{
2024-09-17 23:34:12 +02:00
return format ;
2018-12-27 18:27:57 -05:00
}
}
}
2024-09-17 20:29:43 +02:00
return inputContainer ;
2018-12-27 18:27:57 -05:00
}
2023-03-07 21:51:48 +01:00
private ( DirectPlayProfile ? Profile , PlayMethod ? PlayMethod , TranscodeReason TranscodeReasons ) GetAudioDirectPlayProfile ( MediaSourceInfo item , MediaStream audioStream , MediaOptions options )
2018-12-27 18:27:57 -05:00
{
2022-01-23 23:49:14 +00:00
var directPlayProfile = options . Profile . DirectPlayProfiles
2019-01-09 23:33:17 +01:00
. FirstOrDefault ( x = > x . Type = = DlnaProfileType . Audio & & IsAudioDirectPlaySupported ( x , item , audioStream ) ) ;
2018-12-27 18:27:57 -05:00
2024-07-17 13:35:53 +08:00
TranscodeReason transcodeReasons = 0 ;
2022-12-05 15:00:20 +01:00
if ( directPlayProfile is null )
2018-12-27 18:27:57 -05:00
{
2021-11-02 09:35:09 +01:00
_logger . LogDebug (
2020-10-12 20:05:11 +02:00
"Profile: {0}, No audio direct play profiles found for {1} with codec {2}" ,
2019-01-09 23:33:17 +01:00
options . Profile . Name ? ? "Unknown Profile" ,
2020-10-03 17:14:09 +02:00
item . Path ? ? "Unknown path" ,
audioStream . Codec ? ? "Unknown codec" ) ;
2019-01-09 23:33:17 +01:00
2024-04-22 06:19:17 +08:00
var directStreamProfile = options . Profile . DirectPlayProfiles
. FirstOrDefault ( x = > x . Type = = DlnaProfileType . Audio & & IsAudioDirectStreamSupported ( x , item , audioStream ) ) ;
2018-12-27 18:27:57 -05:00
2024-04-22 06:19:17 +08:00
if ( directStreamProfile is not null )
{
2024-07-17 13:35:53 +08:00
directPlayProfile = directStreamProfile ;
transcodeReasons | = TranscodeReason . ContainerNotSupported ;
}
else
{
return ( null , null , GetTranscodeReasonsFromDirectPlayProfile ( item , null , audioStream , options . Profile . DirectPlayProfiles ) ) ;
2024-04-22 06:19:17 +08:00
}
2018-12-27 18:27:57 -05:00
}
2022-01-23 23:49:14 +00:00
// The profile describes what the device supports
// If device requirements are satisfied then allow both direct stream and direct play
2024-07-17 13:35:53 +08:00
// Note: As of 10.10 codebase, SupportsDirectPlay is always true because the MediaSourceInfo initializes this key as true
// Need to check additionally for current transcode reasons
if ( item . SupportsDirectPlay & & transcodeReasons = = 0 )
2018-12-27 18:27:57 -05:00
{
2022-03-26 12:11:00 +01:00
if ( ! IsBitrateLimitExceeded ( item , options . GetMaxBitrate ( true ) ? ? 0 ) )
2018-12-27 18:27:57 -05:00
{
2022-01-23 23:49:14 +00:00
if ( options . EnableDirectPlay )
2018-12-27 18:27:57 -05:00
{
2022-03-05 13:58:21 -07:00
return ( directPlayProfile , PlayMethod . DirectPlay , 0 ) ;
2018-12-27 18:27:57 -05:00
}
}
2019-01-09 23:33:17 +01:00
else
{
2022-01-23 16:37:52 +00:00
transcodeReasons | = TranscodeReason . ContainerBitrateExceedsLimit ;
2019-01-09 23:33:17 +01:00
}
}
2018-12-27 18:27:57 -05:00
2022-01-23 23:49:14 +00:00
// While options takes the network and other factors into account. Only applies to direct stream
if ( item . SupportsDirectStream )
2019-01-09 23:33:17 +01:00
{
2022-03-26 12:11:00 +01:00
if ( ! IsBitrateLimitExceeded ( item , options . GetMaxBitrate ( true ) ? ? 0 ) )
2018-12-27 18:27:57 -05:00
{
2024-07-18 01:49:55 +08:00
// Note: as of 10.10 codebase, the options.EnableDirectStream is always false due to
// "direct-stream http streaming is currently broken"
// Don't check that option for audio as we always assume that is supported
if ( transcodeReasons = = TranscodeReason . ContainerNotSupported )
2018-12-27 18:27:57 -05:00
{
2022-01-23 23:49:14 +00:00
return ( directPlayProfile , PlayMethod . DirectStream , transcodeReasons ) ;
2018-12-27 18:27:57 -05:00
}
}
2019-01-09 23:33:17 +01:00
else
{
2022-01-23 16:37:52 +00:00
transcodeReasons | = TranscodeReason . ContainerBitrateExceedsLimit ;
2019-01-09 23:33:17 +01:00
}
2018-12-27 18:27:57 -05:00
}
2022-01-23 23:49:14 +00:00
return ( directPlayProfile , null , transcodeReasons ) ;
2018-12-27 18:27:57 -05:00
}
2023-03-07 21:51:48 +01:00
private static TranscodeReason GetTranscodeReasonsFromDirectPlayProfile ( MediaSourceInfo item , MediaStream ? videoStream , MediaStream audioStream , IEnumerable < DirectPlayProfile > directPlayProfiles )
2018-12-27 18:27:57 -05:00
{
2022-12-05 15:00:20 +01:00
var mediaType = videoStream is null ? DlnaProfileType . Audio : DlnaProfileType . Video ;
2021-03-27 11:48:59 +03:00
2018-12-27 18:27:57 -05:00
var containerSupported = false ;
var audioSupported = false ;
var videoSupported = false ;
foreach ( var profile in directPlayProfiles )
{
// Check container type
2021-03-27 11:48:59 +03:00
if ( profile . Type = = mediaType & & profile . SupportsContainer ( item . Container ) )
2018-12-27 18:27:57 -05:00
{
containerSupported = true ;
2022-12-05 15:00:20 +01:00
videoSupported = videoStream is null | | profile . SupportsVideoCodec ( videoStream . Codec ) ;
2018-12-27 18:27:57 -05:00
2022-12-05 15:00:20 +01:00
audioSupported = audioStream is null | | profile . SupportsAudioCodec ( audioStream . Codec ) ;
2018-12-27 18:27:57 -05:00
if ( videoSupported & & audioSupported )
{
break ;
}
}
}
2022-03-26 12:11:00 +01:00
TranscodeReason reasons = 0 ;
2018-12-27 18:27:57 -05:00
if ( ! containerSupported )
{
2022-01-23 16:37:52 +00:00
reasons | = TranscodeReason . ContainerNotSupported ;
2018-12-27 18:27:57 -05:00
}
2022-10-28 22:38:56 -04:00
if ( ! videoSupported )
2018-12-27 18:27:57 -05:00
{
2022-01-23 16:37:52 +00:00
reasons | = TranscodeReason . VideoCodecNotSupported ;
2018-12-27 18:27:57 -05:00
}
2022-10-28 22:38:56 -04:00
if ( ! audioSupported )
2018-12-27 18:27:57 -05:00
{
2022-01-23 16:37:52 +00:00
reasons | = TranscodeReason . AudioCodecNotSupported ;
2018-12-27 18:27:57 -05:00
}
2022-01-23 16:37:52 +00:00
return reasons ;
2018-12-27 18:27:57 -05:00
}
2019-01-20 11:34:33 +01:00
private static int? GetDefaultSubtitleStreamIndex ( MediaSourceInfo item , SubtitleProfile [ ] subtitleProfiles )
2018-12-27 18:27:57 -05:00
{
int highestScore = - 1 ;
2019-01-13 21:37:13 +01:00
foreach ( var stream in item . MediaStreams )
2018-12-27 18:27:57 -05:00
{
2019-01-20 11:34:33 +01:00
if ( stream . Type = = MediaStreamType . Subtitle
& & stream . Score . HasValue
& & stream . Score . Value > highestScore )
2018-12-27 18:27:57 -05:00
{
2019-01-20 11:34:33 +01:00
highestScore = stream . Score . Value ;
2018-12-27 18:27:57 -05:00
}
}
2024-09-17 20:29:43 +02:00
List < MediaStream > topStreams = [ ] ;
2019-01-13 21:37:13 +01:00
foreach ( var stream in item . MediaStreams )
2018-12-27 18:27:57 -05:00
{
if ( stream . Type = = MediaStreamType . Subtitle & & stream . Score . HasValue & & stream . Score . Value = = highestScore )
{
topStreams . Add ( stream ) ;
}
}
// If multiple streams have an equal score, try to pick the most efficient one
if ( topStreams . Count > 1 )
{
2019-01-13 21:37:13 +01:00
foreach ( var stream in topStreams )
2018-12-27 18:27:57 -05:00
{
2019-01-13 21:37:13 +01:00
foreach ( var profile in subtitleProfiles )
2018-12-27 18:27:57 -05:00
{
2020-01-09 17:07:13 +01:00
if ( profile . Method = = SubtitleDeliveryMethod . External & & string . Equals ( profile . Format , stream . Codec , StringComparison . OrdinalIgnoreCase ) )
2018-12-27 18:27:57 -05:00
{
return stream . Index ;
}
}
}
}
// If no optimization panned out, just use the original default
return item . DefaultSubtitleStreamIndex ;
}
2022-01-23 23:49:14 +00:00
private static void SetStreamInfoOptionsFromTranscodingProfile ( MediaSourceInfo item , StreamInfo playlistItem , TranscodingProfile transcodingProfile )
2018-12-27 18:27:57 -05:00
{
2022-01-23 23:49:14 +00:00
var container = transcodingProfile . Container ;
var protocol = transcodingProfile . Protocol ;
2018-12-27 18:27:57 -05:00
2022-01-23 23:49:14 +00:00
item . TranscodingContainer = container ;
item . TranscodingSubProtocol = protocol ;
2018-12-27 18:27:57 -05:00
2022-01-23 23:49:14 +00:00
if ( playlistItem . PlayMethod = = PlayMethod . Transcode )
2018-12-27 18:27:57 -05:00
{
2022-01-23 23:49:14 +00:00
playlistItem . Container = container ;
playlistItem . SubProtocol = protocol ;
2018-12-27 18:27:57 -05:00
}
2022-01-23 23:49:14 +00:00
playlistItem . TranscodeSeekInfo = transcodingProfile . TranscodeSeekInfo ;
2023-02-19 16:52:29 +01:00
if ( int . TryParse ( transcodingProfile . MaxAudioChannels , CultureInfo . InvariantCulture , out int transcodingMaxAudioChannels ) )
2018-12-27 18:27:57 -05:00
{
2022-01-23 23:49:14 +00:00
playlistItem . TranscodingMaxAudioChannels = transcodingMaxAudioChannels ;
2018-12-27 18:27:57 -05:00
}
2022-01-23 23:49:14 +00:00
playlistItem . EstimateContentLength = transcodingProfile . EstimateContentLength ;
2018-12-27 18:27:57 -05:00
playlistItem . CopyTimestamps = transcodingProfile . CopyTimestamps ;
playlistItem . EnableSubtitlesInManifest = transcodingProfile . EnableSubtitlesInManifest ;
playlistItem . EnableMpegtsM2TsMode = transcodingProfile . EnableMpegtsM2TsMode ;
playlistItem . BreakOnNonKeyFrames = transcodingProfile . BreakOnNonKeyFrames ;
2024-05-06 12:48:50 +08:00
playlistItem . EnableAudioVbrEncoding = transcodingProfile . EnableAudioVbrEncoding ;
2018-12-27 18:27:57 -05:00
if ( transcodingProfile . MinSegments > 0 )
{
playlistItem . MinSegments = transcodingProfile . MinSegments ;
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
if ( transcodingProfile . SegmentLength > 0 )
{
playlistItem . SegmentLength = transcodingProfile . SegmentLength ;
}
2022-01-23 23:49:14 +00:00
}
2020-06-16 09:43:52 +12:00
2023-03-07 21:51:48 +01:00
private static void SetStreamInfoOptionsFromDirectPlayProfile ( MediaOptions options , MediaSourceInfo item , StreamInfo playlistItem , DirectPlayProfile ? directPlayProfile )
2022-01-23 23:49:14 +00:00
{
var container = NormalizeMediaSourceFormatIntoSingleContainer ( item . Container , options . Profile , DlnaProfileType . Video , directPlayProfile ) ;
2024-03-08 15:29:22 -07:00
var protocol = MediaStreamProtocol . http ;
2018-12-27 18:27:57 -05:00
2022-01-23 23:49:14 +00:00
item . TranscodingContainer = container ;
item . TranscodingSubProtocol = protocol ;
playlistItem . Container = container ;
playlistItem . SubProtocol = protocol ;
2024-09-17 20:29:43 +02:00
playlistItem . VideoCodecs = [ item . VideoStream . Codec ] ;
playlistItem . AudioCodecs = ContainerHelper . Split ( directPlayProfile ? . AudioCodec ) ;
2018-12-27 18:27:57 -05:00
}
2022-03-26 12:11:00 +01:00
private StreamInfo BuildVideoItem ( MediaSourceInfo item , MediaOptions options )
2018-12-27 18:27:57 -05:00
{
2022-10-06 20:21:23 +02:00
ArgumentNullException . ThrowIfNull ( item ) ;
2018-12-27 18:27:57 -05:00
2019-01-20 11:34:33 +01:00
StreamInfo playlistItem = new StreamInfo
2018-12-27 18:27:57 -05:00
{
ItemId = options . ItemId ,
MediaType = DlnaProfileType . Video ,
MediaSource = item ,
RunTimeTicks = item . RunTimeTicks ,
Context = options . Context ,
2023-07-29 21:35:38 +02:00
DeviceProfile = options . Profile ,
2024-09-22 00:34:47 +08:00
SubtitleStreamIndex = options . SubtitleStreamIndex ? ? GetDefaultSubtitleStreamIndex ( item , options . Profile . SubtitleProfiles ) ,
AlwaysBurnInSubtitleWhenTranscoding = options . AlwaysBurnInSubtitleWhenTranscoding
2018-12-27 18:27:57 -05:00
} ;
2019-01-13 21:37:13 +01:00
var subtitleStream = playlistItem . SubtitleStreamIndex . HasValue ? item . GetMediaStream ( MediaStreamType . Subtitle , playlistItem . SubtitleStreamIndex . Value ) : null ;
2018-12-27 18:27:57 -05:00
2019-01-13 21:37:13 +01:00
var audioStream = item . GetDefaultAudioStream ( options . AudioStreamIndex ? ? item . DefaultAudioStreamIndex ) ;
2022-12-05 15:01:13 +01:00
if ( audioStream is not null )
2018-12-27 18:27:57 -05:00
{
playlistItem . AudioStreamIndex = audioStream . Index ;
}
2022-01-23 23:49:14 +00:00
// Collect candidate audio streams
2024-09-17 20:29:43 +02:00
ICollection < MediaStream > candidateAudioStreams = audioStream is null ? [ ] : [ audioStream ] ;
2025-04-09 09:22:30 +08:00
// When the index is explicitly required by client or the default is specified by user, don't do any stream reselection.
if ( ! item . DefaultAudioIndexSource . HasFlag ( AudioIndexSource . User ) & & ( options . AudioStreamIndex is null or < 0 ) )
2022-01-23 23:49:14 +00:00
{
2025-04-09 09:22:30 +08:00
// When user has no preferences allow stream selection on all streams.
if ( item . DefaultAudioIndexSource = = AudioIndexSource . None & & audioStream is not null )
2022-01-23 23:49:14 +00:00
{
2025-04-09 09:22:30 +08:00
candidateAudioStreams = item . MediaStreams . Where ( stream = > stream . Type = = MediaStreamType . Audio ) . ToArray ( ) ;
if ( audioStream . IsDefault )
{
// If default is picked, only allow selection within default streams.
candidateAudioStreams = candidateAudioStreams . Where ( stream = > stream . IsDefault ) . ToArray ( ) ;
}
2022-01-23 23:49:14 +00:00
}
2025-04-09 09:22:30 +08:00
if ( item . DefaultAudioIndexSource . HasFlag ( AudioIndexSource . Language ) )
2022-01-23 23:49:14 +00:00
{
2025-04-09 09:22:30 +08:00
// If user has language preference, only allow stream selection within the same language.
2022-10-28 22:38:56 -04:00
candidateAudioStreams = item . MediaStreams . Where ( stream = > stream . Type = = MediaStreamType . Audio & & stream . Language = = audioStream ? . Language ) . ToArray ( ) ;
2025-04-09 09:22:30 +08:00
if ( item . DefaultAudioIndexSource . HasFlag ( AudioIndexSource . Default ) )
{
var defaultStreamsInPreferredLanguage = candidateAudioStreams . Where ( stream = > stream . IsDefault ) . ToArray ( ) ;
// If the user also prefers default streams, try limit selection within default tracks in the same language.
// If there is no default stream in the preferred language, allow selection on all default streams to match the "Play default audio track regardless of language" setting.
candidateAudioStreams = defaultStreamsInPreferredLanguage . Length > 0
? defaultStreamsInPreferredLanguage
: item . MediaStreams . Where ( stream = > stream . Type = = MediaStreamType . Audio & & stream . IsDefault ) . ToArray ( ) ;
}
}
else if ( item . DefaultAudioIndexSource . HasFlag ( AudioIndexSource . Default ) )
{
// If user prefers default streams, only allow stream selection on default streams.
candidateAudioStreams = item . MediaStreams . Where ( stream = > stream . Type = = MediaStreamType . Audio & & stream . IsDefault ) . ToArray ( ) ;
2022-01-23 23:49:14 +00:00
}
}
2019-01-13 21:37:13 +01:00
var videoStream = item . VideoStream ;
2018-12-27 18:27:57 -05:00
2022-03-26 12:11:00 +01:00
var bitrateLimitExceeded = IsBitrateLimitExceeded ( item , options . GetMaxBitrate ( false ) ? ? 0 ) ;
var isEligibleForDirectPlay = options . EnableDirectPlay & & ( options . ForceDirectPlay | | ! bitrateLimitExceeded ) ;
var isEligibleForDirectStream = options . EnableDirectStream & & ( options . ForceDirectStream | | ! bitrateLimitExceeded ) ;
TranscodeReason transcodeReasons = 0 ;
2023-02-04 21:39:52 +01:00
// Force transcode or remux for BD/DVD folders
if ( item . VideoType = = VideoType . Dvd | | item . VideoType = = VideoType . BluRay )
{
isEligibleForDirectPlay = false ;
}
2022-03-26 12:11:00 +01:00
if ( bitrateLimitExceeded )
{
transcodeReasons = TranscodeReason . ContainerBitrateExceedsLimit ;
}
2018-12-27 18:27:57 -05:00
2021-11-02 09:35:09 +01:00
_logger . LogDebug (
2020-10-12 20:05:11 +02:00
"Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}" ,
2018-12-27 18:27:57 -05:00
options . Profile . Name ? ? "Unknown Profile" ,
item . Path ? ? "Unknown path" ,
isEligibleForDirectPlay ,
isEligibleForDirectStream ) ;
2023-03-07 21:51:48 +01:00
DirectPlayProfile ? directPlayProfile = null ;
2018-12-27 18:27:57 -05:00
if ( isEligibleForDirectPlay | | isEligibleForDirectStream )
{
// See if it can be direct played
2022-01-23 23:49:14 +00:00
var directPlayInfo = GetVideoDirectPlayProfile ( options , item , videoStream , audioStream , candidateAudioStreams , subtitleStream , isEligibleForDirectPlay , isEligibleForDirectStream ) ;
2021-12-24 14:18:24 -07:00
var directPlay = directPlayInfo . PlayMethod ;
2022-01-23 23:49:14 +00:00
transcodeReasons | = directPlayInfo . TranscodeReasons ;
2018-12-27 18:27:57 -05:00
2022-05-15 20:22:13 -04:00
if ( directPlay . HasValue )
2018-12-27 18:27:57 -05:00
{
2022-01-23 23:49:14 +00:00
directPlayProfile = directPlayInfo . Profile ;
2018-12-27 18:27:57 -05:00
playlistItem . PlayMethod = directPlay . Value ;
2022-01-23 23:49:14 +00:00
playlistItem . Container = NormalizeMediaSourceFormatIntoSingleContainer ( item . Container , options . Profile , DlnaProfileType . Video , directPlayProfile ) ;
2024-09-17 20:29:43 +02:00
var videoCodec = videoStream ? . Codec ;
playlistItem . VideoCodecs = videoCodec is null ? [ ] : [ videoCodec ] ;
2022-01-23 23:49:14 +00:00
if ( directPlay = = PlayMethod . DirectPlay )
{
2024-03-08 15:29:22 -07:00
playlistItem . SubProtocol = MediaStreamProtocol . http ;
2022-01-23 23:49:14 +00:00
var audioStreamIndex = directPlayInfo . AudioStreamIndex ? ? audioStream ? . Index ;
if ( audioStreamIndex . HasValue )
{
playlistItem . AudioStreamIndex = audioStreamIndex ;
2023-07-29 21:35:38 +02:00
var audioCodec = item . GetMediaStream ( MediaStreamType . Audio , audioStreamIndex . Value ) ? . Codec ;
2024-09-17 20:29:43 +02:00
playlistItem . AudioCodecs = audioCodec is null ? [ ] : [ audioCodec ] ;
2022-01-23 23:49:14 +00:00
}
}
else if ( directPlay = = PlayMethod . DirectStream )
{
playlistItem . AudioStreamIndex = audioStream ? . Index ;
2022-12-05 15:01:13 +01:00
if ( audioStream is not null )
2022-01-23 23:49:14 +00:00
{
2024-09-17 20:29:43 +02:00
playlistItem . AudioCodecs = ContainerHelper . Split ( directPlayProfile ? . AudioCodec ) ;
2022-01-23 23:49:14 +00:00
}
SetStreamInfoOptionsFromDirectPlayProfile ( options , item , playlistItem , directPlayProfile ) ;
2023-03-07 21:51:48 +01:00
BuildStreamVideoItem ( playlistItem , options , item , videoStream , audioStream , candidateAudioStreams , directPlayProfile ? . Container , directPlayProfile ? . VideoCodec , directPlayProfile ? . AudioCodec ) ;
2022-01-23 23:49:14 +00:00
}
2018-12-27 18:27:57 -05:00
2022-12-05 15:01:13 +01:00
if ( subtitleStream is not null )
2018-12-27 18:27:57 -05:00
{
2023-03-07 21:51:48 +01:00
var subtitleProfile = GetSubtitleProfile ( item , subtitleStream , options . Profile . SubtitleProfiles , directPlay . Value , _transcoderSupport , directPlayProfile ? . Container , null ) ;
2018-12-27 18:27:57 -05:00
playlistItem . SubtitleDeliveryMethod = subtitleProfile . Method ;
playlistItem . SubtitleFormat = subtitleProfile . Format ;
}
}
2022-03-25 18:02:31 +01:00
_logger . LogDebug (
2022-01-23 23:49:14 +00:00
"DirectPlay Result for Profile: {0}, Path: {1}, PlayMethod: {2}, AudioStreamIndex: {3}, SubtitleStreamIndex: {4}, Reasons: {5}" ,
options . Profile . Name ? ? "Anonymous Profile" ,
item . Path ? ? "Unknown path" ,
directPlayInfo . PlayMethod ,
directPlayInfo . AudioStreamIndex ? ? audioStream ? . Index ,
playlistItem . SubtitleStreamIndex ,
directPlayInfo . TranscodeReasons ) ;
2018-12-27 18:27:57 -05:00
}
2022-01-23 23:49:14 +00:00
playlistItem . TranscodeReasons = transcodeReasons ;
2022-05-15 20:22:13 -04:00
if ( playlistItem . PlayMethod ! = PlayMethod . DirectStream & & playlistItem . PlayMethod ! = PlayMethod . DirectPlay )
2018-12-27 18:27:57 -05:00
{
2022-01-23 23:49:14 +00:00
// Can't direct play, find the transcoding profile
// If we do this for direct-stream we will overwrite the info
2024-09-17 20:29:43 +02:00
var ( transcodingProfile , playMethod ) = GetVideoTranscodeProfile ( item , options , videoStream , audioStream , playlistItem ) ;
2024-09-09 22:16:58 +03:00
if ( transcodingProfile is not null & & playMethod . HasValue )
2022-01-23 23:49:14 +00:00
{
SetStreamInfoOptionsFromTranscodingProfile ( item , playlistItem , transcodingProfile ) ;
BuildStreamVideoItem ( playlistItem , options , item , videoStream , audioStream , candidateAudioStreams , transcodingProfile . Container , transcodingProfile . VideoCodec , transcodingProfile . AudioCodec ) ;
2022-05-15 20:22:13 -04:00
playlistItem . PlayMethod = PlayMethod . Transcode ;
2022-12-05 15:01:13 +01:00
if ( subtitleStream is not null )
2022-01-23 23:49:14 +00:00
{
var subtitleProfile = GetSubtitleProfile ( item , subtitleStream , options . Profile . SubtitleProfiles , PlayMethod . Transcode , _transcoderSupport , transcodingProfile . Container , transcodingProfile . Protocol ) ;
2024-09-22 00:34:47 +08:00
playlistItem . SubtitleDeliveryMethod = subtitleProfile . Method ;
2022-01-23 23:49:14 +00:00
playlistItem . SubtitleFormat = subtitleProfile . Format ;
2024-09-17 20:29:43 +02:00
playlistItem . SubtitleCodecs = [ subtitleProfile . Format ] ;
2022-01-23 23:49:14 +00:00
}
2022-05-15 20:22:13 -04:00
if ( ( playlistItem . TranscodeReasons & ( VideoReasons | TranscodeReason . ContainerBitrateExceedsLimit ) ) ! = 0 )
2022-01-23 23:49:14 +00:00
{
2022-05-15 20:22:13 -04:00
ApplyTranscodingConditions ( playlistItem , transcodingProfile . Conditions , null , true , true ) ;
2022-01-23 23:49:14 +00:00
}
}
2018-12-27 18:27:57 -05:00
}
2022-03-26 12:11:00 +01:00
_logger . LogDebug (
2022-01-23 23:49:14 +00:00
"StreamBuilder.BuildVideoItem( Profile={0}, Path={1}, AudioStreamIndex={2}, SubtitleStreamIndex={3} ) => ( PlayMethod={4}, TranscodeReason={5} ) {6}" ,
options . Profile . Name ? ? "Anonymous Profile" ,
item . Path ? ? "Unknown path" ,
options . AudioStreamIndex ,
options . SubtitleStreamIndex ,
playlistItem . PlayMethod ,
playlistItem . TranscodeReasons ,
2025-03-28 13:51:44 +01:00
playlistItem . ToUrl ( "media:" , "<token>" , null ) ) ;
2022-01-23 23:49:14 +00:00
item . Container = NormalizeMediaSourceFormatIntoSingleContainer ( item . Container , options . Profile , DlnaProfileType . Video , directPlayProfile ) ;
return playlistItem ;
}
2024-09-09 22:16:58 +03:00
private ( TranscodingProfile ? Profile , PlayMethod ? PlayMethod ) GetVideoTranscodeProfile (
2023-03-07 21:51:48 +01:00
MediaSourceInfo item ,
MediaOptions options ,
MediaStream ? videoStream ,
MediaStream ? audioStream ,
StreamInfo playlistItem )
2022-01-23 23:49:14 +00:00
{
2025-03-27 05:24:16 +03:00
var mediaSource = playlistItem . MediaSource ;
ArgumentNullException . ThrowIfNull ( mediaSource ) ;
2022-01-23 23:49:14 +00:00
if ( ! ( item . SupportsTranscoding | | item . SupportsDirectStream ) )
2018-12-27 18:27:57 -05:00
{
2024-09-09 22:16:58 +03:00
return ( null , null ) ;
2018-12-27 18:27:57 -05:00
}
2022-01-23 23:49:14 +00:00
var transcodingProfiles = options . Profile . TranscodingProfiles
. Where ( i = > i . Type = = playlistItem . MediaType & & i . Context = = options . Context ) ;
2024-09-09 16:51:28 +03:00
if ( item . UseMostCompatibleTranscodingProfile )
{
transcodingProfiles = transcodingProfiles . Where ( i = > string . Equals ( i . Container , "ts" , StringComparison . OrdinalIgnoreCase ) ) ;
}
2024-09-09 22:16:58 +03:00
var videoCodec = videoStream ? . Codec ;
var audioCodec = audioStream ? . Codec ;
var analyzedProfiles = transcodingProfiles
. Select ( transcodingProfile = >
2018-12-27 18:27:57 -05:00
{
2024-09-09 22:16:58 +03:00
var rank = ( Video : 3 , Audio : 3 ) ;
var container = transcodingProfile . Container ;
2025-03-27 05:24:16 +03:00
if ( videoStream is not null
& & options . AllowVideoStreamCopy
& & ContainerHelper . ContainsContainer ( transcodingProfile . VideoCodec , videoCodec ) )
2024-09-09 22:16:58 +03:00
{
2025-03-27 05:24:16 +03:00
var failures = GetCompatibilityVideoCodec ( options , mediaSource , container , videoStream ) ;
rank . Video = failures = = 0 ? 1 : 2 ;
2024-09-09 22:16:58 +03:00
}
2025-03-27 05:24:16 +03:00
if ( audioStream is not null
& & options . AllowAudioStreamCopy )
2024-09-09 22:16:58 +03:00
{
2025-02-03 16:56:01 -05:00
// For Audio stream, we prefer the audio codec that can be directly copied, then the codec that can otherwise satisfies
// the transcoding conditions, then the one does not satisfy the transcoding conditions.
// For example: A client can support both aac and flac, but flac only supports 2 channels while aac supports 6.
// When the source audio is 6 channel flac, we should transcode to 6 channel aac, instead of down-mix to 2 channel flac.
var transcodingAudioCodecs = ContainerHelper . Split ( transcodingProfile . AudioCodec ) ;
foreach ( var transcodingAudioCodec in transcodingAudioCodecs )
2024-09-09 22:16:58 +03:00
{
2025-03-27 05:24:16 +03:00
var failures = GetCompatibilityAudioCodec ( options , mediaSource , container , audioStream , transcodingAudioCodec , true , false ) ;
2025-02-03 16:56:01 -05:00
var rankAudio = 3 ;
2025-03-27 05:24:16 +03:00
if ( failures = = 0 )
2025-02-03 16:56:01 -05:00
{
rankAudio = string . Equals ( transcodingAudioCodec , audioCodec , StringComparison . OrdinalIgnoreCase ) ? 1 : 2 ;
}
rank . Audio = Math . Min ( rank . Audio , rankAudio ) ;
if ( rank . Audio = = 1 )
{
break ;
}
2024-09-09 22:16:58 +03:00
}
}
PlayMethod playMethod = PlayMethod . Transcode ;
2022-01-23 23:49:14 +00:00
2024-09-09 22:16:58 +03:00
if ( rank . Video = = 1 )
2022-02-21 07:54:29 -05:00
{
2024-09-09 22:16:58 +03:00
playMethod = PlayMethod . DirectStream ;
2022-02-19 17:02:41 +00:00
}
2024-09-09 22:16:58 +03:00
return ( Profile : transcodingProfile , PlayMethod : playMethod , Rank : rank ) ;
2022-01-23 23:49:14 +00:00
} )
2024-09-09 22:16:58 +03:00
. OrderBy ( analysis = > analysis . Rank ) ;
var profileMatch = analyzedProfiles . FirstOrDefault ( ) ;
2018-12-27 18:27:57 -05:00
2024-09-09 22:16:58 +03:00
return ( profileMatch . Profile , profileMatch . PlayMethod ) ;
2022-01-23 23:49:14 +00:00
}
2023-03-07 21:51:48 +01:00
private void BuildStreamVideoItem (
StreamInfo playlistItem ,
MediaOptions options ,
MediaSourceInfo item ,
MediaStream ? videoStream ,
MediaStream ? audioStream ,
IEnumerable < MediaStream > candidateAudioStreams ,
string? container ,
string? videoCodec ,
string? audioCodec )
2022-01-23 23:49:14 +00:00
{
2022-05-15 20:22:13 -04:00
// Prefer matching video codecs
2024-09-17 20:29:43 +02:00
var videoCodecs = ContainerHelper . Split ( videoCodec ) . ToList ( ) ;
2023-02-27 00:08:25 +01:00
2024-09-17 20:29:43 +02:00
if ( videoCodecs . Count = = 0 & & videoStream is not null )
2023-02-27 00:08:25 +01:00
{
2024-09-17 20:29:43 +02:00
// Add the original codec if no codec is specified
videoCodecs . Add ( videoStream . Codec ) ;
2023-02-27 00:08:25 +01:00
}
2024-09-17 20:29:43 +02:00
// Enforce HLS video codec restrictions
if ( playlistItem . SubProtocol = = MediaStreamProtocol . hls )
2022-05-15 20:25:01 -04:00
{
2024-09-17 20:29:43 +02:00
videoCodecs = videoCodecs . Where ( codec = > _supportedHlsVideoCodecs . Contains ( codec ) ) . ToList ( ) ;
2022-05-15 20:25:01 -04:00
}
playlistItem . VideoCodecs = videoCodecs ;
2022-01-23 23:49:14 +00:00
2022-05-15 20:22:13 -04:00
// Copy video codec options as a starting point, this applies to transcode and direct-stream
2024-09-08 01:16:23 +08:00
playlistItem . MaxFramerate = videoStream ? . ReferenceFrameRate ;
2022-04-04 05:45:45 -06:00
var qualifier = videoStream ? . Codec ;
2022-12-05 15:01:13 +01:00
if ( videoStream ? . Level is not null )
2018-12-27 18:27:57 -05:00
{
2022-01-23 23:49:14 +00:00
playlistItem . SetOption ( qualifier , "level" , videoStream . Level . Value . ToString ( CultureInfo . InvariantCulture ) ) ;
}
2022-12-05 15:01:13 +01:00
if ( videoStream ? . BitDepth is not null )
2022-01-23 23:49:14 +00:00
{
playlistItem . SetOption ( qualifier , "videobitdepth" , videoStream . BitDepth . Value . ToString ( CultureInfo . InvariantCulture ) ) ;
}
2022-04-04 05:45:45 -06:00
if ( ! string . IsNullOrEmpty ( videoStream ? . Profile ) )
2022-01-23 23:49:14 +00:00
{
playlistItem . SetOption ( qualifier , "profile" , videoStream . Profile . ToLowerInvariant ( ) ) ;
}
2022-05-15 20:22:13 -04:00
// Prefer matching audio codecs, could do better here
2024-09-17 20:29:43 +02:00
var audioCodecs = ContainerHelper . Split ( audioCodec ) . ToList ( ) ;
if ( audioCodecs . Count = = 0 & & audioStream is not null )
{
// Add the original codec if no codec is specified
audioCodecs . Add ( audioStream . Codec ) ;
}
2023-02-27 00:08:25 +01:00
// Enforce HLS audio codec restrictions
2024-03-08 15:29:22 -07:00
if ( playlistItem . SubProtocol = = MediaStreamProtocol . hls )
2023-02-27 00:08:25 +01:00
{
if ( string . Equals ( playlistItem . Container , "mp4" , StringComparison . OrdinalIgnoreCase ) )
{
2024-09-17 20:29:43 +02:00
audioCodecs = audioCodecs . Where ( codec = > _supportedHlsAudioCodecsMp4 . Contains ( codec ) ) . ToList ( ) ;
2023-02-27 16:03:12 +01:00
}
else
{
2024-09-17 20:29:43 +02:00
audioCodecs = audioCodecs . Where ( codec = > _supportedHlsAudioCodecsTs . Contains ( codec ) ) . ToList ( ) ;
2023-02-27 00:08:25 +01:00
}
}
2024-09-17 20:29:43 +02:00
var audioStreamWithSupportedCodec = candidateAudioStreams . Where ( stream = > ContainerHelper . ContainsContainer ( audioCodecs , false , stream . Codec ) ) . FirstOrDefault ( ) ;
2024-07-29 06:11:59 +08:00
2025-02-03 16:56:01 -05:00
var channelsExceedsLimit = audioStreamWithSupportedCodec is not null & & audioStreamWithSupportedCodec . Channels > ( playlistItem . TranscodingMaxAudioChannels ? ? int . MaxValue ) ;
2025-03-27 05:24:16 +03:00
var directAudioFailures = audioStreamWithSupportedCodec is null ? default : GetCompatibilityAudioCodec ( options , item , container ? ? string . Empty , audioStreamWithSupportedCodec , null , true , false ) ;
2025-02-03 16:56:01 -05:00
2025-03-27 05:24:16 +03:00
playlistItem . TranscodeReasons | = directAudioFailures ;
var directAudioStreamSatisfied = audioStreamWithSupportedCodec is not null & & ! channelsExceedsLimit
& & directAudioFailures = = 0 ;
2024-07-29 06:11:59 +08:00
2025-02-03 17:07:11 -05:00
directAudioStreamSatisfied = directAudioStreamSatisfied & & ! playlistItem . TranscodeReasons . HasFlag ( TranscodeReason . ContainerBitrateExceedsLimit ) ;
2025-02-03 16:56:01 -05:00
var directAudioStream = directAudioStreamSatisfied ? audioStreamWithSupportedCodec : null ;
2024-07-29 06:11:59 +08:00
if ( channelsExceedsLimit & & playlistItem . TargetAudioStream is not null )
{
playlistItem . TranscodeReasons | = TranscodeReason . AudioChannelsNotSupported ;
playlistItem . TargetAudioStream . Channels = playlistItem . TranscodingMaxAudioChannels ;
}
2022-01-23 23:49:14 +00:00
playlistItem . AudioCodecs = audioCodecs ;
2022-12-05 15:01:13 +01:00
if ( directAudioStream is not null )
2022-01-23 23:49:14 +00:00
{
audioStream = directAudioStream ;
playlistItem . AudioStreamIndex = audioStream . Index ;
2024-09-17 20:29:43 +02:00
audioCodecs = [ audioStream . Codec ] ;
playlistItem . AudioCodecs = audioCodecs ;
2022-01-23 23:49:14 +00:00
2022-05-15 20:22:13 -04:00
// Copy matching audio codec options
2022-01-23 23:49:14 +00:00
playlistItem . AudioSampleRate = audioStream . SampleRate ;
2023-10-24 00:10:31 +02:00
playlistItem . SetOption ( qualifier , "audiochannels" , audioStream . Channels ? . ToString ( CultureInfo . InvariantCulture ) ? ? string . Empty ) ;
2022-01-23 23:49:14 +00:00
if ( ! string . IsNullOrEmpty ( audioStream . Profile ) )
2018-12-27 18:27:57 -05:00
{
2022-01-23 23:49:14 +00:00
playlistItem . SetOption ( audioStream . Codec , "profile" , audioStream . Profile . ToLowerInvariant ( ) ) ;
2018-12-27 18:27:57 -05:00
}
2023-11-15 15:22:51 +01:00
if ( audioStream . Level . HasValue & & audioStream . Level . Value ! = 0 )
2018-12-27 18:27:57 -05:00
{
2023-10-24 00:10:31 +02:00
playlistItem . SetOption ( audioStream . Codec , "level" , audioStream . Level . Value . ToString ( CultureInfo . InvariantCulture ) ) ;
2018-12-27 18:27:57 -05:00
}
2022-01-23 23:49:14 +00:00
}
2018-12-27 18:27:57 -05:00
2022-01-23 23:49:14 +00:00
int? width = videoStream ? . Width ;
int? height = videoStream ? . Height ;
int? bitDepth = videoStream ? . BitDepth ;
int? videoBitrate = videoStream ? . BitRate ;
double? videoLevel = videoStream ? . Level ;
2023-03-07 21:51:48 +01:00
string? videoProfile = videoStream ? . Profile ;
2023-06-15 13:28:01 +02:00
VideoRangeType ? videoRangeType = videoStream ? . VideoRangeType ;
2024-09-08 01:16:23 +08:00
float videoFramerate = videoStream is null ? 0 : videoStream . ReferenceFrameRate ? ? 0 ;
2022-01-23 23:49:14 +00:00
bool? isAnamorphic = videoStream ? . IsAnamorphic ;
bool? isInterlaced = videoStream ? . IsInterlaced ;
2023-03-07 21:51:48 +01:00
string? videoCodecTag = videoStream ? . CodecTag ;
2022-01-23 23:49:14 +00:00
bool? isAvc = videoStream ? . IsAVC ;
2018-12-27 18:27:57 -05:00
2022-12-05 15:00:20 +01:00
TransportStreamTimestamp ? timestamp = videoStream is null ? TransportStreamTimestamp . None : item . Timestamp ;
2022-01-23 23:49:14 +00:00
int? packetLength = videoStream ? . PacketLength ;
int? refFrames = videoStream ? . RefFrames ;
2018-12-27 18:27:57 -05:00
2025-03-28 15:51:22 +03:00
int numStreams = item . MediaStreams . Count ;
2022-01-23 23:49:14 +00:00
int? numAudioStreams = item . GetStreamCount ( MediaStreamType . Audio ) ;
int? numVideoStreams = item . GetStreamCount ( MediaStreamType . Video ) ;
2018-12-27 18:27:57 -05:00
2024-08-10 17:10:07 +08:00
var useSubContainer = playlistItem . SubProtocol = = MediaStreamProtocol . hls ;
2022-01-23 23:49:14 +00:00
var appliedVideoConditions = options . Profile . CodecProfiles
. Where ( i = > i . Type = = CodecType . Video & &
2024-09-17 20:29:43 +02:00
i . ContainsAnyCodec ( playlistItem . VideoCodecs , container , useSubContainer ) & &
2025-03-28 15:51:22 +03:00
i . ApplyConditions . All ( applyCondition = > ConditionProcessor . IsVideoConditionSatisfied ( applyCondition , width , height , bitDepth , videoBitrate , videoProfile , videoRangeType , videoLevel , videoFramerate , packetLength , timestamp , isAnamorphic , isInterlaced , refFrames , numStreams , numVideoStreams , numAudioStreams , videoCodecTag , isAvc ) ) )
2024-08-25 02:46:42 -04:00
// Reverse codec profiles for backward compatibility - first codec profile has higher priority
. Reverse ( ) ;
2024-09-17 20:29:43 +02:00
foreach ( var condition in appliedVideoConditions )
2022-01-23 23:49:14 +00:00
{
2024-09-17 20:29:43 +02:00
foreach ( var transcodingVideoCodec in playlistItem . VideoCodecs )
2018-12-27 18:27:57 -05:00
{
2024-09-17 20:29:43 +02:00
if ( condition . ContainsAnyCodec ( transcodingVideoCodec , container , useSubContainer ) )
2018-12-27 18:27:57 -05:00
{
2024-09-17 20:29:43 +02:00
ApplyTranscodingConditions ( playlistItem , condition . Conditions , transcodingVideoCodec , true , true ) ;
2022-01-23 23:49:14 +00:00
continue ;
2018-12-27 18:27:57 -05:00
}
}
2022-01-23 23:49:14 +00:00
}
2018-12-27 18:27:57 -05:00
2022-01-23 23:49:14 +00:00
// Honor requested max channels
2024-07-29 06:11:59 +08:00
playlistItem . GlobalMaxAudioChannels = channelsExceedsLimit ? playlistItem . TranscodingMaxAudioChannels : options . MaxAudioChannels ;
2018-12-27 18:27:57 -05:00
2022-03-26 12:11:00 +01:00
int audioBitrate = GetAudioBitrate ( options . GetMaxBitrate ( true ) ? ? 0 , playlistItem . TargetAudioCodec , audioStream , playlistItem ) ;
2022-01-23 23:49:14 +00:00
playlistItem . AudioBitrate = Math . Min ( playlistItem . AudioBitrate ? ? audioBitrate , audioBitrate ) ;
2018-12-27 18:27:57 -05:00
2022-12-05 15:00:20 +01:00
bool? isSecondaryAudio = audioStream is null ? null : item . IsSecondaryAudio ( audioStream ) ;
2023-03-07 21:51:48 +01:00
int? inputAudioBitrate = audioStream ? . BitRate ;
int? audioChannels = audioStream ? . Channels ;
string? audioProfile = audioStream ? . Profile ;
int? inputAudioSampleRate = audioStream ? . SampleRate ;
int? inputAudioBitDepth = audioStream ? . BitDepth ;
2022-01-23 23:49:14 +00:00
var appliedAudioConditions = options . Profile . CodecProfiles
2022-04-15 13:29:20 -06:00
. Where ( i = > i . Type = = CodecType . VideoAudio & &
2024-09-17 20:29:43 +02:00
i . ContainsAnyCodec ( playlistItem . AudioCodecs , container ) & &
2024-08-25 02:46:42 -04:00
i . ApplyConditions . All ( applyCondition = > ConditionProcessor . IsVideoAudioConditionSatisfied ( applyCondition , audioChannels , inputAudioBitrate , inputAudioSampleRate , inputAudioBitDepth , audioProfile , isSecondaryAudio ) ) )
// Reverse codec profiles for backward compatibility - first codec profile has higher priority
. Reverse ( ) ;
2022-03-26 12:11:00 +01:00
foreach ( var codecProfile in appliedAudioConditions )
2022-01-23 23:49:14 +00:00
{
2024-09-17 20:29:43 +02:00
foreach ( var transcodingAudioCodec in playlistItem . AudioCodecs )
2018-12-27 18:27:57 -05:00
{
2022-03-26 12:11:00 +01:00
if ( codecProfile . ContainsAnyCodec ( transcodingAudioCodec , container ) )
2018-12-27 18:27:57 -05:00
{
2024-08-25 02:46:42 -04:00
ApplyTranscodingConditions ( playlistItem , codecProfile . Conditions , transcodingAudioCodec , true , true ) ;
2022-01-23 23:49:14 +00:00
break ;
2018-12-27 18:27:57 -05:00
}
}
2022-01-23 23:49:14 +00:00
}
2018-12-27 18:27:57 -05:00
2022-01-23 23:49:14 +00:00
var maxBitrateSetting = options . GetMaxBitrate ( false ) ;
// Honor max rate
if ( maxBitrateSetting . HasValue )
{
var availableBitrateForVideo = maxBitrateSetting . Value ;
2018-12-27 18:27:57 -05:00
2022-01-23 23:49:14 +00:00
if ( playlistItem . AudioBitrate . HasValue )
{
availableBitrateForVideo - = playlistItem . AudioBitrate . Value ;
2018-12-27 18:27:57 -05:00
}
2022-01-23 23:49:14 +00:00
// Make sure the video bitrate is lower than bitrate settings but at least 64k
2022-03-12 11:50:12 -07:00
// Don't use Math.Clamp as availableBitrateForVideo can be lower then 64k.
2022-03-06 18:17:49 -07:00
var currentValue = playlistItem . VideoBitrate ? ? availableBitrateForVideo ;
2022-03-12 11:50:12 -07:00
playlistItem . VideoBitrate = Math . Max ( Math . Min ( availableBitrateForVideo , currentValue ) , 64_000 ) ;
2022-01-23 23:49:14 +00:00
}
2018-12-27 18:27:57 -05:00
2022-03-25 18:02:31 +01:00
_logger . LogDebug (
2022-03-05 13:40:57 -07:00
"Transcode Result for Profile: {Profile}, Path: {Path}, PlayMethod: {PlayMethod}, AudioStreamIndex: {AudioStreamIndex}, SubtitleStreamIndex: {SubtitleStreamIndex}, Reasons: {TranscodeReason}" ,
2025-01-22 17:31:52 +01:00
options . Profile . Name ? ? "Anonymous Profile" ,
2022-01-23 23:49:14 +00:00
item . Path ? ? "Unknown path" ,
2025-01-22 17:31:52 +01:00
playlistItem . PlayMethod ,
2022-03-05 13:40:57 -07:00
audioStream ? . Index ,
2025-01-22 17:31:52 +01:00
playlistItem . SubtitleStreamIndex ,
playlistItem . TranscodeReasons ) ;
2018-12-27 18:27:57 -05:00
}
2023-03-07 21:51:48 +01:00
private static int GetDefaultAudioBitrate ( string? audioCodec , int? audioChannels )
2018-12-27 18:27:57 -05:00
{
2020-11-11 17:08:50 +08:00
if ( ! string . IsNullOrEmpty ( audioCodec ) )
2018-12-27 18:27:57 -05:00
{
2020-11-11 17:08:50 +08:00
// Default to a higher bitrate for stream copy
if ( string . Equals ( audioCodec , "aac" , StringComparison . OrdinalIgnoreCase )
| | string . Equals ( audioCodec , "mp3" , StringComparison . OrdinalIgnoreCase )
| | string . Equals ( audioCodec , "ac3" , StringComparison . OrdinalIgnoreCase )
| | string . Equals ( audioCodec , "eac3" , StringComparison . OrdinalIgnoreCase ) )
{
if ( ( audioChannels ? ? 0 ) < 2 )
{
return 128000 ;
}
return ( audioChannels ? ? 0 ) > = 6 ? 640000 : 384000 ;
}
if ( string . Equals ( audioCodec , "flac" , StringComparison . OrdinalIgnoreCase )
| | string . Equals ( audioCodec , "alac" , StringComparison . OrdinalIgnoreCase ) )
{
if ( ( audioChannels ? ? 0 ) < 2 )
{
return 768000 ;
}
return ( audioChannels ? ? 0 ) > = 6 ? 3584000 : 1536000 ;
}
2018-12-27 18:27:57 -05:00
}
return 192000 ;
}
2024-09-17 20:29:43 +02:00
private static int GetAudioBitrate ( long maxTotalBitrate , IReadOnlyList < string > targetAudioCodecs , MediaStream ? audioStream , StreamInfo item )
2018-12-27 18:27:57 -05:00
{
2024-09-17 20:29:43 +02:00
string? targetAudioCodec = targetAudioCodecs . Count = = 0 ? null : targetAudioCodecs [ 0 ] ;
2018-12-27 18:27:57 -05:00
2019-01-20 12:03:05 +01:00
int? targetAudioChannels = item . GetTargetAudioChannels ( targetAudioCodec ) ;
2018-12-27 18:27:57 -05:00
2019-01-20 12:03:05 +01:00
int defaultBitrate ;
int encoderAudioBitrateLimit = int . MaxValue ;
2018-12-27 18:27:57 -05:00
2022-12-05 15:00:20 +01:00
if ( audioStream is null )
2018-12-27 18:27:57 -05:00
{
2019-01-20 12:03:05 +01:00
defaultBitrate = 192000 ;
2018-12-27 18:27:57 -05:00
}
2019-01-20 12:03:05 +01:00
else
2018-12-27 18:27:57 -05:00
{
2020-11-11 17:08:50 +08:00
if ( targetAudioChannels . HasValue
& & audioStream . Channels . HasValue
& & audioStream . Channels . Value > targetAudioChannels . Value )
2019-01-20 12:03:05 +01:00
{
2024-09-17 20:29:43 +02:00
// Reduce the bitrate if we're down mixing.
2020-11-11 17:08:50 +08:00
defaultBitrate = GetDefaultAudioBitrate ( targetAudioCodec , targetAudioChannels ) ;
}
else if ( targetAudioChannels . HasValue
2020-11-19 15:02:36 +00:00
& & audioStream . Channels . HasValue
& & audioStream . Channels . Value < = targetAudioChannels . Value
& & ! string . IsNullOrEmpty ( audioStream . Codec )
2022-12-05 15:01:13 +01:00
& & targetAudioCodecs is not null
2024-09-17 20:29:43 +02:00
& & targetAudioCodecs . Count > 0
& & ! targetAudioCodecs . Any ( elem = > string . Equals ( audioStream . Codec , elem , StringComparison . OrdinalIgnoreCase ) ) )
2020-11-11 17:08:50 +08:00
{
// Shift the bitrate if we're transcoding to a different audio codec.
defaultBitrate = GetDefaultAudioBitrate ( targetAudioCodec , audioStream . Channels . Value ) ;
2019-01-20 12:03:05 +01:00
}
else
{
2020-11-11 17:08:50 +08:00
defaultBitrate = audioStream . BitRate ? ? GetDefaultAudioBitrate ( targetAudioCodec , targetAudioChannels ) ;
2019-01-20 12:03:05 +01:00
}
2019-01-07 23:27:46 +00:00
// Seeing webm encoding failures when source has 1 audio channel and 22k bitrate.
2018-12-27 18:27:57 -05:00
// Any attempts to transcode over 64k will fail
2019-01-20 12:03:05 +01:00
if ( audioStream . Channels = = 1
& & ( audioStream . BitRate ? ? 0 ) < 64000 )
2018-12-27 18:27:57 -05:00
{
2019-01-20 12:03:05 +01:00
encoderAudioBitrateLimit = 64000 ;
2018-12-27 18:27:57 -05:00
}
}
if ( maxTotalBitrate > 0 )
{
defaultBitrate = Math . Min ( GetMaxAudioBitrateForTotalBitrate ( maxTotalBitrate ) , defaultBitrate ) ;
}
return Math . Min ( defaultBitrate , encoderAudioBitrateLimit ) ;
}
2019-01-20 12:03:05 +01:00
private static int GetMaxAudioBitrateForTotalBitrate ( long totalBitrate )
2018-12-27 18:27:57 -05:00
{
if ( totalBitrate < = 640000 )
{
return 128000 ;
}
2023-04-06 19:38:34 +02:00
if ( totalBitrate < = 2000000 )
2018-12-27 18:27:57 -05:00
{
return 384000 ;
}
2023-04-06 19:38:34 +02:00
if ( totalBitrate < = 3000000 )
2018-12-27 18:27:57 -05:00
{
return 448000 ;
}
2023-04-06 19:38:34 +02:00
if ( totalBitrate < = 4000000 )
2020-11-11 17:08:50 +08:00
{
return 640000 ;
}
2023-04-06 19:38:34 +02:00
if ( totalBitrate < = 5000000 )
2020-11-11 17:08:50 +08:00
{
return 768000 ;
}
2023-04-06 19:38:34 +02:00
if ( totalBitrate < = 10000000 )
2020-11-11 17:08:50 +08:00
{
return 1536000 ;
}
2023-04-06 19:38:34 +02:00
if ( totalBitrate < = 15000000 )
2020-11-11 17:08:50 +08:00
{
return 2304000 ;
}
2023-04-06 19:38:34 +02:00
if ( totalBitrate < = 20000000 )
2020-11-11 17:08:50 +08:00
{
return 3584000 ;
}
2018-12-27 18:27:57 -05:00
2020-11-11 17:08:50 +08:00
return 7168000 ;
2018-12-27 18:27:57 -05:00
}
2023-03-07 21:51:48 +01:00
private ( DirectPlayProfile ? Profile , PlayMethod ? PlayMethod , int? AudioStreamIndex , TranscodeReason TranscodeReasons ) GetVideoDirectPlayProfile (
2022-03-26 12:11:00 +01:00
MediaOptions options ,
2018-12-27 18:27:57 -05:00
MediaSourceInfo mediaSource ,
2023-03-07 21:51:48 +01:00
MediaStream ? videoStream ,
MediaStream ? audioStream ,
2022-10-28 22:38:56 -04:00
ICollection < MediaStream > candidateAudioStreams ,
2023-03-07 21:51:48 +01:00
MediaStream ? subtitleStream ,
2022-01-23 23:49:14 +00:00
bool isEligibleForDirectPlay ,
2018-12-27 18:27:57 -05:00
bool isEligibleForDirectStream )
{
if ( options . ForceDirectPlay )
{
2022-03-05 13:58:21 -07:00
return ( null , PlayMethod . DirectPlay , audioStream ? . Index , 0 ) ;
2018-12-27 18:27:57 -05:00
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
if ( options . ForceDirectStream )
{
2022-03-05 13:58:21 -07:00
return ( null , PlayMethod . DirectStream , audioStream ? . Index , 0 ) ;
2018-12-27 18:27:57 -05:00
}
2019-01-20 12:03:05 +01:00
DeviceProfile profile = options . Profile ;
2021-04-01 19:18:14 +02:00
string container = mediaSource . Container ;
2019-01-20 12:03:05 +01:00
2022-01-23 23:49:14 +00:00
// Check container conditions
2025-03-27 05:24:16 +03:00
var containerProfileReasons = GetCompatibilityContainer ( options , mediaSource , container , videoStream ) ;
2022-01-23 23:49:14 +00:00
// Check video conditions
2025-03-27 05:24:16 +03:00
var videoCodecProfileReasons = videoStream is null ? default : GetCompatibilityVideoCodec ( options , mediaSource , container , videoStream ) ;
2018-12-27 18:27:57 -05:00
2024-09-17 20:29:43 +02:00
// Check audio candidates profile conditions
2025-03-27 05:24:16 +03:00
var audioStreamMatches = candidateAudioStreams . ToDictionary ( s = > s , audioStream = > GetCompatibilityAudioCodecDirect ( options , mediaSource , container , audioStream , true , mediaSource . IsSecondaryAudio ( audioStream ) ? ? false ) ) ;
2018-12-27 18:27:57 -05:00
2022-03-05 13:58:21 -07:00
TranscodeReason subtitleProfileReasons = 0 ;
2022-12-05 15:01:13 +01:00
if ( subtitleStream is not null )
2018-12-27 18:27:57 -05:00
{
2022-01-23 23:49:14 +00:00
var subtitleProfile = GetSubtitleProfile ( mediaSource , subtitleStream , options . Profile . SubtitleProfiles , PlayMethod . DirectPlay , _transcoderSupport , container , null ) ;
2018-12-27 18:27:57 -05:00
2022-01-23 23:49:14 +00:00
if ( subtitleProfile . Method ! = SubtitleDeliveryMethod . Drop
& & subtitleProfile . Method ! = SubtitleDeliveryMethod . External
& & subtitleProfile . Method ! = SubtitleDeliveryMethod . Embed )
{
_logger . LogDebug ( "Not eligible for {0} due to unsupported subtitles" , PlayMethod . DirectPlay ) ;
subtitleProfileReasons | = TranscodeReason . SubtitleCodecNotSupported ;
2018-12-27 18:27:57 -05:00
}
}
2023-01-22 14:05:37 -05:00
var containerSupported = false ;
2024-09-17 23:34:12 +02:00
TranscodeReason [ ] rankings = [ TranscodeReason . VideoCodecNotSupported , VideoCodecReasons , TranscodeReason . AudioCodecNotSupported , AudioCodecReasons , ContainerReasons ] ;
2023-01-22 14:05:37 -05:00
2022-01-23 23:49:14 +00:00
// Check DirectPlay profiles to see if it can be direct played
var analyzedProfiles = profile . DirectPlayProfiles
. Where ( directPlayProfile = > directPlayProfile . Type = = DlnaProfileType . Video )
. Select ( ( directPlayProfile , order ) = >
2018-12-27 18:27:57 -05:00
{
2022-03-05 13:58:21 -07:00
TranscodeReason directPlayProfileReasons = 0 ;
TranscodeReason audioCodecProfileReasons = 0 ;
2022-01-23 23:49:14 +00:00
// Check container type
if ( ! directPlayProfile . SupportsContainer ( container ) )
2018-12-27 18:27:57 -05:00
{
2022-01-23 23:49:14 +00:00
directPlayProfileReasons | = TranscodeReason . ContainerNotSupported ;
}
2023-01-22 14:05:37 -05:00
else
{
containerSupported = true ;
}
2018-12-27 18:27:57 -05:00
2022-01-23 23:49:14 +00:00
// Check video codec
2023-03-07 21:51:48 +01:00
string? videoCodec = videoStream ? . Codec ;
2022-01-23 23:49:14 +00:00
if ( ! directPlayProfile . SupportsVideoCodec ( videoCodec ) )
{
directPlayProfileReasons | = TranscodeReason . VideoCodecNotSupported ;
2018-12-27 18:27:57 -05:00
}
2022-01-23 23:49:14 +00:00
// Check audio codec
2023-03-07 21:51:48 +01:00
MediaStream ? selectedAudioStream = null ;
2024-08-01 17:17:10 +02:00
if ( candidateAudioStreams . Count ! = 0 )
2022-01-23 23:49:14 +00:00
{
2022-10-28 22:38:56 -04:00
selectedAudioStream = candidateAudioStreams . FirstOrDefault ( audioStream = > directPlayProfile . SupportsAudioCodec ( audioStream . Codec ) ) ;
2022-12-05 15:00:20 +01:00
if ( selectedAudioStream is null )
2022-10-28 22:38:56 -04:00
{
directPlayProfileReasons | = TranscodeReason . AudioCodecNotSupported ;
}
else
{
audioCodecProfileReasons = audioStreamMatches . GetValueOrDefault ( selectedAudioStream ) ;
}
2022-01-23 23:49:14 +00:00
}
2022-04-15 13:29:20 -06:00
var failureReasons = directPlayProfileReasons | containerProfileReasons | subtitleProfileReasons ;
if ( ( failureReasons & TranscodeReason . VideoCodecNotSupported ) = = 0 )
{
failureReasons | = videoCodecProfileReasons ;
}
if ( ( failureReasons & TranscodeReason . AudioCodecNotSupported ) = = 0 )
{
failureReasons | = audioCodecProfileReasons ;
}
2022-01-23 23:49:14 +00:00
var directStreamFailureReasons = failureReasons & ( ~ DirectStreamReasons ) ;
PlayMethod ? playMethod = null ;
2022-03-05 13:58:21 -07:00
if ( failureReasons = = 0 & & isEligibleForDirectPlay & & mediaSource . SupportsDirectPlay )
2022-01-23 23:49:14 +00:00
{
playMethod = PlayMethod . DirectPlay ;
}
2023-01-22 14:05:37 -05:00
else if ( directStreamFailureReasons = = 0 & & isEligibleForDirectStream & & mediaSource . SupportsDirectStream )
2022-01-23 23:49:14 +00:00
{
playMethod = PlayMethod . DirectStream ;
}
2024-09-17 20:29:43 +02:00
var ranked = GetRank ( ref failureReasons , rankings ) ;
2022-01-23 23:49:14 +00:00
return ( Result : ( Profile : directPlayProfile , PlayMethod : playMethod , AudioStreamIndex : selectedAudioStream ? . Index , TranscodeReason : failureReasons ) , Order : order , Rank : ranked ) ;
} )
. OrderByDescending ( analysis = > analysis . Result . PlayMethod )
2022-05-15 20:22:13 -04:00
. ThenByDescending ( analysis = > analysis . Rank )
2022-01-23 23:49:14 +00:00
. ThenBy ( analysis = > analysis . Order )
. ToArray ( )
2022-12-05 15:01:13 +01:00
. ToLookup ( analysis = > analysis . Result . PlayMethod is not null ) ;
2022-01-23 23:49:14 +00:00
var profileMatch = analyzedProfiles [ true ]
. Select ( analysis = > analysis . Result )
. FirstOrDefault ( ) ;
2022-12-05 15:01:13 +01:00
if ( profileMatch . Profile is not null )
2022-01-23 23:49:14 +00:00
{
return profileMatch ;
2018-12-27 18:27:57 -05:00
}
2023-01-22 14:05:37 -05:00
var failureReasons = analyzedProfiles [ false ]
. Select ( analysis = > analysis . Result )
2024-09-13 22:46:36 +08:00
. Where ( result = > ! containerSupported | | ! result . TranscodeReason . HasFlag ( TranscodeReason . ContainerNotSupported ) )
2024-09-14 01:24:57 +08:00
. FirstOrDefault ( ) . TranscodeReason ;
if ( failureReasons = = 0 )
2018-12-27 18:27:57 -05:00
{
2024-09-14 01:24:57 +08:00
failureReasons = TranscodeReason . DirectPlayError ;
2018-12-27 18:27:57 -05:00
}
2024-09-14 01:24:57 +08:00
return ( Profile : null , PlayMethod : null , AudioStreamIndex : null , TranscodeReasons : failureReasons ) ;
2022-01-23 23:49:14 +00:00
}
private TranscodeReason AggregateFailureConditions ( MediaSourceInfo mediaSource , DeviceProfile profile , string type , IEnumerable < ProfileCondition > conditions )
{
2022-03-05 13:58:21 -07:00
return conditions . Aggregate < ProfileCondition , TranscodeReason > ( 0 , ( reasons , i ) = >
2022-01-23 23:49:14 +00:00
{
LogConditionFailure ( profile , type , i , mediaSource ) ;
var transcodeReasons = GetTranscodeReasonForFailedCondition ( i ) ;
return reasons | transcodeReasons ;
} ) ;
2018-12-27 18:27:57 -05:00
}
private void LogConditionFailure ( DeviceProfile profile , string type , ProfileCondition condition , MediaSourceInfo mediaSource )
{
2021-11-02 09:35:09 +01:00
_logger . LogDebug (
2020-10-12 20:05:11 +02:00
"Profile: {0}, DirectPlay=false. Reason={1}.{2} Condition: {3}. ConditionValue: {4}. IsRequired: {5}. Path: {6}" ,
2018-12-27 18:27:57 -05:00
type ,
profile . Name ? ? "Unknown Profile" ,
condition . Property ,
condition . Condition ,
condition . Value ? ? string . Empty ,
condition . IsRequired ,
mediaSource . Path ? ? "Unknown path" ) ;
}
2022-03-26 12:11:00 +01:00
/// <summary>
/// Normalizes input container.
/// </summary>
/// <param name="mediaSource">The <see cref="MediaSourceInfo"/>.</param>
/// <param name="subtitleStream">The <see cref="MediaStream"/> of the subtitle stream.</param>
/// <param name="subtitleProfiles">The list of supported <see cref="SubtitleProfile"/>s.</param>
/// <param name="playMethod">The <see cref="PlayMethod"/>.</param>
/// <param name="transcoderSupport">The <see cref="ITranscoderSupport"/>.</param>
/// <param name="outputContainer">The output container.</param>
2024-09-17 20:29:43 +02:00
/// <param name="transcodingSubProtocol">The subtitle transcoding protocol.</param>
2024-03-15 17:08:03 +08:00
/// <returns>The normalized input container.</returns>
2019-01-20 12:03:05 +01:00
public static SubtitleProfile GetSubtitleProfile (
MediaSourceInfo mediaSource ,
MediaStream subtitleStream ,
SubtitleProfile [ ] subtitleProfiles ,
PlayMethod playMethod ,
ITranscoderSupport transcoderSupport ,
2023-03-07 21:51:48 +01:00
string? outputContainer ,
2024-03-05 00:44:54 +01:00
MediaStreamProtocol ? transcodingSubProtocol )
2018-12-27 18:27:57 -05:00
{
2024-03-08 15:29:22 -07:00
if ( ! subtitleStream . IsExternal & & ( playMethod ! = PlayMethod . Transcode | | transcodingSubProtocol ! = MediaStreamProtocol . hls ) )
2018-12-27 18:27:57 -05:00
{
// Look for supported embedded subs of the same format
2019-01-13 21:37:13 +01:00
foreach ( var profile in subtitleProfiles )
2018-12-27 18:27:57 -05:00
{
if ( ! profile . SupportsLanguage ( subtitleStream . Language ) )
{
continue ;
}
if ( profile . Method ! = SubtitleDeliveryMethod . Embed )
{
continue ;
}
2024-09-17 20:29:43 +02:00
if ( ! ContainerHelper . ContainsContainer ( profile . Container , outputContainer ) )
2018-12-27 18:27:57 -05:00
{
continue ;
}
2021-08-28 16:32:50 -06:00
if ( playMethod = = PlayMethod . Transcode & & ! IsSubtitleEmbedSupported ( outputContainer ) )
2018-12-27 18:27:57 -05:00
{
continue ;
}
2020-01-09 17:07:13 +01:00
if ( subtitleStream . IsTextSubtitleStream = = MediaStream . IsTextFormat ( profile . Format ) & & string . Equals ( profile . Format , subtitleStream . Codec , StringComparison . OrdinalIgnoreCase ) )
2018-12-27 18:27:57 -05:00
{
return profile ;
}
}
// Look for supported embedded subs of a convertible format
2019-01-13 21:37:13 +01:00
foreach ( var profile in subtitleProfiles )
2018-12-27 18:27:57 -05:00
{
if ( ! profile . SupportsLanguage ( subtitleStream . Language ) )
{
continue ;
}
if ( profile . Method ! = SubtitleDeliveryMethod . Embed )
{
continue ;
}
2024-09-17 20:29:43 +02:00
if ( ! ContainerHelper . ContainsContainer ( profile . Container , outputContainer ) )
2018-12-27 18:27:57 -05:00
{
continue ;
}
2021-08-28 16:32:50 -06:00
if ( playMethod = = PlayMethod . Transcode & & ! IsSubtitleEmbedSupported ( outputContainer ) )
2018-12-27 18:27:57 -05:00
{
continue ;
}
if ( subtitleStream . IsTextSubtitleStream & & subtitleStream . SupportsSubtitleConversionTo ( profile . Format ) )
{
return profile ;
}
}
}
// Look for an external or hls profile that matches the stream type (text/graphical) and doesn't require conversion
return GetExternalSubtitleProfile ( mediaSource , subtitleStream , subtitleProfiles , playMethod , transcoderSupport , false ) ? ?
GetExternalSubtitleProfile ( mediaSource , subtitleStream , subtitleProfiles , playMethod , transcoderSupport , true ) ? ?
new SubtitleProfile
{
Method = SubtitleDeliveryMethod . Encode ,
Format = subtitleStream . Codec
} ;
}
2023-03-07 21:51:48 +01:00
private static bool IsSubtitleEmbedSupported ( string? transcodingContainer )
2018-12-27 18:27:57 -05:00
{
if ( ! string . IsNullOrEmpty ( transcodingContainer ) )
{
2024-09-17 20:29:43 +02:00
if ( ContainerHelper . ContainsContainer ( transcodingContainer , "ts,mpegts,mp4" ) )
2018-12-27 18:27:57 -05:00
{
return false ;
}
2023-04-06 19:38:34 +02:00
2024-09-17 20:29:43 +02:00
if ( ContainerHelper . ContainsContainer ( transcodingContainer , "mkv,matroska" ) )
2018-12-27 18:27:57 -05:00
{
return true ;
}
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
return false ;
}
2023-03-07 21:51:48 +01:00
private static SubtitleProfile ? GetExternalSubtitleProfile ( MediaSourceInfo mediaSource , MediaStream subtitleStream , SubtitleProfile [ ] subtitleProfiles , PlayMethod playMethod , ITranscoderSupport transcoderSupport , bool allowConversion )
2018-12-27 18:27:57 -05:00
{
2019-01-13 21:37:13 +01:00
foreach ( var profile in subtitleProfiles )
2018-12-27 18:27:57 -05:00
{
if ( profile . Method ! = SubtitleDeliveryMethod . External & & profile . Method ! = SubtitleDeliveryMethod . Hls )
{
continue ;
}
if ( profile . Method = = SubtitleDeliveryMethod . Hls & & playMethod ! = PlayMethod . Transcode )
{
continue ;
}
if ( ! profile . SupportsLanguage ( subtitleStream . Language ) )
{
continue ;
}
if ( ! subtitleStream . IsExternal & & ! transcoderSupport . CanExtractSubtitles ( subtitleStream . Codec ) )
{
continue ;
}
if ( ( profile . Method = = SubtitleDeliveryMethod . External & & subtitleStream . IsTextSubtitleStream = = MediaStream . IsTextFormat ( profile . Format ) ) | |
( profile . Method = = SubtitleDeliveryMethod . Hls & & subtitleStream . IsTextSubtitleStream ) )
{
2020-01-09 17:07:13 +01:00
bool requiresConversion = ! string . Equals ( subtitleStream . Codec , profile . Format , StringComparison . OrdinalIgnoreCase ) ;
2018-12-27 18:27:57 -05:00
if ( ! requiresConversion )
{
return profile ;
}
if ( ! allowConversion )
{
continue ;
}
// TODO: Build this into subtitleStream.SupportsExternalStream
if ( mediaSource . IsInfiniteStream )
{
continue ;
}
if ( subtitleStream . IsTextSubtitleStream & & subtitleStream . SupportsExternalStream & & subtitleStream . SupportsSubtitleConversionTo ( profile . Format ) )
{
return profile ;
}
}
}
return null ;
}
2022-03-26 12:11:00 +01:00
private bool IsBitrateLimitExceeded ( MediaSourceInfo item , long maxBitrate )
2018-12-27 18:27:57 -05:00
{
2022-12-14 22:04:05 +01:00
// Don't restrict bitrate if item is remote.
2018-12-27 18:27:57 -05:00
if ( item . IsRemote )
{
2022-12-04 12:09:10 +01:00
return false ;
2018-12-27 18:27:57 -05:00
}
2022-12-14 22:04:05 +01:00
// If no maximum bitrate is set, default to no maximum bitrate.
long requestedMaxBitrate = maxBitrate > 0 ? maxBitrate : int . MaxValue ;
2018-12-27 18:27:57 -05:00
2022-12-14 22:04:05 +01:00
// If we don't know the item bitrate, then force a transcode if requested max bitrate is under 40 mbps
2019-01-20 12:03:05 +01:00
int itemBitrate = item . Bitrate ? ? 40000000 ;
2018-12-27 18:27:57 -05:00
if ( itemBitrate > requestedMaxBitrate )
{
2021-11-02 09:35:09 +01:00
_logger . LogDebug (
2022-03-26 12:11:00 +01:00
"Bitrate exceeds limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}" ,
2021-02-20 23:13:04 +01:00
itemBitrate ,
requestedMaxBitrate ) ;
2022-03-26 12:11:00 +01:00
return true ;
2018-12-27 18:27:57 -05:00
}
2022-03-26 12:11:00 +01:00
return false ;
2018-12-27 18:27:57 -05:00
}
2022-12-07 18:06:04 +01:00
private static void ValidateMediaOptions ( MediaOptions options , bool isMediaSource )
2018-12-27 18:27:57 -05:00
{
2024-01-17 08:51:39 -07:00
if ( options . ItemId . IsEmpty ( ) )
2018-12-27 18:27:57 -05:00
{
2022-03-26 12:11:00 +01:00
ArgumentException . ThrowIfNullOrEmpty ( options . DeviceId ) ;
2018-12-27 18:27:57 -05:00
}
2020-06-16 09:43:52 +12:00
2022-12-05 15:00:20 +01:00
if ( options . Profile is null )
2018-12-27 18:27:57 -05:00
{
throw new ArgumentException ( "Profile is required" ) ;
}
2020-06-16 09:43:52 +12:00
2022-12-05 15:00:20 +01:00
if ( options . MediaSources is null )
2018-12-27 18:27:57 -05:00
{
throw new ArgumentException ( "MediaSources is required" ) ;
}
2022-03-26 12:11:00 +01:00
2022-12-07 18:06:04 +01:00
if ( isMediaSource )
2022-03-26 12:11:00 +01:00
{
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" ) ;
}
}
2018-12-27 18:27:57 -05:00
}
2022-01-23 23:49:14 +00:00
private static IEnumerable < ProfileCondition > GetProfileConditionsForVideoAudio (
IEnumerable < CodecProfile > codecProfiles ,
string container ,
string codec ,
int? audioChannels ,
int? audioBitrate ,
int? audioSampleRate ,
int? audioBitDepth ,
string audioProfile ,
bool? isSecondaryAudio )
{
return codecProfiles
2023-06-13 10:44:06 -04:00
. Where ( profile = > profile . Type = = CodecType . VideoAudio & &
profile . ContainsAnyCodec ( codec , container ) & &
2022-01-23 23:49:14 +00:00
profile . ApplyConditions . All ( applyCondition = > ConditionProcessor . IsVideoAudioConditionSatisfied ( applyCondition , audioChannels , audioBitrate , audioSampleRate , audioBitDepth , audioProfile , isSecondaryAudio ) ) )
. SelectMany ( profile = > profile . Conditions )
. Where ( condition = > ! ConditionProcessor . IsVideoAudioConditionSatisfied ( condition , audioChannels , audioBitrate , audioSampleRate , audioBitDepth , audioProfile , isSecondaryAudio ) ) ;
}
private static IEnumerable < ProfileCondition > GetProfileConditionsForAudio (
IEnumerable < CodecProfile > codecProfiles ,
string container ,
2023-03-07 21:51:48 +01:00
string? codec ,
2022-01-23 23:49:14 +00:00
int? audioChannels ,
int? audioBitrate ,
int? audioSampleRate ,
int? audioBitDepth ,
bool checkConditions )
{
var conditions = codecProfiles
2023-06-13 10:44:06 -04:00
. Where ( profile = > profile . Type = = CodecType . Audio & &
profile . ContainsAnyCodec ( codec , container ) & &
2022-01-23 23:49:14 +00:00
profile . ApplyConditions . All ( applyCondition = > ConditionProcessor . IsAudioConditionSatisfied ( applyCondition , audioChannels , audioBitrate , audioSampleRate , audioBitDepth ) ) )
. SelectMany ( profile = > profile . Conditions ) ;
if ( ! checkConditions )
{
return conditions ;
}
return conditions . Where ( condition = > ! ConditionProcessor . IsAudioConditionSatisfied ( condition , audioChannels , audioBitrate , audioSampleRate , audioBitDepth ) ) ;
}
2023-03-07 21:51:48 +01:00
private void ApplyTranscodingConditions ( StreamInfo item , IEnumerable < ProfileCondition > conditions , string? qualifier , bool enableQualifiedConditions , bool enableNonQualifiedConditions )
2018-12-27 18:27:57 -05:00
{
2019-01-17 20:24:39 +01:00
foreach ( ProfileCondition condition in conditions )
2018-12-27 18:27:57 -05:00
{
string value = condition . Value ;
if ( string . IsNullOrEmpty ( value ) )
{
continue ;
}
// No way to express this
if ( condition . Condition = = ProfileConditionType . GreaterThanEqual )
{
continue ;
}
switch ( condition . Property )
{
case ProfileConditionValue . AudioBitrate :
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
2023-02-19 16:52:29 +01:00
if ( int . TryParse ( value , CultureInfo . InvariantCulture , out var num ) )
2018-12-27 18:27:57 -05:00
{
if ( condition . Condition = = ProfileConditionType . Equals )
{
item . AudioBitrate = num ;
}
else if ( condition . Condition = = ProfileConditionType . LessThanEqual )
{
item . AudioBitrate = Math . Min ( num , item . AudioBitrate ? ? num ) ;
}
else if ( condition . Condition = = ProfileConditionType . GreaterThanEqual )
{
item . AudioBitrate = Math . Max ( num , item . AudioBitrate ? ? num ) ;
}
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
break ;
}
2020-09-28 15:04:31 -05:00
2020-10-22 11:09:59 +02:00
case ProfileConditionValue . AudioSampleRate :
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
2023-02-19 16:52:29 +01:00
if ( int . TryParse ( value , CultureInfo . InvariantCulture , out var num ) )
2020-10-22 11:09:59 +02:00
{
if ( condition . Condition = = ProfileConditionType . Equals )
{
item . AudioSampleRate = num ;
}
else if ( condition . Condition = = ProfileConditionType . LessThanEqual )
{
item . AudioSampleRate = Math . Min ( num , item . AudioSampleRate ? ? num ) ;
}
else if ( condition . Condition = = ProfileConditionType . GreaterThanEqual )
{
item . AudioSampleRate = Math . Max ( num , item . AudioSampleRate ? ? num ) ;
}
}
break ;
}
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . AudioChannels :
{
if ( string . IsNullOrEmpty ( qualifier ) )
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
}
else
{
if ( ! enableQualifiedConditions )
{
continue ;
}
}
2023-02-19 16:52:29 +01:00
if ( int . TryParse ( value , CultureInfo . InvariantCulture , out var num ) )
2018-12-27 18:27:57 -05:00
{
if ( condition . Condition = = ProfileConditionType . Equals )
{
item . SetOption ( qualifier , "audiochannels" , num . ToString ( CultureInfo . InvariantCulture ) ) ;
}
else if ( condition . Condition = = ProfileConditionType . LessThanEqual )
{
item . SetOption ( qualifier , "audiochannels" , Math . Min ( num , item . GetTargetAudioChannels ( qualifier ) ? ? num ) . ToString ( CultureInfo . InvariantCulture ) ) ;
}
else if ( condition . Condition = = ProfileConditionType . GreaterThanEqual )
{
item . SetOption ( qualifier , "audiochannels" , Math . Max ( num , item . GetTargetAudioChannels ( qualifier ) ? ? num ) . ToString ( CultureInfo . InvariantCulture ) ) ;
}
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
break ;
}
2020-09-28 15:04:31 -05:00
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . IsAvc :
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
2019-01-13 21:46:33 +01:00
if ( bool . TryParse ( value , out var isAvc ) )
2018-12-27 18:27:57 -05:00
{
if ( isAvc & & condition . Condition = = ProfileConditionType . Equals )
{
item . RequireAvc = true ;
}
else if ( ! isAvc & & condition . Condition = = ProfileConditionType . NotEquals )
{
item . RequireAvc = true ;
}
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
break ;
}
2020-09-28 15:04:31 -05:00
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . IsAnamorphic :
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
2019-01-13 21:46:33 +01:00
if ( bool . TryParse ( value , out var isAnamorphic ) )
2018-12-27 18:27:57 -05:00
{
if ( isAnamorphic & & condition . Condition = = ProfileConditionType . Equals )
{
item . RequireNonAnamorphic = true ;
}
else if ( ! isAnamorphic & & condition . Condition = = ProfileConditionType . NotEquals )
{
item . RequireNonAnamorphic = true ;
}
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
break ;
}
2020-09-28 15:04:31 -05:00
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . IsInterlaced :
{
if ( string . IsNullOrEmpty ( qualifier ) )
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
}
else
{
if ( ! enableQualifiedConditions )
{
continue ;
}
}
2019-01-13 21:46:33 +01:00
if ( bool . TryParse ( value , out var isInterlaced ) )
2018-12-27 18:27:57 -05:00
{
if ( ! isInterlaced & & condition . Condition = = ProfileConditionType . Equals )
{
item . SetOption ( qualifier , "deinterlace" , "true" ) ;
}
else if ( isInterlaced & & condition . Condition = = ProfileConditionType . NotEquals )
{
item . SetOption ( qualifier , "deinterlace" , "true" ) ;
}
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
break ;
}
2020-09-28 15:04:31 -05:00
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . AudioProfile :
case ProfileConditionValue . Has64BitOffsets :
case ProfileConditionValue . PacketLength :
2025-03-28 15:51:22 +03:00
case ProfileConditionValue . NumStreams :
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . NumAudioStreams :
case ProfileConditionValue . NumVideoStreams :
case ProfileConditionValue . IsSecondaryAudio :
case ProfileConditionValue . VideoTimestamp :
{
// Not supported yet
break ;
}
2020-09-28 15:04:31 -05:00
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . RefFrames :
{
if ( string . IsNullOrEmpty ( qualifier ) )
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
}
else
{
if ( ! enableQualifiedConditions )
{
continue ;
}
}
2023-02-19 16:52:29 +01:00
if ( int . TryParse ( value , CultureInfo . InvariantCulture , out var num ) )
2018-12-27 18:27:57 -05:00
{
if ( condition . Condition = = ProfileConditionType . Equals )
{
item . SetOption ( qualifier , "maxrefframes" , num . ToString ( CultureInfo . InvariantCulture ) ) ;
}
else if ( condition . Condition = = ProfileConditionType . LessThanEqual )
{
item . SetOption ( qualifier , "maxrefframes" , Math . Min ( num , item . GetTargetRefFrames ( qualifier ) ? ? num ) . ToString ( CultureInfo . InvariantCulture ) ) ;
}
else if ( condition . Condition = = ProfileConditionType . GreaterThanEqual )
{
item . SetOption ( qualifier , "maxrefframes" , Math . Max ( num , item . GetTargetRefFrames ( qualifier ) ? ? num ) . ToString ( CultureInfo . InvariantCulture ) ) ;
}
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
break ;
}
2020-09-28 15:04:31 -05:00
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . VideoBitDepth :
{
if ( string . IsNullOrEmpty ( qualifier ) )
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
}
else
{
if ( ! enableQualifiedConditions )
{
continue ;
}
}
2023-02-19 16:52:29 +01:00
if ( int . TryParse ( value , CultureInfo . InvariantCulture , out var num ) )
2018-12-27 18:27:57 -05:00
{
if ( condition . Condition = = ProfileConditionType . Equals )
{
item . SetOption ( qualifier , "videobitdepth" , num . ToString ( CultureInfo . InvariantCulture ) ) ;
}
else if ( condition . Condition = = ProfileConditionType . LessThanEqual )
{
item . SetOption ( qualifier , "videobitdepth" , Math . Min ( num , item . GetTargetVideoBitDepth ( qualifier ) ? ? num ) . ToString ( CultureInfo . InvariantCulture ) ) ;
}
else if ( condition . Condition = = ProfileConditionType . GreaterThanEqual )
{
item . SetOption ( qualifier , "videobitdepth" , Math . Max ( num , item . GetTargetVideoBitDepth ( qualifier ) ? ? num ) . ToString ( CultureInfo . InvariantCulture ) ) ;
}
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
break ;
}
2020-09-28 15:04:31 -05:00
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . VideoProfile :
{
if ( string . IsNullOrEmpty ( qualifier ) )
{
continue ;
}
2022-03-26 12:11:00 +01:00
// Change from split by | to comma
// Strip spaces to avoid having to encode
2021-08-28 16:32:50 -06:00
var values = value
. Split ( '|' , StringSplitOptions . RemoveEmptyEntries ) ;
2018-12-27 18:27:57 -05:00
2022-01-23 23:49:14 +00:00
if ( condition . Condition = = ProfileConditionType . Equals )
2021-08-28 16:32:50 -06:00
{
item . SetOption ( qualifier , "profile" , string . Join ( ',' , values ) ) ;
2018-12-27 18:27:57 -05:00
}
2022-01-23 23:49:14 +00:00
else if ( condition . Condition = = ProfileConditionType . EqualsAny )
{
var currentValue = item . GetOption ( qualifier , "profile" ) ;
if ( ! string . IsNullOrEmpty ( currentValue ) & & values . Any ( value = > value = = currentValue ) )
{
item . SetOption ( qualifier , "profile" , currentValue ) ;
}
else
{
item . SetOption ( qualifier , "profile" , string . Join ( ',' , values ) ) ;
}
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
break ;
}
2020-09-28 15:04:31 -05:00
2022-06-17 10:01:06 -06:00
case ProfileConditionValue . VideoRangeType :
{
if ( string . IsNullOrEmpty ( qualifier ) )
{
continue ;
}
// change from split by | to comma
// strip spaces to avoid having to encode
var values = value
. Split ( '|' , StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries ) ;
if ( condition . Condition = = ProfileConditionType . Equals )
{
item . SetOption ( qualifier , "rangetype" , string . Join ( ',' , values ) ) ;
}
2023-06-15 13:28:01 +02:00
else if ( condition . Condition = = ProfileConditionType . NotEquals )
{
item . SetOption ( qualifier , "rangetype" , string . Join ( ',' , Enum . GetNames ( typeof ( VideoRangeType ) ) . Except ( values ) ) ) ;
}
2022-06-17 10:01:06 -06:00
else if ( condition . Condition = = ProfileConditionType . EqualsAny )
{
var currentValue = item . GetOption ( qualifier , "rangetype" ) ;
if ( ! string . IsNullOrEmpty ( currentValue ) & & values . Any ( v = > string . Equals ( v , currentValue , StringComparison . OrdinalIgnoreCase ) ) )
{
item . SetOption ( qualifier , "rangetype" , currentValue ) ;
}
else
{
item . SetOption ( qualifier , "rangetype" , string . Join ( ',' , values ) ) ;
}
}
break ;
}
2024-02-05 23:41:43 +08:00
case ProfileConditionValue . VideoCodecTag :
{
if ( string . IsNullOrEmpty ( qualifier ) )
{
continue ;
}
// change from split by | to comma
// strip spaces to avoid having to encode
var values = value
. Split ( '|' , StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries ) ;
if ( condition . Condition = = ProfileConditionType . Equals )
{
item . SetOption ( qualifier , "codectag" , string . Join ( ',' , values ) ) ;
}
else if ( condition . Condition = = ProfileConditionType . EqualsAny )
{
var currentValue = item . GetOption ( qualifier , "codectag" ) ;
if ( ! string . IsNullOrEmpty ( currentValue ) & & values . Any ( v = > string . Equals ( v , currentValue , StringComparison . OrdinalIgnoreCase ) ) )
{
item . SetOption ( qualifier , "codectag" , currentValue ) ;
}
else
{
item . SetOption ( qualifier , "codectag" , string . Join ( ',' , values ) ) ;
}
}
break ;
}
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . Height :
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
2023-02-19 16:52:29 +01:00
if ( int . TryParse ( value , CultureInfo . InvariantCulture , out var num ) )
2018-12-27 18:27:57 -05:00
{
if ( condition . Condition = = ProfileConditionType . Equals )
{
item . MaxHeight = num ;
}
else if ( condition . Condition = = ProfileConditionType . LessThanEqual )
{
item . MaxHeight = Math . Min ( num , item . MaxHeight ? ? num ) ;
}
else if ( condition . Condition = = ProfileConditionType . GreaterThanEqual )
{
item . MaxHeight = Math . Max ( num , item . MaxHeight ? ? num ) ;
}
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
break ;
}
2020-09-28 15:04:31 -05:00
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . VideoBitrate :
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
2023-02-19 16:52:29 +01:00
if ( int . TryParse ( value , CultureInfo . InvariantCulture , out var num ) )
2018-12-27 18:27:57 -05:00
{
if ( condition . Condition = = ProfileConditionType . Equals )
{
item . VideoBitrate = num ;
}
else if ( condition . Condition = = ProfileConditionType . LessThanEqual )
{
item . VideoBitrate = Math . Min ( num , item . VideoBitrate ? ? num ) ;
}
else if ( condition . Condition = = ProfileConditionType . GreaterThanEqual )
{
item . VideoBitrate = Math . Max ( num , item . VideoBitrate ? ? num ) ;
}
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
break ;
}
2020-09-28 15:04:31 -05:00
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . VideoFramerate :
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
2023-02-19 16:52:29 +01:00
if ( float . TryParse ( value , CultureInfo . InvariantCulture , out var num ) )
2018-12-27 18:27:57 -05:00
{
if ( condition . Condition = = ProfileConditionType . Equals )
{
item . MaxFramerate = num ;
}
else if ( condition . Condition = = ProfileConditionType . LessThanEqual )
{
item . MaxFramerate = Math . Min ( num , item . MaxFramerate ? ? num ) ;
}
else if ( condition . Condition = = ProfileConditionType . GreaterThanEqual )
{
item . MaxFramerate = Math . Max ( num , item . MaxFramerate ? ? num ) ;
}
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
break ;
}
2020-09-28 15:04:31 -05:00
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . VideoLevel :
{
if ( string . IsNullOrEmpty ( qualifier ) )
{
continue ;
}
2023-02-19 16:52:29 +01:00
if ( int . TryParse ( value , CultureInfo . InvariantCulture , out var num ) )
2018-12-27 18:27:57 -05:00
{
if ( condition . Condition = = ProfileConditionType . Equals )
{
item . SetOption ( qualifier , "level" , num . ToString ( CultureInfo . InvariantCulture ) ) ;
}
else if ( condition . Condition = = ProfileConditionType . LessThanEqual )
{
item . SetOption ( qualifier , "level" , Math . Min ( num , item . GetTargetVideoLevel ( qualifier ) ? ? num ) . ToString ( CultureInfo . InvariantCulture ) ) ;
}
else if ( condition . Condition = = ProfileConditionType . GreaterThanEqual )
{
item . SetOption ( qualifier , "level" , Math . Max ( num , item . GetTargetVideoLevel ( qualifier ) ? ? num ) . ToString ( CultureInfo . InvariantCulture ) ) ;
}
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
break ;
}
2020-09-28 15:04:31 -05:00
2018-12-27 18:27:57 -05:00
case ProfileConditionValue . Width :
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
2023-02-19 16:52:29 +01:00
if ( int . TryParse ( value , CultureInfo . InvariantCulture , out var num ) )
2018-12-27 18:27:57 -05:00
{
if ( condition . Condition = = ProfileConditionType . Equals )
{
item . MaxWidth = num ;
}
else if ( condition . Condition = = ProfileConditionType . LessThanEqual )
{
item . MaxWidth = Math . Min ( num , item . MaxWidth ? ? num ) ;
}
else if ( condition . Condition = = ProfileConditionType . GreaterThanEqual )
{
item . MaxWidth = Math . Max ( num , item . MaxWidth ? ? num ) ;
}
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
break ;
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
default :
break ;
}
}
}
2025-02-03 16:59:06 -05:00
private static bool IsAudioContainerSupported ( DirectPlayProfile profile , MediaSourceInfo item )
2018-12-27 18:27:57 -05:00
{
// Check container type
if ( ! profile . SupportsContainer ( item . Container ) )
{
return false ;
}
2025-02-03 16:59:06 -05:00
// Never direct play audio in matroska when the device only declare support for webm.
// The first check is not enough because mkv is assumed can be webm.
// See https://github.com/jellyfin/jellyfin/issues/13344
return ! ContainerHelper . ContainsContainer ( "mkv" , item . Container )
| | profile . SupportsContainer ( "mkv" ) ;
}
private static bool IsAudioDirectPlaySupported ( DirectPlayProfile profile , MediaSourceInfo item , MediaStream audioStream )
{
if ( ! IsAudioContainerSupported ( profile , item ) )
{
return false ;
}
2018-12-27 18:27:57 -05:00
// Check audio codec
2023-03-07 21:51:48 +01:00
string? audioCodec = audioStream ? . Codec ;
2018-12-27 18:27:57 -05:00
if ( ! profile . SupportsAudioCodec ( audioCodec ) )
{
return false ;
}
return true ;
}
2024-04-22 06:19:17 +08:00
private static bool IsAudioDirectStreamSupported ( DirectPlayProfile profile , MediaSourceInfo item , MediaStream audioStream )
{
// Check container type, this should NOT be supported
2024-07-17 14:08:39 +08:00
// If the container is supported, the file should be directly played
2025-02-03 16:59:06 -05:00
if ( IsAudioContainerSupported ( profile , item ) )
2024-04-22 06:19:17 +08:00
{
2025-02-03 16:59:06 -05:00
return false ;
2024-04-22 06:19:17 +08:00
}
2025-02-03 16:59:06 -05:00
// Check audio codec, we cannot use the SupportsAudioCodec here
// Because that one assumes empty container supports all codec, which is just useless
string? audioCodec = audioStream ? . Codec ;
return string . Equals ( profile . AudioCodec , audioCodec , StringComparison . OrdinalIgnoreCase )
| | string . Equals ( profile . Container , audioCodec , StringComparison . OrdinalIgnoreCase ) ;
2024-04-22 06:19:17 +08:00
}
2024-09-17 20:29:43 +02:00
private int GetRank ( ref TranscodeReason a , TranscodeReason [ ] rankings )
{
var index = 1 ;
foreach ( var flag in rankings )
{
var reason = a & flag ;
if ( reason ! = 0 )
{
return index ;
}
index + + ;
}
return index ;
}
2025-03-27 05:24:16 +03:00
/// <summary>
/// Check the profile conditions.
/// </summary>
/// <param name="conditions">Profile conditions.</param>
/// <param name="mediaSource">Media source.</param>
/// <param name="videoStream">Video stream.</param>
/// <returns>Failed profile conditions.</returns>
private IEnumerable < ProfileCondition > CheckVideoConditions ( ProfileCondition [ ] conditions , MediaSourceInfo mediaSource , MediaStream ? videoStream )
{
int? width = videoStream ? . Width ;
int? height = videoStream ? . Height ;
int? bitDepth = videoStream ? . BitDepth ;
int? videoBitrate = videoStream ? . BitRate ;
double? videoLevel = videoStream ? . Level ;
string? videoProfile = videoStream ? . Profile ;
VideoRangeType ? videoRangeType = videoStream ? . VideoRangeType ;
float videoFramerate = videoStream is null ? 0 : videoStream . ReferenceFrameRate ? ? 0 ;
bool? isAnamorphic = videoStream ? . IsAnamorphic ;
bool? isInterlaced = videoStream ? . IsInterlaced ;
string? videoCodecTag = videoStream ? . CodecTag ;
bool? isAvc = videoStream ? . IsAVC ;
TransportStreamTimestamp ? timestamp = videoStream is null ? TransportStreamTimestamp . None : mediaSource . Timestamp ;
int? packetLength = videoStream ? . PacketLength ;
int? refFrames = videoStream ? . RefFrames ;
2025-03-28 15:51:22 +03:00
int numStreams = mediaSource . MediaStreams . Count ;
2025-03-27 05:24:16 +03:00
int? numAudioStreams = mediaSource . GetStreamCount ( MediaStreamType . Audio ) ;
int? numVideoStreams = mediaSource . GetStreamCount ( MediaStreamType . Video ) ;
2025-03-28 15:51:22 +03:00
return conditions . Where ( applyCondition = > ! ConditionProcessor . IsVideoConditionSatisfied ( applyCondition , width , height , bitDepth , videoBitrate , videoProfile , videoRangeType , videoLevel , videoFramerate , packetLength , timestamp , isAnamorphic , isInterlaced , refFrames , numStreams , numVideoStreams , numAudioStreams , videoCodecTag , isAvc ) ) ;
2025-03-27 05:24:16 +03:00
}
/// <summary>
/// Check the compatibility of the container.
/// </summary>
/// <param name="options">Media options.</param>
/// <param name="mediaSource">Media source.</param>
/// <param name="container">Container.</param>
/// <param name="videoStream">Video stream.</param>
/// <returns>Transcode reasons if the container is not fully compatible.</returns>
private TranscodeReason GetCompatibilityContainer ( MediaOptions options , MediaSourceInfo mediaSource , string container , MediaStream ? videoStream )
{
var profile = options . Profile ;
var failures = AggregateFailureConditions (
mediaSource ,
profile ,
"VideoCodecProfile" ,
profile . ContainerProfiles
. Where ( containerProfile = > containerProfile . Type = = DlnaProfileType . Video & & containerProfile . ContainsContainer ( container ) )
. SelectMany ( containerProfile = > CheckVideoConditions ( containerProfile . Conditions , mediaSource , videoStream ) ) ) ;
return failures ;
}
/// <summary>
/// Check the compatibility of the video codec.
/// </summary>
/// <param name="options">Media options.</param>
/// <param name="mediaSource">Media source.</param>
/// <param name="container">Container.</param>
/// <param name="videoStream">Video stream.</param>
/// <returns>Transcode reasons if the video stream is not fully compatible.</returns>
private TranscodeReason GetCompatibilityVideoCodec ( MediaOptions options , MediaSourceInfo mediaSource , string container , MediaStream videoStream )
{
var profile = options . Profile ;
string videoCodec = videoStream . Codec ;
var failures = AggregateFailureConditions (
mediaSource ,
profile ,
"VideoCodecProfile" ,
profile . CodecProfiles
. Where ( codecProfile = > codecProfile . Type = = CodecType . Video & &
codecProfile . ContainsAnyCodec ( videoCodec , container ) & &
! CheckVideoConditions ( codecProfile . ApplyConditions , mediaSource , videoStream ) . Any ( ) )
. SelectMany ( codecProfile = > CheckVideoConditions ( codecProfile . Conditions , mediaSource , videoStream ) ) ) ;
return failures ;
}
/// <summary>
/// Check the compatibility of the audio codec.
/// </summary>
/// <param name="options">Media options.</param>
/// <param name="mediaSource">Media source.</param>
/// <param name="container">Container.</param>
/// <param name="audioStream">Audio stream.</param>
/// <param name="transcodingAudioCodec">Override audio codec.</param>
/// <param name="isVideo">The media source is video.</param>
/// <param name="isSecondaryAudio">The audio stream is secondary.</param>
/// <returns>Transcode reasons if the audio stream is not fully compatible.</returns>
private TranscodeReason GetCompatibilityAudioCodec ( MediaOptions options , MediaSourceInfo mediaSource , string container , MediaStream audioStream , string? transcodingAudioCodec , bool isVideo , bool isSecondaryAudio )
{
var profile = options . Profile ;
var audioCodec = transcodingAudioCodec ? ? audioStream . Codec ;
var audioProfile = audioStream . Profile ;
var audioChannels = audioStream . Channels ;
var audioBitrate = audioStream . BitRate ;
var audioSampleRate = audioStream . SampleRate ;
var audioBitDepth = audioStream . BitDepth ;
var audioFailureConditions = isVideo
? GetProfileConditionsForVideoAudio ( profile . CodecProfiles , container , audioCodec , audioChannels , audioBitrate , audioSampleRate , audioBitDepth , audioProfile , isSecondaryAudio )
: GetProfileConditionsForAudio ( profile . CodecProfiles , container , audioCodec , audioChannels , audioBitrate , audioSampleRate , audioBitDepth , true ) ;
var failures = AggregateFailureConditions ( mediaSource , profile , "AudioCodecProfile" , audioFailureConditions ) ;
return failures ;
}
/// <summary>
/// Check the compatibility of the audio codec for direct playback.
/// </summary>
/// <param name="options">Media options.</param>
/// <param name="mediaSource">Media source.</param>
/// <param name="container">Container.</param>
/// <param name="audioStream">Audio stream.</param>
/// <param name="isVideo">The media source is video.</param>
/// <param name="isSecondaryAudio">The audio stream is secondary.</param>
/// <returns>Transcode reasons if the audio stream is not fully compatible for direct playback.</returns>
private TranscodeReason GetCompatibilityAudioCodecDirect ( MediaOptions options , MediaSourceInfo mediaSource , string container , MediaStream audioStream , bool isVideo , bool isSecondaryAudio )
{
var failures = GetCompatibilityAudioCodec ( options , mediaSource , container , audioStream , null , isVideo , isSecondaryAudio ) ;
if ( audioStream . IsExternal )
{
failures | = TranscodeReason . AudioIsExternal ;
}
return failures ;
}
2018-12-27 18:27:57 -05:00
}
}