From f17766aec3978541700b6e23000e9f9e0d09152d Mon Sep 17 00:00:00 2001 From: Ross Patterson Date: Wed, 7 Aug 2019 05:24:09 -0700 Subject: [PATCH] Detect: Basic hwaccel detection based on OS and GPU, Linux only ATM --- Makefile | 19 ++++++ examples/get_detect_data.py | 59 ++++++++++++++++++ ffmpeg/__init__.py | 3 + ffmpeg/_detect.py | 121 ++++++++++++++++++++++++++++++++++++ ffmpeg/detect.json | 71 +++++++++++++++++++++ setup.py | 5 +- 6 files changed, 276 insertions(+), 2 deletions(-) create mode 100644 Makefile create mode 100755 examples/get_detect_data.py create mode 100644 ffmpeg/_detect.py create mode 100644 ffmpeg/detect.json diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..49d55b5 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +## Automate common development tasks + + +.PHONY: default +defalt: ffmpeg/detect.json + + +.tox/py35/bin/python: + tox -e py35 + touch "$(@)" + +.tox/py35/lib/python3.5/site-packages/pandas: .tox/py35/bin/python + .tox/py35/bin/pip install requests lxml pandas + touch "$(@)" + +.PHONY: ffmpeg/detect.json +ffmpeg/detect.json: .tox/py35/lib/python3.5/site-packages/pandas + .tox/py35/bin/python examples/get_detect_data.py >"$(@)" + diff --git a/examples/get_detect_data.py b/examples/get_detect_data.py new file mode 100755 index 0000000..e261a5b --- /dev/null +++ b/examples/get_detect_data.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +""" +Retrieve and process all the external data for hardware detection. +""" + +import sys +import json + +import requests +import pandas + +HWACCELINTRO_URL = 'https://trac.ffmpeg.org/wiki/HWAccelIntro' +API_TO_HWACCEL = { + 'AMF': 'amf', + 'NVENC/NVDEC/CUVID': 'cuvid', + 'Direct3D 11': 'd3d11va', + 'Direct3D 9 (DXVA2)': 'dxva2', + 'libmfx': 'libmfx', + 'MediaCodec': 'mediacodec', + 'Media Foundation': 'mediafoundation', + 'MMAL': 'mmal', + 'OpenCL': 'opencl', + 'OpenMAX': 'omx', + 'V4L2 M2M': 'v4l2m2m', + 'VAAPI': 'vaapi', + 'VDPAU': 'vdpau', + 'VideoToolbox': 'videotoolbox', +} +PLATFORM_TO_PY = { + 'Apple': 'Darwin', +} + + +def main(): + data = {} + + data['hwaccels'] = hwaccels = {} + response = requests.get(HWACCELINTRO_URL) + api_avail_table, impl_table = pandas.read_html(response.content) + + gpu_vendor_cols = api_avail_table.loc[1][1:] + platform_cols = api_avail_table.loc[0][1:] + api_rows = api_avail_table[0][2:] + + hwaccels['api_avail'] = platforms = {} + for gpu_vendor_idx, gpu_vendor in enumerate(gpu_vendor_cols): + platform = platform_cols[gpu_vendor_idx + 1] + platform = PLATFORM_TO_PY.get(platform, platform) + gpu_vendors = platforms.setdefault(platform, {}) + avail_hwaccels = gpu_vendors.setdefault(gpu_vendor, []) + for api_idx, api in enumerate(api_rows): + if api_avail_table[gpu_vendor_idx + 1][api_idx + 2] != 'N': + avail_hwaccels.append(API_TO_HWACCEL[api]) + + json.dump(data, sys.stdout, indent=2) + + +if __name__ == '__main__': + main() diff --git a/ffmpeg/__init__.py b/ffmpeg/__init__.py index a3100aa..1aa61ec 100644 --- a/ffmpeg/__init__.py +++ b/ffmpeg/__init__.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from . import nodes from . import _build +from . import _detect from . import _ffmpeg from . import _filters from . import _probe @@ -8,6 +9,7 @@ from . import _run from . import _view from .nodes import * from ._build import * +from ._detect import * from ._ffmpeg import * from ._filters import * from ._probe import * @@ -17,6 +19,7 @@ from ._view import * __all__ = ( nodes.__all__ + _build.__all__ + + _detect.__all__ + _ffmpeg.__all__ + _probe.__all__ + _run.__all__ diff --git a/ffmpeg/_detect.py b/ffmpeg/_detect.py new file mode 100644 index 0000000..c04081d --- /dev/null +++ b/ffmpeg/_detect.py @@ -0,0 +1,121 @@ +"""Detect optimal arguments for various options. + +This module includes a number of constants used to attempt to detect the +options which will provide the best performance for a given OS/GPU/etc.. + +For most of these constants, it only matters that the best performing option +available for a given OS/platform/hardware rank first for that +OS/platform/hardware, not which OS/platform/hardware performs better. For +example, it doesn't matter if `vdpau` is lower than `cuvid` or vice versa, +because one is only available for Linux and the other for Windows. Similarly, +it doesn't matter how `amf` is ranked with respect to `cuvid` because one is +only available on NVidia GPUs and the other AMD. It *does* matter how +`cuvid`/`amf` are ranked with respect to `dxva2` because those could both be +available on the same OS and GPU. + +Additions and suggestions for these constants are very much welcome, +especially if they come with benchmarks and/or good explanations from those +who understand this domain well. Contributions of more complicated or +involved detection logic may also be welcome, though the case will have to be +made more rigorously. +""" + +import sys +import platform +import os +import json +import logging +import argparse +import subprocess + +import ffmpeg + +logger = logging.getLogger(__name__) + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument( + '--ffmpeg', default='ffmpeg', + help='The path to the ffmpeg execuatble') + +# List `hwaccel` options by order of expected performance when available. +HWACCELS_BY_PERFORMANCE = [ + 'cuvid', 'amf', 'vdpau', + 'qsv', 'd3d11va', 'dxva2', 'vaapi', 'drm'] +# Loaded from JSON +DATA = None + + +def detect_gpu(): + """ + Detect the GPU vendor, generation and model if possible. + """ + plat_sys = platform.system() + if plat_sys == 'Linux': + display_output = subprocess.check_output( + ['lshw', '-class', 'display', '-json']) + return json.loads(display_output.decode().strip().strip(',')) + + +def detect_hwaccels(hwaccels=None, cmd='ffmpeg'): + """ + Extract details about the ffmpeg build. + """ + # Filter against what's available in the ffmpeg build + build_hwaccels = ffmpeg.get_hwaccels(cmd=cmd) + if hwaccels is None: + # Consider all the available hwaccels + hwaccels = build_hwaccels + else: + # Support passing in a restricted set of hwaccels + hwaccels = [ + hwaccel for hwaccel in hwaccels if hwaccel in build_hwaccels] + + # Filter against which APIs are available on this OS+GPU + data = _get_data() + plat_sys = platform.system() + gpu = detect_gpu() + api_avail = data['hwaccels']['api_avail'][plat_sys][ + gpu['vendor'].replace(' Corporation', '')] + hwaccels = [hwaccel for hwaccel in hwaccels if hwaccel in api_avail] + + hwaccels.sort(key=lambda hwaccel: ( + # Sort unranked hwaccels last, but in the order given by ffmpeg + hwaccel not in HWACCELS_BY_PERFORMANCE, + ( + # Sort ranked hwaccels per the constant + hwaccel in HWACCELS_BY_PERFORMANCE and + HWACCELS_BY_PERFORMANCE.index(hwaccel)))) + return hwaccels + + +__all__ = [ + 'detect_gpu', + 'detect_hwaccels', +] + + +def _get_data(): + """ + Don't load the data JSON unless needed, cache in a global. + """ + global DATA + if DATA is None: + with open(os.path.join( + os.path.dirname(__file__), 'detect.json')) as data_opened: + DATA = json.load(data_opened) + return DATA + + +def main(args=None): + """ + Dump all ffmpeg build data to json. + """ + args = parser.parse_args(args) + data = dict( + gpu=detect_gpu(), + hwaccels=detect_hwaccels(args.ffmpeg)) + json.dump(data, sys.stdout, indent=2) + + +if __name__ == '__main__': + main() diff --git a/ffmpeg/detect.json b/ffmpeg/detect.json new file mode 100644 index 0000000..19102f7 --- /dev/null +++ b/ffmpeg/detect.json @@ -0,0 +1,71 @@ +{ + "hwaccels": { + "api_avail": { + "Linux": { + "AMD": [ + "opencl", + "omx", + "vaapi", + "vdpau" + ], + "NVIDIA": [ + "cuvid", + "opencl", + "vaapi", + "vdpau" + ], + "Intel": [ + "libmfx", + "opencl", + "vaapi" + ] + }, + "Darwin": { + "iOS": [ + "videotoolbox" + ], + "macOS": [ + "opencl", + "videotoolbox" + ] + }, + "Android": { + "NaN": [ + "mediacodec", + "opencl", + "omx", + "v4l2m2m" + ] + }, + "Other": { + "Raspberry Pi": [ + "mmal", + "omx" + ] + }, + "Windows": { + "AMD": [ + "amf", + "d3d11va", + "dxva2", + "mediafoundation", + "opencl" + ], + "NVIDIA": [ + "cuvid", + "d3d11va", + "dxva2", + "mediafoundation", + "opencl" + ], + "Intel": [ + "d3d11va", + "dxva2", + "libmfx", + "mediafoundation", + "opencl" + ] + } + } + } +} \ No newline at end of file diff --git a/setup.py b/setup.py index da567b8..1135d0d 100644 --- a/setup.py +++ b/setup.py @@ -95,8 +95,9 @@ setup( 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', ], - entry_points = { + entry_points={ 'console_scripts': [ - 'ffmpeg-build-json=ffmpeg._build:main'], + 'ffmpeg-build-json=ffmpeg._build:main', + 'ffmpeg-detect=ffmpeg._detect:main'], }, )