2018-12-27 23:27:57 +00:00
using MediaBrowser.Model.Dto ;
using MediaBrowser.Model.Entities ;
using MediaBrowser.Model.Extensions ;
2018-12-13 13:18:25 +00:00
using Microsoft.Extensions.Logging ;
2018-12-27 23:27:57 +00:00
using MediaBrowser.Model.MediaInfo ;
using MediaBrowser.Model.Session ;
using System ;
using System.Collections.Generic ;
using System.Globalization ;
using System.Linq ;
namespace MediaBrowser.Model.Dlna
{
public class StreamBuilder
{
private readonly ILogger _logger ;
private readonly ITranscoderSupport _transcoderSupport ;
public StreamBuilder ( ITranscoderSupport transcoderSupport , ILogger logger )
{
_transcoderSupport = transcoderSupport ;
_logger = logger ;
}
public StreamBuilder ( ILogger logger )
: this ( new FullTranscoderSupport ( ) , logger )
{
}
public StreamInfo BuildAudioItem ( AudioOptions options )
{
ValidateAudioInput ( options ) ;
var mediaSources = new List < MediaSourceInfo > ( ) ;
foreach ( MediaSourceInfo i in options . MediaSources )
{
if ( string . IsNullOrEmpty ( options . MediaSourceId ) | |
StringHelper . EqualsIgnoreCase ( i . Id , options . MediaSourceId ) )
{
mediaSources . Add ( i ) ;
}
}
var streams = new List < StreamInfo > ( ) ;
foreach ( MediaSourceInfo i in mediaSources )
{
StreamInfo streamInfo = BuildAudioItem ( i , options ) ;
if ( streamInfo ! = null )
{
streams . Add ( streamInfo ) ;
}
}
foreach ( StreamInfo stream in streams )
{
stream . DeviceId = options . DeviceId ;
stream . DeviceProfileId = options . Profile . Id ;
}
return GetOptimalStream ( streams , options . GetMaxBitrate ( true ) ? ? 0 ) ;
}
public StreamInfo BuildVideoItem ( VideoOptions options )
{
ValidateInput ( options ) ;
var mediaSources = new List < MediaSourceInfo > ( ) ;
foreach ( MediaSourceInfo i in options . MediaSources )
{
if ( string . IsNullOrEmpty ( options . MediaSourceId ) | |
StringHelper . EqualsIgnoreCase ( i . Id , options . MediaSourceId ) )
{
mediaSources . Add ( i ) ;
}
}
var streams = new List < StreamInfo > ( ) ;
foreach ( MediaSourceInfo i in mediaSources )
{
StreamInfo streamInfo = BuildVideoItem ( i , options ) ;
if ( streamInfo ! = null )
{
streams . Add ( streamInfo ) ;
}
}
foreach ( StreamInfo stream in streams )
{
stream . DeviceId = options . DeviceId ;
stream . DeviceProfileId = options . Profile . Id ;
}
return GetOptimalStream ( streams , options . GetMaxBitrate ( false ) ? ? 0 ) ;
}
private StreamInfo GetOptimalStream ( List < StreamInfo > streams , long maxBitrate )
{
var sorted = SortMediaSources ( streams , maxBitrate ) ;
foreach ( StreamInfo stream in sorted )
{
return stream ;
}
return null ;
}
private StreamInfo [ ] SortMediaSources ( List < StreamInfo > streams , long maxBitrate )
{
return streams . OrderBy ( i = >
{
// Nothing beats direct playing a file
if ( i . PlayMethod = = PlayMethod . DirectPlay & & i . MediaSource . Protocol = = MediaProtocol . File )
{
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 = >
{
switch ( i . MediaSource . Protocol )
{
case MediaProtocol . File :
return 0 ;
default :
return 1 ;
}
} ) . ThenBy ( i = >
{
if ( maxBitrate > 0 )
{
if ( i . MediaSource . Bitrate . HasValue )
{
return Math . Abs ( i . MediaSource . Bitrate . Value - maxBitrate ) ;
}
}
return 0 ;
} ) . ThenBy ( streams . IndexOf ) . ToArray ( ) ;
}
private TranscodeReason ? GetTranscodeReasonForFailedCondition ( ProfileCondition condition )
{
switch ( condition . Property )
{
case ProfileConditionValue . AudioBitrate :
if ( condition . Condition = = ProfileConditionType . LessThanEqual )
{
return TranscodeReason . AudioBitrateNotSupported ;
}
return TranscodeReason . AudioBitrateNotSupported ;
case ProfileConditionValue . AudioChannels :
if ( condition . Condition = = ProfileConditionType . LessThanEqual )
{
return TranscodeReason . AudioChannelsNotSupported ;
}
return TranscodeReason . AudioChannelsNotSupported ;
case ProfileConditionValue . AudioProfile :
return TranscodeReason . AudioProfileNotSupported ;
case ProfileConditionValue . AudioSampleRate :
return TranscodeReason . AudioSampleRateNotSupported ;
case ProfileConditionValue . Has64BitOffsets :
// TODO
return null ;
case ProfileConditionValue . Height :
return TranscodeReason . VideoResolutionNotSupported ;
case ProfileConditionValue . IsAnamorphic :
return TranscodeReason . AnamorphicVideoNotSupported ;
case ProfileConditionValue . IsAvc :
// TODO
return null ;
case ProfileConditionValue . IsInterlaced :
return TranscodeReason . InterlacedVideoNotSupported ;
case ProfileConditionValue . IsSecondaryAudio :
return TranscodeReason . SecondaryAudioNotSupported ;
case ProfileConditionValue . NumAudioStreams :
// TODO
return null ;
case ProfileConditionValue . NumVideoStreams :
// TODO
return null ;
case ProfileConditionValue . PacketLength :
// TODO
return null ;
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 :
return TranscodeReason . VideoCodecNotSupported ;
case ProfileConditionValue . VideoFramerate :
return TranscodeReason . VideoFramerateNotSupported ;
case ProfileConditionValue . VideoLevel :
return TranscodeReason . VideoLevelNotSupported ;
case ProfileConditionValue . VideoProfile :
return TranscodeReason . VideoProfileNotSupported ;
case ProfileConditionValue . VideoTimestamp :
// TODO
return null ;
case ProfileConditionValue . Width :
return TranscodeReason . VideoResolutionNotSupported ;
default :
return null ;
}
}
public static string NormalizeMediaSourceFormatIntoSingleContainer ( string inputContainer , string unused1 , DeviceProfile profile , DlnaProfileType type )
{
if ( string . IsNullOrEmpty ( inputContainer ) )
{
return null ;
}
var formats = ContainerProfile . SplitValue ( inputContainer ) ;
if ( formats . Length = = 1 )
{
return formats [ 0 ] ;
}
if ( profile ! = null )
{
foreach ( var format in formats )
{
foreach ( var directPlayProfile in profile . DirectPlayProfiles )
{
if ( directPlayProfile . Type = = type )
{
if ( directPlayProfile . SupportsContainer ( format ) )
{
return format ;
}
}
}
}
}
return formats [ 0 ] ;
}
private StreamInfo BuildAudioItem ( MediaSourceInfo item , AudioOptions options )
{
var transcodeReasons = new List < TranscodeReason > ( ) ;
StreamInfo playlistItem = new StreamInfo
{
ItemId = options . ItemId ,
MediaType = DlnaProfileType . Audio ,
MediaSource = item ,
RunTimeTicks = item . RunTimeTicks ,
Context = options . Context ,
DeviceProfile = options . Profile
} ;
if ( options . ForceDirectPlay )
{
playlistItem . PlayMethod = PlayMethod . DirectPlay ;
playlistItem . Container = NormalizeMediaSourceFormatIntoSingleContainer ( item . Container , item . Path , options . Profile , DlnaProfileType . Audio ) ;
return playlistItem ;
}
if ( options . ForceDirectStream )
{
playlistItem . PlayMethod = PlayMethod . DirectStream ;
playlistItem . Container = NormalizeMediaSourceFormatIntoSingleContainer ( item . Container , item . Path , options . Profile , DlnaProfileType . Audio ) ;
return playlistItem ;
}
MediaStream audioStream = item . GetDefaultAudioStream ( null ) ;
var directPlayInfo = GetAudioDirectPlayMethods ( item , audioStream , options ) ;
var directPlayMethods = directPlayInfo . Item1 ;
transcodeReasons . AddRange ( directPlayInfo . Item2 ) ;
ConditionProcessor conditionProcessor = new ConditionProcessor ( ) ;
int? inputAudioChannels = audioStream = = null ? null : audioStream . Channels ;
int? inputAudioBitrate = audioStream = = null ? null : audioStream . BitDepth ;
int? inputAudioSampleRate = audioStream = = null ? null : audioStream . SampleRate ;
int? inputAudioBitDepth = audioStream = = null ? null : audioStream . BitDepth ;
if ( directPlayMethods . Count > 0 )
{
string audioCodec = audioStream = = null ? null : audioStream . Codec ;
// Make sure audio codec profiles are satisfied
var conditions = new List < ProfileCondition > ( ) ;
foreach ( CodecProfile i in options . Profile . CodecProfiles )
{
if ( i . Type = = CodecType . Audio & & i . ContainsAnyCodec ( audioCodec , item . Container ) )
{
bool applyConditions = true ;
foreach ( ProfileCondition applyCondition in i . ApplyConditions )
{
if ( ! conditionProcessor . IsAudioConditionSatisfied ( applyCondition , inputAudioChannels , inputAudioBitrate , inputAudioSampleRate , inputAudioBitDepth ) )
{
LogConditionFailure ( options . Profile , "AudioCodecProfile" , applyCondition , item ) ;
applyConditions = false ;
break ;
}
}
if ( applyConditions )
{
foreach ( ProfileCondition c in i . Conditions )
{
conditions . Add ( c ) ;
}
}
}
}
bool all = true ;
foreach ( ProfileCondition c in conditions )
{
if ( ! conditionProcessor . IsAudioConditionSatisfied ( c , inputAudioChannels , inputAudioBitrate , inputAudioSampleRate , inputAudioBitDepth ) )
{
LogConditionFailure ( options . Profile , "AudioCodecProfile" , c , item ) ;
var transcodeReason = GetTranscodeReasonForFailedCondition ( c ) ;
if ( transcodeReason . HasValue )
{
transcodeReasons . Add ( transcodeReason . Value ) ;
}
all = false ;
break ;
}
}
if ( all )
{
if ( directPlayMethods . Contains ( PlayMethod . DirectStream ) )
{
playlistItem . PlayMethod = PlayMethod . DirectStream ;
}
playlistItem . Container = NormalizeMediaSourceFormatIntoSingleContainer ( item . Container , item . Path , options . Profile , DlnaProfileType . Audio ) ;
return playlistItem ;
}
}
TranscodingProfile transcodingProfile = null ;
foreach ( TranscodingProfile i in options . Profile . TranscodingProfiles )
{
if ( i . Type = = playlistItem . MediaType & & i . Context = = options . Context )
{
if ( _transcoderSupport . CanEncodeToAudioCodec ( i . AudioCodec ? ? i . Container ) )
{
transcodingProfile = i ;
break ;
}
}
}
if ( transcodingProfile ! = null )
{
if ( ! item . SupportsTranscoding )
{
return null ;
}
SetStreamInfoOptionsFromTranscodingProfile ( playlistItem , transcodingProfile ) ;
var audioCodecProfiles = new List < CodecProfile > ( ) ;
foreach ( CodecProfile i in options . Profile . CodecProfiles )
{
if ( i . Type = = CodecType . Audio & & i . ContainsAnyCodec ( transcodingProfile . AudioCodec , transcodingProfile . Container ) )
{
audioCodecProfiles . Add ( i ) ;
}
if ( audioCodecProfiles . Count > = 1 ) break ;
}
var audioTranscodingConditions = new List < ProfileCondition > ( ) ;
foreach ( CodecProfile i in audioCodecProfiles )
{
bool applyConditions = true ;
foreach ( ProfileCondition applyCondition in i . ApplyConditions )
{
if ( ! conditionProcessor . IsAudioConditionSatisfied ( applyCondition , inputAudioChannels , inputAudioBitrate , inputAudioSampleRate , inputAudioBitDepth ) )
{
LogConditionFailure ( options . Profile , "AudioCodecProfile" , applyCondition , item ) ;
applyConditions = false ;
break ;
}
}
if ( applyConditions )
{
foreach ( ProfileCondition c in i . Conditions )
{
audioTranscodingConditions . Add ( c ) ;
}
}
}
ApplyTranscodingConditions ( playlistItem , audioTranscodingConditions , null , true , true ) ;
// Honor requested max channels
playlistItem . GlobalMaxAudioChannels = options . MaxAudioChannels ;
var configuredBitrate = options . GetMaxBitrate ( true ) ;
long transcodingBitrate = options . AudioTranscodingBitrate ? ?
( options . Context = = EncodingContext . Streaming ? options . Profile . MusicStreamingTranscodingBitrate : null ) ? ?
configuredBitrate ? ?
128000 ;
if ( configuredBitrate . HasValue )
{
transcodingBitrate = Math . Min ( configuredBitrate . Value , transcodingBitrate ) ;
}
var longBitrate = Math . Min ( transcodingBitrate , playlistItem . AudioBitrate ? ? transcodingBitrate ) ;
playlistItem . AudioBitrate = longBitrate > int . MaxValue ? int . MaxValue : Convert . ToInt32 ( longBitrate ) ;
}
playlistItem . TranscodeReasons = transcodeReasons . ToArray ( ) ;
return playlistItem ;
}
private long? GetBitrateForDirectPlayCheck ( MediaSourceInfo item , AudioOptions options , bool isAudio )
{
if ( item . Protocol = = MediaProtocol . File )
{
return options . Profile . MaxStaticBitrate ;
}
return options . GetMaxBitrate ( isAudio ) ;
}
private Tuple < List < PlayMethod > , List < TranscodeReason > > GetAudioDirectPlayMethods ( MediaSourceInfo item , MediaStream audioStream , AudioOptions options )
{
var transcodeReasons = new List < TranscodeReason > ( ) ;
DirectPlayProfile directPlayProfile = null ;
foreach ( DirectPlayProfile i in options . Profile . DirectPlayProfiles )
{
if ( i . Type = = DlnaProfileType . Audio & & IsAudioDirectPlaySupported ( i , item , audioStream ) )
{
directPlayProfile = i ;
break ;
}
}
var playMethods = new List < PlayMethod > ( ) ;
if ( directPlayProfile ! = null )
{
// While options takes the network and other factors into account. Only applies to direct stream
if ( item . SupportsDirectStream )
{
if ( IsAudioEligibleForDirectPlay ( item , options . GetMaxBitrate ( true ) ? ? 0 , PlayMethod . DirectStream ) )
{
if ( options . EnableDirectStream )
{
playMethods . Add ( PlayMethod . DirectStream ) ;
}
}
else
{
transcodeReasons . Add ( TranscodeReason . ContainerBitrateExceedsLimit ) ;
}
}
// The profile describes what the device supports
// If device requirements are satisfied then allow both direct stream and direct play
if ( item . SupportsDirectPlay )
{
if ( IsAudioEligibleForDirectPlay ( item , GetBitrateForDirectPlayCheck ( item , options , true ) ? ? 0 , PlayMethod . DirectPlay ) )
{
if ( options . EnableDirectPlay )
{
playMethods . Add ( PlayMethod . DirectPlay ) ;
}
}
else
{
transcodeReasons . Add ( TranscodeReason . ContainerBitrateExceedsLimit ) ;
}
}
}
else
{
transcodeReasons . InsertRange ( 0 , GetTranscodeReasonsFromDirectPlayProfile ( item , null , audioStream , options . Profile . DirectPlayProfiles ) ) ;
2018-12-13 13:18:25 +00:00
_logger . LogInformation ( "Profile: {0}, No direct play profiles found for Path: {1}" ,
2018-12-27 23:27:57 +00:00
options . Profile . Name ? ? "Unknown Profile" ,
item . Path ? ? "Unknown path" ) ;
}
if ( playMethods . Count > 0 )
{
transcodeReasons . Clear ( ) ;
}
else
{
transcodeReasons = transcodeReasons . Distinct ( ) . ToList ( ) ;
}
return new Tuple < List < PlayMethod > , List < TranscodeReason > > ( playMethods , transcodeReasons ) ;
}
private List < TranscodeReason > GetTranscodeReasonsFromDirectPlayProfile ( MediaSourceInfo item , MediaStream videoStream , MediaStream audioStream , IEnumerable < DirectPlayProfile > directPlayProfiles )
{
var list = new List < TranscodeReason > ( ) ;
var containerSupported = false ;
var audioSupported = false ;
var videoSupported = false ;
foreach ( var profile in directPlayProfiles )
{
audioSupported = false ;
videoSupported = false ;
// Check container type
if ( profile . SupportsContainer ( item . Container ) )
{
containerSupported = true ;
if ( videoStream ! = null )
{
if ( profile . SupportsVideoCodec ( videoStream . Codec ) )
{
videoSupported = true ;
}
}
if ( audioStream ! = null )
{
if ( profile . SupportsAudioCodec ( audioStream . Codec ) )
{
audioSupported = true ;
}
}
if ( videoSupported & & audioSupported )
{
break ;
}
}
}
if ( ! containerSupported )
{
list . Add ( TranscodeReason . ContainerNotSupported ) ;
}
if ( videoStream ! = null & & ! videoSupported )
{
list . Add ( TranscodeReason . VideoCodecNotSupported ) ;
}
if ( audioStream ! = null & & ! audioSupported )
{
list . Add ( TranscodeReason . AudioCodecNotSupported ) ;
}
return list ;
}
private int? GetDefaultSubtitleStreamIndex ( MediaSourceInfo item , SubtitleProfile [ ] subtitleProfiles )
{
int highestScore = - 1 ;
foreach ( MediaStream stream in item . MediaStreams )
{
if ( stream . Type = = MediaStreamType . Subtitle & & stream . Score . HasValue )
{
if ( stream . Score . Value > highestScore )
{
highestScore = stream . Score . Value ;
}
}
}
var topStreams = new List < MediaStream > ( ) ;
foreach ( MediaStream stream in item . MediaStreams )
{
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 )
{
foreach ( MediaStream stream in topStreams )
{
foreach ( SubtitleProfile profile in subtitleProfiles )
{
if ( profile . Method = = SubtitleDeliveryMethod . External & & StringHelper . EqualsIgnoreCase ( profile . Format , stream . Codec ) )
{
return stream . Index ;
}
}
}
}
// If no optimization panned out, just use the original default
return item . DefaultSubtitleStreamIndex ;
}
private void SetStreamInfoOptionsFromTranscodingProfile ( StreamInfo playlistItem , TranscodingProfile transcodingProfile )
{
if ( string . IsNullOrEmpty ( transcodingProfile . AudioCodec ) )
{
playlistItem . AudioCodecs = Array . Empty < string > ( ) ;
}
else
{
playlistItem . AudioCodecs = transcodingProfile . AudioCodec . Split ( ',' ) ;
}
playlistItem . Container = transcodingProfile . Container ;
playlistItem . EstimateContentLength = transcodingProfile . EstimateContentLength ;
playlistItem . TranscodeSeekInfo = transcodingProfile . TranscodeSeekInfo ;
if ( string . IsNullOrEmpty ( transcodingProfile . VideoCodec ) )
{
playlistItem . VideoCodecs = Array . Empty < string > ( ) ;
}
else
{
playlistItem . VideoCodecs = transcodingProfile . VideoCodec . Split ( ',' ) ;
}
playlistItem . CopyTimestamps = transcodingProfile . CopyTimestamps ;
playlistItem . EnableSubtitlesInManifest = transcodingProfile . EnableSubtitlesInManifest ;
playlistItem . EnableMpegtsM2TsMode = transcodingProfile . EnableMpegtsM2TsMode ;
playlistItem . BreakOnNonKeyFrames = transcodingProfile . BreakOnNonKeyFrames ;
if ( transcodingProfile . MinSegments > 0 )
{
playlistItem . MinSegments = transcodingProfile . MinSegments ;
}
if ( transcodingProfile . SegmentLength > 0 )
{
playlistItem . SegmentLength = transcodingProfile . SegmentLength ;
}
playlistItem . SubProtocol = transcodingProfile . Protocol ;
if ( ! string . IsNullOrEmpty ( transcodingProfile . MaxAudioChannels ) )
{
int transcodingMaxAudioChannels ;
if ( int . TryParse ( transcodingProfile . MaxAudioChannels , NumberStyles . Any , CultureInfo . InvariantCulture , out transcodingMaxAudioChannels ) )
{
playlistItem . TranscodingMaxAudioChannels = transcodingMaxAudioChannels ;
}
}
}
private StreamInfo BuildVideoItem ( MediaSourceInfo item , VideoOptions options )
{
if ( item = = null )
{
2019-01-06 20:50:43 +00:00
throw new ArgumentNullException ( nameof ( item ) ) ;
2018-12-27 23:27:57 +00:00
}
var transcodeReasons = new List < TranscodeReason > ( ) ;
StreamInfo playlistItem = new StreamInfo
{
ItemId = options . ItemId ,
MediaType = DlnaProfileType . Video ,
MediaSource = item ,
RunTimeTicks = item . RunTimeTicks ,
Context = options . Context ,
DeviceProfile = options . Profile
} ;
playlistItem . SubtitleStreamIndex = options . SubtitleStreamIndex ? ? GetDefaultSubtitleStreamIndex ( item , options . Profile . SubtitleProfiles ) ;
MediaStream subtitleStream = playlistItem . SubtitleStreamIndex . HasValue ? item . GetMediaStream ( MediaStreamType . Subtitle , playlistItem . SubtitleStreamIndex . Value ) : null ;
MediaStream audioStream = item . GetDefaultAudioStream ( options . AudioStreamIndex ? ? item . DefaultAudioStreamIndex ) ;
if ( audioStream ! = null )
{
playlistItem . AudioStreamIndex = audioStream . Index ;
}
MediaStream videoStream = item . VideoStream ;
// TODO: This doesn't accout for situation of device being able to handle media bitrate, but wifi connection not fast enough
var directPlayEligibilityResult = IsEligibleForDirectPlay ( item , GetBitrateForDirectPlayCheck ( item , options , true ) ? ? 0 , subtitleStream , options , PlayMethod . DirectPlay ) ;
var directStreamEligibilityResult = IsEligibleForDirectPlay ( item , options . GetMaxBitrate ( false ) ? ? 0 , subtitleStream , options , PlayMethod . DirectStream ) ;
bool isEligibleForDirectPlay = options . EnableDirectPlay & & ( options . ForceDirectPlay | | directPlayEligibilityResult . Item1 ) ;
bool isEligibleForDirectStream = options . EnableDirectStream & & ( options . ForceDirectStream | | directStreamEligibilityResult . Item1 ) ;
2018-12-13 13:18:25 +00:00
_logger . LogInformation ( "Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}" ,
2018-12-27 23:27:57 +00:00
options . Profile . Name ? ? "Unknown Profile" ,
item . Path ? ? "Unknown path" ,
isEligibleForDirectPlay ,
isEligibleForDirectStream ) ;
if ( isEligibleForDirectPlay | | isEligibleForDirectStream )
{
// See if it can be direct played
var directPlayInfo = GetVideoDirectPlayProfile ( options , item , videoStream , audioStream , isEligibleForDirectPlay , isEligibleForDirectStream ) ;
var directPlay = directPlayInfo . Item1 ;
if ( directPlay ! = null )
{
playlistItem . PlayMethod = directPlay . Value ;
playlistItem . Container = NormalizeMediaSourceFormatIntoSingleContainer ( item . Container , item . Path , options . Profile , DlnaProfileType . Video ) ;
if ( subtitleStream ! = null )
{
SubtitleProfile subtitleProfile = GetSubtitleProfile ( item , subtitleStream , options . Profile . SubtitleProfiles , directPlay . Value , _transcoderSupport , item . Container , null ) ;
playlistItem . SubtitleDeliveryMethod = subtitleProfile . Method ;
playlistItem . SubtitleFormat = subtitleProfile . Format ;
}
return playlistItem ;
}
transcodeReasons . AddRange ( directPlayInfo . Item2 ) ;
}
if ( directPlayEligibilityResult . Item2 . HasValue )
{
transcodeReasons . Add ( directPlayEligibilityResult . Item2 . Value ) ;
}
if ( directStreamEligibilityResult . Item2 . HasValue )
{
transcodeReasons . Add ( directStreamEligibilityResult . Item2 . Value ) ;
}
// Can't direct play, find the transcoding profile
TranscodingProfile transcodingProfile = null ;
foreach ( TranscodingProfile i in options . Profile . TranscodingProfiles )
{
if ( i . Type = = playlistItem . MediaType & & i . Context = = options . Context )
{
transcodingProfile = i ;
break ;
}
}
if ( transcodingProfile ! = null )
{
if ( ! item . SupportsTranscoding )
{
return null ;
}
if ( subtitleStream ! = null )
{
SubtitleProfile subtitleProfile = GetSubtitleProfile ( item , subtitleStream , options . Profile . SubtitleProfiles , PlayMethod . Transcode , _transcoderSupport , transcodingProfile . Container , transcodingProfile . Protocol ) ;
playlistItem . SubtitleDeliveryMethod = subtitleProfile . Method ;
playlistItem . SubtitleFormat = subtitleProfile . Format ;
playlistItem . SubtitleCodecs = new [ ] { subtitleProfile . Format } ;
}
playlistItem . PlayMethod = PlayMethod . Transcode ;
SetStreamInfoOptionsFromTranscodingProfile ( playlistItem , transcodingProfile ) ;
ConditionProcessor conditionProcessor = new ConditionProcessor ( ) ;
var isFirstAppliedCodecProfile = true ;
foreach ( CodecProfile i in options . Profile . CodecProfiles )
{
if ( i . Type = = CodecType . Video & & i . ContainsAnyCodec ( transcodingProfile . VideoCodec , transcodingProfile . Container ) )
{
bool applyConditions = true ;
foreach ( ProfileCondition applyCondition in i . ApplyConditions )
{
int? width = videoStream = = null ? null : videoStream . Width ;
int? height = videoStream = = null ? null : videoStream . Height ;
int? bitDepth = videoStream = = null ? null : videoStream . BitDepth ;
int? videoBitrate = videoStream = = null ? null : videoStream . BitRate ;
double? videoLevel = videoStream = = null ? null : videoStream . Level ;
string videoProfile = videoStream = = null ? null : videoStream . Profile ;
float videoFramerate = videoStream = = null ? 0 : videoStream . AverageFrameRate ? ? videoStream . AverageFrameRate ? ? 0 ;
bool? isAnamorphic = videoStream = = null ? null : videoStream . IsAnamorphic ;
bool? isInterlaced = videoStream = = null ? ( bool? ) null : videoStream . IsInterlaced ;
string videoCodecTag = videoStream = = null ? null : videoStream . CodecTag ;
bool? isAvc = videoStream = = null ? null : videoStream . IsAVC ;
TransportStreamTimestamp ? timestamp = videoStream = = null ? TransportStreamTimestamp . None : item . Timestamp ;
int? packetLength = videoStream = = null ? null : videoStream . PacketLength ;
int? refFrames = videoStream = = null ? null : videoStream . RefFrames ;
int? numAudioStreams = item . GetStreamCount ( MediaStreamType . Audio ) ;
int? numVideoStreams = item . GetStreamCount ( MediaStreamType . Video ) ;
if ( ! conditionProcessor . IsVideoConditionSatisfied ( applyCondition , width , height , bitDepth , videoBitrate , videoProfile , videoLevel , videoFramerate , packetLength , timestamp , isAnamorphic , isInterlaced , refFrames , numVideoStreams , numAudioStreams , videoCodecTag , isAvc ) )
{
//LogConditionFailure(options.Profile, "VideoCodecProfile.ApplyConditions", applyCondition, item);
applyConditions = false ;
break ;
}
}
if ( applyConditions )
{
var transcodingVideoCodecs = ContainerProfile . SplitValue ( transcodingProfile . VideoCodec ) ;
foreach ( var transcodingVideoCodec in transcodingVideoCodecs )
{
if ( i . ContainsAnyCodec ( transcodingVideoCodec , transcodingProfile . Container ) )
{
ApplyTranscodingConditions ( playlistItem , i . Conditions , transcodingVideoCodec , true , isFirstAppliedCodecProfile ) ;
isFirstAppliedCodecProfile = false ;
}
}
}
}
}
// Honor requested max channels
playlistItem . GlobalMaxAudioChannels = options . MaxAudioChannels ;
int audioBitrate = GetAudioBitrate ( playlistItem . SubProtocol , options . GetMaxBitrate ( false ) ? ? 0 , playlistItem . TargetAudioCodec , audioStream , playlistItem ) ;
playlistItem . AudioBitrate = Math . Min ( playlistItem . AudioBitrate ? ? audioBitrate , audioBitrate ) ;
isFirstAppliedCodecProfile = true ;
foreach ( CodecProfile i in options . Profile . CodecProfiles )
{
if ( i . Type = = CodecType . VideoAudio & & i . ContainsAnyCodec ( transcodingProfile . AudioCodec , transcodingProfile . Container ) )
{
bool applyConditions = true ;
foreach ( ProfileCondition applyCondition in i . ApplyConditions )
{
bool? isSecondaryAudio = audioStream = = null ? null : item . IsSecondaryAudio ( audioStream ) ;
int? inputAudioBitrate = audioStream = = null ? null : audioStream . BitRate ;
int? audioChannels = audioStream = = null ? null : audioStream . Channels ;
string audioProfile = audioStream = = null ? null : audioStream . Profile ;
int? inputAudioSampleRate = audioStream = = null ? null : audioStream . SampleRate ;
int? inputAudioBitDepth = audioStream = = null ? null : audioStream . BitDepth ;
if ( ! conditionProcessor . IsVideoAudioConditionSatisfied ( applyCondition , audioChannels , inputAudioBitrate , inputAudioSampleRate , inputAudioBitDepth , audioProfile , isSecondaryAudio ) )
{
//LogConditionFailure(options.Profile, "VideoCodecProfile.ApplyConditions", applyCondition, item);
applyConditions = false ;
break ;
}
}
if ( applyConditions )
{
var transcodingAudioCodecs = ContainerProfile . SplitValue ( transcodingProfile . AudioCodec ) ;
foreach ( var transcodingAudioCodec in transcodingAudioCodecs )
{
if ( i . ContainsAnyCodec ( transcodingAudioCodec , transcodingProfile . Container ) )
{
ApplyTranscodingConditions ( playlistItem , i . Conditions , transcodingAudioCodec , true , isFirstAppliedCodecProfile ) ;
isFirstAppliedCodecProfile = false ;
}
}
}
}
}
var maxBitrateSetting = options . GetMaxBitrate ( false ) ;
// Honor max rate
if ( maxBitrateSetting . HasValue )
{
var availableBitrateForVideo = maxBitrateSetting . Value ;
if ( playlistItem . AudioBitrate . HasValue )
{
availableBitrateForVideo - = playlistItem . AudioBitrate . Value ;
}
// Make sure the video bitrate is lower than bitrate settings but at least 64k
long currentValue = playlistItem . VideoBitrate ? ? availableBitrateForVideo ;
var longBitrate = Math . Max ( Math . Min ( availableBitrateForVideo , currentValue ) , 64000 ) ;
playlistItem . VideoBitrate = longBitrate > = int . MaxValue ? int . MaxValue : Convert . ToInt32 ( longBitrate ) ;
}
}
playlistItem . TranscodeReasons = transcodeReasons . ToArray ( ) ;
return playlistItem ;
}
private int GetDefaultAudioBitrateIfUnknown ( MediaStream audioStream )
{
if ( ( audioStream . Channels ? ? 0 ) > = 6 )
{
return 384000 ;
}
return 192000 ;
}
private int GetAudioBitrate ( string subProtocol , long maxTotalBitrate , string [ ] targetAudioCodecs , MediaStream audioStream , StreamInfo item )
{
var targetAudioCodec = targetAudioCodecs . Length = = 0 ? null : targetAudioCodecs [ 0 ] ;
var targetAudioChannels = item . GetTargetAudioChannels ( targetAudioCodec ) ;
int defaultBitrate = audioStream = = null ? 192000 : audioStream . BitRate ? ? GetDefaultAudioBitrateIfUnknown ( audioStream ) ;
// Reduce the bitrate if we're downmixing
if ( targetAudioChannels . HasValue & & audioStream ! = null & & audioStream . Channels . HasValue & & targetAudioChannels . Value < audioStream . Channels . Value )
{
defaultBitrate = targetAudioChannels . Value < = 2 ? 128000 : 192000 ;
}
int encoderAudioBitrateLimit = int . MaxValue ;
if ( audioStream ! = null )
{
2019-01-07 23:27:46 +00:00
// Seeing webm encoding failures when source has 1 audio channel and 22k bitrate.
2018-12-27 23:27:57 +00:00
// Any attempts to transcode over 64k will fail
if ( audioStream . Channels . HasValue & &
audioStream . Channels . Value = = 1 )
{
if ( ( audioStream . BitRate ? ? 0 ) < 64000 )
{
encoderAudioBitrateLimit = 64000 ;
}
}
}
if ( maxTotalBitrate > 0 )
{
defaultBitrate = Math . Min ( GetMaxAudioBitrateForTotalBitrate ( maxTotalBitrate ) , defaultBitrate ) ;
}
return Math . Min ( defaultBitrate , encoderAudioBitrateLimit ) ;
}
private int GetMaxAudioBitrateForTotalBitrate ( long totalBitrate )
{
if ( totalBitrate < = 640000 )
{
return 128000 ;
}
if ( totalBitrate < = 2000000 )
{
return 384000 ;
}
if ( totalBitrate < = 3000000 )
{
return 448000 ;
}
return 640000 ;
}
private Tuple < PlayMethod ? , List < TranscodeReason > > GetVideoDirectPlayProfile ( VideoOptions options ,
MediaSourceInfo mediaSource ,
MediaStream videoStream ,
MediaStream audioStream ,
bool isEligibleForDirectPlay ,
bool isEligibleForDirectStream )
{
DeviceProfile profile = options . Profile ;
if ( options . ForceDirectPlay )
{
return new Tuple < PlayMethod ? , List < TranscodeReason > > ( PlayMethod . DirectPlay , new List < TranscodeReason > ( ) ) ;
}
if ( options . ForceDirectStream )
{
return new Tuple < PlayMethod ? , List < TranscodeReason > > ( PlayMethod . DirectStream , new List < TranscodeReason > ( ) ) ;
}
// See if it can be direct played
DirectPlayProfile directPlay = null ;
foreach ( DirectPlayProfile i in profile . DirectPlayProfiles )
{
if ( i . Type = = DlnaProfileType . Video & & IsVideoDirectPlaySupported ( i , mediaSource , videoStream , audioStream ) )
{
directPlay = i ;
break ;
}
}
if ( directPlay = = null )
{
2018-12-13 13:18:25 +00:00
_logger . LogInformation ( "Profile: {0}, No direct play profiles found for Path: {1}" ,
2018-12-27 23:27:57 +00:00
profile . Name ? ? "Unknown Profile" ,
mediaSource . Path ? ? "Unknown path" ) ;
return new Tuple < PlayMethod ? , List < TranscodeReason > > ( null , GetTranscodeReasonsFromDirectPlayProfile ( mediaSource , videoStream , audioStream , profile . DirectPlayProfiles ) ) ;
}
string container = mediaSource . Container ;
var conditions = new List < ProfileCondition > ( ) ;
foreach ( ContainerProfile i in profile . ContainerProfiles )
{
if ( i . Type = = DlnaProfileType . Video & &
i . ContainsContainer ( container ) )
{
foreach ( ProfileCondition c in i . Conditions )
{
conditions . Add ( c ) ;
}
}
}
ConditionProcessor conditionProcessor = new ConditionProcessor ( ) ;
int? width = videoStream = = null ? null : videoStream . Width ;
int? height = videoStream = = null ? null : videoStream . Height ;
int? bitDepth = videoStream = = null ? null : videoStream . BitDepth ;
int? videoBitrate = videoStream = = null ? null : videoStream . BitRate ;
double? videoLevel = videoStream = = null ? null : videoStream . Level ;
string videoProfile = videoStream = = null ? null : videoStream . Profile ;
float videoFramerate = videoStream = = null ? 0 : videoStream . AverageFrameRate ? ? videoStream . AverageFrameRate ? ? 0 ;
bool? isAnamorphic = videoStream = = null ? null : videoStream . IsAnamorphic ;
bool? isInterlaced = videoStream = = null ? ( bool? ) null : videoStream . IsInterlaced ;
string videoCodecTag = videoStream = = null ? null : videoStream . CodecTag ;
bool? isAvc = videoStream = = null ? null : videoStream . IsAVC ;
int? audioBitrate = audioStream = = null ? null : audioStream . BitRate ;
int? audioChannels = audioStream = = null ? null : audioStream . Channels ;
string audioProfile = audioStream = = null ? null : audioStream . Profile ;
int? audioSampleRate = audioStream = = null ? null : audioStream . SampleRate ;
int? audioBitDepth = audioStream = = null ? null : audioStream . BitDepth ;
TransportStreamTimestamp ? timestamp = videoStream = = null ? TransportStreamTimestamp . None : mediaSource . Timestamp ;
int? packetLength = videoStream = = null ? null : videoStream . PacketLength ;
int? refFrames = videoStream = = null ? null : videoStream . RefFrames ;
int? numAudioStreams = mediaSource . GetStreamCount ( MediaStreamType . Audio ) ;
int? numVideoStreams = mediaSource . GetStreamCount ( MediaStreamType . Video ) ;
// Check container conditions
foreach ( ProfileCondition i in conditions )
{
if ( ! conditionProcessor . IsVideoConditionSatisfied ( i , width , height , bitDepth , videoBitrate , videoProfile , videoLevel , videoFramerate , packetLength , timestamp , isAnamorphic , isInterlaced , refFrames , numVideoStreams , numAudioStreams , videoCodecTag , isAvc ) )
{
LogConditionFailure ( profile , "VideoContainerProfile" , i , mediaSource ) ;
var transcodeReason = GetTranscodeReasonForFailedCondition ( i ) ;
var transcodeReasons = transcodeReason . HasValue
? new List < TranscodeReason > { transcodeReason . Value }
: new List < TranscodeReason > { } ;
return new Tuple < PlayMethod ? , List < TranscodeReason > > ( null , transcodeReasons ) ;
}
}
string videoCodec = videoStream = = null ? null : videoStream . Codec ;
conditions = new List < ProfileCondition > ( ) ;
foreach ( CodecProfile i in profile . CodecProfiles )
{
if ( i . Type = = CodecType . Video & & i . ContainsAnyCodec ( videoCodec , container ) )
{
bool applyConditions = true ;
foreach ( ProfileCondition applyCondition in i . ApplyConditions )
{
if ( ! conditionProcessor . IsVideoConditionSatisfied ( applyCondition , width , height , bitDepth , videoBitrate , videoProfile , videoLevel , videoFramerate , packetLength , timestamp , isAnamorphic , isInterlaced , refFrames , numVideoStreams , numAudioStreams , videoCodecTag , isAvc ) )
{
//LogConditionFailure(profile, "VideoCodecProfile.ApplyConditions", applyCondition, mediaSource);
applyConditions = false ;
break ;
}
}
if ( applyConditions )
{
foreach ( ProfileCondition c in i . Conditions )
{
conditions . Add ( c ) ;
}
}
}
}
foreach ( ProfileCondition i in conditions )
{
if ( ! conditionProcessor . IsVideoConditionSatisfied ( i , width , height , bitDepth , videoBitrate , videoProfile , videoLevel , videoFramerate , packetLength , timestamp , isAnamorphic , isInterlaced , refFrames , numVideoStreams , numAudioStreams , videoCodecTag , isAvc ) )
{
LogConditionFailure ( profile , "VideoCodecProfile" , i , mediaSource ) ;
var transcodeReason = GetTranscodeReasonForFailedCondition ( i ) ;
var transcodeReasons = transcodeReason . HasValue
? new List < TranscodeReason > { transcodeReason . Value }
: new List < TranscodeReason > { } ;
return new Tuple < PlayMethod ? , List < TranscodeReason > > ( null , transcodeReasons ) ;
}
}
if ( audioStream ! = null )
{
string audioCodec = audioStream . Codec ;
conditions = new List < ProfileCondition > ( ) ;
bool? isSecondaryAudio = audioStream = = null ? null : mediaSource . IsSecondaryAudio ( audioStream ) ;
foreach ( CodecProfile i in profile . CodecProfiles )
{
if ( i . Type = = CodecType . VideoAudio & & i . ContainsAnyCodec ( audioCodec , container ) )
{
bool applyConditions = true ;
foreach ( ProfileCondition applyCondition in i . ApplyConditions )
{
if ( ! conditionProcessor . IsVideoAudioConditionSatisfied ( applyCondition , audioChannels , audioBitrate , audioSampleRate , audioBitDepth , audioProfile , isSecondaryAudio ) )
{
//LogConditionFailure(profile, "VideoAudioCodecProfile.ApplyConditions", applyCondition, mediaSource);
applyConditions = false ;
break ;
}
}
if ( applyConditions )
{
foreach ( ProfileCondition c in i . Conditions )
{
conditions . Add ( c ) ;
}
}
}
}
foreach ( ProfileCondition i in conditions )
{
if ( ! conditionProcessor . IsVideoAudioConditionSatisfied ( i , audioChannels , audioBitrate , audioSampleRate , audioBitDepth , audioProfile , isSecondaryAudio ) )
{
LogConditionFailure ( profile , "VideoAudioCodecProfile" , i , mediaSource ) ;
var transcodeReason = GetTranscodeReasonForFailedCondition ( i ) ;
var transcodeReasons = transcodeReason . HasValue
? new List < TranscodeReason > { transcodeReason . Value }
: new List < TranscodeReason > { } ;
return new Tuple < PlayMethod ? , List < TranscodeReason > > ( null , transcodeReasons ) ;
}
}
}
if ( isEligibleForDirectStream & & mediaSource . SupportsDirectStream )
{
return new Tuple < PlayMethod ? , List < TranscodeReason > > ( PlayMethod . DirectStream , new List < TranscodeReason > ( ) ) ;
}
return new Tuple < PlayMethod ? , List < TranscodeReason > > ( null , new List < TranscodeReason > { TranscodeReason . ContainerBitrateExceedsLimit } ) ;
}
private void LogConditionFailure ( DeviceProfile profile , string type , ProfileCondition condition , MediaSourceInfo mediaSource )
{
2018-12-13 13:18:25 +00:00
_logger . LogInformation ( "Profile: {0}, DirectPlay=false. Reason={1}.{2} Condition: {3}. ConditionValue: {4}. IsRequired: {5}. Path: {6}" ,
2018-12-27 23:27:57 +00:00
type ,
profile . Name ? ? "Unknown Profile" ,
condition . Property ,
condition . Condition ,
condition . Value ? ? string . Empty ,
condition . IsRequired ,
mediaSource . Path ? ? "Unknown path" ) ;
}
private ValueTuple < bool , TranscodeReason ? > IsEligibleForDirectPlay ( MediaSourceInfo item ,
long maxBitrate ,
MediaStream subtitleStream ,
VideoOptions options ,
PlayMethod playMethod )
{
if ( subtitleStream ! = null )
{
SubtitleProfile subtitleProfile = GetSubtitleProfile ( item , subtitleStream , options . Profile . SubtitleProfiles , playMethod , _transcoderSupport , item . Container , null ) ;
if ( subtitleProfile . Method ! = SubtitleDeliveryMethod . External & & subtitleProfile . Method ! = SubtitleDeliveryMethod . Embed )
{
2018-12-13 13:18:25 +00:00
_logger . LogInformation ( "Not eligible for {0} due to unsupported subtitles" , playMethod ) ;
2018-12-27 23:27:57 +00:00
return new ValueTuple < bool , TranscodeReason ? > ( false , TranscodeReason . SubtitleCodecNotSupported ) ;
}
}
var result = IsAudioEligibleForDirectPlay ( item , maxBitrate , playMethod ) ;
if ( result )
{
return new ValueTuple < bool , TranscodeReason ? > ( result , null ) ;
}
return new ValueTuple < bool , TranscodeReason ? > ( result , TranscodeReason . ContainerBitrateExceedsLimit ) ;
}
public static SubtitleProfile GetSubtitleProfile ( MediaSourceInfo mediaSource , MediaStream subtitleStream , SubtitleProfile [ ] subtitleProfiles , PlayMethod playMethod , ITranscoderSupport transcoderSupport , string outputContainer , string transcodingSubProtocol )
{
if ( ! subtitleStream . IsExternal & & ( playMethod ! = PlayMethod . Transcode | | ! string . Equals ( transcodingSubProtocol , "hls" , StringComparison . OrdinalIgnoreCase ) ) )
{
// Look for supported embedded subs of the same format
foreach ( SubtitleProfile profile in subtitleProfiles )
{
if ( ! profile . SupportsLanguage ( subtitleStream . Language ) )
{
continue ;
}
if ( profile . Method ! = SubtitleDeliveryMethod . Embed )
{
continue ;
}
if ( ! ContainerProfile . ContainsContainer ( profile . Container , outputContainer ) )
{
continue ;
}
if ( playMethod = = PlayMethod . Transcode & & ! IsSubtitleEmbedSupported ( subtitleStream , profile , transcodingSubProtocol , outputContainer ) )
{
continue ;
}
if ( subtitleStream . IsTextSubtitleStream = = MediaStream . IsTextFormat ( profile . Format ) & & StringHelper . EqualsIgnoreCase ( profile . Format , subtitleStream . Codec ) )
{
return profile ;
}
}
// Look for supported embedded subs of a convertible format
foreach ( SubtitleProfile profile in subtitleProfiles )
{
if ( ! profile . SupportsLanguage ( subtitleStream . Language ) )
{
continue ;
}
if ( profile . Method ! = SubtitleDeliveryMethod . Embed )
{
continue ;
}
if ( ! ContainerProfile . ContainsContainer ( profile . Container , outputContainer ) )
{
continue ;
}
if ( playMethod = = PlayMethod . Transcode & & ! IsSubtitleEmbedSupported ( subtitleStream , profile , transcodingSubProtocol , outputContainer ) )
{
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
} ;
}
private static bool IsSubtitleEmbedSupported ( MediaStream subtitleStream , SubtitleProfile subtitleProfile , string transcodingSubProtocol , string transcodingContainer )
{
if ( ! string . IsNullOrEmpty ( transcodingContainer ) )
{
var normalizedContainers = ContainerProfile . SplitValue ( transcodingContainer ) ;
if ( ContainerProfile . ContainsContainer ( normalizedContainers , "ts" ) )
{
return false ;
}
if ( ContainerProfile . ContainsContainer ( normalizedContainers , "mpegts" ) )
{
return false ;
}
if ( ContainerProfile . ContainsContainer ( normalizedContainers , "mp4" ) )
{
return false ;
}
if ( ContainerProfile . ContainsContainer ( normalizedContainers , "mkv" ) | |
ContainerProfile . ContainsContainer ( normalizedContainers , "matroska" ) )
{
return true ;
}
}
return false ;
}
private static SubtitleProfile GetExternalSubtitleProfile ( MediaSourceInfo mediaSource , MediaStream subtitleStream , SubtitleProfile [ ] subtitleProfiles , PlayMethod playMethod , ITranscoderSupport transcoderSupport , bool allowConversion )
{
foreach ( SubtitleProfile profile in subtitleProfiles )
{
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 ) )
{
bool requiresConversion = ! StringHelper . EqualsIgnoreCase ( subtitleStream . Codec , profile . Format ) ;
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 ;
}
private bool IsAudioEligibleForDirectPlay ( MediaSourceInfo item , long maxBitrate , PlayMethod playMethod )
{
// Don't restrict by bitrate if coming from an external domain
if ( item . IsRemote )
{
return true ;
}
var requestedMaxBitrate = maxBitrate > 0 ? maxBitrate : 1000000 ;
// If we don't know the bitrate, then force a transcode if requested max bitrate is under 40 mbps
var itemBitrate = item . Bitrate ? ?
40000000 ;
if ( itemBitrate > requestedMaxBitrate )
{
2018-12-13 13:18:25 +00:00
_logger . LogInformation ( "Bitrate exceeds " + playMethod + " limit: media bitrate: {0}, max bitrate: {1}" , itemBitrate . ToString ( CultureInfo . InvariantCulture ) , requestedMaxBitrate . ToString ( CultureInfo . InvariantCulture ) ) ;
2018-12-27 23:27:57 +00:00
return false ;
}
return true ;
}
private void ValidateInput ( VideoOptions options )
{
ValidateAudioInput ( options ) ;
if ( options . AudioStreamIndex . HasValue & & string . IsNullOrEmpty ( options . MediaSourceId ) )
{
throw new ArgumentException ( "MediaSourceId is required when a specific audio stream is requested" ) ;
}
if ( options . SubtitleStreamIndex . HasValue & & string . IsNullOrEmpty ( options . MediaSourceId ) )
{
throw new ArgumentException ( "MediaSourceId is required when a specific subtitle stream is requested" ) ;
}
}
private void ValidateAudioInput ( AudioOptions options )
{
if ( options . ItemId . Equals ( Guid . Empty ) )
{
throw new ArgumentException ( "ItemId is required" ) ;
}
if ( string . IsNullOrEmpty ( options . DeviceId ) )
{
throw new ArgumentException ( "DeviceId is required" ) ;
}
if ( options . Profile = = null )
{
throw new ArgumentException ( "Profile is required" ) ;
}
if ( options . MediaSources = = null )
{
throw new ArgumentException ( "MediaSources is required" ) ;
}
}
private void ApplyTranscodingConditions ( StreamInfo item , List < CodecProfile > codecProfiles )
{
foreach ( var profile in codecProfiles )
{
ApplyTranscodingConditions ( item , profile ) ;
}
}
private void ApplyTranscodingConditions ( StreamInfo item , CodecProfile codecProfile )
{
var codecs = ContainerProfile . SplitValue ( codecProfile . Codec ) ;
if ( codecs . Length = = 0 )
{
ApplyTranscodingConditions ( item , codecProfile . Conditions , null , true , true ) ;
return ;
}
var enableNonQualified = true ;
foreach ( var codec in codecs )
{
ApplyTranscodingConditions ( item , codecProfile . Conditions , codec , true , enableNonQualified ) ;
enableNonQualified = false ;
}
}
private void ApplyTranscodingConditions ( StreamInfo item , IEnumerable < ProfileCondition > conditions , string qualifier , bool enableQualifiedConditions , bool enableNonQualifiedConditions )
{
foreach ( ProfileCondition condition in conditions )
{
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 ;
}
int num ;
if ( int . TryParse ( value , NumberStyles . Any , CultureInfo . InvariantCulture , out num ) )
{
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 ) ;
}
}
break ;
}
case ProfileConditionValue . AudioChannels :
{
if ( string . IsNullOrEmpty ( qualifier ) )
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
}
else
{
if ( ! enableQualifiedConditions )
{
continue ;
}
}
int num ;
if ( int . TryParse ( value , NumberStyles . Any , CultureInfo . InvariantCulture , out num ) )
{
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 ) ) ;
}
}
break ;
}
case ProfileConditionValue . IsAvc :
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
bool isAvc ;
if ( bool . TryParse ( value , out isAvc ) )
{
if ( isAvc & & condition . Condition = = ProfileConditionType . Equals )
{
item . RequireAvc = true ;
}
else if ( ! isAvc & & condition . Condition = = ProfileConditionType . NotEquals )
{
item . RequireAvc = true ;
}
}
break ;
}
case ProfileConditionValue . IsAnamorphic :
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
bool isAnamorphic ;
if ( bool . TryParse ( value , out isAnamorphic ) )
{
if ( isAnamorphic & & condition . Condition = = ProfileConditionType . Equals )
{
item . RequireNonAnamorphic = true ;
}
else if ( ! isAnamorphic & & condition . Condition = = ProfileConditionType . NotEquals )
{
item . RequireNonAnamorphic = true ;
}
}
break ;
}
case ProfileConditionValue . IsInterlaced :
{
if ( string . IsNullOrEmpty ( qualifier ) )
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
}
else
{
if ( ! enableQualifiedConditions )
{
continue ;
}
}
bool isInterlaced ;
if ( bool . TryParse ( value , out isInterlaced ) )
{
if ( ! isInterlaced & & condition . Condition = = ProfileConditionType . Equals )
{
item . SetOption ( qualifier , "deinterlace" , "true" ) ;
}
else if ( isInterlaced & & condition . Condition = = ProfileConditionType . NotEquals )
{
item . SetOption ( qualifier , "deinterlace" , "true" ) ;
}
}
break ;
}
case ProfileConditionValue . AudioProfile :
case ProfileConditionValue . Has64BitOffsets :
case ProfileConditionValue . PacketLength :
case ProfileConditionValue . NumAudioStreams :
case ProfileConditionValue . NumVideoStreams :
case ProfileConditionValue . IsSecondaryAudio :
case ProfileConditionValue . VideoTimestamp :
{
// Not supported yet
break ;
}
case ProfileConditionValue . RefFrames :
{
if ( string . IsNullOrEmpty ( qualifier ) )
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
}
else
{
if ( ! enableQualifiedConditions )
{
continue ;
}
}
int num ;
if ( int . TryParse ( value , NumberStyles . Any , CultureInfo . InvariantCulture , out num ) )
{
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 ) ) ;
}
}
break ;
}
case ProfileConditionValue . VideoBitDepth :
{
if ( string . IsNullOrEmpty ( qualifier ) )
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
}
else
{
if ( ! enableQualifiedConditions )
{
continue ;
}
}
int num ;
if ( int . TryParse ( value , NumberStyles . Any , CultureInfo . InvariantCulture , out num ) )
{
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 ) ) ;
}
}
break ;
}
case ProfileConditionValue . VideoProfile :
{
if ( string . IsNullOrEmpty ( qualifier ) )
{
continue ;
}
if ( ! string . IsNullOrEmpty ( value ) )
{
// change from split by | to comma
// strip spaces to avoid having to encode
var values = value
. Split ( new [ ] { '|' } , StringSplitOptions . RemoveEmptyEntries ) ;
if ( condition . Condition = = ProfileConditionType . Equals | | condition . Condition = = ProfileConditionType . EqualsAny )
{
item . SetOption ( qualifier , "profile" , string . Join ( "," , values ) ) ;
}
}
break ;
}
case ProfileConditionValue . Height :
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
int num ;
if ( int . TryParse ( value , NumberStyles . Any , CultureInfo . InvariantCulture , out num ) )
{
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 ) ;
}
}
break ;
}
case ProfileConditionValue . VideoBitrate :
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
int num ;
if ( int . TryParse ( value , NumberStyles . Any , CultureInfo . InvariantCulture , out num ) )
{
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 ) ;
}
}
break ;
}
case ProfileConditionValue . VideoFramerate :
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
float num ;
if ( float . TryParse ( value , NumberStyles . Any , CultureInfo . InvariantCulture , out num ) )
{
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 ) ;
}
}
break ;
}
case ProfileConditionValue . VideoLevel :
{
if ( string . IsNullOrEmpty ( qualifier ) )
{
continue ;
}
int num ;
if ( int . TryParse ( value , NumberStyles . Any , CultureInfo . InvariantCulture , out num ) )
{
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 ) ) ;
}
}
break ;
}
case ProfileConditionValue . Width :
{
if ( ! enableNonQualifiedConditions )
{
continue ;
}
int num ;
if ( int . TryParse ( value , NumberStyles . Any , CultureInfo . InvariantCulture , out num ) )
{
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 ) ;
}
}
break ;
}
default :
break ;
}
}
}
private bool IsAudioDirectPlaySupported ( DirectPlayProfile profile , MediaSourceInfo item , MediaStream audioStream )
{
// Check container type
if ( ! profile . SupportsContainer ( item . Container ) )
{
return false ;
}
// Check audio codec
string audioCodec = audioStream = = null ? null : audioStream . Codec ;
if ( ! profile . SupportsAudioCodec ( audioCodec ) )
{
return false ;
}
return true ;
}
private bool IsVideoDirectPlaySupported ( DirectPlayProfile profile , MediaSourceInfo item , MediaStream videoStream , MediaStream audioStream )
{
// Check container type
if ( ! profile . SupportsContainer ( item . Container ) )
{
return false ;
}
// Check video codec
string videoCodec = videoStream = = null ? null : videoStream . Codec ;
if ( ! profile . SupportsVideoCodec ( videoCodec ) )
{
return false ;
}
// Check audio codec
if ( audioStream ! = null )
{
string audioCodec = audioStream = = null ? null : audioStream . Codec ;
if ( ! profile . SupportsAudioCodec ( audioCodec ) )
{
return false ;
}
}
return true ;
}
}
}