2019-01-13 20:02:23 +00:00
using System ;
2018-12-14 09:40:55 +00:00
using System.Collections.Generic ;
2019-09-27 23:29:54 +00:00
using System.Diagnostics ;
2020-06-01 05:10:15 +00:00
using System.Globalization ;
2019-01-02 00:23:49 +00:00
using System.Linq ;
2019-09-27 23:29:54 +00:00
using System.Text ;
2019-01-02 09:48:10 +00:00
using System.Text.RegularExpressions ;
2018-12-14 23:48:06 +00:00
using Microsoft.Extensions.Logging ;
2018-12-14 09:40:55 +00:00
namespace MediaBrowser.MediaEncoding.Encoder
{
public class EncoderValidator
{
2019-09-27 23:29:54 +00:00
private const string DefaultEncoderPath = "ffmpeg" ;
2018-12-14 09:40:55 +00:00
2020-06-01 05:10:15 +00:00
private static readonly string [ ] _requiredDecoders = new [ ]
2018-12-14 09:40:55 +00:00
{
2019-09-27 23:29:54 +00:00
"mpeg2video" ,
"h264_qsv" ,
"hevc_qsv" ,
"mpeg2_qsv" ,
2019-11-25 22:09:23 +00:00
"mpeg2_mmal" ,
"mpeg4_mmal" ,
2019-09-27 23:29:54 +00:00
"vc1_qsv" ,
2019-11-25 22:09:23 +00:00
"vc1_mmal" ,
2019-09-27 23:29:54 +00:00
"h264_cuvid" ,
"hevc_cuvid" ,
"dts" ,
"ac3" ,
"aac" ,
"mp3" ,
"h264" ,
2019-11-25 22:09:23 +00:00
"h264_mmal" ,
2019-09-27 23:29:54 +00:00
"hevc"
} ;
2018-12-14 09:40:55 +00:00
2020-06-01 05:10:15 +00:00
private static readonly string [ ] _requiredEncoders = new [ ]
2019-09-27 23:29:54 +00:00
{
"libx264" ,
"libx265" ,
"mpeg4" ,
"msmpeg4" ,
"libvpx" ,
"libvpx-vp9" ,
"aac" ,
2020-03-30 07:53:49 +00:00
"libfdk_aac" ,
2019-09-27 23:29:54 +00:00
"libmp3lame" ,
"libopus" ,
"libvorbis" ,
"srt" ,
"h264_nvenc" ,
"hevc_nvenc" ,
"h264_qsv" ,
"hevc_qsv" ,
"h264_omx" ,
"hevc_omx" ,
"h264_vaapi" ,
"hevc_vaapi" ,
"h264_v4l2m2m" ,
2020-01-10 17:36:25 +00:00
"ac3" ,
2020-01-15 10:45:28 +00:00
"h264_amf" ,
"hevc_amf"
2019-09-27 23:29:54 +00:00
} ;
2020-06-01 05:10:15 +00:00
// These are the library versions that corresponds to our minimum ffmpeg version 4.x according to the version table below
private static readonly IReadOnlyDictionary < string , double > _ffmpegMinimumLibraryVersions = new Dictionary < string , double >
{
{ "libavutil" , 56.14 } ,
{ "libavcodec" , 58.18 } ,
{ "libavformat" , 58.12 } ,
{ "libavdevice" , 58.3 } ,
{ "libavfilter" , 7.16 } ,
{ "libswscale" , 5.1 } ,
{ "libswresample" , 3.1 } ,
{ "libpostproc" , 55.1 }
} ;
2019-09-27 23:29:54 +00:00
// This lookup table is to be maintained with the following command line:
// $ ffmpeg -version | perl -ne ' print "$1=$2.$3," if /^(lib\w+)\s+(\d+)\.\s*(\d+)/'
private static readonly IReadOnlyDictionary < string , Version > _ffmpegVersionMap = new Dictionary < string , Version >
2018-12-14 09:40:55 +00:00
{
2019-09-27 23:29:54 +00:00
{ "libavutil=56.31,libavcodec=58.54,libavformat=58.29,libavdevice=58.8,libavfilter=7.57,libswscale=5.5,libswresample=3.5,libpostproc=55.5," , new Version ( 4 , 2 ) } ,
{ "libavutil=56.22,libavcodec=58.35,libavformat=58.20,libavdevice=58.5,libavfilter=7.40,libswscale=5.3,libswresample=3.3,libpostproc=55.3," , new Version ( 4 , 1 ) } ,
{ "libavutil=56.14,libavcodec=58.18,libavformat=58.12,libavdevice=58.3,libavfilter=7.16,libswscale=5.1,libswresample=3.1,libpostproc=55.1," , new Version ( 4 , 0 ) } ,
{ "libavutil=55.78,libavcodec=57.107,libavformat=57.83,libavdevice=57.10,libavfilter=6.107,libswscale=4.8,libswresample=2.9,libpostproc=54.7," , new Version ( 3 , 4 ) } ,
{ "libavutil=55.58,libavcodec=57.89,libavformat=57.71,libavdevice=57.6,libavfilter=6.82,libswscale=4.6,libswresample=2.7,libpostproc=54.5," , new Version ( 3 , 3 ) } ,
{ "libavutil=55.34,libavcodec=57.64,libavformat=57.56,libavdevice=57.1,libavfilter=6.65,libswscale=4.2,libswresample=2.3,libpostproc=54.1," , new Version ( 3 , 2 ) } ,
{ "libavutil=54.31,libavcodec=56.60,libavformat=56.40,libavdevice=56.4,libavfilter=5.40,libswscale=3.1,libswresample=1.2,libpostproc=53.3," , new Version ( 2 , 8 ) }
} ;
2018-12-14 09:40:55 +00:00
2019-09-27 23:29:54 +00:00
private readonly ILogger _logger ;
2018-12-14 09:40:55 +00:00
2019-09-27 23:29:54 +00:00
private readonly string _encoderPath ;
2018-12-14 09:40:55 +00:00
2019-09-27 23:29:54 +00:00
public EncoderValidator ( ILogger logger , string encoderPath = DefaultEncoderPath )
{
_logger = logger ;
_encoderPath = encoderPath ;
2018-12-14 09:40:55 +00:00
}
2019-09-27 23:29:54 +00:00
public static Version MinVersion { get ; } = new Version ( 4 , 0 ) ;
public static Version MaxVersion { get ; } = null ;
public bool ValidateVersion ( )
2018-12-14 09:40:55 +00:00
{
2019-01-02 00:23:49 +00:00
string output = null ;
2018-12-14 09:40:55 +00:00
try
{
2019-09-27 23:29:54 +00:00
output = GetProcessOutput ( _encoderPath , "-version" ) ;
2018-12-14 09:40:55 +00:00
}
catch ( Exception ex )
{
2019-09-27 23:29:54 +00:00
_logger . LogError ( ex , "Error validating encoder" ) ;
2018-12-14 09:40:55 +00:00
}
if ( string . IsNullOrWhiteSpace ( output ) )
{
2019-09-27 23:29:54 +00:00
_logger . LogError ( "FFmpeg validation: The process returned no result" ) ;
2018-12-14 09:40:55 +00:00
return false ;
}
2019-01-05 21:40:33 +00:00
_logger . LogDebug ( "ffmpeg output: {Output}" , output ) ;
2018-12-14 09:40:55 +00:00
2019-09-27 23:29:54 +00:00
return ValidateVersionInternal ( output ) ;
}
internal bool ValidateVersionInternal ( string versionOutput )
{
if ( versionOutput . IndexOf ( "Libav developers" , StringComparison . OrdinalIgnoreCase ) ! = - 1 )
2018-12-14 09:40:55 +00:00
{
2019-09-27 23:29:54 +00:00
_logger . LogError ( "FFmpeg validation: avconv instead of ffmpeg is not supported" ) ;
2018-12-14 09:40:55 +00:00
return false ;
}
2019-02-14 22:01:09 +00:00
// Work out what the version under test is
2019-09-27 23:29:54 +00:00
var version = GetFFmpegVersion ( versionOutput ) ;
2019-02-14 22:01:09 +00:00
2020-06-01 05:10:15 +00:00
_logger . LogInformation ( "Found ffmpeg version {Version}" , version ! = null ? version . ToString ( ) : "unknown" ) ;
2019-02-15 23:51:22 +00:00
2019-09-29 11:41:24 +00:00
if ( version = = null )
2019-09-27 23:29:54 +00:00
{
2020-06-01 05:10:15 +00:00
if ( MaxVersion ! = null ) // Version is unknown
2019-02-14 22:01:09 +00:00
{
2019-09-29 11:41:24 +00:00
if ( MinVersion = = MaxVersion )
{
2020-06-01 05:10:15 +00:00
_logger . LogWarning ( "FFmpeg validation: We recommend version {MinVersion}" , MinVersion ) ;
2019-09-29 11:41:24 +00:00
}
else
{
2020-06-01 05:10:15 +00:00
_logger . LogWarning ( "FFmpeg validation: We recommend a minimum of {MinVersion} and maximum of {MaxVersion}" , MinVersion , MaxVersion ) ;
2019-09-29 11:41:24 +00:00
}
2019-02-14 22:01:09 +00:00
}
2020-06-01 05:10:15 +00:00
else
{
_logger . LogWarning ( "FFmpeg validation: We recommend minimum version {MinVersion}" , MinVersion ) ;
}
2019-09-27 23:29:54 +00:00
return false ;
}
2020-06-01 05:10:15 +00:00
else if ( version < MinVersion ) // Version is below what we recommend
2019-09-27 23:29:54 +00:00
{
2020-06-01 05:10:15 +00:00
_logger . LogWarning ( "FFmpeg validation: The minimum recommended version is {MinVersion}" , MinVersion ) ;
2019-09-27 23:29:54 +00:00
return false ;
}
else if ( MaxVersion ! = null & & version > MaxVersion ) // Version is above what we recommend
{
2020-06-01 05:10:15 +00:00
_logger . LogWarning ( "FFmpeg validation: The maximum recommended version is {MaxVersion}" , MaxVersion ) ;
2019-09-27 23:29:54 +00:00
return false ;
2019-02-14 22:01:09 +00:00
}
2019-09-27 23:29:54 +00:00
return true ;
2019-02-14 22:01:09 +00:00
}
2019-09-27 23:29:54 +00:00
public IEnumerable < string > GetDecoders ( ) = > GetCodecs ( Codec . Decoder ) ;
public IEnumerable < string > GetEncoders ( ) = > GetCodecs ( Codec . Encoder ) ;
2019-02-14 22:01:09 +00:00
/// <summary>
/// Using the output from "ffmpeg -version" work out the FFmpeg version.
/// For pre-built binaries the first line should contain a string like "ffmpeg version x.y", which is easy
2020-06-01 05:10:15 +00:00
/// to parse. If this is not available, then we try to match known library versions to FFmpeg versions.
/// If that fails then we test the libraries to determine if they're newer than our minimum versions.
2019-02-14 22:01:09 +00:00
/// </summary>
/// <param name="output"></param>
/// <returns></returns>
2020-06-01 05:10:15 +00:00
internal Version GetFFmpegVersion ( string output )
2019-02-14 22:01:09 +00:00
{
// For pre-built binaries the FFmpeg version should be mentioned at the very start of the output
2019-09-29 11:41:24 +00:00
var match = Regex . Match ( output , @"^ffmpeg version n?((?:\d+\.?)+)" ) ;
2019-02-14 22:01:09 +00:00
if ( match . Success )
{
2019-02-27 18:20:48 +00:00
return new Version ( match . Groups [ 1 ] . Value ) ;
2019-02-14 22:01:09 +00:00
}
else
{
2020-06-01 05:10:15 +00:00
if ( ! TryGetFFmpegLibraryVersions ( output , out string versionString , out IReadOnlyDictionary < string , double > versionMap ) )
{
_logger . LogError ( "No ffmpeg library versions found" ) ;
return null ;
}
// First try to lookup the full version string
if ( _ffmpegVersionMap . TryGetValue ( versionString , out Version version ) )
{
return version ;
}
// Then try to test for minimum library versions
return TestMinimumFFmpegLibraryVersions ( versionMap ) ;
}
}
private Version TestMinimumFFmpegLibraryVersions ( IReadOnlyDictionary < string , double > versionMap )
{
var allVersionsValidated = true ;
2019-02-14 22:01:09 +00:00
2020-06-01 05:10:15 +00:00
foreach ( var minimumVersion in _ffmpegMinimumLibraryVersions )
{
if ( versionMap . TryGetValue ( minimumVersion . Key , out var foundVersion ) )
{
if ( foundVersion > = minimumVersion . Value )
{
_logger . LogInformation ( "Found {Library} version {FoundVersion} ({MinimumVersion})" , minimumVersion . Key , foundVersion , minimumVersion . Value ) ;
}
else
{
_logger . LogWarning ( "Found {Library} version {FoundVersion} lower than recommended version {MinimumVersion}" , minimumVersion . Key , foundVersion , minimumVersion . Value ) ;
allVersionsValidated = false ;
}
}
else
{
_logger . LogError ( "{Library} version not found" , minimumVersion . Key ) ;
allVersionsValidated = false ;
}
2019-02-14 22:01:09 +00:00
}
2020-06-01 05:10:15 +00:00
return allVersionsValidated ? MinVersion : null ;
2019-02-14 22:01:09 +00:00
}
/// <summary>
/// Grabs the library names and major.minor version numbers from the 'ffmpeg -version' output
/// </summary>
/// <param name="output"></param>
2020-06-01 05:10:15 +00:00
/// <param name="versionString"></param>
/// <param name="versionMap"></param>
2019-02-14 22:01:09 +00:00
/// <returns></returns>
2020-06-01 05:10:15 +00:00
private static bool TryGetFFmpegLibraryVersions ( string output , out string versionString , out IReadOnlyDictionary < string , double > versionMap )
2019-02-14 22:01:09 +00:00
{
2020-06-01 05:10:15 +00:00
var sb = new StringBuilder ( 144 ) ;
var map = new Dictionary < string , double > ( ) ;
foreach ( Match match in Regex . Matches (
2019-09-27 23:29:54 +00:00
output ,
@"((?<name>lib\w+)\s+(?<major>\d+)\.\s*(?<minor>\d+))" ,
RegexOptions . Multiline ) )
2019-02-14 22:01:09 +00:00
{
2020-06-01 05:10:15 +00:00
sb . Append ( match . Groups [ "name" ] )
2019-09-27 23:29:54 +00:00
. Append ( '=' )
2020-06-01 05:10:15 +00:00
. Append ( match . Groups [ "major" ] )
2019-09-27 23:29:54 +00:00
. Append ( '.' )
2020-06-01 05:10:15 +00:00
. Append ( match . Groups [ "minor" ] )
2019-09-27 23:29:54 +00:00
. Append ( ',' ) ;
2020-06-01 05:10:15 +00:00
var str = $"{match.Groups[" major "]}.{match.Groups[" minor "]}" ;
var versionNumber = double . Parse ( str , NumberStyles . AllowDecimalPoint , CultureInfo . InvariantCulture ) ;
map . Add ( match . Groups [ "name" ] . Value , versionNumber ) ;
2018-12-14 09:40:55 +00:00
}
2020-06-01 05:10:15 +00:00
versionString = sb . ToString ( ) ;
versionMap = map as IReadOnlyDictionary < string , double > ;
return sb . Length > 0 ;
2018-12-14 09:40:55 +00:00
}
2019-01-02 09:48:10 +00:00
private enum Codec
{
Encoder ,
Decoder
}
2019-09-27 23:29:54 +00:00
private IEnumerable < string > GetCodecs ( Codec codec )
2019-01-02 09:48:10 +00:00
{
string codecstr = codec = = Codec . Encoder ? "encoders" : "decoders" ;
string output = null ;
try
{
2019-09-27 23:29:54 +00:00
output = GetProcessOutput ( _encoderPath , "-" + codecstr ) ;
2019-01-02 09:48:10 +00:00
}
catch ( Exception ex )
{
_logger . LogError ( ex , "Error detecting available {Codec}" , codecstr ) ;
}
if ( string . IsNullOrWhiteSpace ( output ) )
{
return Enumerable . Empty < string > ( ) ;
}
2020-06-01 05:10:15 +00:00
var required = codec = = Codec . Encoder ? _requiredEncoders : _requiredDecoders ;
2019-01-02 09:48:10 +00:00
2019-01-02 10:32:35 +00:00
var found = Regex
. Matches ( output , @"^\s\S{6}\s(?<codec>[\w|-]+)\s+.+$" , RegexOptions . Multiline )
. Cast < Match > ( )
2019-01-02 09:48:10 +00:00
. Select ( x = > x . Groups [ "codec" ] . Value )
. Where ( x = > required . Contains ( x ) ) ;
_logger . LogInformation ( "Available {Codec}: {Codecs}" , codecstr , found ) ;
2019-01-02 00:01:36 +00:00
2018-12-14 09:40:55 +00:00
return found ;
}
private string GetProcessOutput ( string path , string arguments )
{
2019-09-27 23:29:54 +00:00
using ( var process = new Process ( )
2018-12-14 09:40:55 +00:00
{
2019-09-27 23:29:54 +00:00
StartInfo = new ProcessStartInfo ( path , arguments )
{
CreateNoWindow = true ,
UseShellExecute = false ,
WindowStyle = ProcessWindowStyle . Hidden ,
ErrorDialog = false ,
RedirectStandardOutput = true ,
// ffmpeg uses stderr to log info, don't show this
RedirectStandardError = true
}
} )
2018-12-14 09:40:55 +00:00
{
2019-09-27 23:29:54 +00:00
_logger . LogDebug ( "Running {Path} {Arguments}" , path , arguments ) ;
2018-12-14 09:40:55 +00:00
process . Start ( ) ;
2019-09-27 23:29:54 +00:00
return process . StandardOutput . ReadToEnd ( ) ;
2018-12-14 09:40:55 +00:00
}
}
}
}