Add support for extracting ffmpeg build data, such as hwaccels

This can be useful for projects that want to auto-detect optimal options for
invoking ffmpeg.
This commit is contained in:
Ross Patterson 2019-08-05 07:40:09 -07:00
parent 78fb6cf2f1
commit e09c440989
2 changed files with 310 additions and 0 deletions

306
ffmpeg/_build.py Normal file
View File

@ -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<version>[^ ]+) ')
MUXER_RE = re.compile(
r'^ (?P<demuxing>[D ])(?P<muxing>[E ]) '
r'(?P<name>[^ ]+) +(?P<description>.+)$',
re.M)
MUXER_FLAGS = dict(demuxing='D', muxing='E')
CODEC_RE = re.compile(
r'^ (?P<decoding>[D.])(?P<encoding>[E.])'
r'(?P<stream>[VAS.])(?P<intra_frame>[I.])'
r'(?P<lossy>[L.])(?P<lossless>[S.]) '
r'(?P<name>[^ ]+) +(?P<description>.+)$',
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<description>.+?) \((de|en)coders: [^)]+ \)')
CODEC_CODERS_RE = re.compile(
r' \((?P<type>(de|en)coders): (?P<coders>[^)]+) \)')
FILTER_RE = re.compile(
r'^ (?P<timeline>[T.])(?P<slice>[S.])(?P<command>[C.]) '
r'(?P<name>[^ ]+) +(?P<io>[^ ]+) +(?P<description>.+)$',
re.M)
FILTER_FLAGS = dict(timeline='T', slice='S', command='C')
PIX_FMT_RE = re.compile(
r'^(?P<input>[I.])(?P<output>[O.])(?P<accelerated>[H.])'
r'(?P<palleted>[P.])(?P<bitstream>[B.]) '
r'(?P<name>[^ ]+) +(?P<components>[0-9]+) +(?P<bits>[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()

View File

@ -95,4 +95,8 @@ setup(
'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.6',
], ],
entry_points = {
'console_scripts': [
'ffmpeg-build-json=ffmpeg._build:main'],
},
) )