From 7486f878b982867667892611e181dc4eab3298c8 Mon Sep 17 00:00:00 2001
From: Leandro Santiago <leandrosansilva@gmail.com>
Date: Wed, 12 Mar 2025 13:42:42 +0100
Subject: [PATCH] avfilter: Proof of Concept: enable out-of-tree filters

This is a POC/prototype that aims to enable out of tree filters on
FFmpeg.

Here is how to test it, with an example filter:

```
mkdir -p ext/libavfilter
pushd ext/libavfilter
git clone https://gitlab.com/leandrosansilva/ffmpeg-extra-filter-example example
popd
```

Then compile ffmpeg as usual:

```
./configure && make && make install
```

Now you can use the filters:

```
ffplay /path/to/file.webm -vf 'foo,bar'
```

What works:

- Building C based filters with no extra, or simple dependencies.
- Multiple filters in the same object file.

What is ugly:

- The filter metadata is hidden in the Makefile comments, and this is
  needed because at `./configure` time, executed before make, some code
  needs to be generated and, although such generation could in theory be
  done via make, this feels like too much change at the moment.

- Among the metadata, there are linker and compiler flags, which would
  look much better if they were simple make variables.

- At the moment it's possible for only one `check` metadata entry to be
  included in the Makefile, but we should support multiple of them, for
  the case when there are multiple extra dependencies.

What was not implemented:

- I believe it would be useful to check if the license of the filter is
  compatible with the license used to build FFmpeg.

- Only extra filters written in C (maybe C++?) are supported for now.
  One of my goals is to enable Rust as well.

- There should be a way to have optional dependencies, for filters that
  can have multiple "backends", for instance, to be chosen at build time.

Signed-off-by: Leandro Santiago <leandrosansilva@gmail.com>
---
 .gitignore               |  4 +++
 configure                | 75 ++++++++++++++++++++++++++++++++++++++++
 libavfilter/Makefile     |  4 +++
 libavfilter/allfilters.c |  1 +
 4 files changed, 84 insertions(+)

diff --git a/.gitignore b/.gitignore
index 430abaf91b..4eae911379 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,3 +44,7 @@
 /tools/python/__pycache__/
 /libavcodec/vulkan/*.c
 /libavfilter/vulkan/*.c
+/ffbuild/external-filters.txt
+/ffbuild/external-filters.mak
+/libavfilter/external_filters_extern.h
+/ext
diff --git a/configure b/configure
index d84e32196d..f7bb2cc38a 100755
--- a/configure
+++ b/configure
@@ -1798,6 +1798,7 @@ AVDEVICE_COMPONENTS="
 
 AVFILTER_COMPONENTS="
     filters
+    external_filters
 "
 
 AVFORMAT_COMPONENTS="
@@ -4489,6 +4490,50 @@ for opt do
     esac
 done
 
+symbols_from_external_filter_makefile() {
+  grep '^#.*symbol:' < "$1" | sed -e 's|#\s*symbol: \(.*\)|\1|g'
+}
+
+list_external_filter_makefiles() {
+  [ ! -d "ext/libavfilter" ] && return
+
+  for f in ext/libavfilter/*; do
+    [ -f "$f/Makefile" ] && echo $f/Makefile
+  done
+}
+
+list_external_filter_makefiles > ffbuild/external-filters.txt
+
+find_external_filters_extern() {
+  # TODO: handle invalid filter
+  while read Makefile; do
+    symbols_from_external_filter_makefile "$Makefile" | sed 's/^ff_[avfsinkrc]\{2,5\}_\([[:alnum:]_]\{1,\}\)/\1_filter/'
+  done < ffbuild/external-filters.txt
+}
+
+EXTERNAL_FILTER_LIST=$(find_external_filters_extern)
+
+for n in external_filters; do
+    v=$(toupper ${n%s})_LIST
+    eval enable \$$v
+    eval ${n}_if_any="\$$v"
+done
+
+FILTER_LIST="
+    $FILTER_LIST
+    $EXTERNAL_FILTER_LIST
+"
+
+AVFILTER_COMPONENTS_LIST="
+    $AVFILTER_COMPONENTS_LIST
+    $EXTERNAL_FILTER_LIST
+"
+
+ALL_COMPONENTS="
+    $ALL_COMPONENTS
+    $EXTERNAL_FILTER_LIST
+"
+
 for e in $env; do
     eval "export $e"
 done
@@ -7173,6 +7218,11 @@ enabled rkmpp             && { require_pkg_config rkmpp rockchip_mpp  rockchip/r
                              }
 enabled vapoursynth       && require_headers "vapoursynth/VSScript4.h vapoursynth/VapourSynth4.h"
 
+# Check for the dependencies of the external filters
+while read Makefile; do
+  # NOTE: this eval is dangerous, as it allows arbitrary code execution!
+  eval $(grep '^#.*check:' "$Makefile" | sed 's|#\s*check: \(.*\)|\1|g')
+done < ffbuild/external-filters.txt
 
 if enabled gcrypt; then
     GCRYPT_CONFIG="${cross_prefix}libgcrypt-config"
@@ -8250,12 +8300,23 @@ IGNORE_TESTS=$ignore_tests
 VERSION_TRACKING=$version_tracking
 EOF
 
+while read Makefile; do
+  # NOTE: this eval is dangerous, as it allows arbitrary code execution!
+  eval "avfilter_extralibs=\"\$avfilter_extralibs $(grep '^#.*ldflags:' "$Makefile" | sed 's|#\s*ldflags: \(.*\)|\1|g')\""
+done < ffbuild/external-filters.txt
+
 map 'eval echo "${v}_FFLIBS=\$${v}_deps" >> ffbuild/config.mak' $LIBRARY_LIST
 
 for entry in $LIBRARY_LIST $PROGRAM_LIST $EXTRALIBS_LIST; do
     eval echo "EXTRALIBS-${entry}=\$${entry}_extralibs" >> ffbuild/config.mak
 done
 
+echo "" > ffbuild/external-filters.mak
+
+while read Makefile; do
+    echo "include $Makefile" >> ffbuild/external-filters.mak
+done < ffbuild/external-filters.txt
+
 cat > $TMPH <<EOF
 /* Automatically generated by configure - do not modify! */
 #ifndef FFMPEG_CONFIG_H
