diff --git a/ffmpeg/_build.py b/ffmpeg/_build.py new file mode 100644 index 0000000..f6a59de --- /dev/null +++ b/ffmpeg/_build.py @@ -0,0 +1,306 @@ +""" +Extract details about the ffmpeg build. +""" + +import sys +import re +import subprocess +import json +import logging +import argparse + +logger = logging.getLogger(__name__) + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument( + '--ffmpeg', type=argparse.FileType('r'), + help='The path to the ffmpeg execuatble') + + +VERSION_RE = re.compile(r' version (?P[^ ]+) ') + +MUXER_RE = re.compile( + r'^ (?P[D ])(?P[E ]) ' + r'(?P[^ ]+) +(?P.+)$', + re.M) +MUXER_FLAGS = dict(demuxing='D', muxing='E') + +CODEC_RE = re.compile( + r'^ (?P[D.])(?P[E.])' + r'(?P[VAS.])(?P[I.])' + r'(?P[L.])(?P[S.]) ' + r'(?P[^ ]+) +(?P.+)$', + re.M) +CODEC_FLAGS = dict( + decoding='D', encoding='E', + stream=dict(video='V', audio='A', subtitle='S'), + intra_frame='I', lossy='L', lossless='S') +CODEC_DESCRIPTION_RE = re.compile( + r'^(?P.+?) \((de|en)coders: [^)]+ \)') +CODEC_CODERS_RE = re.compile( + r' \((?P(de|en)coders): (?P[^)]+) \)') + +FILTER_RE = re.compile( + r'^ (?P[T.])(?P[S.])(?P[C.]) ' + r'(?P[^ ]+) +(?P[^ ]+) +(?P.+)$', + re.M) +FILTER_FLAGS = dict(timeline='T', slice='S', command='C') + +PIX_FMT_RE = re.compile( + r'^(?P[I.])(?P[O.])(?P[H.])' + r'(?P[P.])(?P[B.]) ' + r'(?P[^ ]+) +(?P[0-9]+) +(?P[0-9]+)$', + re.M) +PIX_FMT_FLAGS = dict( + input='I', output='O', accelerated='H', palleted='P', bitstream='B') +PIX_FMT_INT_FIELDS = {'components', 'bits'} + + +def _run(args): + """ + Run the command and return stdout but only print stderr on failure. + """ + process = subprocess.Popen( + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + if process.returncode != 0: + logger.error(stderr.decode()) + raise subprocess.CalledProcessError( + process.returncode, process.args, output=stdout, stderr=stderr) + return stdout.decode() + +def _get_line_fields( + stdout, header_lines, line_re, flags={}, int_fields=set()): + """ + Extract field values from a line using the regular expression. + """ + non_fields = set(flags).union({'name'}) + lines = stdout.split('\n', header_lines)[header_lines] + data = {} + for match in line_re.finditer(lines): + groupdict = match.groupdict() + + data[match.group('name')] = fields = { + key: key in int_fields and int(value) or value + for key, value in groupdict.items() + if key not in non_fields} + + if flags: + fields['flags'] = {} + for key, flag in flags.items(): + if isinstance(flag, dict): + fields['flags'][key] = groupdict[key] + for sub_key, sub_flag in flag.items(): + fields['flags'][sub_key] = groupdict[key] == sub_flag + else: + fields['flags'][key] = groupdict[key] == flag + + return data + + +def get_version(cmd='ffmpeg'): + """ + Extract the version of the ffmpeg build. + """ + stdout = _run([cmd, '-version']) + match = VERSION_RE.search(stdout.split('\n')[0]) + return match.group('version') + + +def get_formats(cmd='ffmpeg'): + """ + Extract the formats of the ffmpeg build. + """ + stdout = _run([cmd, '-formats']) + return _get_line_fields(stdout, 4, MUXER_RE, MUXER_FLAGS) + + +def get_demuxers(cmd='ffmpeg'): + """ + Extract the demuxers of the ffmpeg build. + """ + stdout = _run([cmd, '-demuxers']) + return _get_line_fields(stdout, 4, MUXER_RE, MUXER_FLAGS) + + +def get_muxers(cmd='ffmpeg'): + """ + Extract the muxers of the ffmpeg build. + """ + stdout = _run([cmd, '-muxers']) + return _get_line_fields(stdout, 4, MUXER_RE, MUXER_FLAGS) + + +def get_codecs(cmd='ffmpeg'): + """ + Extract the codecs of the ffmpeg build. + """ + stdout = _run([cmd, '-codecs']) + codecs = _get_line_fields(stdout, 10, CODEC_RE, CODEC_FLAGS) + for codec in codecs.values(): + for coders_match in CODEC_CODERS_RE.finditer(codec['description']): + codec[coders_match.group(1)] = coders_match.group(3).split() + description_match = CODEC_DESCRIPTION_RE.search(codec['description']) + if description_match is not None: + codec['description'] = description_match.group('description') + return codecs + + +def get_bsfs(cmd='ffmpeg'): + """ + Extract the bsfs of the ffmpeg build. + """ + stdout = _run([cmd, '-bsfs']) + return stdout.split('\n')[1:-2] + + +def get_protocols(cmd='ffmpeg'): + """ + Extract the protocols of the ffmpeg build. + """ + stdout = [ + line.strip() for line in + _run([cmd, '-protocols']).split('\n')] + input_idx = stdout.index('Input:') + output_idx = stdout.index('Output:') + return dict( + input=stdout[input_idx + 1:output_idx], + output=stdout[output_idx + 1:-1]) + + +def get_filters(cmd='ffmpeg'): + """ + Extract the filters of the ffmpeg build. + """ + stdout = _run([cmd, '-filters']) + return _get_line_fields(stdout, 8, FILTER_RE, FILTER_FLAGS) + + +def get_pix_fmts(cmd='ffmpeg'): + """ + Extract the pix_fmts of the ffmpeg build. + """ + stdout = _run([cmd, '-pix_fmts']) + return _get_line_fields( + stdout, 8, PIX_FMT_RE, PIX_FMT_FLAGS, PIX_FMT_INT_FIELDS) + + +def get_sample_fmts(cmd='ffmpeg'): + """ + Extract the sample_fmts of the ffmpeg build. + """ + stdout = _run([cmd, '-sample_fmts']) + fmts = {} + for line in stdout.split('\n')[1:-1]: + name, depth = line.split() + fmts[name] = int(depth) + return fmts + + +def get_layouts(cmd='ffmpeg'): + """ + Extract the layouts of the ffmpeg build. + """ + stdout = _run([cmd, '-layouts']).split('\n') + channels_idx = stdout.index('Individual channels:') + layouts_idx = stdout.index('Standard channel layouts:') + data = {} + + data['channels'] = channels = {} + for line in stdout[channels_idx + 2:layouts_idx - 1]: + name, description = line.split(None, 1) + channels[name] = description + + data['layouts'] = layouts = {} + for line in stdout[layouts_idx + 2:-1]: + name, decomposition = line.split(None, 1) + layouts[name] = decomposition.split('+') + + return data + + +def get_colors(cmd='ffmpeg'): + """ + Extract the colors of the ffmpeg build. + """ + stdout = _run([cmd, '-colors']) + return dict(line.split() for line in stdout.split('\n')[1:-1]) + + +def get_devices(cmd='ffmpeg'): + """ + Extract the devices of the ffmpeg build. + """ + stdout = _run([cmd, '-devices']) + return _get_line_fields(stdout, 4, MUXER_RE, MUXER_FLAGS) + + +def get_hw_devices(cmd='ffmpeg'): + """ + Extract the hardware devices of the ffmpeg build. + """ + stdout = _run([cmd, '-init_hw_device', 'list']) + return stdout.split('\n')[1:-2] + + +def get_hwaccels(cmd='ffmpeg'): + """ + Extract the hwaccels of the ffmpeg build. + """ + stdout = _run([cmd, '-hwaccels']) + return stdout.split('\n')[1:-2] + + +def get_build_data(cmd='ffmpeg'): + """ + Extract details about the ffmpeg build. + """ + return dict( + version=get_version(cmd='ffmpeg'), + formats=get_formats(cmd='ffmpeg'), + demuxers=get_demuxers(cmd='ffmpeg'), + muxers=get_muxers(cmd='ffmpeg'), + codecs=get_codecs(cmd='ffmpeg'), + bsfs=get_bsfs(cmd='ffmpeg'), + protocols=get_protocols(cmd='ffmpeg'), + filters=get_filters(cmd='ffmpeg'), + pix_fmts=get_pix_fmts(cmd='ffmpeg'), + sample_fmts=get_sample_fmts(cmd='ffmpeg'), + layouts=get_layouts(cmd='ffmpeg'), + colors=get_colors(cmd='ffmpeg'), + devices=get_devices(cmd='ffmpeg'), + hw_devices=get_hw_devices(cmd='ffmpeg'), + hwaccels=get_hwaccels(cmd='ffmpeg')) + +__all__ = [ + 'get_build_data', + 'get_version', + 'get_version', + 'get_formats', + 'get_demuxers', + 'get_muxers', + 'get_codecs', + 'get_bsfs', + 'get_protocols', + 'get_filters', + 'get_pix_fmts', + 'get_sample_fmts', + 'get_layouts', + 'get_colors', + 'get_devices', + 'get_hw_devices', + 'get_hwaccels', +] + + +def main(args=None): + """ + Dump all ffmpeg build data to json. + """ + args = parser.parse_args(args) + data = get_build_data(args.ffmpeg) + json.dump(data, sys.stdout, indent=2) + + +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py index 0282c67..da567b8 100644 --- a/setup.py +++ b/setup.py @@ -95,4 +95,8 @@ setup( 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', ], + entry_points = { + 'console_scripts': [ + 'ffmpeg-build-json=ffmpeg._build:main'], + }, )