|
1 | 1 | # TODO add function to guess media type given extension |
2 | 2 |
|
| 3 | +import logging |
3 | 4 | import re, fractions, subprocess as sp |
4 | 5 | from collections import namedtuple |
| 6 | +from fractions import Fraction |
| 7 | +from functools import partial |
| 8 | + |
5 | 9 |
|
6 | 10 | from .path import where |
7 | 11 | from .utils.error import FFmpegError |
@@ -832,53 +836,228 @@ def resolveFs(s): |
832 | 836 | return data |
833 | 837 |
|
834 | 838 |
|
| 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 | + |
835 | 950 | def filter_info(name): |
836 | 951 | """get detailed info of a filter |
837 | 952 |
|
838 | 953 | :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' |
855 | 972 | defining the pad name and pad stream type ('audio' or 'video') |
856 | 973 |
|
| 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 | +
|
857 | 990 | """ |
858 | 991 |
|
859 | 992 | # // according to fftools/comdutils.c show_help_filter() |
860 | 993 | stdout, data = __("filter", name) |
861 | 994 | if data: |
862 | 995 | return data |
863 | 996 |
|
| 997 | + blocks = re.split(r"\n(?! |\n|$)", stdout) |
| 998 | + |
864 | 999 | m = re.match( |
865 | 1000 | r"Filter (\S+)\s*?\n" |
866 | 1001 | r"(?: (.+?)\s*?\n)?" |
867 | 1002 | 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" |
870 | 1006 | 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." |
872 | 1017 | ) |
873 | 1018 |
|
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 | + ) |
882 | 1061 |
|
883 | 1062 | if not "filter" in _cache: |
884 | 1063 | _cache["filter"] = {} |
|
0 commit comments