@@ -8343,6 +8404,20 @@ cp_if_changed $TMPH libavutil/avconfig.h
 # ...
 eval "$(sed -n "s/^extern const FFFilter ff_\([avfsinkrc]\{2,5\}\)_\(.*\);/full_filter_name_\2=\1_\2/p" $source_path/libavfilter/allfilters.c)"
 
+rm -f libavfilter/external_filters_extern.h
+
+# register the symbols of the external filters
+while read Makefile; do
+    eval "$(symbols_from_external_filter_makefile "$Makefile" \
+      | sed 's/^ff_\([avfsinkrc]\{2,5\}\)_\([[:alnum:]]\{1,\}\)$/full_filter_name_\2=\1_\2/')"
+
+    symbols_from_external_filter_makefile "$Makefile" | while read symbol; do
+        echo "extern const FFFilter $symbol;" >> libavfilter/external_filters_extern.h
+        echo "EXTERNAL_FILTER_$(echo $symbol | sed 's/^ff_[avfsinkrc]\{2,5\}_\([[:alnum:]]\{1,\}\)$/\1/' | tr a-z A-Z)_LOCATION = $f" \
+            >> ffbuild/external-filters.mak
+    done
+done < ffbuild/external-filters.txt
+
 # generate the lists of enabled components
 print_enabled_components(){
     file=$1
diff --git a/libavfilter/Makefile b/libavfilter/Makefile
index 7c0d879ec9..877b24c30f 100644
--- a/libavfilter/Makefile
+++ b/libavfilter/Makefile
@@ -27,6 +27,10 @@ OBJS = allfilters.o                                                     \
 include $(SRC_PATH)/libavfilter/dnn/Makefile
 include $(SRC_PATH)/libavfilter/vulkan/Makefile
 
+# external filters handling
+include $(SRC_PATH)/ffbuild/external-filters.mak
+EXTERNAL_FILTER_FLAGS = -I$(PWD) -I$(PWD)/libavfilter
+
 OBJS-$(HAVE_LIBC_MSVCRT)                     += file_open.o
 OBJS-$(HAVE_THREADS)                         += pthread.o
 
diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
index 740d9ab265..c2d576e4be 100644
--- a/libavfilter/allfilters.c
+++ b/libavfilter/allfilters.c
@@ -621,6 +621,7 @@ extern  const FFFilter ff_vsrc_buffer;
 extern  const FFFilter ff_asink_abuffer;
 extern  const FFFilter ff_vsink_buffer;
 
+#include "libavfilter/external_filters_extern.h"
 #include "libavfilter/filter_list.c"
 
 
-- 
2.48.1