Skip to content

Commit ddb7df1

Browse files
committed
updated filter_info() to return FilterInfo
namedtuple with fully parsed options
1 parent 652ce5b commit ddb7df1

1 file changed

Lines changed: 206 additions & 27 deletions

File tree

src/ffmpegio/caps.py

Lines changed: 206 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# TODO add function to guess media type given extension
22

3+
import logging
34
import re, fractions, subprocess as sp
45
from collections import namedtuple
6+
from fractions import Fraction
7+
from functools import partial
8+
59

610
from .path import where
711
from .utils.error import FFmpegError
@@ -832,53 +836,228 @@ def resolveFs(s):
832836
return data
833837

834838

839+
# fmt: off
840+
FilterInfo = namedtuple(
841+
"FilterInfo",
842+
[ "name", "description", "threading", "inputs", "outputs",
843+
"options", "extra_options", "timeline_support",
844+
],
845+
)
846+
FilterOption = namedtuple(
847+
"FilterOption",
848+
["name", "type", "help", "ranges", "constants", "default",
849+
"video", "audio", "runtime"],
850+
)
851+
# fmt:on
852+
853+
854+
def _get_filter_pad_info(str):
855+
if str.startswith(" dynamic"):
856+
return "dynamic"
857+
elif str.startswith(" none"):
858+
return None
859+
860+
matches = re.finditer(r" #\d+: (\S+)(?= \() \((\S+)\)\s*?\n", str)
861+
if not matches:
862+
raise Exception("Failed to parse filter port info: %s" % str)
863+
return [{"name": m[1], "type": m[2]} for m in matches]
864+
865+
866+
def _conv_func(type, s):
867+
try:
868+
return type(s)
869+
except:
870+
return s
871+
872+
873+
def _get_filter_option_constant(str):
874+
m = re.match(
875+
r" ([^ \n]+) {1,16}(?:([^ ]+) {1,12}| {13})"
876+
r"[.E][.D][.F][.V][.A][.S][.X][.R][.B][.T][.P]"
877+
r"(?: (.+))?\n?",
878+
str,
879+
)
880+
return m[1], (m[3] or "", m[2] and int(m[2]))
881+
882+
883+
def _get_filter_option(str, name):
884+
# libavutil/opt.c/opt_list
885+
lines = str.splitlines()
886+
887+
# first line is the main option definition
888+
m0 = re.match(
889+
r" (?: |-)([^ \n]+) {1,17}(?:\<([^ >]+)\> {1,12}| {13})"
890+
r"[.E][.D][.F]([.V])([.A])[.S][.X][.R][.B]([.T])[.P]",
891+
lines[0],
892+
)
893+
if not m0:
894+
# likely deprecated
895+
logging.info(
896+
f"_get_filter_option(): invalid option line found for {name} filter. Likely deprecated:\n{lines[0]}"
897+
)
898+
return None
899+
name, type, *flags = m0.groups()
900+
901+
m1 = re.search(r"( \(from \S+? to \S+?\))*(?: \(default (.+)\))?$", lines[0])
902+
ranges_str, default = m1.groups()
903+
904+
help = lines[0][m0.end() + 1 : m1.start()]
905+
906+
if default:
907+
if type == "string":
908+
# remove quotes
909+
default = default[1:-1]
910+
elif type == "boolean":
911+
default = {"true": True, "false": False}.get(default, default)
912+
913+
conv = (
914+
partial(_conv_func, int)
915+
if type in ("int", "int64", "uint64")
916+
else partial(_conv_func, float)
917+
if type in ("float", "double")
918+
else partial(_conv_func, Fraction)
919+
if type == "rational"
920+
else (lambda s: s)
921+
)
922+
923+
ranges = (
924+
None
925+
if ranges_str is None
926+
else [
927+
(conv(m[1]), conv(m[2]))
928+
for m in re.finditer(r"\(from (\S+?) to (\S+?)\)", ranges_str)
929+
]
930+
)
931+
932+
return FilterOption(
933+
name,
934+
type,
935+
help,
936+
ranges,
937+
dict(_get_filter_option_constant(l) for l in lines[1:] if l),
938+
conv(default),
939+
*(fl != "." for fl in flags),
940+
)
941+
942+
943+
def _get_filter_options(str):
944+
m = re.match(r"(.+)? AVOptions:\n", str)
945+
name = m[1]
946+
blocks = re.split(r"\n(?! |\n|$)", str[m.end() :])
947+
return name, [_get_filter_option(line, name) for line in blocks if line]
948+
949+
835950
def filter_info(name):
836951
"""get detailed info of a filter
837952
838953
:return: list of features
839-
:rtype: dict
840-
841-
The returned dict has following entries:
842-
843-
=========== ============== ================================================
844-
Key type description
845-
=========== ============== ================================================
846-
name str Name
847-
description str Description
848-
threading list(str) List of threading capabilities
849-
inputs list(dict)|str List of input pads or 'dynamic' if variable
850-
outputs list(dict)|str List of output pads or 'dynamic' if variable
851-
options str Unparsed string, listing supported options
852-
=========== ============== ================================================
853-
854-
The dict iterms of 'inputs' and 'outputs' entries has two keys: 'name' and 'type'
954+
:rtype: FilterInfo (namedtuple)
955+
956+
The returned FilterInfo named tuple has following entries:
957+
958+
================ =========================== ================================================
959+
Key type description
960+
================ ============================ ================================================
961+
name str Name
962+
description str Description
963+
threading list(str) List of threading capabilities
964+
inputs list(dict)|str List of input pads or 'dynamic' if variable
965+
outputs list(dict)|str List of output pads or 'dynamic' if variable
966+
options list(FilterOption) List of filter options
967+
extra_options dict(str,list(FilterOption)) Extra options co-listed
968+
timeline_support bool True if `enable` timeline option is supported
969+
=============--- ============================ ================================================
970+
971+
'inputs' and 'outputs' entries has two keys: 'name' and 'type'
855972
defining the pad name and pad stream type ('audio' or 'video')
856973
974+
FilterOption is a namedtuple with the following entries:
975+
976+
========= =========================== ================================================
977+
Key type description
978+
========= ============================ ================================================
979+
name str Name
980+
type str Data type
981+
help str Help text
982+
ranges list(tuple(any,any))|None List of ranges of values
983+
constants dict(str:any) List of defined constant/enum values
984+
default any Default value
985+
video bool True if option for video stream
986+
audio bool True if option for audio stream
987+
runtime bool True if modifiable during runtime
988+
========= ============================ ================================================
989+
857990
"""
858991

