@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization ;
using System.IO ;
using System.Linq ;
using System.Threading ;
using MediaBrowser.Controller.Entities ;
using MediaBrowser.Model.Configuration ;
using MediaBrowser.Model.Dlna ;
@ -21,6 +22,8 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly IMediaEncoder _mediaEncoder ;
private readonly IFileSystem _fileSystem ;
private readonly ISubtitleEncoder _subtitleEncoder ;
// private readonly IApplicationPaths _appPaths;
// private readonly IAssemblyInfo _assemblyInfo;
public EncodingHelper ( IMediaEncoder mediaEncoder , IFileSystem fileSystem , ISubtitleEncoder subtitleEncoder )
{
@ -40,56 +43,55 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var hwType = encodingOptions . HardwareAccelerationType ;
if ( ! encodingOptions . EnableHardwareEncoding )
var codecMap = new Dictionary < string , string > ( StringComparer . OrdinalIgnoreCase )
{
hwType = null ;
}
if ( string . Equals ( hwType , "qsv" , StringComparison . OrdinalIgnoreCase ) | |
string . Equals ( hwType , "h264_qsv" , StringComparison . OrdinalIgnoreCase ) )
{
return GetAvailableEncoder ( "h264_qsv" , defaultEncoder ) ;
}
{ "qsv" , "h264_qsv" } ,
{ "h264_qsv" , "h264_qsv" } ,
{ "nvenc" , "h264_nvenc" } ,
{ "amf" , "h264_amf" } ,
{ "omx" , "h264_omx" } ,
{ "h264_v4l2m2m" , "h264_v4l2m2m" } ,
{ "mediacodec" , "h264_mediacodec" } ,
{ "vaapi" , "h264_vaapi" }
} ;
if ( string . Equals ( hwType , "nvenc" , StringComparison . OrdinalIgnoreCase ) )
{
return GetAvailableEncoder ( "h264_nvenc" , defaultEncoder ) ;
}
if ( string . Equals ( hwType , "amf" , StringComparison . OrdinalIgnoreCase ) )
{
return GetAvailableEncoder ( "h264_amf" , defaultEncoder ) ;
}
if ( string . Equals ( hwType , "omx" , StringComparison . OrdinalIgnoreCase ) )
if ( ! string . IsNullOrEmpty ( hwType )
& & encodingOptions . EnableHardwareEncoding & & codecMap . ContainsKey ( hwType ) )
{
return GetAvailableEncoder ( "h264_omx" , defaultEncoder ) ;
}
if ( string . Equals ( hwType , "h264_v4l2m2m" , StringComparison . OrdinalIgnoreCase ) )
if ( CheckVaapi ( state , hwType , encodingOptions ) )
{
return GetAvailableEncoder ( "h264_v4l2m2m" , defaultEncoder ) ;
}
if ( string . Equals ( hwType , "mediacodec" , StringComparison . OrdinalIgnoreCase ) )
var preferredEncoder = codecMap [ hwType ] ;
if ( _mediaEncoder . SupportsEncoder ( preferredEncoder ) )
{
return GetAvailableEncoder ( "h264_mediacodec" , defaultEncoder ) ;
return preferredEncoder ;
}
if ( string . Equals ( hwType , "vaapi" , StringComparison . OrdinalIgnoreCase ) & & ! string . IsNullOrEmpty ( encodingOptions . VaapiDevice ) )
{
if ( IsVaapiSupported ( state ) )
{
return GetAvailableEncoder ( "h264_vaapi" , defaultEncoder ) ;
}
}
}
// Avoid performing a second attempt when the first one
// hasn't tried hardware encoding anyway.
encodingOptions . EnableHardwareEncoding = false ;
return defaultEncoder ;
}
private string GetAvailableEncoder ( string preferredEncoder , string defaultEncoder )
private bool CheckVaapi ( EncodingJobInfo state , string hwType , EncodingOptions encodingOptions )
{
if ( _mediaEncoder . SupportsEncoder ( preferredEncoder ) )
if ( ! string . Equals ( hwType , "vaapi" , StringComparison . OrdinalIgnoreCase ) )
{
return preferredEncoder ;
// No vaapi requested, return OK.
return true ;
}
return defaultEncoder ;
if ( string . IsNullOrEmpty ( encodingOptions . VaapiDevice ) )
{
// No device specified, return OK.
return true ;
}
return IsVaapiSupported ( state ) ;
}
private bool IsVaapiSupported ( EncodingJobInfo state )
@ -340,7 +342,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public int GetVideoProfileScore ( string profile )
{
string[ ] list =
var list = new [ ]
{
"ConstrainedBaseline" ,
"Baseline" ,
@ -538,14 +540,54 @@ namespace MediaBrowser.Controller.MediaEncoding
? string . Empty
: string . Format ( ",setpts=PTS -{0}/TB" , seconds . ToString ( _usCulture ) ) ;
string fallbackFontParam = string . Empty ;
// TODO
// var fallbackFontPath = Path.Combine(_appPaths.ProgramDataPath, "fonts", "DroidSansFallback.ttf");
// string fallbackFontParam = string.Empty;
// if (!_fileSystem.FileExists(fallbackFontPath))
// {
// _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(fallbackFontPath));
// using (var stream = _assemblyInfo.GetManifestResourceStream(GetType(), GetType().Namespace + ".DroidSansFallback.ttf"))
// {
// using (var fileStream = _fileSystem.GetFileStream(fallbackFontPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
// {
// stream.CopyTo(fileStream);
// }
// }
// }
// fallbackFontParam = string.Format(":force_style='FontName=Droid Sans Fallback':fontsdir='{0}'", _mediaEncoder.EscapeSubtitleFilterPath(_fileSystem.GetDirectoryName(fallbackFontPath)));
if ( state . SubtitleStream . IsExternal )
{
var subtitlePath = state . SubtitleStream . Path ;
var charsetParam = string . Empty ;
if ( ! string . IsNullOrEmpty ( state . SubtitleStream . Language ) )
{
var charenc = _subtitleEncoder . GetSubtitleFileCharacterSet ( subtitlePath , state . SubtitleStream . Language , state . MediaSource . Protocol , CancellationToken . None ) . Result ;
if ( ! string . IsNullOrEmpty ( charenc ) )
{
charsetParam = ":charenc=" + charenc ;
}
}
// TODO: Perhaps also use original_size=1920x800 ??
return string . Format ( "subtitles=filename='{0}'{1}{2}{3}" ,
_mediaEncoder . EscapeSubtitleFilterPath ( subtitlePath ) ,
charsetParam ,
// fallbackFontParam,
setPtsParam ) ;
}
var mediaPath = state . MediaPath ? ? string . Empty ;
return string . Format ( "subtitles='{0}:si={1}'{2}{3}" ,
return string . Format ( "subtitles='{0}:si={1}'{2} ",
_mediaEncoder . EscapeSubtitleFilterPath ( mediaPath ) ,
state . InternalSubtitleStreamOffset . ToString ( _usCulture ) ,
fallbackFontParam ,
// fallbackFontParam ,
setPtsParam ) ;
}
@ -718,14 +760,18 @@ namespace MediaBrowser.Controller.MediaEncoding
var request = state . BaseRequest ;
var profile = state . GetRequestedProfiles ( targetVideoCodec ) . FirstOrDefault ( ) ;
if ( string . Equals ( videoEncoder , "h264_vaapi" , StringComparison . OrdinalIgnoreCase ) )
// vaapi does not support Baseline profile, force Constrained Baseline in this case,
// which is compatible (and ugly)
if ( string . Equals ( videoEncoder , "h264_vaapi" , StringComparison . OrdinalIgnoreCase ) & &
profile ! = null & & profile . ToLower ( ) . Contains ( "baseline" ) )
{
param + = " -profile:v 578" ;
profile = "constrained_baseline ";
}
else if ( ! string . IsNullOrEmpty ( profile ) )
if ( ! string . IsNullOrEmpty ( profile ) )
{
if ( ! string . Equals ( videoEncoder , "h264_omx" , StringComparison . OrdinalIgnoreCase ) & &
! string . Equals ( videoEncoder , "h264_vaapi" , StringComparison . OrdinalIgnoreCase ) & &
! string . Equals ( videoEncoder , "h264_v4l2m2m" , StringComparison . OrdinalIgnoreCase ) )
{
// not supported by h264_omx
@ -1039,26 +1085,18 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var bitrate = request . VideoBitRate ;
if ( videoStream ! = null )
{
var isUpscaling = request . Height . HasValue & & videoStream . Height . HasValue & &
request . Height . Value > videoStream . Height . Value & & request . Width . HasValue & & videoStream . Width . HasValue & &
request . Width . Value > videoStream . Width . Value ;
// Don't allow bitrate increases unless upscaling
if ( ! isUpscaling )
// If specific values were requested, then force the caller to supply a bitrate as well
if ( request . Height . HasValue & & request . Width . HasValue )
{
if ( bitrate . HasValue & & videoStream . BitRate . HasValue )
{
bitrate = GetMinBitrate ( videoStream . BitRate . Value , bitrate . Value ) ;
}
}
return bitrate ;
}
if ( videoStream ! = null )
{
if ( bitrate . HasValue )
{
var inputVideoCodec = videoStream = = null ? null : videoStream . Codec ;
bitrate = ResolutionNormalizer . ScaleBitrate ( bitrate . Value , inputVideoCodec , outputVideoCodec ) ;
var inputVideoCodec = videoStream . Codec ;
bitrate = ScaleBitrate ( bitrate . Value , inputVideoCodec , outputVideoCodec ) ;
// If a max bitrate was requested, don't let the scaled bitrate exceed it
if ( request . VideoBitRate . HasValue )
@ -1066,24 +1104,48 @@ namespace MediaBrowser.Controller.MediaEncoding
bitrate = Math . Min ( bitrate . Value , request . VideoBitRate . Value ) ;
}
}
}
return bitrate ;
}
private int GetMinBitrate ( int sourceBitrate , int requestedBitrate )
private static double GetVideoBitrateScaleFactor ( string codec )
{
if ( sourceBitrate < = 2000000 )
if ( StringHelper . EqualsIgnoreCase ( codec , "h265" ) | |
StringHelper . EqualsIgnoreCase ( codec , "hevc" ) | |
StringHelper . EqualsIgnoreCase ( codec , "vp9" ) )
{
sourceBitrate = Convert . ToInt32 ( sourceBitrate * 2.5 ) ;
return . 5 ;
}
return 1 ;
}
else if ( sourceBitrate < = 3000000 )
private static int ScaleBitrate ( int bitrate , string inputVideoCodec , string outputVideoCodec )
{
sourceBitrate = Convert . ToInt32 ( sourceBitrate * 2 ) ;
var inputScaleFactor = GetVideoBitrateScaleFactor ( inputVideoCodec ) ;
var outputScaleFactor = GetVideoBitrateScaleFactor ( outputVideoCodec ) ;
var scaleFactor = outputScaleFactor / inputScaleFactor ;
if ( bitrate < = 500000 )
{
scaleFactor = Math . Max ( scaleFactor , 4 ) ;
}
else if ( bitrate < = 1000000 )
{
scaleFactor = Math . Max ( scaleFactor , 3 ) ;
}
else if ( bitrate < = 2000000 )
{
scaleFactor = Math . Max ( scaleFactor , 2.5 ) ;
}
else if ( bitrate < = 3000000 )
{
scaleFactor = Math . Max ( scaleFactor , 2 ) ;
}
var bitrate = Math . Min ( sourceBitrate , requestedBitrate ) ;
var newBitrate = scaleFactor * bitrate ;
return bitrate ;
return Convert. ToInt32 ( newBitrate ) ;
}
public int? GetAudioBitrateParam ( BaseEncodingJobOptions request , MediaStream audioStream )
@ -1405,7 +1467,7 @@ namespace MediaBrowser.Controller.MediaEncoding
videoSizeParam ) ;
}
private Tuple< int? , int? > GetFixedOutputSize ( int? videoWidth ,
private Value Tuple< int? , int? > GetFixedOutputSize ( int? videoWidth ,
int? videoHeight ,
int? requestedWidth ,
int? requestedHeight ,
@ -1414,11 +1476,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if ( ! videoWidth . HasValue & & ! requestedWidth . HasValue )
{
return new Tuple< int? , int? > ( null , null ) ;
return new Value Tuple< int? , int? > ( null , null ) ;
}
if ( ! videoHeight . HasValue & & ! requestedHeight . HasValue )
{
return new Tuple< int? , int? > ( null , null ) ;
return new Value Tuple< int? , int? > ( null , null ) ;
}
decimal inputWidth = Convert . ToDecimal ( videoWidth ? ? requestedWidth ) ;
@ -1438,7 +1500,7 @@ namespace MediaBrowser.Controller.MediaEncoding
outputWidth = 2 * Math . Truncate ( outputWidth / 2 ) ;
outputHeight = 2 * Math . Truncate ( outputHeight / 2 ) ;
return new Tuple< int? , int? > ( Convert . ToInt32 ( outputWidth ) , Convert . ToInt32 ( outputHeight ) ) ;
return new Value Tuple< int? , int? > ( Convert . ToInt32 ( outputWidth ) , Convert . ToInt32 ( outputHeight ) ) ;
}
public List < string > GetScalingFilters ( int? videoWidth ,
@ -1669,7 +1731,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var inputHeight = videoStream = = null ? null : videoStream . Height ;
var threeDFormat = state . MediaSource . Video3DFormat ;
var videoDecoder = Get VideoDecoder( state , options ) ;
var videoDecoder = this . Get HardwareAccelerated VideoDecoder( state , options ) ;
filters . AddRange ( GetScalingFilters ( inputWidth , inputHeight , threeDFormat , videoDecoder , outputVideoCodec , request . Width , request . Height , request . MaxWidth , request . MaxHeight ) ) ;
@ -1851,7 +1913,7 @@ namespace MediaBrowser.Controller.MediaEncoding
inputModifier + = " -fflags " + string . Join ( "" , flags . ToArray ( ) ) ;
}
var videoDecoder = Get VideoDecoder( state , encodingOptions ) ;
var videoDecoder = this . Get HardwareAccelerated VideoDecoder( state , encodingOptions ) ;
if ( ! string . IsNullOrEmpty ( videoDecoder ) )
{
inputModifier + = " " + videoDecoder ;
@ -1893,7 +1955,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if ( state . MediaSource . RequiresLooping )
{
inputModifier + = " -stream_loop -1 ";
inputModifier + = " -stream_loop -1 -reconnect_at_eof 1 -reconnect_streamed 1 -reconnect_delay_max 2 ";
}
return inputModifier ;
@ -1989,7 +2051,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if ( string . IsNullOrEmpty ( requestedUrl ) )
{
requestedUrl = "test." + videoRequest . Output Container;
requestedUrl = "test." + videoRequest . Container;
}
videoRequest . VideoCodec = InferVideoCodec ( requestedUrl ) ;
@ -2015,6 +2077,49 @@ namespace MediaBrowser.Controller.MediaEncoding
}
state . MediaSource = mediaSource ;
var request = state . BaseRequest ;
if ( ! string . IsNullOrWhiteSpace ( request . AudioCodec ) )
{
var supportedAudioCodecsList = request . AudioCodec . Split ( new [ ] { ',' } , StringSplitOptions . RemoveEmptyEntries ) . ToList ( ) ;
ShiftAudioCodecsIfNeeded ( supportedAudioCodecsList , state . AudioStream ) ;
state . SupportedAudioCodecs = supportedAudioCodecsList . ToArray ( ) ;
request . AudioCodec = state . SupportedAudioCodecs . FirstOrDefault ( i = > _mediaEncoder . CanEncodeToAudioCodec ( i ) )
? ? state . SupportedAudioCodecs . FirstOrDefault ( ) ;
}
}
private void ShiftAudioCodecsIfNeeded ( List < string > audioCodecs , MediaStream audioStream )
{
// Nothing to do here
if ( audioCodecs . Count < 2 )
{
return ;
}
var inputChannels = audioStream = = null ? 6 : audioStream . Channels ? ? 6 ;
if ( inputChannels > = 6 )
{
return ;
}
// Transcoding to 2ch ac3 almost always causes a playback failure
// Keep it in the supported codecs list, but shift it to the end of the list so that if transcoding happens, another codec is used
var shiftAudioCodecs = new [ ] { "ac3" , "eac3" } ;
if ( audioCodecs . All ( i = > shiftAudioCodecs . Contains ( i , StringComparer . OrdinalIgnoreCase ) ) )
{
return ;
}
while ( shiftAudioCodecs . Contains ( audioCodecs [ 0 ] , StringComparer . OrdinalIgnoreCase ) )
{
var removed = shiftAudioCodecs [ 0 ] ;
audioCodecs . RemoveAt ( 0 ) ;
audioCodecs . Add ( removed ) ;
}
}
private void NormalizeSubtitleEmbed ( EncodingJobInfo state )
@ -2035,17 +2140,17 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Gets the name of the output video codec
/// </summary>
protected string Get VideoDecoder( EncodingJobInfo state , EncodingOptions encodingOptions )
protected string Get HardwareAccelerated VideoDecoder( EncodingJobInfo state , EncodingOptions encodingOptions )
{
if ( string . Equals ( state . OutputVideoCodec , "copy" , StringComparison . OrdinalIgnoreCase ) )
{
return null ;
}
return Get VideoDecoder( state . MediaSource . VideoType ? ? VideoType . VideoFile , state . VideoStream , encodingOptions ) ;
return this . Get HardwareAccelerated VideoDecoder( state . MediaSource . VideoType ? ? VideoType . VideoFile , state . VideoStream , encodingOptions ) ;
}
public string Get VideoDecoder( VideoType videoType , MediaStream videoStream , EncodingOptions encodingOptions )
public string Get HardwareAccelerated VideoDecoder( VideoType videoType , MediaStream videoStream , EncodingOptions encodingOptions )
{
// Only use alternative encoders for video files.
// When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
@ -2070,6 +2175,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// qsv decoder does not support 10-bit input
if ( ( videoStream . BitDepth ? ? 8 ) > 8 )
{
encodingOptions . HardwareDecodingCodecs = Array . Empty < string > ( ) ;
return null ;
}
return "-c:v h264_qsv " ;
@ -2204,6 +2310,11 @@ namespace MediaBrowser.Controller.MediaEncoding
else if ( string . Equals ( encodingOptions . HardwareAccelerationType , "amf" , StringComparison . OrdinalIgnoreCase ) )
{
if ( Environment . OSVersion . Platform = = PlatformID . Win32NT )
{
return "-hwaccel dxva2" ;
}
switch ( videoStream . Codec . ToLower ( ) )
{
case "avc" :
@ -2223,6 +2334,9 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
// Avoid a second attempt if no hardware acceleration is being used
encodingOptions . HardwareDecodingCodecs = Array . Empty < string > ( ) ;
// leave blank so ffmpeg will decide
return null ;
}