859992
# // according to fftools/comdutils.c show_help_filter()
860993
stdout, data = __("filter", name)
861994
if data:
862995
return data
863996

997+
blocks = re.split(r"\n(?! |\n|$)", stdout)
998+
864999
m = re.match(
8651000
r"Filter (\S+)\s*?\n"
8661001
r"(?: (.+?)\s*?\n)?"
8671002
r"(?: (slice threading supported)\s*?\n)?"
868-
r" Inputs:\s*?\n([\s\S]*?)(?= Outputs)"
869-
r" Outputs:\s*?\n([\s\S]*?\s*?\n)(?!S)"
1003+
r" Inputs:\n"
1004+
r"([\s\S]*)"
1005+
r" Outputs:\n"
8701006
r"([\s\S]*)",
871-
stdout,
1007+
blocks[0],
1008+
)
1009+
name = m[1]
1010+
desc = m[2]
1011+
threading = ["slice"] if m[3] else []
1012+
inputs = _get_filter_pad_info(m[4])
1013+
outputs = _get_filter_pad_info(m[5])
1014+
timeline = (
1015+
blocks[-1].rstrip()
1016+
== "This filter has support for timeline through the 'enable' option."
8721017
)
8731018

874-
data = {
875-
"name": m[1],
876-
"description": m[2],
877-
"threading": ["slice"] if m[3] else [],
878-
"inputs": _getFilterPortInfo(m[4]),
879-
"outputs": _getFilterPortInfo(m[5]),
880-
"options": m[6],
881-
}
1019+
extra_options = dict(
1020+
(_get_filter_options(b) for b in (blocks[1:-1] if timeline else blocks[1:]))
1021+
)
1022+
1023+
options = extra_options.pop(name, None)
1024+
if options is None and len(extra_options):
1025+
opt_name = next(
1026+
(
1027+
o_name
1028+
for o_name in extra_options.keys()
1029+
# if (o_name == f"(a){name}")
1030+
# or (name[0] == "a" and o_name == f"(a){name[1:]}")
1031+
# or re.match(o_name, name)
1032+
# or re.search(rf"(?:^|[^a-z]){name}($|[^a-z])", o_name)
1033+
# or (name == "highshelf" and o_name == "treble/high/tiltshelf")
1034+
# or (name == "chromakey_cuda" and o_name == "cudachromakey")
1035+
# or (name == "hwupload_cuda" and o_name == "cudaupload")
1036+
),
1037+
None,
1038+
)
1039+
if opt_name:
1040+
options = extra_options.pop(opt_name)
1041+
elif len(extra_options) == 1:
1042+
o_name, options = extra_options.popitem()
1043+
logging.info(
1044+
f"filter_info({name}): assigned mismatched AVOptions {o_name}."
1045+
)
1046+
else:
1047+
logging.warning(
1048+
f"filter_info({name}): none of the AVOption sets appears to be the main option set:\n {[k for k in extra_options]}"
1049+
)
1050+
1051+
data = FilterInfo(
1052+
name,
1053+
desc,
1054+
threading,
1055+
inputs,
1056+
outputs,
1057+
options,
1058+
extra_options,
1059+
timeline,
1060+
)
8821061

8831062
if not "filter" in _cache:
8841063
_cache["filter"] = {}

0 commit comments

Comments
 (0)